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)

A forwarded port (SSH -L, VS Code, a devcontainer) passes the Host check whatever port it uses. The Origin check is stricter: the browser’s origin must use the same port number as the server, or be listed in GRAPHMIND_ALLOWED_ORIGINS. With a mismatched port the viewer loads a blank page, because its own scripts are refused. See remote development.

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. In package.json it recognises ai (or @ai-sdk/anthropic, @ai-sdk/openai, @ai-sdk/react), @anthropic-ai/sdk, openai, @modelcontextprotocol/sdk (1.x) and @modelcontextprotocol/server (2.x, with a serveStdio snippet), and @langchain/langgraph, @langchain/core or langchain. In pyproject.toml, requirements.txt, Pipfile or setup.cfg it recognises openai, anthropic, langgraph, langchain, langchain-core and crewai. It does not read a Gemfile: for Ruby, follow Ruby. For an MCP server it also offers graphmind mcp-proxy, which needs no adapter. 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. The proxy speaks up on stderr in three situations:

What happenedstderrExitOn the graph
The command cannot be started (ENOENT; on Windows a .cmd/.bat target, EINVAL)cannot run "<cmd>": <reason> — <hint>127SpawnError on the session node; the run ends error
The server exits before its first JSON-RPC frameThe exit reason and the last 40 lines of its stderrthe server’sMcpServerExitedEarly with the last 200 lines / 32 KB of stderr; the run ends error
The server writes a non-JSON-RPC line to stdoutOne line quoting up to 200 bytes, plus a count at the end of the session—A red stdout noise node under protocol (the first 25 lines)

The environment variables GRAPHMIND_HIDE_*, GRAPHMIND_LOOP_THRESHOLD, GRAPHMIND_ON_LOOP and GRAPHMIND_LOOP_ALLOW apply to the proxy too; see environment. 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.

Secret-looking keys are redacted by default

Section titled “Secret-looking keys are redacted by default”

Both formats replace the value under any secret-looking key with "__REDACTED__", and say how many they replaced:

$ graphmind record run_db4bd5541228 --out a.ndjson
recorded 4 events of run run_db4bd5541228 → …/a.ndjson
redacted 4 values under 4 distinct keys (--no-redact-secrets keeps them)
"input":{"user":"alice","password":"__REDACTED__","api_key":"__REDACTED__","max_tokens":42,"accessToken":"__REDACTED__"}
FlagEffect
--redact-secretsThe default.
--no-redact-secretsKeep every value as recorded; prints secrets NOT redacted (--no-redact-secrets): the file holds every value as recorded. If both flags are given, the last one wins.

The keys: authorization, cookie, set-cookie, api_key, apikey, password, passwd, secret, client_secret, private_key, access_token, refresh_token, token, bearer. A key matches in any case, either whole or as a run of its parts split on _, - or a camelCase boundary:

MatchesDoes not match
Authorization, x-api-key, http_authorization, accessToken, X-Refresh-Token, PassWordmax_tokens, maxTokens, tokenizer, apiKeys, secrets

Over-matching is deliberate: token_count and client_secret_expires_at are redacted because one of their parts is a listed key. Keys are matched at any depth, arrays included. The loop-hold fingerprint (exec.paused.loop.fingerprint) is also replaced, because it is a digest of the whole input.

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
--no-redact-secrets / --redact-secrets(record) keep / replace values under secret-looking keys (default: replace)
--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; hello → hello.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. The server refuses an exec.resume with action inject whose output still contains "__REDACTED__". It answers { type: 'error', runId, message: 'inject refused: …' } and does not relay it.

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. Every size ceiling and timeout is on limits.