CLI reference

Engrava ships an engrava command-line tool for inspecting, querying, and maintaining a database without writing code. This page documents every command and option.

engrava [GLOBAL OPTIONS] COMMAND [ARGS]...

Global options

These apply to every command and go before the command name:

Option Values / type Default Description
--db path ./engrava.db Path to the SQLite database. Falls back to the ENGRAVA_DB env var, then the default.
--config path — Path to engrava.yaml. Falls back to the ENGRAVA_CONFIG env var.
--format table | json | csv table Output format for commands that print records.
--verbose flag off Emit DEBUG logs from Engrava modules to stderr for this invocation. Command data on stdout keeps its selected format.
--no-extensions flag off Prevent loading both engrava.cli and engrava.extensions entry points. ENGRAVA_DISABLE_EXTENSIONS=1 provides the same control.
--help flag — Show help and exit (works on the root and on every command).

Environment variables. ENGRAVA_DB and ENGRAVA_CONFIG are CLI fallbacks for --db and --config respectively; the explicit flag always wins (--db > ENGRAVA_DB > ./engrava.db). ENGRAVA_DISABLE_EXTENSIONS=1 activates --no-extensions before command resolution, including for root help and built-in commands.

export ENGRAVA_DB=/data/engrava.db
engrava info                       # uses /data/engrava.db
engrava --db other.db info         # flag overrides the env var

Commands

Command Purpose
info Show a metrics snapshot for the database.
verify Verify the audit journal’s hash chain.
query Run a MindQL query.
remember Store a thought in one call and print its id.
recall Search for thoughts relevant to a query and print ranked results.
link Create a typed edge between two existing thoughts.
snapshot Export the database’s core tables to a JSONL snapshot.
restore Restore a database from a JSONL snapshot.
gc Garbage-collect archived thoughts (and optionally expired ones).
migrate Run pending schema migrations.
export Export thoughts to a portable JSON file.

Extension discovery

The executable discovers installed CLI extensions lazily. Built-in commands are resolved without scanning engrava.cli. Root help scans that entry-point group so installed commands appear in the command list, and an otherwise unknown command triggers the scan so an installed extension command can resolve.

The built-in query command performs a second scan of engrava.extensions. It loads each discovered manifest and registers its mindql_extensions for that query. This query-time scan does not apply manifest schema migrations.

Both scans import and may execute code from installed packages. Failed entry points are skipped with a warning log. Put --no-extensions before the command, or set ENGRAVA_DISABLE_EXTENSIONS=1, to prevent both entry-point groups from being scanned or loaded:

engrava --no-extensions --help
ENGRAVA_DISABLE_EXTENSIONS=1 engrava --db memory.db info
engrava --no-extensions --db memory.db query "SELECT thought_id FROM thought LIMIT 5"

This control is specific to automatic CLI entry-point loading. It does not disable explicit manifest paths or library-side manifests.discover settings used by an application-created store. Continue to install only trusted packages; see Security and Trust Boundaries for the full threat model.

--verbose enables DEBUG logging for the engrava logger hierarchy for the duration of the command. Logs go to stderr, leaving JSON/CSV/table command data on stdout.

Service resolution

The --service option on snapshot and restore resolves to the same directory in both commands; whether the target database must already exist does not:

--service Services config loaded? Result
--service NAME (explicit) either Targets service NAME in the services data_dir if a config is loaded, otherwise in the parent directory of --db (i.e. <parent-of-db>/NAME.db). restore creates that database there if it does not already exist; snapshot requires it to already exist and exits if it is not found.
omitted yes Falls back to services.default_service.
omitted no Operates on the single --db database (not service mode).

In short: an explicit --service works even without a services config (using --db’s directory as the data directory), while omitting it only enters multi-service mode when a services config is present.

Schema-version checks

info, verify, query, snapshot, restore, gc, export, recall, remember, and link check the database’s schema version before they act, and the kinds of command are held to different rules:

Kind Commands Schema below head Schema above head
Destructive gc, restore Refuses, exit 1, names engrava migrate Refuses, exit 1
Read info, verify, export, snapshot, recall (with --db or the default path), a query parsing as FIND/COUNT/SELECT Warns on stderr, then runs anyway Refuses, exit 1

Under --config, recall refuses on a schema below head as well as above it, because a configured store applies pending core migrations as it opens and a read must not. Once the core schema is at head, a recall whose target resolves from --config also applies pending migrations for the extensions listed under manifests.

remember and link do not migrate the core schema of a database that already exists. Below head they refuse, exit 1, naming engrava migrate; above head they refuse the way the commands above do. A path that does not exist yet is created at head, and so is an empty file (no bytes, no schema).

A destructive command refuses below head because deleting rows through an engine that does not understand the schema it is deleting from is how a deleted thought’s vector came back on an unmigrated 0.6 database (see Known Limitations). Apart from recall under --config, a read command runs anyway below head, but is never silent about the gap. query is classified by the parsed command, not by the fact that it was typed as query: a registered extension command that writes is refused like a destructive one. migrate sits outside every row — running (or refusing) pending migrations is its entire job.

A refusal is a plain message on stderr, exit 1, and nothing on stdout — also under --json. It is not an engrava.cli.error.v1 object.

$ engrava --db old.db gc
Database schema is at version 11; this engrava build's head version is 21. Run 'engrava migrate' before running 'gc' on it.
$ echo $?
1

Unreadable or corrupt databases

A database file that exists but is not a valid SQLite database (truncated, corrupted, or a plain text file) now makes every built-in command exit 1 with a message naming the configured path, instead of hanging or raising a raw traceback — except snapshot --service / restore --service, which resolve a database through EngravaManager instead of a single file, so the message names no path:

$ engrava --db corrupt.sqlite info
info: corrupt.sqlite: unexpected DatabaseError: file is not a database
$ echo $?
1

Rerun with --verbose to log the caught exception’s stack (frame filename, line, and function only, not a full traceback) at DEBUG for a bug report.

Command Reference

info

Shows a metrics snapshot (counts, etc.) for the current database. Takes no command-specific options.

engrava --db engrava.db info

--format json info reports two separate version numbers: metrics_schema_version (the shape of the EngravaMetrics object itself) and database_schema_version (the database’s own schema version, the one Schema-version checks above gates on) — replacing the single schema_version key a parser built against an earlier release may still read.

Use this after an upgrade or a restore to confirm the database is readable and the counts look right.

verify

Verifies the audit journal’s hash chain. It walks every recorded journal_entry in sequence order, recomputes each SHA-256 hash, and checks the parent-hash linkage. Takes no command-specific options. The chain is verified regardless of whether journaling is currently enabled, so a journal recorded in an earlier session is still auditable.

engrava --db engrava.db verify
# Journal integrity OK — 128 entries verified.

engrava --db engrava.db --format json verify
# {"valid": true, "entries_checked": 128, ...}

The exit code is 0 when the chain verifies, 1 when it does not (the text output names the first broken sequence, and the JSON output carries first_invalid_sequence / error_message) or when the database is missing — so it drops straight into a CI job, a pre-backup hook, or a monitoring check. An empty or absent journal verifies as valid with entries_checked: 0.

Read the Security model first: this is a keyless in-file chain, so it detects covered-field changes and broken linkage among retained rows — including an unrecomputed middle deletion or reordering — not deletion of a self-consistent suffix, or a chain-aware actor who rewrites the whole .db.

query

Executes a MindQL query and prints the results in the chosen --format.

engrava query "MQL"

The MQL string is a positional argument. It accepts FIND, COUNT, SELECT, or registered extension commands:

engrava query "FIND thoughts WHERE lifecycle_status = 'ACTIVE'"
engrava query "COUNT thoughts WHERE priority = 'P1'"
engrava --format json query "SELECT thought_id, essence FROM thought LIMIT 5"
engrava query "FIND thoughts WHERE valid_now"          # only currently-valid facts

The bi-temporal valid_now, valid_at, valid_within, and valid_between predicates work here too — see MindQL for their full semantics.

remember, recall, and link are one-shot verbs: store or search a thought, or create an edge, in a single invocation, without writing a Python script. None of the three takes a --service option, so they never read services.default_service — that config section is consulted only by snapshot / restore. Instead each resolves the database it acts on through its own two-tier precedence, highest first:

  1. A non-empty --db (or ENGRAVA_DB). This wins outright — a --config file is never even read.
  2. Otherwise, a --config file’s own database.path, loaded the same way SqliteEngravaCore.from_config() loads it, so a configured embeddings, search, and journal section all apply. A named --config file that does not exist or fails to parse is always an error (exit 2), never silently skipped.
  3. Otherwise, the CLI’s own default (./engrava.db).

--verbose reports which tier fired and the resolved path.

Exit codes, consistent across all three commands: 0 success; 1 a database refused for its schema version (see Schema-version checks), or an unanticipated failure (a corrupt database, an unreadable --config, …); 2 a usage or validation error (an unknown edge type, empty text, a malformed --meta / --filter token, an out-of-range --top-k / --weight, or an invalid --config); 3 the resolved database does not exist (recall only — remember / link create it instead); 4 (link only) a FROM or TO thought id that does not reference an existing thought.

--json on any of the three emits one JSON object instead of a bare id or a formatted table: to stdout on success (schema engrava.cli.<command>.v1), or to stderr on a failure the command body itself catches (schema always engrava.cli.error.v1, with an error kind and message). A failure Click’s own argument parser catches first — an invalid --type choice, a missing required argument — prints plain usage text and exits 2 regardless of --json, since that happens before the command body (and --json itself) is ever read. A schema-version refusal is not covered either: it is a plain message on stderr (see Schema-version checks). A close failure after an otherwise-successful body still emits a second, error object to stderr and exits 1, so a caller should treat only the first object on each stream as authoritative.

remember

Stores TEXT as a thought and prints its id. Built over create_thought() with an explicitly constructed ThoughtRecord — not the library’s remember() shorthand, which always produces a NOTE / P3 thought and so cannot honour --type / --priority. Creates the resolved database (and its parent directory) if it does not already exist, printing the path to stderr. The core schema of a database that already exists is never migrated by remember or link; see Schema-version checks.

Option Type Default Description
TEXT positional required Content to store. - reads it from stdin instead.
--type thought type NOTE One of TASK, OBSERVATION, BELIEF, REFLECTION, OUTPUT_DRAFT, NOTE.
--priority priority P3 One of P1, P2, P3, P4 (P1 highest).
--meta KEY=VALUE — Metadata entry (repeatable).
--dedup flag off On matching content_hash with an existing row, bump confirmation_count and print that row’s id instead of inserting a duplicate — editing a thought’s content does not recompute its hash, so this can match content the row no longer holds.
--json flag off Emit a JSON object instead of a bare id.
engrava --db my.db remember "User prefers concise answers"
engrava --db my.db remember "Escalate the outage" --type REFLECTION --priority P1
echo "piped content" | engrava --db my.db remember -
engrava --db my.db remember "same content twice" --dedup   # run again: same id, no new row
{"schema": "engrava.cli.remember.v1", "thought_id": "091aa106-fcc0-45a3-a19b-d335ad05ea45", "deduplicated": false}

deduplicated is true only when --dedup was given and it matched an existing thought — the printed thought_id is then that existing thought’s, not a new row’s.

recall

Searches for thoughts relevant to QUERY and prints ranked results. Calls the library recall() directly, so a configured embedding provider and hybrid-search weights apply exactly as they would to a direct library call — unlike a bare connection, which has no embedding provider and so no vector arm. This is not a substitute for query: query runs structural MindQL with no ranking and no embedding provider of its own. Unlike remember / link, recall never creates a database — against one that does not exist, it exits 3 naming the resolved path rather than reporting zero hits.

Option Type Default Description
QUERY positional required Natural-language text to search for.
--top-k int 10 Maximum results to return.
--filter KEY=VALUE — KEY is a metadata filter path (a dotted key such as a.b addresses a nested field); equality only; repeatable — different keys are AND-combined, a repeated key keeps its last value.
--json flag off Emit a JSON object instead of a formatted table.
engrava --db my.db recall "concise answers"
engrava --db my.db recall "tagged" --filter topic=weather --top-k 5

Three checks run in this order. Token-shape checks run before the database is resolved: a token missing =, or with an empty KEY, is malformed_filter, exit 2. Next, a database that does not exist exits 3. Once the database exists, a non-empty KEY that is not a valid metadata filter path is malformed_filter, exit 2 too, with the offending KEY=VALUE token named in the message.

{"schema": "engrava.cli.recall.v1", "query": "concise answers", "top_k": 10, "backends_used": ["fts5", "priority", "vector"], "results": [{"thought_id": "091aa106-fcc0-45a3-a19b-d335ad05ea45", "score": 0.4714285714285715, "essence": "User prefers concise answers"}]}

backends_used reports FTS5, vector, and recency when those signals are active, even if they score no candidates; priority is reported only when candidates exist, and graph-derived entries only when they produce boosts or additions. "vector" appears only when a configured embedding provider produced a query vector.

Creates a typed edge from FROM to TO and prints its id. Builds an EdgeRecord and calls the public create_edge(). Creates the resolved database (and its parent directory) if it does not already exist.

Option Type Default Description
FROM positional required Source thought id.
TO positional required Target thought id.
--type edge type required One of ASSOCIATED, DEPENDS_ON, DERIVED_FROM, MESSAGE_OF, BRIDGE, CONSOLIDATED_FROM, CONTESTED_BY.
--weight float 1.0 Relation strength, 0.0-1.0.
--json flag off Emit a JSON object instead of a bare id.
engrava --db my.db link 091aa106-fcc0-45a3-a19b-d335ad05ea45 f4620859-3dfa-4f13-8d2e-d35df62dbba4 --type ASSOCIATED --weight 0.8

An unknown --type exits 2 naming the value and every valid member; a FROM or TO that does not resolve to an existing thought exits 4, naming which one:

$ engrava --db my.db link ghost-id some-real-id --type ASSOCIATED
link: from_thought_id 'ghost-id' does not reference an existing thought.
$ echo $?
4
{"schema": "engrava.cli.link.v1", "edge_id": "b190dc41-9c87-4a66-9291-d70974fd2342", "from_thought_id": "091aa106-fcc0-45a3-a19b-d335ad05ea45", "to_thought_id": "f4620859-3dfa-4f13-8d2e-d35df62dbba4", "edge_type": "ASSOCIATED", "weight": 0.8}

snapshot

Exports the database’s core tables to a JSONL snapshot (one record per line) — see below for exactly what a snapshot includes and excludes.

Option Type Default Description
-o, --output path derived (see below) Output JSONL file path.
--service name see below The service to snapshot (multi-service mode only).

Default output path depends on the mode:

  • Single database: <db-stem>.snapshot.jsonl next to the database — e.g. --db engrava.db → engrava.snapshot.jsonl (the .db suffix is replaced).
  • Multi-service: <data_dir>/<service>.snapshot.jsonl.

--service resolves in three ways (see Service resolution):

  • Explicit --service NAME targets that service even with no services config — the service database must already exist in the data directory, which is the services config’s data_dir if one is loaded, otherwise the parent directory of --db; snapshot exits if it is not found there.
  • Omitted, with a services config loaded → falls back to services.default_service.
  • Omitted, with no services config → snapshots the single --db database.
engrava --db engrava.db snapshot -o backup.jsonl
engrava --db engrava.db snapshot               # -> engrava.snapshot.jsonl
engrava --db /data/engrava.db snapshot --service tenant_a   # -> /data/tenant_a.snapshot.jsonl
engrava --config engrava.yaml snapshot --service tenant_a   # data_dir from config

A snapshot exports every column of the thought, edge, embedding, and action records — including the bi-temporal valid_from / valid_until fields — but not the audit journal (journal_entry) or the extension-manifest table (extension_schema_versions). See Backup & Recovery for what this means and when to use a physical file backup instead.

Failure safety. snapshot writes to a temporary file next to -o and replaces it only once every row is read and the export’s own read transaction has closed, so an existing file at -o is left byte-identical on an exception or a cancelled run rather than replaced by a truncated one. -o may not name the database currently open for --db, or that database’s -wal / -shm companion files — pointing it there is refused before anything is written.

restore

Restores a database from a JSONL snapshot produced by snapshot.

Option Type Default Description
-i, --input path required JSONL snapshot file to restore.
--clear flag off Replace the four core snapshot tables and the audit journal (and reset a persisted sqlite-vec index) before restoring — not the whole target database.
--clear-identity flag off Also clear the target’s stored embedding identity (model name, dimension, prefix metadata). Requires --clear; recovers a target whose stored embedding_dimension is corrupt — --clear alone preserves an existing identity, corrupt or not.
--skip-embeddings flag off Import without embedding records.
--re-embed flag off Re-embed all thoughts via the target provider, ignoring source embeddings. Requires --config with top-level or per-service embeddings.
--orphan-journal-entries flag off Allow a merge restore (no --clear) to replace a row that collides with the target’s journal, even though the journal will then describe data the merge discarded. Without it, such a collision is refused.
--service name see below The service to restore into.

For any restore --clear, an existing sqlite-vec table is dropped in the same transaction as the canonical rows. The next sqlite-vec-enabled open recreates and backfills it. Because SQLite must load the virtual-table module before it can remove that table safely, install engrava[vec] before clearing a database that already contains a persisted sqlite-vec index.

--service resolves exactly as for snapshot: an explicit --service NAME targets that service even without a services config (its database resolves in the services data_dir, or the parent directory of --db when no config is loaded); omitted with a services config falls back to services.default_service; omitted with no services config restores into the single --db database.

Thought and edge timestamps are written in the canonical UTC form Engrava stores them in (a value without an offset is read as UTC), so a snapshot taken by an earlier version, which may hold another ISO-8601 form, restores with its timestamps compared by instant. A value that cannot be read as an ISO-8601 instant is restored as it was.

--skip-embeddings and --re-embed are mutually exclusive — passing both fails with:

Error: --re-embed and --skip-embeddings are mutually exclusive.

Use --re-embed when the target should use a different embedding model than the snapshot. In single-database mode, the provider comes from the top-level embeddings section of the file passed with --config. In service mode, services.configs.<name>.embeddings takes precedence; when no override exists, the same top-level embeddings section is the fallback. Restore discards source embedding rows and generates a replacement vector with the resolved provider for each restored thought. A target that already contains embeddings requires --clear; without it, restore refuses to relabel vectors that are not part of the snapshot. The sqlite-vec reset described above prevents stale index rows from surviving the re-embed.

If neither level declares a provider, restore fails before importing records and names the missing configuration. An explicit --service without a config-backed provider fails for the same reason. Use --skip-embeddings to import text without vectors, or restore normally to retain the snapshot vectors.

engrava --db fresh.db restore -i backup.jsonl
engrava --db fresh.db restore -i backup.jsonl --clear --skip-embeddings
engrava --db fresh.db --config engrava.yaml restore -i backup.jsonl --clear --re-embed
engrava --config engrava.yaml restore -i backup.jsonl --service main --clear --re-embed

When services.default_service names the configured target, --service main may be omitted. A restore that reaches its insert phase, whether or not --re-embed or --skip-embeddings is given, first checks that whatever the target holds at that point in the restore is internally consistent: its stored embedding-model lock, if one survives (a --clear restore preserves the lock even though it wipes the rows), or its existing embedding rows when there is no lock and nothing was cleared. A dreaming-centroid row is compared on dimension only, never on model_name. A normal restore (neither flag given) additionally checks every embedding row the snapshot declares against that same reference before inserting it, with the same centroid exemption — never against a configured provider, which a plain restore never resolves. On a mismatch, restore fails before committing anything, naming both identities; choose --re-embed or --skip-embeddings as above. Restoring into a target that, when the inserts begin, holds neither a stored model nor any non-centroid embeddings writes the identity established by the snapshot’s own non-centroid embedding rows as the target’s new lock — nothing is adopted if the snapshot inserts no non-centroid embedding row. This check cannot see the document or query prefix the corpus was built with — see Embeddings → Asymmetric prefixes for instruction-tuned models.

Restore recreates thoughts, edges, embeddings, and actions, not the audit journal. A fresh target starts with an empty journal, and --clear empties the journal along with the data it wipes. A restore without --clear merges into the target instead — and if that target’s journal is non-empty, it refuses any record that collides with an existing row (primary key or UNIQUE constraint) and rolls the whole restore back, rather than letting the merge silently replace it. --orphan-journal-entries allows the merge anyway, restoring the pre-gate behaviour where a collision can orphan journal entries and verify still reports the chain as valid. The gate never fires against an empty journal — the ordinary case, since journaling is opt-in. See Audit Trail and Backup & Recovery for the full mechanism.

gc

Garbage-collects ARCHIVED thoughts together with every edge touching one on either end — including edges whose other end is still live — their embeddings and the actions sourced from them, then reconciles the vector index by removing every vec0 row whose embedding row is missing or no longer joins to a live thought. With --expired it also runs the TTL expiry cleanup first.

gc keeps pinned and protected-priority rows by default. An ARCHIVED thought that is pinned, or at a priority in the hygiene policy’s protected set (P1 unless --config names an engrava.yaml whose hygiene_policy: sets protected_priorities), survives gc, and the run reports how many it kept. Pass --include-pinned to delete them too. The TTL pass of gc --expired follows its own rule: it never expires a pinned row, whatever the flag, but a protected-priority row whose TTL has passed still expires — see Data Lifecycle → Time-to-live. When --config names an engrava.yaml with journal.enabled: true, each row gc deletes gets a DELETE_THOUGHT journal entry in the same transaction; without it, nothing is journaled.

gc refuses on a database that is not on this build’s head schema. If --db is below or above head, gc deletes nothing and exits 1 naming engrava migrate — see Schema-version checks above. If a gc that used to run cleanly now exits non-zero, that is this refusal working as intended, not a regression in what it collects: run engrava migrate first.

Option Type Default Description
--dry-run flag off Preview without changing anything; under the archive strategy the preview can list more than the real run does — see below.
--expired flag off Also run expiry cleanup (archive or delete per ttl.strategy) before collecting.
--include-pinned flag off Also delete pinned and protected-priority archived thoughts, which are kept otherwise.
engrava --db engrava.db gc                 # delete ARCHIVED thoughts + their edges/embeddings/actions
engrava --db engrava.db gc --include-pinned  # also delete pinned / protected-priority rows
engrava --db engrava.db gc --expired       # run expiry cleanup first (per strategy)
engrava --db engrava.db gc --expired --dry-run

The behaviour of gc --expired depends on ttl.strategy: with delete it removes expired rows and then collects pre-existing archived rows; with the default archive it archives the expired rows and stops only if it archived at least one — collecting the rows it just archived would defeat the soft-retire. With no expired rows to archive it falls through to collecting pre-existing ARCHIVED rows. See Data Lifecycle for how that cleanup pass runs — including how --dry-run can preview both stages at once under the archive strategy, which a real run under the same starting conditions would not reach until its next invocation.

gc refuses to delete on a vec0-indexed store without the vector extra. If the database carries an embedding_vec table and sqlite-vec cannot be loaded — most commonly because engrava[vec] is not installed, though an unsupported build, an OS error or a SQLite error fail the same way — a pass that is about to physically delete stops before deleting anything and exits 1 with:

Error: This database has a sqlite-vec index, and collecting thoughts without removing their vectors would strand them in it. Install 'engrava[vec]' and retry.

Removing the rows without removing their vectors would strand those vectors in an index nothing can then reach them through. Only deleting passes are refused: --dry-run is never refused, and neither is a run with nothing to delete. Read the archive strategy carefully, though — gc --expired under the default ttl.strategy: archive stops after archiving only when it actually archived something. With no expired rows to archive it falls through to the archived-collection pass, which is refused when there are archived rows to collect. Install the extra (pip install 'engrava[vec]') and retry.

migrate

Runs pending schema migrations (ensures the core tables exist and are up to date). Takes no command-specific options. Safe to run after an upgrade.

engrava --db engrava.db migrate

export

Exports thoughts to a portable JSON file (with edges and metadata). Unlike snapshot (JSONL, core tables, for backup/restore), export writes a single indented JSON document and can be filtered by lifecycle status.

Option Type Default Description
-o, --output path <db-stem>.export.json (derived) Output JSON file path.
--status lifecycle status all Only export thoughts with this lifecycle_status (e.g. ACTIVE).
engrava --db engrava.db export -o thoughts.json
engrava --db engrava.db export --status ACTIVE    # -> engrava.export.json

Failure safety. Like snapshot, export reads from one consistent point in time and writes to a temporary file next to -o, replacing it only once every read has completed — an existing file at -o is left byte-identical on an exception or a cancelled run. -o may not name the database currently open for --db, or its -wal / -shm companion files.

See Verifying integrity for the equivalent Python API, store.verify_journal().

For the exhaustive package reference, see the OSS cli.md docs.