Skip to content
kzshPublic

About

Record shell commands into SQLite for later recall

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

10 Commits

Folders and files

Repository files navigation

recall

Records shell commands into SQLite and searches them.

Build and install

just install          # release build, then into $BIN_DIR (default ~/bin)
just --list           # all recipes

Recipes run with this directory as the working directory. The SQLite build flags live in .cargo/config.toml, and cargo finds them by walking up from the current directory, so a cargo build invoked from elsewhere produces a differently configured binary.

Database

Path resolution order:

  1. --db PATH
  2. $RECALL_DB
  3. $XDG_DATA_HOME/recall/history.db
  4. $HOME/.local/share/recall/history.db

--db is a global flag, valid before or within any subcommand. An unset or empty XDG_DATA_HOME falls through to HOME; with neither set, the command fails. Missing parent directories are created when the database is opened for writing.

recall path prints the resolved location.

This tool was previously called cli-history, and read $CLI_HISTORY_DB, $CLI_HISTORY_SESSION and .local/share/cli-history/. Nothing falls back to those names; an existing database moves once, WAL sidecars included:

data="${XDG_DATA_HOME:-$HOME/.local/share}"
mkdir -p "$data/recall"
for f in "$data"/cli-history/history.db*; do
    [ -e "$f" ] && mv "$f" "$data/recall/${f##*/}"
done
rmdir "$data/cli-history"

Connections use WAL journaling, synchronous = NORMAL, and a five second busy timeout.

Schema (user_version = 3), table commands:

column type null notes
id INTEGER no primary key, insertion order
ts_ms INTEGER no Unix epoch milliseconds, UTC
session TEXT yes from --session or $RECALL_SESSION
host TEXT yes from --host or $HOSTNAME
cwd TEXT no directory the command ran in
exit_code INTEGER yes null when the caller reported none
duration_ms INTEGER yes wall clock runtime, null when unmeasured
command TEXT no the command line as bash recorded it

Indexes: commands(ts_ms), commands(cwd), commands(session, id).

Migrations run when the database is opened for writing. v1 gains duration_ms via ALTER TABLE; v2 to v3 rebuilds the table to drop NOT NULL from exit_code and recreates the indexes, in one transaction. Rows written before an upgrade hold nulls in the added columns.

A null exit_code prints as - in human output and as an empty field in tsv. It is matched by neither --exit-code N nor --failed, because SQL comparisons against null are never true; recall sql "SELECT ... WHERE exit_code IS NULL" reaches those rows.

log

Records one command. The command text comes from trailing arguments, or from stdin when none are given.

printf '%s' "$cmd" | recall log --exit-code "$?" --cwd "$PWD" --duration-ms 251
recall log --exit-code 0 -- git status
recall log -- something-whose-status-is-unknown
flag default
--exit-code null
--cwd current directory
--session $RECALL_SESSION, else null
--host $HOSTNAME, else null
--duration-ms null

Blank or whitespace-only --session and --host values are stored as null. An empty command text is a no-op.

A command is dropped when it repeats the previous command (by id) of the same session in the same directory.

--duration-ms can be measured in bash without forking: stamp $EPOCHREALTIME (bash 5+) in PS0, which is expanded after a command line is read and before it runs, then subtract in PROMPT_COMMAND. Lines that ran no command get no duration.

search

Prints recorded commands, ordered by ts_ms descending.

recall search                       # 50 most recent
recall search cargo --since 7d      # substring match, last week
recall search --cwd . --limit 200   # one directory
recall search --failed --since 12h  # non-zero exits
recall search --exit-code 130       # interrupted commands
recall search --succeeded --unique  # deduplicated, exit 0 only
recall search --format tsv          # tab-delimited fields

Filters: positional substring query, --cwd (. resolves to the current directory, all values canonicalized), --session, --since, and one of --exit-code N, --succeeded (--exit-code 0) or --failed (non-zero). The three exit filters conflict with each other. --limit defaults to 50; --all removes the limit and conflicts with --limit.

--format:

value output
human timestamp exit duration cwd command, default
tsv timestamp \t cwd \t exit_code \t command
command the command line only

--unique collapses repeated command lines to their most recent occurrence, after the other filters. --null (-0) separates records with NUL instead of newline, for fzf --read0.

A command killed by a signal records the shell's status, so Ctrl-C appears as --exit-code 130 and is also matched by --failed.

An fzf picker over the current directory's successful commands:

recall search --format command --unique --null --succeeded --cwd . \
  | fzf --read0 +m

--since accepts relative offsets 45s, 30m, 12h, 7d, 2w, or absolute 2025-12-01 and 2025-12-01T09:30:00. Absolute values are read in the local time zone.

Durations print as 251ms, 1.25s, 3m04s, 2h07m.

sql

Runs one statement and prints tab-separated rows, streaming, so | head stops early. The statement can come from an argument or stdin.

recall sql --header "SELECT command, count(*) AS n, avg(duration_ms) AS avg_ms
                          FROM commands GROUP BY command ORDER BY n DESC LIMIT 20"
echo 'SELECT count(*) FROM commands' | recall sql
recall sql --write "DELETE FROM commands WHERE command LIKE 'kubectl%'"

The database is opened read-only unless --write is given, in which case it is opened through the normal path and migrations run first. --header prints column names as the first row. NULL renders as an empty field, blobs as x'..'. A SQL error exits non-zero with the SQLite message in the cause chain.

Shell integration

Working examples:

  • prompt.sh calls log from PROMPT_COMMAND and measures --duration-ms in PS0.
  • functions.sh wires search into the Ctrl-E, Ctrl-F and Ctrl-G fzf pickers.

Development

just check                 # fmt --check, clippy -D warnings, tests
just test-one <name>       # one test, with output
just compile-options       # SQLite options the bundled amalgamation was built with
just db-path

Release profile: opt-level = 3, fat LTO, one codegen unit, panic = "abort", strip.

.cargo/config.toml removes SQLite subsystems this tool does not call (FTS3, FTS5, R-tree, extension loading, the deprecated API, UTF-16, SOUNDEX) and sets SQLITE_UNTESTABLE. Nothing there caps row counts, table sizes, expression depth or query complexity, and SQLITE_ENABLE_STAT4 remains enabled.

Cross-compiling and publishing binaries: RELEASING.md.

About

Record shell commands into SQLite for later recall

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages