Files
boocode/apps/coder/CLAUDE.md
indifferentketchup afaca9e426 feat: MCP {env:VAR} key substitution + coder model/tool-result fixes + docs refactor (v2.7.9)
- MCP secrets: substituteEnvVars recursively resolves {env:NAME} in mcp.json string values from process.env before Zod (opencode-compatible); unset -> '' + boot warning, and invalid-config log names the unset vars (an empty {env:VAR} in a strict url/command field invalidates the whole config)
- data/mcp.json now untracked (.gitignore flips !data/mcp.json -> !data/mcp.example.json); tracked template data/mcp.example.json carries "{env:CONTEXT7_API_KEY}"; .env.example documents the key (9 mcp-config tests)
- Coder fix: message_complete frame model widened string -> string|null (server+web ws-frames parity); dispatcher publishes model: task.model at all 4 external completion points — a null model otherwise fail-closed in publishFrame and dropped the whole frame incl. status:'complete' (regression test)
- Coder fix: claude-sdk mapUserToolResults maps user-message tool_result blocks -> terminal tool_update events (completed/failed w/ output) so tool snapshots resolve instead of spinning forever
- Composer: AgentComposerBar drops §9b resumed/history/new chip + token readout, loses flex-wrap so the row stays one line; CoderPane gains a per-chat localStorage agent-config cache (restores last model on reopen) + threads model into the timeline/chip
- Docs: root CLAUDE.md slimmed (~190 lines), per-app refs split to apps/{coder,server,web}/CLAUDE.md; new docs/coder-backends.md, docs/project-discovery.md, docs/coding-standards/ (cross-app-contract-parity); ARCHITECTURE.md links the backends doc

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:01:03 +00:00

7.9 KiB
Raw Blame History

apps/coder — BooCoder (deep reference)

Per-app engineering notes for apps/coder/src/. BooCoder runs as a systemd service on the host (boocoder.service), NOT in Docker — Fastify at port 9502, postgres at 127.0.0.1:5500. Cross-cutting commands, database, environment, workflow, and cross-app contracts live in the root CLAUDE.md. This file auto-loads when you read/edit files under apps/coder/.

Probe & provider discovery

  • services/provider-registry.ts — Static registry of provider metadata (label, transport, model source). PROVIDERS array, PROVIDERS_BY_NAME map. 5 providers: boocode (native), opencode (acp), goose (pty), claude (pty), qwen (pty). PROBED_AGENT_NAMES derives from it — adding/removing providers means editing this file, not the frontend.
  • services/agent-probe.ts — Startup probe via direct exec() (not SSH): discovers installed agents, versions, ACP support, models. Qwen models from ~/.qwen/settings.json; Claude models static from the registry. Persisted to available_agents.
  • routes/providers.tsGET /api/providers returns installed providers with models. Transport reflects actual capability (checks supports_acp from DB, not just registry preference). The apps/server side is "Provider picker dispatch" (see apps/server/CLAUDE.md).
  • Provider snapshot lifecycle (services/): provider-config.ts (Zod config, never-throws) → provider-config-registry.ts (buildResolvedRegistry, singleton) → provider-snapshot.ts (two-tier probe: tier-1 fast presence, tier-2 cold ACP probe skipped unless force / stale PROVIDER_PROBE_TTL_MS 24h / dbEmpty; cached). Verify live: curl http://100.114.205.53:9502/api/providers/snapshot — returns providers + models + commands, the exact shape AgentComposerBar renders.
  • PATCH /api/providers/config replaces a provider id's override object wholesale (per-id shallow merge) — to flip one field send {...existing, enabled}, or a custom ACP entry's command/label is wiped and it drops out of the resolved registry. data/coder-providers.json is gitignored (live runtime config — the coder reads AND writes it on UI toggles); tracked reference is data/coder-providers.example.json. The loader falls back to {providers:{}} (built-ins only) when absent, so a fresh checkout needs no copy.

Build, deploy, dispatch

  • Workspace dependency on @boocode/server: imports createInferenceRunner, createBroker, ALL_TOOLS, appendMcpTools from the server's compiled dist/. apps/server's package.json has an exports map with types conditions for NodeNext resolution. apps/server must build FIRST.
  • Build + deploy: pnpm -C apps/server build && pnpm -C apps/coder build && sudo systemctl restart boocoder. Env file at apps/coder/.env.host. Service file at /etc/systemd/system/boocoder.service.
  • After pnpm -C apps/coder build the host service keeps running the OLD process until sudo systemctl restart boocoder — a stale process shows new routes 404 with {error:'not found'} while old routes still 200 (the /api not-found handler shape). Restart, don't re-debug.
  • :9502/api/health is down ~1520s after a boocoder restart while the startup agent-probe scan runs — retry; an early connection-refused is not a failed deploy.
  • Agent dispatch spawns binaries directly using install_path from available_agents — no spawn('sh', ['-c', ...]) (fails under systemd). Paseo's pattern: spawn(fullBinaryPath, argsArray, { cwd }).
  • systemd hardening: only NoNewPrivileges=true is safe. ProtectSystem, ProtectHome, PrivateTmp all break agent dispatch (agents need full filesystem access to read configs, write to worktrees).
  • apps/server/tsconfig.json has declaration: true so .d.ts files exist for workspace consumers. The provider's package.json needs exports with types + default conditions per subpath ("./inference": { "types": "./dist/.../index.d.ts", "default": "./dist/.../index.js" }) — without the types condition, NodeNext can't find .d.ts files and tsc fails "Cannot find module" here.
  • Write tools (edit_file, create_file, delete_file, apply_pending, rewind) queue in pending_changes. Nothing hits disk until apply_pending. write_guard.ts validates paths (resolve + prefix-check, no realpath since files may not exist for creates).

Backends

Behavioral overview + flows + data model: see /docs/coder-backends.md. The notes below are the deep per-fact reference.

  • opencode runs as a warm HTTP server (services/backends/opencode-server.tsopencode serve per BooCoder process, one opencode session per BooCode session, resumed via agent_sessions). goose/qwen/claude dispatch one-shot ACP/PTY with no ctx/token usage; only native boocode (llama-swap) tracks ctx.
  • opencode SSE (opencode-server.ts): live streaming is session.next.text.delta / .reasoning.delta / .tool.{called,success,failed} — NOT message.part.* (terminal/post-hoc). client.event.subscribe({ directory }) MUST pass the session's worktree dir; omit it and opencode scopes events to the server process.cwd() → zero session events (empty turns, 180s timeout). Each live session owns its own subscribe loop + AbortController (a sessionID demux guard drops cross-session events when two share a dir). Turn completes on session.idle; promptAsync is fire-and-forget (204).
  • opencode model strings must be provider-prefixed (llama-swap/<model>) AND exist in ~/.config/opencode/opencode.json provider.llama-swap.models — not merely loadable by llama-swap. parseModel infers llama-swap/ for a bare id; the dispatcher coalesces empty→DEFAULT_MODEL then prefixes. agent-probe populates opencode's available_agents.models via mergeLlamaSwap (fetches /v1/models); empty model list → frontend sends '' → no inference (empty turn).
  • agent_sessions resume: config_hash = sha256('opencode_server|<model>') — must NOT include the server port (random per boot; breaks cross-restart resume). Keyed (chat_id, agent) — the tab/chat is the context unit (two opencode tabs = two contexts sharing one worktree). chat_id CASCADEs from chats; session_id/worktree_id are informational SET NULL. The worktrees table (one-per-session, survives session delete) supersedes the defanged session_worktrees. tasks.chat_id threads the tab id to the dispatcher; runOpenCodeServerTask resolves-or-creates a chat when null. The @opencode-ai/sdk v2 client takes flattened params ({sessionID, directory, parts, model:{providerID,modelID}}), createOpencodeClient from @opencode-ai/sdk/v2/client.
  • Claude SDK backend tool RESULTS arrive as type:'user' SDK messages (tool_result content blocks): mapSdkMessage (claude-sdk-map.ts) MUST map the user case → a terminal tool_update (completed/failed + output), else the tool_call persists status:'running' and the UI spinner never stops. The dispatcher's tool_update path then publishes + persists it.
  • ACP command discovery is async: acp-probe.ts must poll after newSession for available_commands_update (commands arrive in a later notification; reading synchronously captures 0). PTY providers (claude) discover from disk via claude-command-discovery.ts (~/.claude/commands + enabledPlugins, bare names, deduped). AgentCommand.kind tags 'command' vs 'skill'; CoderPane's slashGroups splits them into icon'd groups. SlashCommandPicker's groups? prop is opt-in.
  • A new per-message coder field silently drops unless you update every mapper: server read SELECT + mapCoderMessageRow (apps/coder/src/routes/messages.ts), CoderPane.tsx (RawCoderMessage/CoderMessage/mapCoderTimelineRow + the live message_complete WS reducer), CoderMessageWire (CoderMessageList.tsx), and api/types.ts. The client mapCoderTimelineRow whitelists fields — easiest to forget (this is how the model chip silently vanished in the coder).