Skip to content

CLI

indx CLI — JSON-on-stdout, atomic verbs, schema-introspectable.

Status: Draft v0.1 · 2026-05-03 Audience: This document describes the CLI as designed for an AI agent to drive. Humans can use it too, but every choice here optimizes for the agent’s experience.


1. Design principles (the AI’s wishlist)

Section titled “1. Design principles (the AI’s wishlist)”

An AI driving a CLI cares about different things than a human at a keyboard:

PropertyWhy it matters to an agent
Deterministic outputSame input → same bytes. Lets the agent cache, diff, and reason.
Structured by defaultJSON, not prose. No ANSI. No spinners. Stdout = data, stderr = telemetry.
Idempotent writesThe agent retries when uncertain; the CLI must make that safe.
Atomic verbsOne verb does one thing. No interactive multi-step prompts. Ever.
Schema introspection--help is the contract; indx schema returns the OpenAPI of every command.
Read-after-writeWrite commands echo the new resource (or its ETag) so the agent doesn’t need a second call.
Predictable exit codesDiscoverable failure mode without parsing English.
ComposableOutput of one command (a path, an ETag) is input to the next. Pipe-friendly.
BoundedPagination required; --limit defaults; large outputs stream NDJSON.
No hidden stateNo config files in $HOME unless explicit. No daemons unless serve.

These principles are the product spec. If a feature can’t honor them, it doesn’t ship.

indx [global flags] <noun> <verb> [args] [flags]
  • Nouns: note, link, tag, canvas, base, vault, ai, mcp, serve, schema.
  • Every verb has the same shape: take args + flags, return JSON to stdout, exit 0/1/2/3/4/5.
  • No prompts. No y/N. Destructive ops require --yes or fail with exit 4.
FlagDefaultNotes
--vault <path>$INDX_VAULT or .Local vault directory (used in --local mode)
--remote <url>$INDX_URLTalk to a running server instead of disk
--token <t>$INDX_TOKENBearer token (remote mode only)
--localautoForce local FS mode
--jsondefaultJSON output (the only option in v1)
--ndjsonoffStream NDJSON for list/search verbs
--no-colorautoAlways off when not a TTY
--quietoffSuppress stderr telemetry below warn
--dry-runoffValidate + plan, don’t write
--yesoffRequired for destructive verbs
--idempotency-key <k>random uuidForwarded to the server in remote mode
--if-match <etag>unsetOptimistic concurrency on writes
CodeMeaning
0success
1generic error (unexpected)
2usage error (bad flags / args)
3not found
4conflict (etag mismatch, exists, would overwrite, missing --yes)
5unauthorized / forbidden
6validation error (parse failed, schema invalid)
7rate limited
8upstream / network error (remote mode)
  • Stdout is always valid JSON or NDJSON.
  • Stderr carries human-readable telemetry; never required for correctness.
  • A single response object always has { ok: true, data } on success or { ok: false, error: { code, message, details? } } on failure. The same envelope as the HTTP API. (See API.md.)
Terminal window
$ indx note get Home
{ "ok": true, "data": { "path": "Home.md", "etag": "…", "frontmatter": {…}, "body": "…" } }
$ indx note get Missing
{ "ok": false, "error": { "code": "not_found", "message": "no such note", "path": "Missing.md" } }
# exit code: 3
indx note get <path> [--include outline,links,tags]
indx note create <path> [--from-stdin | --content <md> | --frontmatter <yaml>]
indx note update <path> [--from-stdin | --content <md>] [--if-match <etag>]
indx note patch <path> --op <patch.json> # patch ops, see SPEC §6.2
indx note delete <path> --yes
indx note move <from> <to> [--no-update-links]
indx note list [--path-glob <g>] [--tag <t>] [--limit 50] [--cursor <c>] [--ndjson]
indx note exists <path>
indx note outline <path>

create/update/patch return the full new resource including new etag, mtime_ms, and outline. The agent never needs a follow-up GET.

--op accepts a JSON patch object or an array of objects (applied in order). Example:

Terminal window
indx note patch Home --op '{
"op": "insert_after_heading",
"heading": "## Today",
"markdown": "- [ ] write the spec\n"
}'
indx link backlinks <path> # who links to <path>
indx link forward <path> # who does <path> link to
indx link unresolved [--limit] # all [[targets]] that don't resolve
indx link orphans # notes with no in/out links

Every link result includes { src_path, dst_path, line, anchor, kind } — agents can rewrite directly using note patch.

indx tag list # all tags + counts
indx tag notes <tag> [--limit] # notes carrying a tag
indx tag rename <from> <to> # bulk-rename, returns affected paths

3.4 search — full-text + semantic + structural

Section titled “3.4 search — full-text + semantic + structural”
indx search "<query>" \
[--mode lexical|semantic|hybrid] \
[--tag <t>]... \
[--path-glob <g>] \
[--frontmatter '<key>=<value>']... \
[--limit 20] \
[--cursor <c>] \
[--ndjson]
  • Default mode: lexical. hybrid requires embeddings configured; falls back to lexical with a warning field if not.
  • Results are ranked; each item has { path, title, score, snippet, tags, frontmatter }.
indx canvas get <path>
indx canvas create <path> --from-stdin
indx canvas patch <path> --op <canvas-patch.json>

Canvas patch ops:

  • add_node, update_node, remove_node
  • add_edge, remove_edge
  • move_node (x, y, width, height)
indx base list
indx base get <path>
indx base query <path> [--limit] [--cursor]

base query runs the .base file’s filter+formula plan against the vault index and streams matching rows.

indx vault status # health, counts, last reindex
indx vault index --rebuild # full reindex (background by default)
indx vault index --watch # foreground, follow events
indx vault export <out.tar.zst> # snapshot of vault + .indx
indx vault import <in.tar.zst> --yes
indx vault config get [<key>]
indx vault config set <key> <value>
indx serve [--port 3000] [--vault ./my-vault]

Boots the same Next.js server that ships in the Docker image. Useful for local dev without Docker.

Full surface lives in AI.md; the CLI exposes one verb per op. All AI verbs require a configured provider; without one they exit 8 with code: ai_unavailable. Every verb honors --idempotency-key, --stream (NDJSON deltas, terminated by ai.complete), and the standard --ndjson flag for downstream piping of structured outputs.

indx ai status
indx ai summarize \
[--scope-paths a.md,b.md | --scope-glob 'Specs/**' | --scope-tag ops | --scope-query "..."] \
[--style neutral|bullets|executive|technical] \
[--length short|medium|long] \
[--include tldr,key_points,open_questions,outline] \
[--language en] [--max-tokens N] [--seed N] [--no-cache] [--stream]
indx ai ask "<question>" \
[--scope-glob ... | --scope-tag ... | --scope-paths ...] \
[--mode lexical|semantic|hybrid] [--top-k 8] \
[--style ...] [--language ...] [--max-tokens N] [--no-cite] [--stream]
indx ai toc \
--note <path> [--depth 1..6] [--descriptions] [--no-links] [--style compact|expanded]
indx ai toc \
--moc \
[--scope-glob ... | --scope-tag ... | --scope-paths ...] \
[--group-by tag|folder|topic|frontmatter] [--group-by-key <k>] \
[--max-groups N] [--max-per-group N] [--summaries] \
[--title <t>] [--write <path> [--if-not-exists] [--if-match <etag>]]
indx ai relate \
[--note <path> | --notes a.md,b.md | --scope-glob ...] \
[--candidates-glob ... | --candidates-tag ...] \
[--top-k 10] [--mode hybrid|semantic|lexical] \
[--classify] [--threshold 0.55] [--relations extends,cites,...] \
[--propose-links]
indx ai tag \
[--scope-paths a.md,b.md | --scope-glob ... | --scope-tag ...] \
[--use-existing] [--allow-new | --no-allow-new] \
[--candidates t1,t2,...] [--forbid t9,t8,...] [--namespace <ns>] \
[--max-per-note 5] [--min-confidence 0.6] \
[--apply [--apply-mode merge|replace] [--if-match <path>=<etag>]...]
indx ai metadata \
[--scope-paths ... | --scope-glob ... | --scope-tag ...] \
--field 'key:type[:enum=a|b|c][:required][:pattern=<regex>][:min=<n>][:max=<n>]' \
[--field ...]... \
[--apply [--apply-mode set_missing|overwrite|merge]] \
[--if-match <path>=<etag>]...
indx ai extract \
[--scope-paths ... | --scope-glob ... | --scope-tag ...] \
--schema-file <path.json> [--schema-id <name>] \
[--destination json|frontmatter] [--destination-key <key>] \
[--apply [--apply-mode set_missing|overwrite|merge]] \
[--if-match <path>=<etag>]...

--field is parsed as key:type[:flag=value]…; flags are colon-separated.

--field 'title:string:required'
--field 'status:enum:enum=idea|active|blocked|done:required'
--field 'due_date:date'
--field 'stakeholders:list:list_item_type=string:max=5'

For complex schemas (especially for ai extract), prefer --schema-file <path.json> — flag parsing is not a substitute for a real JSON Schema.

stdout is the same { ok, data } envelope used elsewhere; data is the op’s structured payload from AI.md §5. With --stream, each delta is one NDJSON line:

{"type":"ai.partial","delta":"…"}
{"type":"ai.citation","citation":{…}}
{"type":"ai.usage","usage":{…}}
{"type":"ai.complete","data":{…}}

When materializing a MOC, the response includes written: { path, etag } sourced from the standard atomic write pipeline — agents do not need a follow-up note get.

The standard table in §2.2 still applies. Notable reuses:

  • 8ai_unavailable, ai_provider_error (treated as upstream).
  • 7ai_quota_exceeded.
  • 6ai_grounding_failed, ai_input_too_large, ai_schema_invalid.
  • 4ai toc --moc --write against an existing path without --yes / --if-match / --if-not-exists; ai_apply_conflict from per-path --if-match mismatch on ai tag --apply / ai metadata --apply / ai extract --apply.
indx mcp serve --stdio # for Claude Code / Cursor / etc.
indx mcp serve --http --port 3001 # for remote agents
indx mcp tools # list tools + JSON Schemas
indx mcp call <tool> --input <json> # one-shot tool invocation, for testing

mcp tools is the agent’s discovery endpoint — the schema for every tool, machine-readable.

indx schema openapi # full OpenAPI 3.1 (same as /openapi.json)
indx schema mcp # MCP tool catalog with JSON Schemas
indx schema patch-ops # patch op grammar
indx schema events # event payload shapes

4.1 “Add a TODO under today’s heading in the daily note”

Section titled “4.1 “Add a TODO under today’s heading in the daily note””
Terminal window
TODAY=$(date +%F)
PATH_="Daily/${TODAY}.md"
# Ensure the note exists. `create` is idempotent with --if-not-exists.
indx note create "$PATH_" --if-not-exists --content "# ${TODAY}\n\n## Tasks\n"
# Patch under the Tasks heading.
indx note patch "$PATH_" --op '{
"op": "insert_after_heading",
"heading": "## Tasks",
"markdown": "- [ ] follow up with Alice\n"
}'

The agent reads the JSON return of each command; no parsing of human prose.

Section titled “4.2 “Find every note tagged #project that has no backlinks””
Terminal window
indx tag notes project --ndjson \
| jq -r '.path' \
| while read -r p; do
n=$(indx link backlinks "$p" | jq '.data | length')
[[ "$n" -eq 0 ]] && echo "$p"
done
Terminal window
indx search "deployment runbook" --mode hybrid --limit 5 --ndjson \
| while read -r line; do
p=$(jq -r '.path' <<<"$line")
indx note get "$p" --include outline
done
Terminal window
indx note patch Architecture.md --op '{
"op": "replace_section",
"heading": "## Components",
"markdown": "## Components\n\n- web\n- core\n- cli\n"
}'

4.5 “Atomic update with optimistic concurrency”

Section titled “4.5 “Atomic update with optimistic concurrency””
Terminal window
ETAG=$(indx note get Spec.md | jq -r '.data.etag')
indx note update Spec.md --if-match "$ETAG" --content "$(cat new.md)"
# exit 4 if someone changed Spec.md in the meantime

5. Anti-patterns (intentionally not supported)

Section titled “5. Anti-patterns (intentionally not supported)”
  • Interactive prompts. The CLI never asks a question. If something is ambiguous, it errors with code: needs_more_info and a structured details field describing the choice.
  • Colored or formatted human-only output as the default.
  • Implicit defaults that read from $HOME. The vault must be specified (env or flag).
  • Long-running commands without --watch mode and SSE-backed events.
  • Fuzzy path matching as a default. Exact match wins; the agent can opt into --fuzzy if it wants to gamble.
  • The CLI prints its version with indx --version.
  • The CLI is pinned to a server major version. indx note get against a newer-major server returns code: version_mismatch with the supported range.
  • Adding new verbs/flags is non-breaking. Removing or renaming requires a new major.