codingetandClaude a5f62512e8 initial implementation: brain-dump session server with dual LLM backends
Headless Koa v3 server exposing REST + SSE around the brain-dump skill:
- AgentBackend adapter seam with two implementations: Claude Agent SDK
  (skill loading, session resume via sdk_session_id) and a hand-rolled
  OpenAI-compatible tool-calling loop (vault-scoped file tools, SKILL.md
  injected into the system prompt, history persisted in SQLite)
- session CRUD + one-turn-at-a-time SSE streaming (text deltas, file_write
  events, turn_complete), per-session lock, abort on disconnect
- node:sqlite storage (sessions, messages, turn_events) with migrations
- vault allow-list + symlink-aware path-escape prevention (incl. dangling
  symlink defense), read-only git status/diff endpoints, configurable CORS
- deterministic mock chat-completions server (scripts/mock-openai.ts) for
  end-to-end testing without a real model

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:48:17 +00:00

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, 13 questions per message, no extrapolation, TODO markers, projects/<name>/ layout with wikilinks).

Two interchangeable LLM backends, selected per session:

  • claude-sdk — the 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

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":"<code>"}.

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

# 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

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).

S
Description
No description provided
Readme
134 KiB
Languages
TypeScript 99%
Dockerfile 1%