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>
This commit is contained in:
@@ -1,2 +1,102 @@
|
||||
# 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/<name>/` 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":"<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
|
||||
|
||||
```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`).
|
||||
|
||||
Reference in New Issue
Block a user