smcli is two executables that must stay side by side:
smcli: the command you run, and the target of the shell hooks.smcli-service: a background helper thatsmclistarts on demand and that exits when idle. It keeps history in memory and powers the file and grep picker.smclifinds it in its own directory (smcli→smcli-service,smcli-<build>→smcli-service-<build>); setSMCLI_SERVICE_BINto an absolute path to override.
Both depend only on system libraries; the fff file-search library is built in. The Potion search model is shipped separately in the potion-shell-16m/ folder beside both executables.
Requirements: macOS 14 or later, or Linux. Bash 4+ for the picker keys (macOS's /bin/bash 3.2 still records history, without
keys), any recent Zsh, or Fish.
- Quick install with the script (recommended)
- Install a release
- Install from source
- Set up your shell (both)
- Upgrade · Uninstall · Troubleshooting
install.sh does everything below in one step:
- It installs both executables and
smcli-uninstallto~/.local/bin, clearing the macOS quarantine flag. - It adds the shell hooks to your startup files.
- It asks for the picker keys and search model.
- It starts the service.
./install.sh # from an unpacked release (the binaries sit beside it)
scripts/install.sh # from a source checkout: builds with `make release` first (needs Rust)| Option | Meaning |
|---|---|
--prefix DIR |
Where the executables go (default ~/.local/bin) |
--from DIR |
Install the smcli and smcli-service found in DIR (e.g. dist) |
--shells LIST |
zsh,bash,fish to hook; all (every one installed) or none. Default: your login shell |
--keys LIST |
Picker keys: ctrl-r,left,right, ctrl-r, left, right or none. Default: ask in a terminal |
--yes |
Ask nothing; without --keys, keeps the default keys (Up and Ctrl-R) |
The hooks go in a marked block at the end of each file, so they load after other tools that bind keys:
# >>> smcli >>>
# Added by smcli install.sh; remove with smcli-uninstall. Keep it below other tools that bind keys.
eval "$('/Users/you/.local/bin/smcli' shell-init zsh)"
# <<< smcli <<<Zsh uses $ZDOTDIR/.zshrc (else ~/.zshrc), Bash ~/.bashrc, and Fish $XDG_CONFIG_HOME/fish/config.fish. Each
file is backed up first as <file>.smcli-backup. Rerunning the script upgrades in place: it replaces the binaries
and the block, and never adds a second block. For Bash, it warns when ~/.bash_profile doesn't read ~/.bashrc
(macOS opens login shells).
Unattended, e.g. in a dotfiles bootstrap: scripts/install.sh --shells all --keys ctrl-r --yes.
Open a new terminal afterwards. The rest of this page is the same steps done by hand.
-
Unpack the release archive and copy both executables to one directory on your
PATH:mkdir -p ~/.local/bin cp smcli smcli-service ~/.local/bin/ cp -R potion-shell-16m ~/.local/bin/
Keep them together. Moving only
smclileaves it without its service: commands still work, but more slowly, and the file and grep picker stops working. -
If macOS blocks a downloaded binary ("cannot be opened because the developer cannot be verified"), clear the quarantine flag:
xattr -d com.apple.quarantine ~/.local/bin/smcli ~/.local/bin/smcli-service
-
Continue with Set up your shell.
Tools:
- A C toolchain: Xcode or the Command Line Tools on macOS;
build-essentialon Debian/Ubuntu. - Rust via rustup (
cargo).fff-searchand its vendored libgit2 are built from source: the first build downloads and compiles the locked crate sources; later builds reuse them. - On Linux,
pkg-configand the SQLite development headers (e.g.libsqlite3-devon Debian/Ubuntu). curlandgit(included with macOS and most Linux distributions).
Build:
git clone <repository> smcli && cd smcli
make freshmake fresh does a clean release build into dist/: dist/smcli-<timestamp>-<commit> and
dist/smcli-service-<timestamp>-<commit>, plus dist/smcli and dist/smcli-service symlinks to the newest pair.
make release does the same without cleaning first (faster). make clean keeps the newest pair and both
symlinks, because your shell hooks may point at them.
Point your shell at the dist/smcli symlink, not at a timestamped file, so every rebuild is picked up by new
shells:
# in ~/.zshrc (use the absolute path of your checkout)
eval "$("$HOME/Dev/smcli/dist/smcli" shell-init zsh)"To try a build in the current shell only, run the eval line that make fresh prints.
For development:
| Command | What it does |
|---|---|
make build |
Debug build (target/debug/) |
make run ARGS="search git" |
Debug build, then run it |
make test |
Unit tests, then the interactive Bash/Zsh/Fish, service and file-picker tests on real terminals |
make ranking |
Ranking quality report; fails when the ranking tests fail |
make baseline |
Black-box benchmark of the release build |
make test uses throwaway homes and databases and never reads your real history. Shells that aren't installed are
skipped and reported. On Linux, install the packages above, then run make release. make linux runs the full
suite in a rust:latest Docker container (needs a running Docker daemon).
Then continue with Set up your shell.
-
Choose the picker keys and the search model:
smcli init
It asks which picker keys to bind (press Enter to keep the default):
Open history with Ctrl-R? [Y/n] Open history with Left on an empty line? [y/N] Open suggestions with Right on an empty line? [y/N] Open files with Ctrl-T? [Y/n] Open grep with Ctrl-G? [Y/n]Up always opens the picker. On an empty line in Bash and Zsh, Left opens the same history search as Ctrl-R and Right opens directory suggestions. A line that already has text keeps normal arrow movement. Then it offers any Ollama embedding models you have installed; the default is the potion-shell-16m model, installed in the
potion-shell-16m/folder beside the executables.For scripts and dotfiles, use flags instead; they never ask:
smcli init --keys ctrl-r,left,right # or ctrl-r, left, right, none smcli init --default --keys ctrl-r # model and keys together
Settings are saved to
~/.config/smcli/config.json(or$XDG_CONFIG_HOME/smcli/config.json, orSMCLI_CONFIG). -
Load the hooks. Add one line to your shell's startup file. Use the binary's absolute path:
Shell File Line Zsh ~/.zshrceval "$(~/.local/bin/smcli shell-init zsh)"Bash ~/.bashrceval "$(~/.local/bin/smcli shell-init bash)"Fish ~/.config/fish/config.fish~/.local/bin/smcli shell-init fish | sourcePut the line after other tools that bind keys (fzf, atuin, oh-my-zsh, zsh-autosuggestions): smcli leaves keys that another tool already bound alone and says which it kept. The hooks never modify your native history file.
Optional, set before the line:
SMCLI_NO_KEYS=1: bind no keys (history is still recorded).SMCLI_EXTRA_KEYS=1: also Left and Right on an empty line, Ctrl-Right and Alt-; (Bash and Zsh). Fish keeps Right for autosuggestions.SMCLI_DB=/path/history.db: another database (it must not be your shell'sHISTFILE).
-
Open a new terminal and check:
smcli health # search index and model smcli service status # the background service (it starts on first use)
Run a few commands, then press Up on an empty prompt. Optionally, bring in existing history:
smcli import --atuin.
Key choices take effect in new shells. To change them later, run smcli init --keys (it asks) or pass a list.
- Script: run
install.shagain (from the new release, orgit pull && scripts/install.shin a checkout). - By hand, release: replace both executables together.
- By hand, source:
git pull && make fresh.
A running service from the previous build is stopped and replaced automatically on the next command. Open a new terminal so the shell reloads the new hooks; until then, inserting file paths from the picker is refused with a message asking you to reload.
smcli-uninstallIt asks once, then:
-
Exports your history to
~/smcli-export-<date>/(--export DIRto choose):history.dbis a consistent copy of the database. Use it again withSMCLI_DB=…/history.db, or open it withsqlite3.history.jsonholds every recorded command with its directory, exit code, times and session.config.jsonis your settings.
Export requires
sqlite3; install it or pass--no-exportto skip the export. The export directory is private (0700), and exported history and config files are owner-readable/writable only (0600), including existing destinations. If the export fails, nothing is removed. -
Stops the service.
-
Removes the smcli block from each startup file and leaves the rest of the file as it was. It warns about
shell-initlines you added by hand, which it doesn't touch. -
Deletes the history database, search index, settings and service socket.
-
Deletes
smcli,smcli-serviceand itself.
| Option | Meaning |
|---|---|
--export DIR |
Where the export goes |
--no-export |
Delete the history without exporting it |
--keep-data |
Remove the program and hooks only; keep history, index and settings |
--prefix DIR |
Where smcli is installed (default: the uninstaller's own directory) |
--yes |
Don't ask (required when not run from a terminal) |
Terminals that are already open keep the old hooks until they exit. Native Bash, Zsh and Fish history is never touched.
By hand: run smcli service stop, remove the shell-init line, delete the executables, then delete these:
| What | Where |
|---|---|
| Recorded history | ~/.local/share/smcli/history.db (or SMCLI_DB) |
| Search index | ~/.local/share/smcli/history.db.search/ |
| Settings | ~/.config/smcli/ |
| Service socket | $SMCLI_RUNTIME_DIR, else $XDG_RUNTIME_DIR/smcli, else $TMPDIR/smcli-<uid> |
- No picker on Up or Ctrl-R: another tool may own the key; the shell prints
smcli: kept existing key bindings: …at startup. Move the smcli line after that tool, or bind the key to_smcli_pickyourself. Also checkSMCLI_NO_KEYSisn't set and, for Bash, that it's 4.0 or later (echo $BASH_VERSION). - "files and grep need smcli-service":
smcli-serviceisn't next tosmcli, orSMCLI_SERVICE=offis set. - "not inserting a path inside an open quote": close the quote first. File paths are only inserted where the shell reads plain words.
smcli: command metadata was not saved: runSMCLI_SYNC=1in the shell to record synchronously and see the error, or check that theSMCLI_DBdirectory is writable.- Alt-; does nothing (macOS): set your terminal's Option key to send Meta/Esc+.