Files
codingetandClaude ec6c4e8df0 add Dockerfile + deployment docs; make config dbPath optional
node:24-bookworm-slim image with git for the read-only vault endpoints,
non-root user with system-wide git safe.directory (mounted vaults are
usually owned by a host uid), /config + /data + /vaults mount
conventions baked in via BRAINDUMP_CONFIG/BRAINDUMP_DB, and an
/api/health healthcheck. README gains a Deployment section (mounts,
claude-sdk skill/auth requirements in-container, compose example,
reverse-proxy SSE buffering notes).

dbPath in the config file now defaults to ./braindump.sqlite instead of
being required — it was validated before the BRAINDUMP_DB env override
was applied, so the image's baked-in db path couldn't rescue a config
that omitted it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 02:44:06 +00:00

148 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](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).
## Deployment (Docker)
```sh
docker build -t braindump .
```
The image expects three mounts (paths inside the container are up to you; these match the defaults baked into the image via `BRAINDUMP_CONFIG=/config/config.jsonc` and `BRAINDUMP_DB=/data/braindump.sqlite`):
| Mount | Purpose |
|---|---|
| `/config/config.jsonc` | your config — point `vaults[].path` at the container-side vault paths |
| `/data` | persistent volume for the SQLite db |
| `/vaults/...` | your vault(s), read-write |
Backend-specific requirements:
- **`claude-sdk`**: pass `ANTHROPIC_API_KEY`, and mount the brain-dump skill into the container user's home so the SDK's skill loading finds it: `-v ~/.claude/skills/brain-dump:/home/braindump/.claude/skills/brain-dump:ro`.
- **`openai-compat`**: mount the skill file wherever `skillPath` in your config points (e.g. `/config/SKILL.md`), and pass any `${ENV_VAR}` referenced by the config (e.g. `OPENROUTER_API_KEY`).
Example `compose.yaml`:
```yaml
services:
braindump:
build: . # or image: gitea.codinget.me/codinget/braindump
ports: ["3000:3000"]
environment:
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
volumes:
- ./config.jsonc:/config/config.jsonc:ro
- braindump-data:/data
- ~/git/brain:/vaults/brain
- ~/.claude/skills/brain-dump:/home/braindump/.claude/skills/brain-dump:ro
volumes:
braindump-data:
```
with the corresponding config using container paths: `"vaults": [{ "name": "brain", "path": "/vaults/brain" }]` and `"skillPath": "/home/braindump/.claude/skills/brain-dump/SKILL.md"`.
Notes:
- The container runs as a non-root `braindump` user; git `safe.directory` is pre-configured system-wide so the read-only git endpoints work on vaults owned by your host uid. Vault *writes* need the mounted vault to be writable by the container user (`chmod`/`chown` or a matching uid via `user:`).
- The healthcheck and config default both use port 3000; if you change the port, change both (`PORT` env works too, and overrides the config).
- **Reverse proxy + SSE**: disable response buffering for `/api/sessions/*/messages` (nginx: `proxy_buffering off;`, or honor the `X-Accel-Buffering: no` pattern) and raise/disable the proxy read timeout for long turns — heartbeat comments are sent every ~15s to keep the connection alive. This is also where your auth goes.
## 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`).