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:
| Property | Why it matters to an agent |
|---|---|
| Deterministic output | Same input → same bytes. Lets the agent cache, diff, and reason. |
| Structured by default | JSON, not prose. No ANSI. No spinners. Stdout = data, stderr = telemetry. |
| Idempotent writes | The agent retries when uncertain; the CLI must make that safe. |
| Atomic verbs | One 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-write | Write commands echo the new resource (or its ETag) so the agent doesn’t need a second call. |
| Predictable exit codes | Discoverable failure mode without parsing English. |
| Composable | Output of one command (a path, an ETag) is input to the next. Pipe-friendly. |
| Bounded | Pagination required; --limit defaults; large outputs stream NDJSON. |
| No hidden state | No 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.
2. Invocation model
Section titled “2. Invocation model”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--yesor fail with exit 4.
2.1 Global flags
Section titled “2.1 Global flags”| Flag | Default | Notes |
|---|---|---|
--vault <path> | $INDX_VAULT or . | Local vault directory (used in --local mode) |
--remote <url> | $INDX_URL | Talk to a running server instead of disk |
--token <t> | $INDX_TOKEN | Bearer token (remote mode only) |
--local | auto | Force local FS mode |
--json | default | JSON output (the only option in v1) |
--ndjson | off | Stream NDJSON for list/search verbs |
--no-color | auto | Always off when not a TTY |
--quiet | off | Suppress stderr telemetry below warn |
--dry-run | off | Validate + plan, don’t write |
--yes | off | Required for destructive verbs |
--idempotency-key <k> | random uuid | Forwarded to the server in remote mode |
--if-match <etag> | unset | Optimistic concurrency on writes |
2.2 Exit codes
Section titled “2.2 Exit codes”| Code | Meaning |
|---|---|
| 0 | success |
| 1 | generic error (unexpected) |
| 2 | usage error (bad flags / args) |
| 3 | not found |
| 4 | conflict (etag mismatch, exists, would overwrite, missing --yes) |
| 5 | unauthorized / forbidden |
| 6 | validation error (parse failed, schema invalid) |
| 7 | rate limited |
| 8 | upstream / network error (remote mode) |
2.3 Output contract
Section titled “2.3 Output contract”- 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. (SeeAPI.md.)
$ 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: 33. Command surface
Section titled “3. Command surface”3.1 note — markdown notes
Section titled “3.1 note — markdown notes”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.2indx note delete <path> --yesindx 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>Read-after-write
Section titled “Read-after-write”create/update/patch return the full new resource including new etag, mtime_ms, and outline. The agent never needs a follow-up GET.
Patch grammar
Section titled “Patch grammar”--op accepts a JSON patch object or an array of objects (applied in order). Example:
indx note patch Home --op '{ "op": "insert_after_heading", "heading": "## Today", "markdown": "- [ ] write the spec\n"}'3.2 link — graph operations
Section titled “3.2 link — graph operations”indx link backlinks <path> # who links to <path>indx link forward <path> # who does <path> link toindx link unresolved [--limit] # all [[targets]] that don't resolveindx link orphans # notes with no in/out linksEvery link result includes { src_path, dst_path, line, anchor, kind } — agents can rewrite directly using note patch.
3.3 tag — tag operations
Section titled “3.3 tag — tag operations”indx tag list # all tags + countsindx tag notes <tag> [--limit] # notes carrying a tagindx tag rename <from> <to> # bulk-rename, returns affected paths3.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.hybridrequires embeddings configured; falls back to lexical with awarningfield if not. - Results are ranked; each item has
{ path, title, score, snippet, tags, frontmatter }.
3.5 canvas — JSON Canvas files
Section titled “3.5 canvas — JSON Canvas files”indx canvas get <path>indx canvas create <path> --from-stdinindx canvas patch <path> --op <canvas-patch.json>Canvas patch ops:
add_node,update_node,remove_nodeadd_edge,remove_edgemove_node(x, y, width, height)
3.6 base — Bases databases
Section titled “3.6 base — Bases databases”indx base listindx 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.
3.7 vault — admin
Section titled “3.7 vault — admin”indx vault status # health, counts, last reindexindx vault index --rebuild # full reindex (background by default)indx vault index --watch # foreground, follow eventsindx vault export <out.tar.zst> # snapshot of vault + .indxindx vault import <in.tar.zst> --yesindx vault config get [<key>]indx vault config set <key> <value>3.8 serve — run the server
Section titled “3.8 serve — run the server”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.
3.9 ai — built-in AI runtime
Section titled “3.9 ai — built-in AI runtime”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 statusindx 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>]...Example: indx ai metadata field syntax
Section titled “Example: indx ai metadata field syntax”--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.
Output
Section titled “Output”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":{…}}Read-after-write for ai toc --moc --write
Section titled “Read-after-write for ai toc --moc --write”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.
Exit codes specific to ai
Section titled “Exit codes specific to ai”The standard table in §2.2 still applies. Notable reuses:
8—ai_unavailable,ai_provider_error(treated as upstream).7—ai_quota_exceeded.6—ai_grounding_failed,ai_input_too_large,ai_schema_invalid.4—ai toc --moc --writeagainst an existing path without--yes/--if-match/--if-not-exists;ai_apply_conflictfrom per-path--if-matchmismatch onai tag --apply/ai metadata --apply/ai extract --apply.
3.10 mcp — Model Context Protocol
Section titled “3.10 mcp — Model Context Protocol”indx mcp serve --stdio # for Claude Code / Cursor / etc.indx mcp serve --http --port 3001 # for remote agentsindx mcp tools # list tools + JSON Schemasindx mcp call <tool> --input <json> # one-shot tool invocation, for testingmcp tools is the agent’s discovery endpoint — the schema for every tool, machine-readable.
3.11 schema — introspection
Section titled “3.11 schema — introspection”indx schema openapi # full OpenAPI 3.1 (same as /openapi.json)indx schema mcp # MCP tool catalog with JSON Schemasindx schema patch-ops # patch op grammarindx schema events # event payload shapes4. Examples (annotated for an agent)
Section titled “4. Examples (annotated for an agent)”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””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.
4.2 “Find every note tagged #project that has no backlinks”
Section titled “4.2 “Find every note tagged #project that has no backlinks””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" done4.3 “Hybrid search and open top 5”
Section titled “4.3 “Hybrid search and open top 5””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 done4.4 “Patch a section by heading”
Section titled “4.4 “Patch a section by heading””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””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 meantime5. 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_infoand a structureddetailsfield 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
--watchmode and SSE-backed events. - Fuzzy path matching as a default. Exact match wins; the agent can opt into
--fuzzyif it wants to gamble.
6. Versioning & compatibility
Section titled “6. Versioning & compatibility”- The CLI prints its version with
indx --version. - The CLI is pinned to a server major version.
indx note getagainst a newer-major server returnscode: version_mismatchwith the supported range. - Adding new verbs/flags is non-breaking. Removing or renaming requires a new major.