Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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