Skip to content

HungKNguyen/command-recommendation

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

54 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

command-rec

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.

How It Works

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 pullnpm install)
  • Fuzzy text matching — partial and prefix matches against the query
  • Embedding similarity (with neural-embeddings feature) — find commands by intent, not just text

Quick Start

1. Clone and run the installer

git clone https://github.com/your-org/command-recommendation
cd command-recommendation
./install.sh

The 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 source line to your shell's rc file (~/.zshrc, ~/.bashrc, or config.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, fish

2. Start the daemon

The 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 running

Once 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

Manual build (alternative to the installer)

# 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,accelerate

On 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

3. Use it

  • 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.

Commands

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

Daemon lifecycle

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.

Uninstall

./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)

Import existing history

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 fish

Import is idempotent — running it twice won't create duplicates.

Migrate from another command-rec SQLite database

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.db

Imports 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.

Remove a bad entry

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.

Configuration

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 type

If 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.

Backends

SQLite (default)

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-embeddings feature)

MongoDB

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.

  • $search full-text retrieval + $vectorSearch semantic retrieval (hybrid; the vector branch is added only when neural-embeddings is enabled)
  • All scoring signals computed in-pipeline; SQLite, by contrast, retrieves and ranks in Rust
  • Needs two Search indexes on commands: command_search (text) and embedding_index (vector, 384-dim cosine)

Local MongoDB + Search via Docker (recommended)

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 -d
backend = "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), localhost connections hit that server (which has no Search) and you'll see 31082 SearchNotEnabled against an apparently empty DB. Stop it (brew services stop mongodb-community) or publish the stack on another port. See docker/README.md for details and teardown.

Manual setup (your own MongoDB 8.2+ with Search)

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/$vectorSearch require the mongot binary (Community 8.2+ Search, public preview, Linux/Docker). A plain mongod without Search cannot serve the MongoDB backend.

Neural Embeddings Feature

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).

MongoDB Scoring (aggregation pipeline)

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 ($lookup to the transitions collection, cwd → repo → global), and embedding similarity ($vectorSearch, with neural-embeddings) are summed into relevance, then multiplied by recency_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.

Interaction Modes

Each mode can be independently enabled/disabled in the [features] config section.

Search Overlay (enable_overlay)

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")

Post-command Predictions (enable_predictions)

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.

Prefix Predictions / Ghost Text (enable_ghost_text)

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.

Debug Server (command-rec demo)

Launch an interactive web UI for exploring and tuning the scoring engine:

command-rec demo --port 3000

Open 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.

Development

See docs/DEVELOPMENT.md for building from source, running tests, benchmarks, and project structure.

License

MIT

About

A context-aware terminal command recommendation engine. It learns your command patterns per project and suggests what you'll run next.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors