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.
npx graphmind-ai # servenpx graphmind-ai demo # keyless demonpm i -g graphmind-aigraphmindRequires Node ≥ 22.13. SQLite is built into Node, so there are no native dependencies.
graphmind serve (default)
Section titled “graphmind serve (default)”Start the local server and open the viewer. Running graphmind with no command does this.
graphmind # port 4747, open the browsergraphmind --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 browsergraphmind --pause-on-error tool # arm the default error breakpoint on tools onlygraphmind --pause-on-error off # start with no breakpoints at allWho is allowed to connect
Section titled “Who is allowed to connect”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:
| Client | Result |
|---|---|
No Origin header — the SDK, curl, the CLI itself, tests | Allowed. A web page cannot omit it. |
| A loopback origin on this server’s own port — the viewer it serves | Allowed |
| Any other origin | 403 |
A Host header that is not a loopback name | 403 (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:
GRAPHMIND_ALLOWED_ORIGINS=http://localhost:5199,http://127.0.0.1:5199 graphmindgraphmind demo
Section titled “graphmind demo”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.
graphmind demo # replay the recording (no API key)graphmind demo --live # run the real demo agent with your API keygraphmind 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.
graphmind init [dir]
Section titled “graphmind init [dir]”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.
graphmind init # detect and print the stepsgraphmind init --install # also run the package-manager installgraphmind init --write # write a graphmind.example.ts snippet fileDetection 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.
graphmind import <trace-file>
Section titled “graphmind import <trace-file>”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.
graphmind import trace.jsongraphmind import trace.json --db ./x.dbAccepted 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.
graphmind mcp-proxy -- <command>
Section titled “graphmind mcp-proxy -- <command>”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.
graphmind mcp-proxy -- node my-server.jsgraphmind mcp-proxy -- python -m my_servergraphmind mcp-proxy --trace -- ./target/debug/my-serverPoint a host at it by moving the existing command inside the proxy:
claude mcp add my-server-debug -- npx -y graphmind-ai mcp-proxy -- node my-server.js| Flag | Meaning |
|---|---|
--trace | One line per frame on stderr — works with no GraphMind running |
--wait-for-attach | Hold the first frame until the debugger attaches (≤3s), so initialize can be gated |
--inherit-stderr | Give 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.
graphmind mcp
Section titled “graphmind mcp”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.
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.
graphmind runs
Section titled “graphmind runs”List what is stored, and keep it from growing forever.
graphmind runs # list recent runsgraphmind runs --keep 50 # show the 50 newestgraphmind runs --prune # apply the retention policy nowgraphmind runs --prune --keep 50 # ...keeping only the 50 newestgraphmind runs --prune --days 7 # ...and only the last 7 daysgraphmind runs --rm <runId> # delete one rungraphmind runs --clear --yes # delete every runDefault retention is 200 runs / 30 days. --clear refuses to act without --yes.
graphmind record <runId>
Section titled “graphmind record <runId>”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.
graphmind record run_01H8… # → graphmind-run-<runId>.ndjsongraphmind record run_01H8… --out bug-4471.ndjsongraphmind record run_01H8… --html # → graphmind-run-<runId>.htmlgraphmind 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.
Global flags
Section titled “Global flags”| Flag | Meaning |
|---|---|
--port <n> | Port (default 4747; the server always binds 127.0.0.1) |
--db <path> | SQLite database file (default ~/.graphmind/graphmind.db) |
--no-open / --open | Suppress / 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 |
--yes | Confirm a destructive command |
-v, --version | Print the version and exit |
-h, --help | Show help |
Flags accept both --port 4848 and --port=4848.
Environment variables live on their own page: environment.
HTTP & WebSocket endpoints
Section titled “HTTP & WebSocket endpoints”| Endpoint | Purpose |
|---|---|
WS /ingest | Instrumented apps. Schema envelopes; hello → hello.ack handshake (the ack carries current breakpoints + mode). |
WS /ws/ui | Viewers. UI subprotocol (below). |
GET /health | { ok, name, version } |
GET /api/runs | { runs: RunInfo[] }, most recent first |
GET /api/runs/:id/events | Paginated events (?afterSeq=&limit=) |
GET /* | The built viewer (a placeholder page when the bundle is absent) |
GET /api/runs
Section titled “GET /api/runs”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 }.
UI subprotocol (WS /ws/ui)
Section titled “UI subprotocol (WS /ws/ui)”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
| Message | When |
|---|---|
{ 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
| Message | Meaning |
|---|---|
{ 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.
Storage
Section titled “Storage”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.