localdb CLI reference

localdb is a local-first hybrid-search document index. This page is the complete reference for its command-line interface.

For design decisions and process-model details see specs/05-surfaces.md. For the HTTP daemon surface see docs/http-api.md. For the MCP stdio surface see docs/mcp.md.


Global flags

These flags are accepted by every subcommand.

Flag Description
--config <PATH> Path to the config file. Default: the platform config dir — ~/Library/Application Support/localdb/config.yaml on macOS, ~/.config/localdb/config.yaml on Linux. Can also be set via the LOCALDB_CONFIG environment variable.
--json Emit machine-readable JSON instead of human-readable text. All JSON shapes are stable API.
-s, --store <NAME> Narrow to these stores; repeatable. It is a filter, so omitting it means all stores for search, status, store list, source list, source remove <ULID>, document list, document get, index and mcp. Three exceptions: source add (and the add alias) defaults to the store named default, exit 2 if absent; source remove <path\|url> requires it, exit 2 without it; and init, serve, store add, store remove, db status/migrate/downgrade/vacuum reject it outright (exit 2) because they aren’t store-scoped. An explicit name is always validated — unknown is exit 3, never silently ignored. document get’s omitted case can additionally be invalid_request (exit 2) if the id exists in more than one store — see below. See specs/05-surfaces.md §2.2.
-y, --yes Skip confirmation prompts for destructive operations (db migrate legacy rebuild, db downgrade).
-h, --help Print help.
-V, --version Print version.

Environment variable: LOCALDB_CONFIG=<path> is equivalent to --config <path>.


Exit codes

Exit codes are stable API. See specs/05-surfaces.md §5 for the full error taxonomy that drives them.

Code Meaning Example trigger
0 OK Successful command
1 Internal error Bug or unrecoverable runtime failure
2 Invalid usage or config Unknown subcommand, duplicate store, bad config file
3 Not found store remove <name> — store does not exist
4 Conflict / locked serve when a daemon is already running on the same data dir
5 Unavailable Daemon unreachable (stale socket)

localdb init

Optional bootstrap — never a prerequisite. Every other command except db status/migrate/downgrade/vacuum scaffolds the config file and data/models/logs directories implicitly on first use, so you never have to run init before store add, source add, index, or search. Run it if you’d rather do that setup explicitly up front: it prints every resolved path, and --download-model lets you pull the embedding model ahead of time instead of deferring it to the first indexing or search operation (including source add’s auto-index).

Optional bootstrap: write the config, create the data/models/logs directories, and print the resolved paths

Usage: localdb init [OPTIONS]

Options:
      --config <PATH>   Path to config file (default: platform data dir / localdb / config.yaml)
      --download-model  Prepare the configured embedder now, downloading a local model up front instead of on the first `index`/`search`
      --json            Emit JSON output instead of human-readable text
  -s, --store <NAME>    Operate on these stores (repeatable); a filter, not a selector
  -y, --yes             Skip confirmation prompts for destructive operations
  -h, --help            Print help (see more with '--help')
  -V, --version         Print version

Writes the config file (if it doesn’t already exist) and creates the data/models/logs directories, then prints all four resolved paths. The generated config file is the full commented template with every key at its default value, not a bare stub — see configuration.md#config-is-created-for-you. It also creates a store named default, unless the database can’t be opened (see below).

--download-model: prepares the configured embedder immediately. For the default local provider this downloads the ~706 MB model (pplx-embed-context-v1-0.6b, from HuggingFace, no API key or license click-through required) right away instead of deferring it to the first indexing or search operation (including source add’s auto-index). For a hosted provider (openai-compatible, perplexity, voyage) it just validates that the client can be constructed (e.g. that an API key is present). When this flag succeeds, init omits the “downloads its embedding model on first index” note from its output, since it’s no longer true.

If the database can’t be opened — most commonly because it needs a schema migration — init prints a Warning: ... on stderr and still exits 0. It still writes the config and creates the directories; it just skips creating the default store. For example:

Warning: invalid config: database schema version 5 is behind this build (v6); run 'localdb db migrate' to apply pending migrations

Not store-scoped: init runs before any store exists — the only store it creates is default, which --store cannot rename or redirect — so passing --store exits 2 rather than being silently ignored. The check runs first, so a misused flag creates no directories and writes no config.

Example (healthy run):

$ localdb init
Initialized localdb at ~/Library/Application Support/localdb
  Config: ~/Library/Application Support/localdb/config.yaml
  Data:   ~/Library/Application Support/localdb/data
  Models: ~/Library/Caches/localdb/models
  Logs:   ~/Library/Logs/localdb

Note: the default 'local' provider downloads its embedding model on first index.
      Hosted providers (openai-compatible, perplexity, voyage) require an API key in config.
Run `localdb store add <name>` to create a store.

(The local-model note is omitted when --download-model succeeded; the Run localdb store add line is omitted when the default store was skipped because the database couldn’t be opened. Paths shown are the macOS defaults.)

--json output:

{
  "status": "ok",
  "config_path": "…",
  "data_dir": "…",
  "models_dir": "…",
  "logs_dir": "…",
  "default_store": "ok",
  "model_download": "skipped",
  "warnings": []
}

default_store and model_download are each "ok" or "skipped".


localdb status

Show stores, document/chunk counts, and daemon state.

Show stores, counts, policy staleness, and daemon state

Usage: localdb status [OPTIONS]

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

Examples (a scratch database with two stores, books and notes, each already indexed):

$ localdb status
daemon: not running (embedded mode)
stores (2):
  books [libsql] 1 documents, 1 chunks
  notes [libsql] 2 documents, 2 chunks

database: /home/user/localdb/data/localdb.db
  size: 164.0 KB (+ 0 B WAL)
  ~54.7 KB per chunk (3 chunks total)
  largest tables:
    chunks_vec_idx_shadow — 32.0 KB
    sources — 24.0 KB
    resources — 16.0 KB
    chunks — 16.0 KB
    stores — 12.0 KB
$ localdb status --json
{
  "daemon": "not running (embedded mode)",
  "stores": [
    {
      "name": "books",
      "visibility": "private",
      "backend": "libsql",
      "document_count": 1,
      "chunk_count": 1
    },
    {
      "name": "notes",
      "visibility": "private",
      "backend": "libsql",
      "document_count": 2,
      "chunk_count": 2
    }
  ],
  "database": {
    "path": "/home/user/localdb/data/localdb.db",
    "exists": true,
    "size_bytes": 167936,
    "wal_size_bytes": 0,
    "total_size_bytes": 167936,
    "bytes_per_chunk": 55978,
    "largest_tables": [
      {
        "name": "chunks_vec_idx_shadow",
        "bytes": 32768
      },
      {
        "name": "sources",
        "bytes": 24576
      },
      {
        "name": "resources",
        "bytes": 16384
      },
      {
        "name": "chunks",
        "bytes": 16384
      },
      {
        "name": "stores",
        "bytes": 12288
      }
    ]
  }
}

(path shown from a scratch run; document_count/chunk_count appear per store, and the database block reports on-disk size and a breakdown of the largest tables — useful for deciding whether localdb db vacuum is worth running.)


localdb store

Manage stores.

Manage stores

Usage: localdb store [OPTIONS] <COMMAND>

Commands:
  add     Add a new store
  list    List all stores
  remove  Remove a store
  help    Print this message or the help of the given subcommand(s)

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

localdb store add

Add a new store

Usage: localdb store add [OPTIONS] <NAME>

Arguments:
  <NAME>  Store name

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

Creates a store backed by libsql. Stores are persisted in the unified database (<data_dir>/localdb.db) and survive restarts.

Not store-scoped: the store is named by the <NAME> argument, so passing --store exits 2 rather than being silently ignored.

Exits 2 (invalid_request) if a store with that name already exists:

$ localdb store add notes
Added store: notes

$ localdb store add notes
error: invalid request: store 'notes' already exists
exit: 2

localdb store list

List all stores

Usage: localdb store list [OPTIONS]

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

Lists stores created with store add.

$ localdb store list
notes [libsql]

$ localdb store list --json
{
  "stores": [
    {
      "backend": "libsql",
      "name": "notes",
      "visibility": "private"
    }
  ]
}

localdb store remove

Remove a store

Usage: localdb store remove [OPTIONS] <NAME>

Arguments:
  <NAME>  Store name or ID

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

Exits 3 (store_not_found) if the name does not match any known store:

$ localdb store remove nope
error: store not found: nope
exit: 3

Not store-scoped: the store is named by the <NAME> argument, so passing --store exits 2 rather than being silently ignored. This is checked before the confirmation prompt, so a misused flag never gets as far as asking you to confirm a deletion.


localdb source

Manage sources on a store. With --store omitted, list and remove <ULID> span every store — -s is a filter. add is the exception: a write has to land in one named place, so it targets the store named default and exits 2 if there isn’t one. remove <path|url> is the other: the same path can be a source in several stores, so it requires an explicit --store (specs/05-surfaces.md §2.2).

Manage sources on a store

Usage: localdb source [OPTIONS] <COMMAND>

Commands:
  add     Add a new source to a store
  list    List sources across stores
  remove  Remove a source from a store
  help    Print this message or the help of the given subcommand(s)

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

localdb source add

Add a new source to a store

Usage: localdb source add [OPTIONS] <SOURCES>...

Arguments:
  <SOURCES>...  Source paths or URLs (one or more)

Options:
      --config <PATH>          Path to config file (default: platform data dir / localdb / config.yaml)
      --refresh <REFRESH>      Refresh interval for URL and feed sources (e.g. "1h", "30m", "3600")
      --json                   Emit JSON output instead of human-readable text
      --kind <KIND>            Override source-kind classification instead of inferring it from the argument (path vs. `http(s)://` URL). `feed` treats the argument as an Atom/RSS feed URL, which fetches every entry page at index time — pass `--max-entries` to bound that [possible values: path, url, feed]
      --max-entries <N>        Cap on feed entries considered per indexing run (feed sources only)
  -s, --store <NAME>           Operate on these stores (repeatable); a filter, not a selector
      --no-fetch-full-content  For feed sources, index only the feed-supplied summary instead of fetching each entry's full page content (feed sources only)
  -y, --yes                    Skip confirmation prompts for destructive operations
  -h, --help                   Print help (see more with '--help')
  -V, --version                Print version

Registers one or more filesystem paths, URLs, or (with --kind feed) Atom/RSS feed URLs as sources for a store. --store is repeatable; omit it and the source is added to the store named default (exit 2 if no such store exists) — it is never guessed from whatever stores happen to exist (specs/05-surfaces.md §2.2).

--kind: by default the source kind is inferred from the argument (a filesystem path vs. an http(s):// URL). Pass --kind feed to treat the argument as an Atom/RSS feed instead of a plain URL source — indexing then fetches every entry’s full page content by default (bound the number of entries considered with --max-entries, or index only the feed-supplied summaries with --no-fetch-full-content).

This is the one command where omitting --store narrows rather than spans. Everything else treats -s as a filter over all stores; a write can’t, because “add this source to every store” is not what anyone means.

Note: path existence is validated at registration time — source add /does/not/exist fails immediately with invalid request: path '/does/not/exist' does not exist (exit 2), and the source is never added.

$ localdb source add ~/notes --store notes
Added source 01KTVH6AY4DC84HWW7M2PP4F0X to store 'notes'
Auto-indexing source 01KTVH6AY4DC84HWW7M2PP4F0X ...
Indexing /home/user/notes
  discovered 1 files
  indexed 1 docs, 0 skipped, 0 deleted, 2 chunks

localdb source list

List sources across stores

Usage: localdb source list [OPTIONS]

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

Omit --store and this lists every store’s sources; pass --store (repeatable) to narrow to one or more specific stores. A store-name column appears in the output only when more than one store is in scope (specs/05-surfaces.md §2.2), so a single-store database and an explicit -s <one-store> both keep the original column-free format.

$ localdb source list                     # no --store: every store
books    01KWEZN72MJ4T8Q1V3XA9BCDEF [path] /Volumes/Archive/books
default  01KTVH6AY4DC84HWW7M2PP4F0X [path] /home/user/notes
hydra    01KWEXGA9YR5S2P7N4MB6GHIJK [path] /home/user/hydra-docs

$ localdb source list --store notes
01KTVH6AY4DC84HWW7M2PP4F0X [path] /home/user/notes

$ localdb source list --store notes --json
{
  "sources": [
    {
      "id": "01KTVH6AY4DC84HWW7M2PP4F0X",
      "kind": "path",
      "preset": "prose",
      "root": "/home/user/notes",
      "store": {
        "name": "notes"
      },
      "store_id": "01KTVGQ62TQN8X6XN9E5FDZN67",
      "url": null
    }
  ]
}

(paths shown from a scratch run)

localdb source remove

Remove a source from a store

Usage: localdb source remove [OPTIONS] <IDS>...

Arguments:
  <IDS>...  Source IDs, paths, or URLs (one or more)

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

A <ID> may be the ULID shown by source list, or a source’s path/URL. The two shapes have different --store rules, because they differ in whether they identify a store on their own (specs/05-surfaces.md §2.2):

Argument --store omitted
ULID Searches every store — a ULID is globally unique, so its owning store is not in question
path or URL Exit 2, asking for --store — the same path can be registered in several stores at once
$ localdb source remove 01KWEZN72MJ4T8Q1V3XA9BCDEF   # found wherever it lives
Removed source: 01KWEZN72MJ4T8Q1V3XA9BCDEF

$ localdb source remove ~/notes
error: source remove by path/url requires --store; pass --store <name> or use the source ULID
exit: 2

An explicit --store still hard-filters a ULID removal: if the source exists but lives outside the named scope, this is source_not_found (exit 3) rather than a silent redirect to its real store.


localdb add

Alias for localdb source add — same arguments, same options, same store-scoping rule (defaults to the store named default, exit 2 if it doesn’t exist). See localdb source add above for the full reference; this section only covers what’s different.

Alias for `source add`: add one or more sources to a store

Usage: localdb add [OPTIONS] <SOURCES>...

Arguments:
  <SOURCES>...  Source paths or URLs (one or more)

Options:
      --config <PATH>          Path to config file (default: platform data dir / localdb / config.yaml)
      --refresh <REFRESH>      Refresh interval for URL and feed sources (e.g. "1h", "30m", "3600")
      --json                   Emit JSON output instead of human-readable text
      --kind <KIND>            Override source-kind classification instead of inferring it from the argument (path vs. `http(s)://` URL). `feed` treats the argument as an Atom/RSS feed URL, which fetches every entry page at index time — pass `--max-entries` to bound that [possible values: path, url, feed]
      --max-entries <N>        Cap on feed entries considered per indexing run (feed sources only)
  -s, --store <NAME>           Operate on these stores (repeatable); a filter, not a selector
      --no-fetch-full-content  For feed sources, index only the feed-supplied summary instead of fetching each entry's full page content (feed sources only)
  -y, --yes                    Skip confirmation prompts for destructive operations
  -h, --help                   Print help (see more with '--help')
  -V, --version                Print version

On a genuinely first run (no config file exists yet at the resolved path), add — like every command except db status/migrate/downgrade/vacuum — implicitly scaffolds the config file and data/models/logs directories, then also creates the default store, so localdb add ~/notes works immediately with nothing set up beforehand:

$ localdb add ~/notes
Added source 01M0WYAT70DK1N87E6VHDJG07Y to store 'default'
Auto-indexing source 01M0WYAT70DK1N87E6VHDJG07Y ...
Indexing /home/user/notes
  discovered 1 files
  indexed 1 docs, 0 skipped, 0 deleted, 1 chunks

Once a config file exists, that implicit default-store creation no longer happens — add/source add then requires an explicit default store (or an explicit --store <name>), same as any other run:

$ localdb add ~/notes
error: invalid request: no store named 'default'; pass --store <name>
exit: 2

(paths and IDs shown from a scratch run)


localdb document

Read documents indexed into a store. With --store omitted, list spans every store like source list; get looks up the given document id across every store, disambiguating by scope when the id exists in more than one — the same “id identifies its own store” idea as source remove <ULID>, except a document id (unlike a ULID) can legitimately exist in more than one store, so the omitted---store case can be a genuine ambiguity error (specs/05-surfaces.md §2.2).

Read documents indexed into a store

Usage: localdb document [OPTIONS] <COMMAND>

Commands:
  list  List documents across stores
  get   Get a single document by id
  help  Print this message or the help of the given subcommand(s)

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

localdb document list

List documents across stores

Usage: localdb document list [OPTIONS]

Options:
      --config <PATH>       Path to config file (default: platform data dir / localdb / config.yaml)
      --source <SOURCE_ID>  Limit to documents from a specific source (by ID)
      --json                Emit JSON output instead of human-readable text
  -s, --store <NAME>        Operate on these stores (repeatable); a filter, not a selector
  -y, --yes                 Skip confirmation prompts for destructive operations
  -h, --help                Print help (see more with '--help')
  -V, --version             Print version

Omit --store and this lists every store’s documents; pass --store (repeatable) to narrow. --source filters to one source’s documents — an unknown source id yields an empty list, not an error. A store-name column appears in the output only when more than one store is in scope, exactly like source list (specs/05-surfaces.md §2.2).

$ localdb document list --store notes
a86bf252232bcec2a7da314d11e4c6005918f7930c7b9e1b081ef528034a34e8 file:///home/user/notes/meeting.txt

$ localdb document list --store notes --json
{
  "documents": [
    {
      "id": "a86bf252232bcec2a7da314d11e4c6005918f7930c7b9e1b081ef528034a34e8",
      "uri": "file:///home/user/notes/meeting.txt",
      "title": null,
      "store": {
        "name": "notes"
      },
      "store_id": "01KTVGQ62TQN8X6XN9E5FDZN67",
      "source_id": "01KTVH6AY4DC84HWW7M2PP4F0X",
      "content_hash": "e3732cc41f646a4bc94bc3611b8b6fd9d7f31f1c192748d586f55b8e7e171fd2",
      "fetched_at": "2026-08-17T20:25:09Z"
    }
  ]
}

(ids shown from a scratch run)

localdb document get

Get a single document by id

Usage: localdb document get [OPTIONS] <ID>

Arguments:
  <ID>  Document ID

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --text           Include the document's reconstructed full text in the output
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

Prints the document’s identity and metadata by default; pass --text to append its reconstructed full text (rebuilt from persisted blocks, falling back to joined chunk text — same reconstruction core::documents::reconstruct_document_text uses everywhere). --json always includes the text regardless of --text — that flag only governs the human-readable renderer. An unknown id exits 3.

--store resolves the id’s owning store with the same three-way rule as source remove <ULID>’s argument shape, extended for a genuinely-ambiguous id (specs/05-surfaces.md §2.2):

--store passed Behavior
none Looks the id up across every store; invalid_request (exit 2) if it exists in more than one store
exactly one Scopes the lookup to that store unambiguously
more than one Looks the id up unscoped, then checks its store against the given set (resource_not_found, exit 3, if outside it)
$ localdb document get a86bf252232bcec2a7da314d11e4c6005918f7930c7b9e1b081ef528034a34e8
id: a86bf252232bcec2a7da314d11e4c6005918f7930c7b9e1b081ef528034a34e8
uri: file:///home/user/notes/meeting.txt
store_id: 01KTVGQ62TQN8X6XN9E5FDZN67
source_id: 01KTVH6AY4DC84HWW7M2PP4F0X
content_hash: e3732cc41f646a4bc94bc3611b8b6fd9d7f31f1c192748d586f55b8e7e171fd2
fetched_at: 2026-08-17T20:25:09Z
dc.format: text/plain

$ localdb document get a86bf252232bcec2a7da314d11e4c6005918f7930c7b9e1b081ef528034a34e8 --text
id: a86bf252232bcec2a7da314d11e4c6005918f7930c7b9e1b081ef528034a34e8
uri: file:///home/user/notes/meeting.txt
store_id: 01KTVGQ62TQN8X6XN9E5FDZN67
source_id: 01KTVH6AY4DC84HWW7M2PP4F0X
content_hash: e3732cc41f646a4bc94bc3611b8b6fd9d7f31f1c192748d586f55b8e7e171fd2
fetched_at: 2026-08-17T20:25:09Z
dc.format: text/plain

Meeting 2026-06-02: decided to adopt reciprocal rank fusion for combining dense and sparse retrieval results.

$ localdb document get doesnotexist
error: resource not found: doesnotexist
exit: 3

Only Dublin Core fields actually present are printed (dc.format above; a document with richer metadata would also show dc.creator, dc.subject, etc. — see specs/02-domain-model.md §7).

(output shown from a scratch run)


localdb index

Run a one-shot scan-and-index job.

Run a one-shot scan-and-index job

Usage: localdb index [OPTIONS]

Options:
      --config <PATH>       Path to config file (default: platform data dir / localdb / config.yaml)
      --source <SOURCE_ID>  Limit to a specific source (by ID)
      --json                Emit JSON output instead of human-readable text
      --strict              Exit with code 2 if any document failed extraction (never aborts mid-run)
      --delete              Remove indexed documents that no longer exist at their source
  -s, --store <NAME>        Operate on these stores (repeatable); a filter, not a selector
  -y, --yes                 Skip confirmation prompts for destructive operations
  -h, --help                Print help (see more with '--help')
  -V, --version             Print version

--delete: off by default, like rsync --delete — indexing never removes anything unless you ask. Without it, documents whose files were deleted (or whose URLs now 404) stay searchable, and the run reports how many could be pruned. With it, they’re actually removed (docs_deleted in --json).

Omit --store and every store in the database is indexed; pass --store (repeatable) to index only specific stores. Indexing more than one store prints a [store]-prefixed line per store plus a combined Total: line (--json wraps into {"stores": [...], "total": {...}}); a single store in scope keeps the original unprefixed output (specs/05-surfaces.md §2.2).

Walks every registered source for the targeted store(s), extracts and chunks documents, and writes them to the unified libsql database on disk (<data_dir>/localdb.db). Progress is printed to stderr; the final summary goes to stdout (or is omitted from stdout entirely in --json mode until the summary JSON itself).

Embeddings: the CLI calls embed::create_embedder from the config policy. The default embedder (pplx-embed-context-v1-0.6b, local ONNX) is downloaded automatically on first run (~706 MB). See specs/04-search-pipeline.md for the pipeline.

$ localdb index --store notes
Indexing /home/user/notes
Index complete: 3 indexed, 0 skipped, 3 chunks written, 0 unsupported, 0 errors

Use --source <ID> to re-index a single source without touching others in the same store.


Hybrid search with citations.

Hybrid search with citations

Usage: localdb search [OPTIONS] <QUERY>...

Arguments:
  <QUERY>...  Natural language query (no quotes needed; everything after the options is treated as the query)

Options:
      --config <PATH>
          Path to config file (default: platform data dir / localdb / config.yaml)
      --limit <LIMIT>
          Maximum number of results to return (must be >= 1) [default: 3]
      --content-length <CONTENT_LENGTH>
          Max characters of snippet text shown per result in human-readable output [default: 1000]
      --json
          Emit JSON output instead of human-readable text
  -s, --store <NAME>
          Operate on these stores (repeatable); a filter, not a selector
  -y, --yes
          Skip confirmation prompts for destructive operations
  -h, --help
          Print help (see more with '--help')
  -V, --version
          Print version

Omit --store and every store is searched; pass --store (repeatable) to narrow to specific stores (specs/05-surfaces.md §2.2) — unchanged behavior, listed here for completeness.

Options-first: flags (--limit, --content-length, --store, -s, --json) must appear before the query words. Anything after the first query word is captured verbatim as query text — so localdb search --limit 5 rank fusion works, but localdb search rank fusion --limit 5 treats --limit 5 as part of the query.

Runs hybrid BM25 + dense-vector search across the targeted stores and returns ranked citations. The Citation JSON shape is documented in specs/02-domain-model.md §6.

Ranking: hybrid BM25 + dense (RRF fusion). With the default binary-quantized local model, dense is the normalized Hamming similarity (1.0 - hamming_dist / nbits); a float32 embedder yields cosine similarity instead. fused is the final RRF score.

Examples:

$ localdb search hybrid search
1. file:///home/user/notes/lancedb-notes.md > LanceDB notes
   LanceDB is an embedded vector database built on the Lance columnar format. It supports hybrid search combining vector similarity with BM25 full-text scoring.

2. file:///home/user/notes/meeting.txt
   Meeting 2026-06-02: decided to adopt reciprocal rank fusion for combining dense and sparse retrieval results. Aardvark connectors are deferred to the next milestone.

(paths shown from a scratch run)

$ localdb search --limit 1 rank fusion
1. file:///home/user/notes/meeting.txt
   Meeting 2026-06-02: decided to adopt reciprocal rank fusion for combining dense and sparse retrieval results. Aardvark connectors are deferred to the next milestone.

JSON output (full citation shape):

$ localdb search -s notes --json hybrid search
{
  "citations": [
    {
      "block": {
        "kind": "text",
        "seq": 1
      },
      "chunk_id": "82b4631e898166f7834a786b1e8e56125ce6bfc2193fc210f591179527abbdcb",
      "chunk_position": {
        "seq_in_block": 0
      },
      "heading_path": [
        "LanceDB notes"
      ],
      "location": {
        "span": {
          "end": 157,
          "start": 0
        }
      },
      "metadata": {
        "contributor": [],
        "coverage": null,
        "creator": [],
        "date": null,
        "description": null,
        "format": "text/markdown",
        "identifier": null,
        "kind": "document",
        "language": null,
        "page_count": null,
        "publisher": null,
        "relation": [],
        "rights": null,
        "source": null,
        "subject": [],
        "title": "LanceDB notes",
        "type": null,
        "word_count": null
      },
      "provenance": {
        "content_hash": "55567825f371ea048f61a59fa156068945a7ef0d9276b7813438820002ce72a2",
        "fetched_at": "2026-06-11T14:17:30Z"
      },
      "resource_id": "ee2cfd35725ead3b0fb7ebccdcc4cf9fa0ea6990ac2fa1276dc689e1abed6700",
      "score": {
        "bm25": 1.9203118085861206,
        "dense": 0.640625,
        "fused": 0.032266458495966696
      },
      "snippet": "LanceDB is an embedded vector database built on the Lance columnar format. It supports hybrid search combining vector similarity with BM25 full-text scoring.",
      "store": {
        "id": "01KTVGQ62TQN8X6XN9E5FDZN67",
        "name": "notes"
      },
      "title": "LanceDB notes",
      "uri": "file:///home/user/notes/lancedb-notes.md"
    }
  ]
}

(The structural fields above — block, chunk_position, heading_path, location.span, snippet, metadata, chunk_id, resource_id and provenance.content_hash — are captured from a real indexing run. score, store and provenance.fetched_at are illustrative.)

There is no top-level document_id, block_seq, block_kind, or span in the Citation shape — those are superseded by resource_id, the nested block {seq, kind}, chunk_position {seq_in_block}, and location {span, window_block_seqs} respectively. See specs/02-domain-model.md §6.


localdb db

Inspect or migrate the database schema. See docs/migrations.md for the full migration walkthrough and the migration-authoring guide, and specs/05-surfaces.md §2.1 for the design.

Inspect or migrate the database schema (specs/05-surfaces.md §2.1)

Usage: localdb db [OPTIONS] <COMMAND>

Commands:
  status     Show schema version, pending migrations, and migration history
  migrate    Apply pending migrations to bring the database up to this binary's head version
  downgrade  Reverse migrations using stored down-SQL (default: one step back)
  vacuum     Reclaim disk space freed by prior migrations/deletes by rewriting the whole database file (SQLite `VACUUM`)
  help       Print this message or the help of the given subcommand(s)

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

Opening a store never migrates it — a version mismatch on open is refused (exit 2) with a hint pointing at one of these commands. They are the only surfaces allowed to change a store’s schema version.

None of the four subcommands are store-scoped. They operate on the whole database file passed via --config/the default data dir, not a single named store, so --store/-s is rejected outright — exit 2 — rather than silently ignored (specs/05-surfaces.md §2.2):

$ localdb db status --store notes
error: invalid request: `db` commands operate on the whole database file; --store is not applicable
exit: 2

All four subcommands require the daemon to be stopped. Run against a live daemon they exit 4 (daemon_running), the same as every other daemon-aware write command — the daemon never applies migrations itself:

$ localdb db migrate
error: daemon is already running
exit: 4

localdb db status

Show schema version, pending migrations, and migration history

Usage: localdb db status [OPTIONS]

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

Read-only. Never refuses — a store newer than this binary, or one that predates the migration framework entirely, is reportable state, not an error.

$ localdb db status
schema version: 6 (this binary's head: 6, baseline: 4)
up to date
history:
  v4 baseline  applied 2026-08-25T17:07:04Z  (not downgradable: baseline schema predates the migration framework; cannot downgrade below v4)
  v5 drop_chunks_block_id_and_retag_resource_metadata  applied 2026-08-25T17:07:04Z  (not downgradable: chunks.block_id cannot be reconstructed; re-index required after downgrade)
  v6 shrink_vector_index  applied 2026-08-25T17:07:04Z  (downgradable)

(from a scratch run — all three migrations were applied at store-creation time, since a freshly created store is built directly at head rather than replayed step by step)

With pending migrations the second line becomes 2 pending migrations; run `localdb db migrate`. --json emits current_version, head_version, baseline_version, pending, legacy, too_new, uninitialized, table_present, and a migrations history array (per row: version, name, applied_at, downgradable, down_unsupported_reason).

An existing-but-uninitialized store — a store file that opens fine but has no schema at all yet (PRAGMA user_version is 0; a zero-byte file the user pointed at is the common case) — is reported distinctly, never as “up to date”:

$ localdb db status
schema version: 0 (this binary's head: 6, baseline: 4)
store exists but is uninitialized (no schema yet); any normal localdb command, or `localdb db migrate`, will initialize it to v6

--json sets "uninitialized": true for this case. pending stays 0 rather than reporting head_version - 0: an uninitialized store has no schema to incrementally apply on top of, only a fresh create (any normal command, or localdb db migrate, both of which create it fresh at head) — so callers should check uninitialized before treating pending == 0 as “nothing to do”.

localdb db migrate

Apply pending migrations to bring the database up to this binary's head version

Usage: localdb db migrate [OPTIONS]

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

Applies every pending migration in ascending order, one transaction per step, with per-step progress on stderr, then a summary:

$ localdb db migrate
note: this migration rebuilds the vector index (~9x smaller) by re-reading the stored embeddings — no re-embedding, but it does one index insert per chunk and can take a long time on a large store.
      it does NOT shrink the file: the space it frees goes to SQLite's free list, so '/home/user/localdb/data/localdb.db' (268.0 KiB) will briefly grow before `localdb db vacuum` reclaims it.
applying 1 pending migration
applying 1/1: shrink_vector_index
applied migration v6 'shrink_vector_index' in 1ms
migrated: v5 -> v6 (1 step applied)
hint: this migration shrank the vector index but freed pages stay in the file until reclaimed — run `localdb db vacuum` to shrink it on disk

(from a scratch run; a migration that also rewrites large derived data — like the v6 vector-index shrink above — prints a cost/space-tradeoff note before applying, and a matching hint pointing at db vacuum afterward)

If nothing is pending it prints already at head (vN) and exits 0. If any applied migration marks derived data stale (a re-embedding/re-extraction-class migration), it ends with a hint — the migration itself never re-indexes:

hint: run `localdb index` to re-index stale content

An ordinary forward migration needs no confirmation. A legacy store (schema v1–v3, predating the migration baseline) is the exception: migrating it is a destructive rebuild — all indexed data is lost — so it prompts first:

$ localdb db migrate
This store's schema (v2) predates the migration baseline (v4); migrating it erases ALL indexed data and rebuilds from scratch. Continue? [y/N] y
rebuilt legacy store: v2 -> v6 (all indexed data erased)
hint: run `localdb index` to re-index stale content

(the rebuild drops and recreates the schema directly at this binary’s head version — v6 here — not at the baseline; a legacy rebuild always marks derived data stale, so the re-index hint always follows it)

Declining leaves the store untouched (prints Aborted., exit 0). --yes skips the prompt; a non-interactive session (or --json) without --yes exits 2 (this command is destructive; re-run with --yes to confirm). Exits 2 without touching anything if the store is newer than this binary (the hint points at db downgrade or upgrading localdb).

localdb db downgrade

Reverse migrations using stored down-SQL (default: one step back)

Usage: localdb db downgrade [OPTIONS]

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --to <VERSION>   Target schema version to downgrade to (default: one step below the current version)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

Steps the store’s schema back to --to <VERSION> (default: one step, i.e. the current version minus one) by replaying the down-SQL stored in the store’s own schema_migrations table — not the compiled-in chain, which is why an older localdb binary can downgrade a store a newer binary migrated forward. Requires confirmation for every plausible downgrade (--yes to skip; same non-interactive rule as migrate):

$ localdb db downgrade --to 5
This reverses the store's schema to version 5, replaying stored down-SQL and discarding any data or structure introduced by later migrations. Continue? [y/N] y
downgraded migration v6 'shrink_vector_index' in 1ms
downgraded: v6 -> v5 (1 step)

An impossible target is checked and refused — exit 2, store untouched — before that confirmation prompt is ever shown, so a request that can only fail never asks “are you sure”. Three cases are impossible: already at or below the frozen baseline (v4); a --to at or above the current version (nothing to downgrade); or a migration with no down-SQL (irreversible; its row records a down_unsupported_reason instead) somewhere on the path to the target, in which case the error names the blocking migration and the nearest reachable target instead:

$ localdb db downgrade --to 6
error: invalid config: nothing to downgrade: target version 6 must be below the current version 6
exit: 2
$ localdb db downgrade --to 4
error: invalid config: cannot downgrade past migration 'drop_chunks_block_id_and_retag_resource_metadata' (version 5): chunks.block_id cannot be reconstructed; re-index required after downgrade. Nothing was changed. Downgrade to version 5 instead (`db downgrade --to 5`) to keep it applied and only replay the migrations above it.
exit: 2

A store with no migration history yet (run 'localdb db migrate' first) is also refused inside downgrade_store, after confirmation.

localdb db vacuum

Reclaim disk space freed by prior migrations/deletes by rewriting the whole database file (SQLite `VACUUM`)

Usage: localdb db vacuum [OPTIONS]

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

A schema migration (e.g. v6 shrink_vector_index) or an ordinary bulk delete frees pages onto SQLite’s own free list, but the file itself does not shrink until something rewrites it — db vacuum does that, via SQLite’s VACUUM. Data-preserving (an interrupted VACUUM leaves the original file untouched), but needs roughly the current file size again in free disk space and can take minutes on a large store. No confirmation prompt, since it never discards data.

$ localdb db vacuum
vacuuming '/home/user/localdb/data/localdb.db': this rewrites the entire database file and needs roughly its current size again in free disk space; large stores can take minutes
vacuumed: 340.0 KiB -> 164.0 KiB (176.0 KiB reclaimed, 0.0s)
$ localdb db vacuum --json
{
  "status": "ok",
  "size_before_bytes": 167936,
  "size_after_bytes": 167936,
  "bytes_reclaimed": 0,
  "duration_ms": 2
}

(the warning above is always printed, even in --json mode, since it’s progress/cost information rather than the command’s result; path and sizes shown from a scratch run — this second --json run reclaims nothing because an earlier vacuum in the same session had already returned the store’s free pages to the OS)


localdb job

Manage jobs on a running daemon.

Manage running/queued jobs on a daemon

Usage: localdb job [OPTIONS] <COMMAND>

Commands:
  cancel  Request cancellation of a queued or running job
  list    List every job on the daemon's queue, regardless of state or store
  help    Print this message or the help of the given subcommand(s)

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

Daemon-only: there is no embedded equivalent. An embedded (non-daemon) job lives and dies within a single CLI invocation, so there is nothing for job list/job cancel to look up afterward. Both subcommands exit 5 (daemon is unreachable) if no daemon is running:

$ localdb job list
error: daemon is unreachable
exit: 5

$ localdb job cancel some-job-id
error: daemon is unreachable
exit: 5

Neither subcommand is store-scoped. --store/-s is rejected outright — exit 2 — on both: cancel operates on a job id, which is already globally unique across every store; list spans every job on the queue regardless of which store it belongs to:

$ localdb job list --store notes
error: invalid request: `job list` shows every job regardless of store; --store is not applicable
exit: 2

localdb job list

List every job on the daemon's queue, regardless of state or store

Usage: localdb job list [OPTIONS]

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

With no jobs on the queue:

$ localdb job list
No jobs.

$ localdb job list --json
[]

With a completed job in history (jobs are ephemeral operational records with bounded retention, not permanent history — see specs/05-surfaces.md §3 for the eviction policy):

$ localdb job list
ID                          STORE  STATE  ERROR_CODE  CREATED_AT
01M0WYCSV7HPJY5ADKQP59D8S7  notes  done   -           2026-08-25T17:09:56Z
$ localdb job list --json
[
  {
    "id": "01M0WYD5119M0MVZ6G9NZFX4JC",
    "store_id": "notes",
    "scope": {
      "type": "store"
    },
    "state": "done",
    "stats": {
      "docs_seen": 3,
      "docs_indexed": 0,
      "docs_skipped": 3,
      "docs_deleted": 0,
      "docs_prunable": 0,
      "chunks_written": 0,
      "unsupported_format_count": 0,
      "error_count": 0,
      "sources_count": 2
    },
    "error": null,
    "error_code": null,
    "created_at": "2026-08-25T17:10:07Z",
    "started_at": "2026-08-25T17:10:07Z",
    "completed_at": "2026-08-25T17:10:08Z"
  }
]

(job ids and timestamps shown from a scratch run against a real daemon)

localdb job cancel

Request cancellation of a queued or running job

Usage: localdb job cancel [OPTIONS] <ID>

Arguments:
  <ID>  Job ID

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

Requests cancellation of a queued or running job by id. Exit codes: 0 cancellation requested (the daemon accepted it, HTTP 202 — cancellation is asynchronous, so this does not mean the job has actually stopped yet), 3 the id doesn’t match any job, 4 the job already reached a terminal state (done/failed — a cancelled job lands in failed with error_code: "job_cancelled") and can no longer be cancelled:

$ localdb job cancel not-a-real-job-id
error: job not found: not-a-real-job-id
exit: 3

$ localdb job cancel 01M0WYCSV7HPJY5ADKQP59D8S7   # already 'done'
error: job already reached a terminal state; cannot cancel
exit: 4

(job ids shown from a scratch run against a real daemon; the exit-0/202-accepted case requires cancelling a job while it’s still queued or running, which needs a slower job than this reference’s scratch fixtures produce — see specs/05-surfaces.md §5 and server/src/job_exec.rs for the cancellation contract)


localdb serve

Experimental. The HTTP daemon is an experimental preview. See limitations below.

Start the HTTP API daemon.

Start the HTTP API daemon (file watching, scheduled refresh, REST API)

Usage: localdb serve [OPTIONS]

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

Binds 127.0.0.1:7700 by default (configurable via server.bind / server.port in config.yaml). Prints an announce line on startup:

$ localdb serve
daemon listening on http://127.0.0.1:7700

Also creates a Unix socket at <data_dir>/daemon.sock that CLI commands use to detect the daemon.

Not store-scoped: the daemon serves every store in the database, on /v1 and /mcp alike, so there is nothing for --store to narrow — passing it exits 2 rather than being silently ignored. The check runs before the daemon binds a port. To limit an MCP client to a subset of stores, scope the client instead: localdb mcp --store <name> (see docs/mcp.md).

Exits 4 (daemon_running) if a daemon is already running on the same data dir:

$ localdb serve
error: daemon is already running
exit: 4

For the full HTTP API reference see docs/http-api.md.

Known limitations


localdb mcp

Run the MCP server on stdio for use with AI agents.

Run the MCP server on stdio for use with AI agents.

Exposes every store when `--store` is omitted; pass `--store <NAME>` (repeatable) to limit the session to those stores. The limit is enforced whether the server runs embedded or proxies to a running daemon, and an unknown name exits 3. Note this is a guardrail, not a security boundary: the daemon's MCP endpoint is unauthenticated, so a client that bypasses `localdb mcp` can still reach every store.

Usage: localdb mcp [OPTIONS]

Options:
      --allow-write
          Enable write tools (reserved for future use; no effect in v1).

          v1 registers no mutating tool, so the tool set is identical with and without this flag; passing it prints a warning. Parsing it now makes the CLI stable for callers.

      --config <PATH>
          Path to config file (default: platform data dir / localdb / config.yaml)

      --json
          Emit JSON output instead of human-readable text

  -s, --store <NAME>
          Operate on these stores (repeatable); a filter, not a selector.

          Omitted, this means "all stores" for `search`, `status`, `store list`, `source list`, `source remove <ULID>`, `index` and `mcp`; the store named `default` for `source add` and the `add` alias (exit 2 if absent). `source remove <path|url>` requires it (exit 2 without it). It is rejected outright (exit 2) by `init`, `serve`, `store add`, `store remove` and the `db` subcommands, which are not store-scoped. An explicit name is always validated: unknown is exit 3. See `--help` on the specific subcommand for its exact rule.

  -y, --yes
          Skip confirmation prompts for destructive operations

  -h, --help
          Print help (see a summary with '-h')

  -V, --version
          Print version

Starts a JSON-RPC 2.0 MCP server on stdin/stdout. If no daemon is running it uses embedded mode; if one is, it proxies to that daemon’s /mcp route. The server exposes five read-only tools: search, get_document, get_chunks, list_documents, and list_stores.

Omitting --store exposes every store; pass --store (repeatable) to limit the session to those stores. The limit is enforced in both modes, and an unknown name exits 3:

$ localdb mcp --store books --store research   # only these two are reachable
$ localdb mcp --store typo
error: store not found: typo
exit: 3

A database with no stores at all is not an error here — the server starts and exposes zero stores, because an MCP server that exits non-zero at startup reads to its client as broken rather than as empty.

Scoping, not a security boundary. The daemon’s /mcp route is loopback and unauthenticated, so anything that can open a socket can bypass localdb mcp and talk to it unscoped. --store stops an agent from accidentally reading another project’s docs; it does not contain a hostile one. See docs/mcp.md.

--allow-write is accepted on the command line for forward compatibility, but v1 registers no mutating tool at all — the tool set is identical with and without it, and passing it prints a warning to stderr saying so.

See docs/mcp.md for the full tool reference, input schemas, and example JSON-RPC exchanges.

Example (connect via any MCP-capable client, or pipe JSON-RPC by hand):

localdb mcp --config ~/notes/localdb-config.yaml

The server reads newline-delimited JSON-RPC from stdin and writes responses to stdout. MCP clients (Claude Desktop, etc.) handle the transport automatically.


localdb completions

Generate a shell completion script.

Generate a shell completion script on stdout

Usage: localdb completions [OPTIONS] <SHELL>

Arguments:
  <SHELL>  Shell to generate completions for [possible values: bash, elvish, fish, powershell, zsh]

Options:
      --config <PATH>  Path to config file (default: platform data dir / localdb / config.yaml)
      --json           Emit JSON output instead of human-readable text
  -s, --store <NAME>   Operate on these stores (repeatable); a filter, not a selector
  -y, --yes            Skip confirmation prompts for destructive operations
  -h, --help           Print help (see more with '--help')
  -V, --version        Print version

Pure codegen: it prints a completion script for the named shell to stdout and exits — no config load, no daemon probe, and it works before init (nothing needs to exist yet). Install by sourcing the output into your shell’s completion path:

localdb completions zsh > "${fpath[1]}/_localdb"
localdb completions bash >> ~/.bash_completion
localdb completions fish > ~/.config/fish/completions/localdb.fish

Typical workflow

# 1. Create a runtime store
localdb store add notes

# 2. Register a source directory
localdb source add ~/notes --store notes

# 3. Index
localdb index --store notes

# 4. Search
localdb search "how does rust handle errors"

# 5. Search with JSON output for scripting (flags before the query)
localdb search --store notes --json "hybrid search"

Config validation errors

Bad config files exit 2 with a path-precise message. Common cases:

Config problem Error message
Unknown top-level key invalid config: unknown field 'bogus_key', expected one of 'version', 'server', 'paths', 'defaults', 'providers'
Wrong version invalid config: unsupported config version 2; only version 1 is supported. Hint: add 'version: 1' at the top of your config file.
Source missing required field invalid config: stores[0].sources[0].root: required for kind 'path'
Config file not found invalid config: cannot read config file '/path/to/config.yaml': No such file or directory
Not valid YAML invalid config: invalid type: map, expected field identifier at line 1 column 2