Splits the previous /opt:/opt:rw bind into two mounts to narrow the writable scope of the container: - /opt:/opt:ro — read-only mount for legacy/existing project add-existing flow. resolveProjectPath still uses PROJECT_ROOT_WHITELIST (/opt by default) so existing projects under /opt/<name> (analytics, boolab, boocode itself) continue to resolve and serve their file-tree via the read-only tools. - /opt/projects:/opt/projects:rw — writable mount targeted at the create-new-project bootstrap path. Picked Option B from the spec (simpler than two scan roots): PROJECT_ROOT_WHITELIST stays /opt, new BOOTSTRAP_ROOT env var defaults to /opt/projects and is used by project_bootstrap.ts as the mkdir target. Bootstrap path-escape check now compares against BOOTSTRAP_ROOT. Prereq: host must `mkdir -p /opt/projects` before next container restart. Documented in CLAUDE.md and .env.example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
8.0 KiB
Markdown
110 lines
8.0 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## What is BooCode
|
||
|
||
Self-hosted single-user developer chat app. AI assistant with read-only file tools (view_file, list_dir, grep, find_files) running against a local llama-swap inference server. Sessions organized by project, with a multi-pane workspace (chat + file browser side by side).
|
||
|
||
## Commands
|
||
|
||
```bash
|
||
# Development (run in separate terminals)
|
||
pnpm dev:server # tsx watch, port 3000
|
||
pnpm dev:web # Vite dev server, port 5173 (proxies /api to :3000)
|
||
|
||
# Build
|
||
pnpm build # builds web then server
|
||
pnpm -C apps/server build # server only (tsc + copy schema.sql)
|
||
pnpm -C apps/web build # web only (vite)
|
||
|
||
# Type checking (no emit)
|
||
npx tsc --noEmit # project references (root)
|
||
npx tsc -p apps/web/tsconfig.app.json --noEmit # web app specifically
|
||
|
||
# IMPORTANT: root tsc --noEmit uses project references and can miss errors
|
||
# that the per-app tsconfig catches. Always verify with the per-app command
|
||
# when editing web code. The server build (pnpm -C apps/server build) is
|
||
# authoritative for server code.
|
||
|
||
# Production
|
||
docker compose build --no-cache boocode && docker compose up -d
|
||
```
|
||
|
||
There are no tests or linters configured.
|
||
|
||
## Architecture
|
||
|
||
**Monorepo**: pnpm workspaces with `apps/server` (Fastify + postgres) and `apps/web` (React + Vite).
|
||
|
||
### Server (`apps/server/src/`)
|
||
|
||
- **Fastify** with `@fastify/websocket` and `@fastify/static` (serves built frontend)
|
||
- **postgres** (porsager/postgres) with tagged-template SQL — no ORM. Schema in `schema.sql`, applied on startup. LSP may false-positive on `sql<Type[]>\`...\`` generics; CLI `tsc` / `pnpm build` is authoritative.
|
||
- **Zod** for request validation and config parsing.
|
||
|
||
Key services:
|
||
- **`services/inference.ts`** — Streams LLM responses, executes tool loops (max 5 depth), flushes to DB every 500ms. Publishes `InferenceFrame` events through the broker.
|
||
- **`services/broker.ts`** — In-memory pub/sub with two channel types: per-session (message streaming) and per-user (sidebar updates). No persistence; clients reconnect on restart.
|
||
- **`services/tools.ts`** — Four read-only file tools exposed as OpenAI function-calling schemas. All file access goes through `path_guard.ts` which resolves against project root.
|
||
- **`services/file_ops.ts`** — Shared file operation implementations used by both inference tools and HTTP routes.
|
||
- **`services/auto_name.ts`** — Non-streaming LLM call to generate 4-word session titles after first assistant reply.
|
||
|
||
Route registration: all routes registered in `index.ts` via `register*Routes(app, sql, ...)` functions. Routes are in `routes/*.ts`.
|
||
|
||
### Frontend (`apps/web/src/`)
|
||
|
||
- **React 18** + React Router v6 + **Tailwind v4** + shadcn/radix-ui primitives.
|
||
- **Shiki** for syntax highlighting (async `codeToHtml` in `CodeBlock.tsx` and `FileViewer` in `FileBrowserPane.tsx`).
|
||
- Path alias: `@/` maps to `src/`.
|
||
|
||
Key patterns:
|
||
- **`hooks/sessionEvents.ts`** — Module-singleton event bus (Set of listeners). Used for cross-component communication: session renames, file-open events, attachment dispatch. 9 event types in the discriminated union. When adding a new event type to the `SessionEvent` union, you must also add a case to the `applyEvent` switch in `useSidebar.ts` (even if it's a no-op `return prev`).
|
||
- **`hooks/useSessionStream.ts`** — WebSocket per session, `applyFrame` reducer builds message list from streaming frames.
|
||
- **`hooks/useUserEvents.ts`** — Single app-level WS to `/api/ws/user` with exponential backoff reconnect. Forwards frames onto the sessionEvents bus.
|
||
- **`hooks/useSidebar.ts`** — Module-singleton with Set<setState> subscriber pattern; one bus subscription guarded by `globalThis.__boocode_sidebar_subscribed` for HMR safety. Every new `SessionEvent` type needs a `case` in the `applyEvent` switch (no-op `return prev` is fine).
|
||
- **`api/client.ts`** — Centralized typed fetch wrapper. All endpoints under `api.*` namespace.
|
||
|
||
### Data flow for chat
|
||
|
||
1. User sends message → POST `/api/sessions/:id/messages` creates user + assistant (status=streaming) rows
|
||
2. `inference.enqueue()` starts async streaming loop
|
||
3. LLM deltas published via `broker.publish(sessionId, frame)`
|
||
4. Client's `useSessionStream` WS receives frames, `applyFrame` reducer updates message list
|
||
5. Tool calls: inference executes tools server-side, publishes tool_call/tool_result frames, loops back to LLM
|
||
6. Terminal states (complete/error): DB updated with final content + token counts, `session_updated` frame published on user channel
|
||
|
||
### Multi-pane workspace
|
||
|
||
Sessions hold 1–5 panes (chat / empty / placeholder terminal+agent). Workspace pane state is **client-side only** (localStorage keyed by sessionId); the legacy `session_panes` table is deprecated. Each chat lives in at most one pane; tab strip is per-pane and tracks `chatIds[]` + `activeChatIdx`. Sessions 1:N chats; chats own messages. Tab reorder via native HTML5 drag events.
|
||
|
||
## Database
|
||
|
||
PostgreSQL 16. Tables: `projects`, `sessions`, `chats`, `messages`, `settings`, `session_panes` (deprecated). Schema applied idempotently on startup via `applySchema()`. Use `clock_timestamp()` (not `NOW()`) inside transactions. CHECK constraints in place: `projects_status_chk` ('open'|'archived'), `sessions_status_chk` (same), `chats_status_chk` (same), `messages_role_chk`, `messages_status_chk` — keep in sync with the `*_STATUSES` const arrays in `apps/server/src/types/api.ts`.
|
||
|
||
Schema CHECK migration order when renaming allowed values: (1) `ALTER TABLE ... DROP CONSTRAINT IF EXISTS <system_name>` (inline `CREATE TABLE` checks get `<table>_<column>_check`), (2) `UPDATE` rows to new values, (3) wrap new constraint ADD in `DO $$ ... pg_constraint` guard — that block is the only way to get `ADD CONSTRAINT IF NOT EXISTS`.
|
||
|
||
Position-shift pattern for panes (legacy `session_panes` table): negate-and-restore to avoid UNIQUE(session_id, position) collisions during reorder/insert/delete. Sentinel value -100 for the moving pane.
|
||
|
||
## Environment
|
||
|
||
Required: `DATABASE_URL`, `LLAMA_SWAP_URL`. Optional: `PORT` (3000), `HOST` (0.0.0.0), `PROJECT_ROOT_WHITELIST` (/opt, read-only scope for add-existing path resolution), `BOOTSTRAP_ROOT` (/opt/projects, writable scope for create-new-project bootstrap mkdir target — host must `mkdir -p /opt/projects` before container start), `DEFAULT_MODEL`, `LOG_LEVEL`.
|
||
|
||
## Workflow
|
||
|
||
- Sam reviews all diffs and commits manually. Do not commit unless explicitly asked.
|
||
- Deploy: `cd /opt/boocode && docker compose up --build -d` (or `docker compose build --no-cache boocode && docker compose up -d` if you suspect a layer-cache issue).
|
||
- Don't accumulate `.bak-*` files. Clean them up in the same batch or immediately after merge.
|
||
- Fastify global JSON parser tolerates empty bodies (overridden in `index.ts`); bodyless POSTs (archive, unarchive, stop) work without setting `Content-Type` tricks on the client.
|
||
- Event dedup discipline: for any mutation the server publishes via `broker.publishUser`, do NOT add a local `sessionEvents.emit(...)` after the API call — `useUserEvents` forwards the WS frame onto the bus. Frontend mutation handlers must be idempotent (dedup by id, no-op on already-present).
|
||
|
||
## Conventions
|
||
|
||
- `overflowWrap` not `wordWrap` — TypeScript's CSSStyleDeclaration marks `wordWrap` as deprecated (error 6385).
|
||
- No app-layer auth. Authelia handles auth at the reverse proxy. All `broker.publishUser`/`subscribeUser` calls use `'default'` as the user key.
|
||
- TypeScript strict mode. Both apps share `tsconfig.base.json`.
|
||
- Server uses NodeNext module resolution (`.js` extensions in imports).
|
||
- Discriminated unions for type narrowing: `Pane` (by `kind`), `SessionEvent` (by `type`), `InferenceFrame` (by `type`).
|
||
- shadcn primitives live in `components/ui/`. Don't modify them unless adding a new primitive.
|
||
- `inferLanguage()` from `lib/attachments.ts` is the canonical file-extension-to-language map. `CodeBlock.tsx` keeps its own `LANG_MAP` because it also resolves markdown fence names.
|