ML from the Command Line without Writing Code
Run sentiment analysis, generate embeddings, detect toxicity, and search documents from your terminal. One binary, reads stdin, writes JSON, pipes into any script or CI pipeline.
Classify text, generate embeddings, detect toxicity, and search documents from your terminal. One binary, no code to write.
$ kjarni classify "I love this product!" --model roberta-sentiment
Input "I love this product!"
✓ positive ████████████████████ 98.5%
neutral ░░░░░░░░░░░░░░░░░░░░ 1.1%
negative ░░░░░░░░░░░░░░░░░░░░ 0.5%
$ kjarni similarity doctor physician
█████████████████░░░ 86.0% highly similar
↔ "doctor"
↔ "physician"
Kjarni is a single binary. Install it, run it. Models download on first use and cache locally.
Commands print a line or two of progress to stderr while a model loads. Those are left
out of the examples below for readability; pass -q to silence them.
Install
curl -fsSL https://kjarni.ai/install.sh | sh
No runtime, no dependencies. The binary links against libc and nothing else:
$ ldd $(which kjarni)
linux-vdso.so.1
libgcc_s.so.1
libm.so.6
libc.so.6
/lib64/ld-linux-x86-64.so.2
Sentiment Analysis
The default model (distilbert-sentiment) does binary positive/negative classification:
$ kjarni classify "Best purchase I've made this year"
✓ POSITIVE ████████████████████ 100.0%
NEGATIVE ░░░░░░░░░░░░░░░░░░░░ 0.0%
For three-class sentiment (positive/negative/neutral), use roberta-sentiment:
$ kjarni classify "It's okay I guess" --model roberta-sentiment
✓ positive ██████████░░░░░░░░░░ 51.6%
neutral █████████░░░░░░░░░░░ 44.6%
negative █░░░░░░░░░░░░░░░░░░░ 3.8%
The model picks up on hedging: "okay I guess" is barely positive at 51.6%, with almost as much weight on neutral. A single label would throw that away; the full distribution is what tells you the model is unsure.
Toxicity Detection
Switch to toxic-bert for content moderation:
$ kjarni classify "i hate mondays" --model toxic-bert
✓ toxic ██████████████░░░░░░ 69.8%
obscene ░░░░░░░░░░░░░░░░░░░░ 1.1%
insult ░░░░░░░░░░░░░░░░░░░░ 0.9%
threat ░░░░░░░░░░░░░░░░░░░░ 0.5%
identity_hate ░░░░░░░░░░░░░░░░░░░░ 0.4%
Multi-label, so each category is scored independently. A comment can be both toxic and an insult. Set a threshold (say 80%) and flag content above it.
JSON Output for Scripting
Add --format json to get structured output:
$ kjarni classify "Great service" --format json
{
"label": "POSITIVE",
"label_index": 1,
"predictions": [
{
"label": "POSITIVE",
"score": 0.9998435
},
{
"label": "NEGATIVE",
"score": 0.00015648185
}
],
"score": 0.9998435,
"text": "Great service"
}
Pipe into jq for extraction:
$ kjarni classify "Great service" --format json | jq '.label'
"POSITIVE"
$ kjarni classify "Great service" --format json | jq '.predictions'
[
{ "label": "POSITIVE", "score": 0.9998435 },
{ "label": "NEGATIVE", "score": 0.00015648185 }
]
Batch Processing
Classify a file of reviews, one per line:
$ cat reviews.txt | while read -r line; do
echo "$line → $(kjarni classify "$line" --format json | jq -r '.label')"
done
Fast shipping, great product → POSITIVE
Arrived damaged, no response from support → NEGATIVE
Best purchase I've made this year → POSITIVE
Embeddings
Generate a 384-dimensional vector from any text:
$ kjarni embed "hello world"
-0.1974462 0.17766516 0.038570307 0.14952245 -0.22542025 -0.9180284 ...
Space-separated floats, one vector, ready to store or compare. Add --normalize to get
unit-length vectors, which is what you want if you are going to compare them with a dot
product, and what the C# and Go bindings do by default.
Semantic Similarity
Compare two texts by meaning:
$ kjarni similarity doctor physician
█████████████████░░░ 86.0% highly similar
↔ "doctor"
↔ "physician"
$ kjarni similarity doctor banana
███████░░░░░░░░░░░░░ 33.8% somewhat related
↔ "doctor"
↔ "banana"
The model knows "doctor" and "physician" mean the same thing despite sharing no letters.
Add -q and you get the bare score instead of the chart, which is the form you want in
a script:
$ kjarni similarity doctor physician -q
0.859813
Index and Search
Create an index from a folder of text files:
$ kjarni index create ./my-index.idx ./docs/
Indexed 15 documents
✓ Index created: ./my-index.idx
Documents: 15
Dimension: 384
Size: 39.52 KB
Search by meaning:
$ kjarni search ./my-index.idx "war"
Results for "war"
1. ./docs/romanempire.txt
████████████████████ 100.0%
"The Roman Empire collapsed in 476 AD after centuries of political insta…"
2. ./docs/industrialrevolution.txt
█████████████████░░░ 87.5%
"The Industrial Revolution began in Britain with mechanized textile prod…"
3. ./docs/blackholes.txt
███████████████░░░░░ 75.3%
"Black holes form when massive stars exhaust their nuclear fuel and unde…"
The index combines BM25 keyword matching with semantic vector search. Add a reranker for more precise results:
$ kjarni search ./my-index.idx "artificial intelligence" --rerank-model minilm-l6-v2-cross-encoder
Reranking top 15 results with 'minilm-l6-v2-cross-encoder'...
Results for "artificial intelligence"
1. ./docs/neuralnetworks.txt
████████████████████ 100.0%
"Neural networks consist of interconnected layers of artificial neurons …"
2. ./docs/renaissance.txt
█████████░░░░░░░░░░░ 43.2%
"During the Renaissance, Florence became a center of artistic innovation…"
The reranker reads the query and each document together (cross-encoder), producing a more precise relevance ranking than embeddings alone.
Text Generation
Complete text with a base language model, which continues your text rather than answering it:
$ kjarni generate "The future of AI is" --model qwen2.5-1.5b --max-tokens 20
exciting but also very uncertain. The technology has the potential to
revolutionize many aspects of our lives,
For instruction-following and Q&A, use chat instead, or one of the -instruct
models.
Interactive Chat
Chat with instruct-tuned LLMs locally:
$ kjarni chat --model qwen2.5-0.5b-instruct
Kjarni Chat: qwen2.5-0.5b-instruct
Device: Cpu
Type '/help' for commands, '/quit' to exit.
> hello
Hello! How can I assist you today?
A local chatbot running from a single binary, offline once the model has downloaded. Models range from 490MB (qwen2.5-0.5b) to 8B parameters, so you can pick the size that fits your hardware.
Transcription
Transcribe audio files to text using Whisper:
$ kjarni transcribe recording.wav
Supports wav, mp3, flac, and ogg formats. Auto-detects language, or specify with --language en. Add --timestamps for timed output, or --translate to translate to English.
Model Management
List all available models with download status:
$ kjarni model list
Cache: /home/olafurj/.cache/kjarni
Models: 23/28 downloaded
LLM (DECODER)
------------------------------------------------------------------------------------------
✓ st qwen2.5-0.5b-instruct 490M [GGUF] Tiny logic engine. Perfect ...
✓ st llama3.2-1b-instruct 1.2B [GGUF] Official Meta edge model. V...
✓ st llama3.2-3b-instruct 3.2B [GGUF] The 3B standard. Excellent ...
✓ st phi3.5-mini 3.8B [GGUF] Microsoft's 3.8B reasoning ...
mistral-7b 7.2B [GGUF] Mistral v0.3. Extremely rel...
llama3.1-8b-instruct 8.0B [GGUF] The open source standard. R...
SEQ2SEQ
------------------------------------------------------------------------------------------
✓ st flan-t5-base 250M General purpose instruction...
✓ st whisper-small 244M OpenAI Whisper small for sp...
EMBEDDING
------------------------------------------------------------------------------------------
✓ st minilm-l6-v2 22M Fastest sentence embedding ...
✓ st mpnet-base-v2 110M High-quality sentence embed...
CLASSIFIER
------------------------------------------------------------------------------------------
✓ st distilbert-sentiment 66M Fast binary sentiment ...
✓ st roberta-sentiment 125M 3-class sentiment ...
✓ st toxic-bert 110M Toxic comment classifier ...
A tick means the weights are cached locally. [GGUF] marks models available in
quantised form, which is the format you want for decoders.
Download, inspect, or remove models:
$ kjarni model download llama3.2-1b-instruct
$ kjarni model info minilm-l6-v2
$ kjarni model remove qwen2.5-1.5b
Filter by task or architecture:
$ kjarni model list --task chat
$ kjarni model list --task embedding
$ kjarni model list --downloaded
Inspecting a Model
inspect prints what a model file says about itself: hyperparameters, tensor names,
shapes and dtypes. It reads GGUF and safetensors, and takes a path or the name of
anything already in your cache.
$ kjarni inspect llama3.2-3b-instruct-q4_k_m
File ~/.cache/kjarni/llama-3.2-3b-instruct-q4_k_m/Llama-3.2-3B-Instruct-Q4_K_M.gguf
Format GGUF, 1.88 GiB
Architecture llama
Metadata
llama.attention.head_count 24
llama.attention.head_count_kv 8
llama.attention.layer_norm_rms_epsilon 0.00001
llama.block_count 28
llama.context_length 131072
llama.embedding_length 3072
llama.feed_forward_length 8192
llama.rope.freq_base 500000.0
llama.vocab_size 128256
tokenizer.ggml.bos_token_id 128000
tokenizer.ggml.eos_token_id 128009
Tensors (255 total)
per layer
blk.{i}.attn_k.weight [1024, 3072] Q4_K x28
blk.{i}.attn_q.weight [3072, 3072] Q4_K x28
blk.{i}.attn_v.weight [1024, 3072] Q4_K/Q6_K x28 (varies)
blk.{i}.ffn_down.weight [3072, 8192] Q4_K/Q6_K x28 (varies)
blk.{i}.ffn_gate.weight [8192, 3072] Q4_K x28
single
output_norm.weight [3072] F32
token_embd.weight [128256, 3072] Q6_K
The per-layer block is folded to {i} with a count, because a 3B model has a couple
of hundred tensors and almost all of them are the same dozen names repeated once per
layer. Printing them in full buries the thing you are looking for.
(varies) marks a tensor whose dtype changes between layers. That is not a bug in the
file: a _M quantisation deliberately keeps some layers at higher precision, and if
you are writing code against these weights it is exactly the sort of thing that will
catch you out.
All Commands
$ kjarni
Kjarni: The SQLite of AI
Usage: kjarni [OPTIONS] <COMMAND>
Commands:
model Manage models (list, download, info)
generate Generate text from a prompt
summarize Summarize text
translate Translate text between languages
inspect Show a model's metadata, config and tensor layout
embed Generate embeddings for text
transcribe Transcribe audio to text
classify Classify text using a classification model
rerank Rerank documents by relevance to a query
chat Interactive chat mode
index Create or manage search indexes
search Search an index
similarity Compute similarity between two texts
Commands read from arguments or stdin. The four that produce structured data,
classify, embed, search and rerank, take --format json and print something
jq can read. The rest write text meant for a person. Most commands also take -q to
drop progress output, which is what you want in a pipeline. Standard UNIX behaviour:
pipe it, script it, cron it.
Practical Recipes
CI Pipeline: Scan PR Comments for Toxicity
gh pr view $PR_NUMBER --json comments -q '.comments[].body' | \
while read -r comment; do
score=$(kjarni classify "$comment" --model toxic-bert --format json | \
jq '.predictions[] | select(.label == "toxic") | .score')
if (( $(echo "$score > 0.8" | bc -l) )); then
echo "⚠️ Toxic comment detected: $comment"
fi
done
Batch Classify a CSV Column
cut -d',' -f3 reviews.csv | tail -n +2 | \
while read -r text; do
kjarni classify "$text" --format json
done | jq -s '.' > results.json
Quick Sentiment Check on Logs
grep "customer feedback" app.log | \
sed 's/.*feedback: //' | \
while read -r line; do
echo "$(kjarni classify "$line" --format json | jq -r '.label') | $line"
done
How It Works
The CLI is a thin wrapper around the same Rust inference engine that powers the C# NuGet package. Same models, same accuracy, same local execution. The binary is self-contained, and the only system dependency is glibc.
For the full technical story, see Why I Built a Native ML Inference Engine in Rust.
Install: curl -fsSL https://kjarni.ai/install.sh | sh
GitHub: https://github.com/olafurjohannsson/kjarni
Next Steps
- Semantic Search in C#: use the same engine from C# code
- Sentiment Analysis in C#: classification with the NuGet package
- Build a Document Search Engine in C#: full hybrid search with indexing and reranking
Kjarni runs the same engine on every platform: