A context-aware terminal command recommendation engine. It learns your command patterns per project and suggests what you'll run next.
Unlike shell history search (Ctrl-R), command-rec ranks commands by project context — same repo, same directory, recency, frequency, and command sequences (Markov chains). With the neural-embeddings feature, both backends gain semantic search via sentence-transformer embeddings.
Shell Plugin (zsh/bash/fish)
↕ UNIX socket (JSON)
Recommendation Daemon (Rust)
↕
SQLite or MongoDB
Every command you run is captured with metadata (cwd, git repo, exit code, session). The daemon scores recommendations using configurable weighted signals (see [scoring] in config):
- Repo/directory matching — commands from the same project rank higher
- Recency/frequency — recent and frequently-used commands score higher
- Markov chains — learns command sequences (e.g.,
git pull→npm install) - Fuzzy text matching — partial and prefix matches against the query
- Embedding similarity (with
neural-embeddingsfeature) — find commands by intent, not just text
git clone https://github.com/your-org/command-recommendation
cd command-recommendation
./install.shThe installer will:
- Ask whether to include neural embeddings (semantic search)
- Build the binary with
cargo - Install it to
/usr/local/bin/command-rec(requires sudo) - Copy the shell plugin to
~/.config/command-rec/ - Add a
sourceline to your shell's rc file (~/.zshrc,~/.bashrc, orconfig.fish) - Start the daemon
Open a new shell session and run command-rec status to confirm everything is working.
Shell override — if auto-detection picks the wrong shell:
./install.sh --shell zsh # or bash, fishThe daemon is started automatically by the installer and by the shell plugin when your shell session begins.
command-rec start # start manually
command-rec stop # stop — stays stopped until you restart or open a new shell
command-rec status # check if runningOnce you explicitly run command-rec stop, the daemon stays stopped. It will not auto-restart until you run command-rec start or open a new shell session.
Restart:
command-rec stop && command-rec start# Standard build (text search + heuristics, no embeddings)
cargo build --release
# With neural embeddings (adds semantic search via all-MiniLM-L6-v2)
cargo build --release --features neural-embeddings
# macOS: add accelerate for faster embedding inference via Apple's BLAS
cargo build --release --features neural-embeddings,accelerateOn first run with neural-embeddings, the daemon downloads the model weights (~91MB) from Hugging Face Hub, cached in ~/.cache/huggingface/hub/ for subsequent runs.
The plugins need one of these for UNIX socket communication: socat (recommended), zsh/net/socket (zsh only), or perl (universal fallback). Install socat for best performance:
brew install socat # macOS
sudo apt install socat # Debian/Ubuntu
- Search overlay (default Ctrl-G) — opens with your most recent commands, ranked by context. Type to filter, arrow keys to navigate, Enter to select, Escape to cancel.
- Tab — accepts a post-command prediction (shown after each command you run) or a prefix suggestion.
- Right arrow — accepts inline ghost-text suggestion while typing.
- Just use your terminal normally — commands are captured automatically.
command-rec start # Start the daemon in background
command-rec stop # Stop the daemon
command-rec status # Show daemon status, backend, command count
command-rec import # Import existing shell history
command-rec delete # Purge a command from history + transitions| Item | Detail |
|---|---|
| Socket | ~/.config/command-rec/daemon.sock (or $XDG_CONFIG_HOME/command-rec/daemon.sock) |
| PID file | ~/.config/command-rec/daemon.pid — same directory as the socket |
| Config | ~/.config/command-rec/config.toml — created with defaults on first run |
| History DB | ~/.config/command-rec/history.db |
| Logs | No log file by default. For debug output: RUST_LOG=debug command-rec start (logs to stderr before the process daemonizes) |
| Restart | command-rec stop && command-rec start |
| Auto-start | The shell plugin starts the daemon once when your shell session loads, if it isn't already running. command-rec stop keeps it stopped for that session. |
./install.sh --uninstall # remove binary, plugin copy, and rc line (keeps history/config)
./install.sh --purge # everything above + delete ~/.config/command-rec/ (asks to confirm)Bootstrap the system with your existing shell history:
# Auto-detect and import all found history files
command-rec import
# Import from a specific shell
command-rec import --shell zsh
command-rec import --shell bash
command-rec import --shell fishImport is idempotent — running it twice won't create duplicates.
To move history from a SQLite-backed install into a different backend (e.g., a
MongoDB destination), point at the source .db file:
command-rec import --from-sqlite ~/old-machine/history.dbImports both commands and aggregated transition counts. Re-running is idempotent
for commands; transition counts are upserted, so don't replay the same source
twice into the same destination. Embeddings are not copied — the destination
recomputes them on save_command if neural-embeddings is enabled.
Captured a typo? Purge it from both the command history and the Markov transition table:
command-rec delete "the typo you ran"This goes through the running daemon over IPC. Inside the search overlay, press
Ctrl-X to delete the currently-highlighted entry and refresh the list.
The daemon will happily record command-rec invocations like any other command.
To avoid the delete itself getting written back into history, pass --no-capture
— the shell plugin's preexec hook recognizes the flag and skips capture for that
line:
command-rec delete --no-capture "the typo you ran"The overlay's Ctrl-X handler uses --no-capture automatically.
Config file: ~/.config/command-rec/config.toml
# Storage backend: "sqlite" (default) or "mongodb"
backend = "sqlite"
# SQLite database path
sqlite_db_path = "~/.config/command-rec/history.db"
# MongoDB connection string (required if backend = "mongodb")
# mongodb_connection_string = "mongodb://localhost:27017"
# MongoDB database name (default: "command_rec")
# mongodb_database_name = "command_rec"
# UNIX socket path for daemon IPC
socket_path = "~/.config/command-rec/daemon.sock"
# Keybinding for the search overlay (default: ctrl-g)
keybinding = "ctrl-g"
# Debounce interval for prefix predictions in milliseconds (default: 100)
debounce_ms = 100
# Scoring — tune how recommendations are ranked.
#
# Scoring is two-stage:
#
# relevance = repo*w_repo + cwd*w_cwd + freq*w_freq + fuzzy*w_fuzzy
# + markov*w_markov + embedding*w_emb
# total = relevance * max(recency_floor, exp(-Δt * ln2 / recency_half_life_hours))
#
# So `recency` isn't one weighted signal among many — it's a global multiplier
# on the relevance sum. A command's recency contribution **halves every
# `recency_half_life_hours` hours of disuse**, never falling below
# `recency_floor` × its relevance (so a strongly relevant ancient command
# isn't driven to zero).
#
# Set `recency_half_life_hours = 0` to disable the multiplier entirely
# (multiplier is always 1.0).
#
# Each interaction mode (search overlay, ghost text, predict next) has its
# own fully-independent profile — no inheritance between profiles.
[scoring.search_overlay]
repo_weight = 1.0 # Same git repo
cwd_weight = 1.0 # Same directory
frequency_weight = 1.0 # GROUP BY count from the DB
fuzzy_match_weight = 4.0 # Primary signal — user is typing a query
markov_weight = 1.0 # Transition from previous_command
embedding_weight = 3.0 # neural-embeddings feature; semantic recall
recency_half_life_hours = 720.0 # 30 days — "what have I been doing"
recency_floor = 0.1 # Stale commands keep ≥ 10% of relevance
candidate_pool_size = 200
[scoring.ghost_text]
repo_weight = 2.0
cwd_weight = 2.0
frequency_weight = 1.0
fuzzy_match_weight = 2.0 # Prefix match against the typed buffer
markov_weight = 1.5 # Lower — less reliable without full query context
embedding_weight = 0.0 # Off — inline completion needs lexical matches
recency_half_life_hours = 168.0 # 1 week — sharper, what you're doing now
recency_floor = 0.1
candidate_pool_size = 200
[scoring.predict_next]
repo_weight = 2.0
cwd_weight = 2.0
frequency_weight = 0.5
fuzzy_match_weight = 0.0 # No text query, so irrelevant
markov_weight = 4.0 # Primary signal: what usually follows the last command
embedding_weight = 0.0 # Off — semantic recall doesn't help here
recency_half_life_hours = 336.0 # 2 weeks — adapts to workflow drift
recency_floor = 0.1
candidate_pool_size = 200
# Data retention — automatic cleanup of old commands and cold Markov edges.
# Transition pruning uses the SAME formula and floor as scoring above (taken
# from scoring.search_overlay), so an edge that scoring still values is also
# kept by the prune.
[retention]
max_commands = 50000 # Keep at most this many commands (0 = unlimited)
max_age_days = 365 # Delete commands older than this (0 = unlimited)
max_transitions = 50000 # Cap on unique Markov transitions; weakest edges
# (by `count * max(floor, exp(-Δt / half_life))`,
# using search_overlay's half-life and floor) are
# evicted on the next prune tick. 0 = unlimited.
prune_interval_hours = 24 # How often the daemon checks for pruning (0 = disabled)
# Feature toggles — enable/disable shell plugin behaviors.
[features]
enable_overlay = true # Search overlay (default Ctrl-G binding)
enable_predictions = true # Post-command predictions (hint line after each command)
enable_ghost_text = true # Inline prefix suggestions as you typeIf the config file doesn't exist, sensible defaults are used (SQLite backend, ctrl-g keybinding). All sections and keys are optional — omitted values use their defaults shown above.
Zero-setup, fully offline. Stores everything in a single file. Supports:
- FTS5 full-text search
- Heuristic scoring (repo, cwd, recency, frequency, fuzzy match)
- Markov chain sequence prediction
- Vector search via sqlite-vec (with
neural-embeddingsfeature)
The MongoDB backend scores entirely inside the database via a single
aggregation pipeline (no Rust re-rank): hybrid retrieval ($search +
$unionWith($vectorSearch)) followed by the weighted signals and a 3-level
Markov blend (cwd → repo → global). This requires the mongot Search
component — public preview in MongoDB Community 8.2+, Linux-only, but runs
anywhere via Docker.
$searchfull-text retrieval +$vectorSearchsemantic retrieval (hybrid; the vector branch is added only whenneural-embeddingsis enabled)- All scoring signals computed in-pipeline; SQLite, by contrast, retrieves and ranks in Rust
- Needs two Search indexes on
commands:command_search(text) andembedding_index(vector, 384-dim cosine)
The repo ships a one-command stack (MongoDB 8.2 Community + mongot) that also creates both Search indexes:
docker compose -f docker/docker-compose.yml up -dbackend = "mongodb"
mongodb_connection_string = "mongodb://localhost:27017/?directConnection=true"This works for real shell usage on macOS via Docker Desktop — $search,
$vectorSearch, and embeddings all run through the published port (verified
end-to-end). directConnection=true is required so the host skips replica-set
discovery (the member is advertised under its in-container name).
One gotcha — port 27017: if a native MongoDB is already running on
127.0.0.1:27017(e.g.brew services start mongodb-community),localhostconnections hit that server (which has no Search) and you'll see31082 SearchNotEnabledagainst an apparently empty DB. Stop it (brew services stop mongodb-community) or publish the stack on another port. Seedocker/README.mdfor details and teardown.
mongosh command_rec --eval '
db.createCollection("commands");
db.commands.createSearchIndex("command_search",
{ mappings: { dynamic: false, fields: { command: { type: "string" } } } });
db.commands.createSearchIndex("embedding_index", "vectorSearch",
{ fields: [ { type: "vector", path: "embedding", numDimensions: 384, similarity: "cosine" } ] });
'
$search/$vectorSearchrequire themongotbinary (Community 8.2+ Search, public preview, Linux/Docker). A plainmongodwithout Search cannot serve the MongoDB backend.
Build with --features neural-embeddings to enable semantic search on both backends:
- Model:
sentence-transformers/all-MiniLM-L6-v2(384-dim embeddings) - Framework: Candle (pure Rust, no Python/ONNX needed)
- SQLite vector index: sqlite-vec (vec0 virtual table, stored inside the database file)
- First-run download: ~91MB model weights, cached in
~/.cache/huggingface/hub/ - Latency: ~5-15ms per embedding on CPU
- Binary size: +5-15MB
Without this feature, the daemon uses text search + heuristic scoring only (no embedding computation, no model download).
There is no separate scoring feature flag. The two backends are distinct:
- SQLite → Rust scoring: retrieve candidates (FTS5 + optional sqlite-vec),
then rank in Rust (
score_candidates). - MongoDB → aggregation pipeline: retrieve and score inside one pipeline.
Repo match, cwd match, frequency (normalized via
$setWindowFields), fuzzy text match ($split/$filter/$indexOfCP), a 3-level Markov blend ($lookupto the transitions collection, cwd → repo → global), and embedding similarity ($vectorSearch, withneural-embeddings) are summed intorelevance, then multiplied byrecency_factor = max(floor, $exp(...))for the final score.
See docs/mongodb-architecture.md for the full
pipeline. The debug server (command-rec demo) renders the live MongoDB pipeline
for the MongoDB backend, and the Rust scoring breakdown for SQLite.
Each mode can be independently enabled/disabled in the [features] config section.
Press the configured key (default Ctrl-G) to open a full-screen overlay. It immediately shows your most recent commands, re-ranked by the scoring engine for your current context (repo, directory, Markov chain). Type to filter with fuzzy matching.
Each result shows:
- Command text
- Repository name (if different from current)
- Relative timestamp (e.g., "2h ago")
After each command, the system predicts likely next commands based on Markov chains and shows them as a hint line below the prompt. Press Tab on an empty prompt to accept the top prediction.
As you type, ghost text appears showing the top suggestion matching your prefix. Press Tab or Right arrow to accept, or keep typing to refine. The debounce interval (debounce_ms) controls how frequently the daemon is queried.
Launch an interactive web UI for exploring and tuning the scoring engine:
command-rec demo --port 3000Open http://localhost:3000 to get:
- Live search — type a query, set repo/cwd/previous command context
- Score breakdown table — see each signal's contribution per result, color-coded
- Weight sliders — drag to re-rank results instantly (no DB round-trip)
- DB query inspector — see the actual FTS5 SQL or MongoDB pipeline sent to the database
- Raw candidates — inspect pre-scoring results from the database
With the MongoDB backend, the demo renders the live aggregation pipeline (six weighted signals + recency multiplier, hybrid $search + $vectorSearch) that produced the results; with SQLite it shows the Rust scoring breakdown.
See docs/DEVELOPMENT.md for building from source, running tests, benchmarks, and project structure.
MIT