Skip to content

CLI reference

The graphmind-ai package installs the graphmind binary: the local server, SQLite storage, the keyless demo, the trace importer, the MCP proxy, the MCP server, and the bundled viewer UI.

Terminal window
npx graphmind-ai # serve
npx graphmind-ai demo # keyless demo

Requires Node ≥ 22.13. SQLite is built into Node, so there are no native dependencies.

Start the local server and open the viewer. Running graphmind with no command does this.

Terminal window
graphmind # port 4747, open the browser
graphmind --port 4848 # different port (always binds 127.0.0.1)
graphmind --db ./x.db # database file (default ~/.graphmind/graphmind.db)
graphmind --no-open # do not open a browser
graphmind --pause-on-error tool # arm the default error breakpoint on tools only
graphmind --pause-on-error off # start with no breakpoints at all

Binding loopback is not on its own enough, because two browser mechanisms reach a loopback server from a page you merely visited: a WebSocket handshake is exempt from the same-origin policy, and DNS rebinding makes an attacker’s domain same-origin for a plain fetch. Since 0.3.0 every HTTP request and every upgrade is checked:

ClientResult
No Origin header — the SDK, curl, the CLI itself, testsAllowed. A web page cannot omit it.
A loopback origin on this server’s own port — the viewer it servesAllowed
Any other origin403
A Host header that is not a loopback name403 (closes DNS rebinding; SSH tunnels still work)

GRAPHMIND_ALLOWED_ORIGINS opens it explicitly — a comma-separated list, or * to disable the check entirely. You need it for exactly one ordinary case, a viewer running on its own dev server:

Terminal window
GRAPHMIND_ALLOWED_ORIGINS=http://localhost:5199,http://127.0.0.1:5199 graphmind

The keyless first-run experience. Replays a bundled recording of a trip-planner agent with a planted bug: convertCurrency inverts an exchange rate, and the budget check throws on the absurd total.

The replay goes through the real ingest pipeline and honours the real control protocol — the planted error genuinely pauses, and inject / continue / retry / abort steer the replay onto the matching pre-recorded branch. It uses a server already running on the target port, or starts one in-process and keeps it up until Ctrl+C.

Terminal window
graphmind demo # replay the recording (no API key)
graphmind demo --live # run the real demo agent with your API key
graphmind demo --port 4848 # target a specific server port

--live needs a GraphMind monorepo checkout (or GRAPHMIND_DEMO_AGENT_DIR pointing at the demo agent) plus ANTHROPIC_API_KEY or OPENAI_API_KEY.

Look at a project, work out which agent framework it uses, and print the exact install command plus a copy-pasteable snippet that instruments it.

Terminal window
graphmind init # detect and print the steps
graphmind init --install # also run the package-manager install
graphmind init --write # write a graphmind.example.ts snippet file

Detection is deliberately dumb and transparent: it reads the manifest, matches dependency names, and reports what it found. There are no prompts (so it is safe in CI and from coding agents) and nothing is written unless you pass --write.

Convert an exported trace file into a run, stored with source: "import" and rendered with the viewer’s “imported” treatment — history only, no live features. Best-effort; prints a summary (nodes, errors, duration, skipped spans) and a viewer deep link.

Terminal window
graphmind import trace.json
graphmind import trace.json --db ./x.db

Accepted inputs: OTLP/JSON (OTel collector file exporter, SDK JSON exporters) and flat span lists (OpenInference / Arize Phoenix-style JSON or JSONL). Recognised span dialects: Vercel AI SDK OTel spans, OTel GenAI semantic conventions, and OpenInference. Unrecognised spans are imported as generic nodes or skipped with a note.

Debug your MCP server. Spawns <command> as a child, speaks stdio JSON-RPC to your MCP host on one side and to the server on the other, relays every frame verbatim, and reports the conversation to GraphMind as a live run you can hold and steer. Any language, no code changes.

Terminal window
graphmind mcp-proxy -- node my-server.js
graphmind mcp-proxy -- python -m my_server
graphmind mcp-proxy --trace -- ./target/debug/my-server

Point a host at it by moving the existing command inside the proxy:

Terminal window
claude mcp add my-server-debug -- npx -y graphmind-ai mcp-proxy -- node my-server.js
FlagMeaning
--traceOne line per frame on stderr — works with no GraphMind running
--wait-for-attachHold the first frame until the debugger attaches (≤3s), so initialize can be gated
--inherit-stderrGive the server the real stderr fd; its log then does not appear on the session node
--max-frame-bytes <n>Frame-assembly ceiling (default 64 MiB); above it the relay becomes a raw byte pipe
--port <n>GraphMind port to report to

Run it with no command and it prints a working recipe. Nothing is ever written to stdout except the protocol. Full detail: MCP proxy and debugging MCP servers.

The other direction: serve your recorded runs to MCP clients (Claude Code, Cursor, …) over stdio. Read-only tools: list_runs, get_run, get_node, find_errors — each result carries a deep link into the viewer. Reads the SQLite database directly, so it works while the server and viewer are closed.

Terminal window
claude mcp add graphmind -- npx graphmind-ai mcp

--db selects the database; --port sets the port used in generated deep links (default 4747 — pass it if you run graphmind on a different port). Full detail: MCP tools.

List what is stored, and keep it from growing forever.

Terminal window
graphmind runs # list recent runs
graphmind runs --keep 50 # show the 50 newest
graphmind runs --prune # apply the retention policy now
graphmind runs --prune --keep 50 # ...keeping only the 50 newest
graphmind runs --prune --days 7 # ...and only the last 7 days
graphmind runs --rm <runId> # delete one run
graphmind runs --clear --yes # delete every run

Default retention is 200 runs / 30 days. --clear refuses to act without --yes.

Export a persisted run — as NDJSON (one wire envelope per line, the same shape graphmind demo replays and WS /ingest accepts), or as one self-contained HTML page.

Terminal window
graphmind record run_01H8… # → graphmind-run-<runId>.ndjson
graphmind record run_01H8… --out bug-4471.ndjson
graphmind record run_01H8… --html # → graphmind-run-<runId>.html
graphmind record run_01H8… --html --out bug-4471.html

--html inlines the viewer’s own JS and CSS and embeds the run’s envelopes in the page, so the file opens in any browser with no server and no network — the same UI you were just looking at, reading a frozen run. It needs the built viewer, which the published package ships (from a monorepo checkout, run pnpm build:viewer first). Live-looking debugger controls are hidden in an export, because there is nothing to control.

Lists recent run ids when the given one is not found. See exporting a run.

FlagMeaning
--port <n>Port (default 4747; the server always binds 127.0.0.1)
--db <path>SQLite database file (default ~/.graphmind/graphmind.db)
--no-open / --openSuppress / force opening the viewer in a browser
--pause-on-error <on|off|kind>Scope the default error breakpoint (default on: every node)
--live(demo) run the real demo agent instead of the replay
--install(init) run the package-manager install
--write(init) write a graphmind.example.ts snippet file
--out <file>(record) output path
--html(record) export a self-contained HTML page instead of NDJSON
--trace(mcp-proxy) one line per frame on stderr
--wait-for-attach(mcp-proxy) hold the first frame until the debugger attaches
--inherit-stderr(mcp-proxy) do not pipe the server’s stderr
--max-frame-bytes <n>(mcp-proxy) frame-assembly ceiling (default 64 MiB)
--prune(runs) apply the retention policy now
--keep <n>(runs) keep / show the n newest runs
--days <n>(runs) keep runs from the last n days
--rm <runId>(runs) delete one run
--clear(runs) delete all runs — requires --yes
--yesConfirm a destructive command
-v, --versionPrint the version and exit
-h, --helpShow help

Flags accept both --port 4848 and --port=4848.

Environment variables live on their own page: environment.

EndpointPurpose
WS /ingestInstrumented apps. Schema envelopes; hellohello.ack handshake (the ack carries current breakpoints + mode).
WS /ws/uiViewers. UI subprotocol (below).
GET /health{ ok, name, version }
GET /api/runs{ runs: RunInfo[] }, most recent first
GET /api/runs/:id/eventsPaginated events (?afterSeq=&limit=)
GET /*The built viewer (a placeholder page when the bundle is absent)

Each run: { id, app, startedAt, finishedAt, status, schemaVersion, source, eventCount, errorCount, live }. status is running | ok | error | aborted; source is live | import | demo; live is whether the owning app socket is currently connected. Timestamps are epoch milliseconds; finishedAt is null while running.

GET /api/runs/:id/events?afterSeq=N&limit=M

Section titled “GET /api/runs/:id/events?afterSeq=N&limit=M”

Returns { runId, total, events, nextAfterSeq }. events are full wire envelopes in ascending seq order with seq > afterSeq (default: from the start). limit defaults to 1000 (max 5000). nextAfterSeq is the cursor for the next page, or null on the last one. Unknown run: 404 { error }.

JSON text frames, each an object with a type discriminant. Wire envelopes are never sent bare — they ride inside event / control messages. Types are exported from the package (UiClientMessage, UiServerMessage, RunInfo, WireEnvelope).

Server → viewer

MessageWhen
{ type: 'welcome', versions, breakpoints, mode }Immediately on connect
{ type: 'state', breakpoints, mode }Whenever debug state changes (from any viewer)
{ type: 'runs', runs }Reply to subscribe '*'
{ type: 'run.update', run }Pushed to '*' subscribers on run lifecycle changes
{ type: 'replay.start', runId, count }Reply to subscribe of a run
{ type: 'event', runId, envelope }One envelope — replayed history first, then live tail
{ type: 'replay.end', runId }History done; everything after is live
{ type: 'error', message, runId? }Bad request / unroutable control

Viewer → server

MessageMeaning
{ type: 'subscribe', runId }A run id → replay-then-tail; '*' → run-list snapshot + updates
{ type: 'unsubscribe', runId }Stop that subscription
{ type: 'control', envelope }A full control envelope: exec.resume (routed to the app socket owning envelope.runId), or breakpoint.set / breakpoint.clear / mode.set (update server state, relay to all apps, broadcast state)

Replayed envelopes keep their original seq; dedupe on (runId, seq). Control envelopes can be built with createEnvelope from @graphmind-ai/schema with any seq — the server re-mints sequence numbers when relaying to apps.

node:sqlite (built into Node ≥ 22.13, zero native deps), WAL mode, at ~/.graphmind/graphmind.db (override: GRAPHMIND_DB or --db). Events are keyed (run_id, seq) with INSERT OR IGNORE, so replay dedup falls out of the protocol. A single payload larger than 512 KB is stored as a marker so one enormous event cannot bloat the database or wedge the viewer.