# braindump A headless server that runs brain-dump note-taking sessions into an Obsidian-style vault, exposing a web API meant to be consumed by a web UI or Obsidian plugin. An LLM agent interviews you conversationally and writes linked markdown notes into the vault as the conversation progresses, following the `brain-dump` skill (incremental writes, 1–3 questions per message, no extrapolation, TODO markers, `projects//` layout with wikilinks). Two interchangeable LLM backends, selected per session: - **`claude-sdk`** — the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk). Gets file tools, skill loading (`~/.claude/skills/brain-dump`), and session resume from the SDK. Uses your Anthropic credentials. - **`openai-compat`** — any OpenAI-compatible chat-completions endpoint (openrouter, vllm, ollama). A built-in agent loop supplies vault-scoped file tools and injects the skill into the system prompt. ## Requirements - Node ≥ 22 (uses `node:sqlite`) - For `claude-sdk`: Anthropic credentials (the SDK inherits Claude Code auth or `ANTHROPIC_API_KEY`) ## Setup ```sh npm install cp config.example.jsonc config.jsonc # edit: vaults, backends, models npm run web # boots, auto-migrates the db, listens ``` Config is JSONC; path from `BRAINDUMP_CONFIG` (default `./config.jsonc`). `"${ENV_VAR}"` string values are resolved from the environment; `~` is expanded in paths. Only vaults listed in `vaults` are writable — that allow-list is the write fence. > **Note:** an exported `PORT` env var overrides the config file's `port` (watch out under code-server, which exports `PORT` globally). `BRAINDUMP_DB` overrides `dbPath`. There is **no auth** — put it behind an authenticating reverse proxy. CORS origins are configurable (`app://obsidian.md` is in the example for Obsidian plugins). ## API Base path `/api`. Errors are `{"ok":false,"error":""}`. | Method | Path | Description | |---|---|---| | GET | `/health` | liveness | | GET | `/vaults` | configured vaults | | POST | `/sessions` | create session — `{vault, topic, backend?, model?}` | | GET | `/sessions[?status=]` | list sessions | | GET | `/sessions/:id` | session + transcript | | PATCH | `/sessions/:id` | update `{topic?, status?}` | | DELETE | `/sessions/:id[?hard=true]` | archive (soft) or delete row; never touches vault files | | POST | `/sessions/:id/messages` | send a user message — **response is an SSE stream** for the turn | | GET | `/sessions/:id/file?path=` | read a vault file (path-validated) | | GET | `/sessions/:id/git/status` | `git status --porcelain`, parsed | | GET | `/sessions/:id/git/diff[?path=]` | raw diff (read-only; commits are always left to you) | ### The turn stream `POST /sessions/:id/messages` with `{"text": "..."}` responds with `text/event-stream`: ``` event: text data: {"delta":"What's the goal of the project?"} event: tool_start data: {"tool":"write_file","path":"projects/x/x.md"} event: file_write data: {"path":"projects/x/x.md","bytes":412} event: tool_end data: {"tool":"write_file","ok":true} event: assistant_message data: {"text":"...full assistant text..."} event: turn_complete data: {"turnCount":1} ``` Consume with `fetch` + a ReadableStream reader (or `curl -N`). One turn at a time per session — a concurrent POST gets `409 turn_in_progress`. Closing the connection aborts the turn; files already written stay (writes are incremental by design). Heartbeat comments keep proxies from timing out. ## Curl walkthrough ```sh # create a session SID=$(curl -s localhost:3000/api/sessions -H 'content-type: application/json' \ -d '{"vault":"brain","topic":"my-project","backend":"claude-sdk"}' | jq -r .session.id) # first turn — watch text stream and files land curl -N localhost:3000/api/sessions/$SID/messages -H 'content-type: application/json' \ -d '{"text":"Brain dump my-project: a CLI tool that does X, written in Go, status: early prototype."}' # what changed in the vault? curl -s localhost:3000/api/sessions/$SID/git/status | jq curl -s "localhost:3000/api/sessions/$SID/file?path=projects/my-project/my-project.md" | jq -r .content # keep going — the agent asks 1-3 questions per turn and extends the notes curl -N localhost:3000/api/sessions/$SID/messages -H 'content-type: application/json' \ -d '{"text":"It also has a TUI mode. TODO on my side: pick a license."}' # review + commit yourself when happy (braindump never commits) git -C ~/git/brain status ``` ## Development ```sh npm run typecheck npm test # pathSafety + Db unit tests PORT=11500 npx tsx scripts/mock-openai.ts # deterministic mock chat-completions server ``` `scripts/mock-openai.ts` scripts a full tool-calling session (including a vault-escape probe when the user message contains `ESCAPE`), so the whole openai-compat path can be tested end-to-end without a real model — point `backends.openai-compat.baseUrl` at it. ### Layout - `src/agent/` — `AgentBackend` interface + the two adapters (`claudeSdk/`, `openaiCompat/`) and the stub - `src/vault/` — vault allow-list resolution, `pathSafety` (symlink-aware escape prevention), read-only git helpers - `src/webapi/` — Koa router, SSE plumbing, middleware - `src/db/` — `node:sqlite` wrapper + migrations (sessions, messages, turn_events) Backends own message persistence: `openai-compat` stores its full replayable history in SQLite; `claude-sdk` stores display rows and resumes via the SDK's own transcript (`sdk_session_id`).