# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Repo layout (important) The active codebase lives entirely in **`node/`** (TypeScript, Node 24+). The top-level `src/ClaudeMailbox/` and `tests/ClaudeMailbox.Tests/` directories are stale .NET build artifacts (`bin/`, `obj/` only) left over from an abandoned C# prototype — ignore them, don't build them, don't grep them. Other top-level dirs: - `plugin/` — the Claude Code plugin (`hooks/hooks.json`, slash `commands/`, the `mailbox-collaborate` skill). Loaded via `.claude-plugin/marketplace.json`. - `homebrew/`, `install.ps1`, `install.sh` — distribution shims for the published npm package `@kuns/claude-mailbox`. - `docs/superpowers/` — design specs and plans (history, not runtime). ## Development commands All commands run from `node/`: ```sh cd node npm install npm run build # tsc → dist/ npm test # pretest builds, then vitest run npm run test:watch # vitest in watch mode npx vitest run tests/cli-watch.test.ts # single test file npx vitest run -t "rename" # by test name pattern npm start # node dist/cli.js serve (run daemon in foreground) ``` `npm test` runs `pretest` (build) first — vitest executes against `dist/`, not via a TS transformer, so always rebuild after editing `src/` if running vitest directly. Vitest config: `tests/**/*.test.ts`, `pool: "forks"`, 15 s timeout. Tests spin up real Fastify servers and real SQLite files in temp dirs — they are integration tests, not unit tests with mocks. ## Runtime CLI surface (after `npm run build`) `dist/cli.js` is the single entry point (bin name `claude-mailbox`): ``` serve | send | peek | check [--hook] | watch --block | list | status session-announce # SessionStart hook helper, reads stdin JSON session-end # SessionEnd hook helper, deletes mailbox if empty install-hook / uninstall-hook # patch settings.json install-autostart / uninstall-autostart [--service] # OS autostart registration mcp-stdio # stdio MCP wrapper that proxies to the HTTP daemon ``` All subcommands accept `--url `; `CLAUDE_MAILBOX_URL` env overrides the default `http://127.0.0.1:37849`. ## Architecture One long-running daemon, single SQLite file, multiple thin clients. ``` clients (Claude sessions, CLI, scripts) │ HTTP loopback ▼ claude-mailbox serve (Fastify, port 37849) ├─ /mcp MCP Streamable HTTP transport (registerMcp) ├─ /v1/send /peek /check-inbox /watch /list REST └─ /health │ ▼ ~/.claude-mailbox/mailbox.db (SQLite WAL, two tables: mailboxes, messages) ``` Module map under `node/src/`: | File | Responsibility | |---|---| | `cli.ts` | Commander CLI; dispatches to every other module. Single entry. | | `server.ts` | Fastify app, request hook that enforces `X-Mailbox` header on non-anonymous paths, REST routes. | | `mcp.ts` | Registers MCP tools (`send`, `check_inbox`, `peek_inbox`, `list_mailboxes`, `rename`) on the same Fastify app at `/mcp`. | | `mcp-stdio.ts` | Stdio MCP wrapper used by the plugin's `.mcp.json` — proxies tool calls to the HTTP daemon (workaround for Claude Code not yet supporting env-var substitution in HTTP MCP URLs). | | `db.ts` | `MailboxStore` — all SQL via `node:sqlite` (no ORM). Owns DDL idempotency, atomic `check_inbox`, rename-with-message-transfer, long-poll `wait` for push delivery. | | `hook.ts` | Helpers shared by `session-announce` / `check --hook` / `install-hook`: stdin parsing, identity derivation (`-<8hex>` from `session_id`), settings.json patching, peer formatting. | | `config.ts` | Config precedence: CLI flag > `mailbox.json` > defaults. Looks in `~/.claude-mailbox/mailbox.json` and on Windows also `%ProgramData%\ClaudeMailbox\mailbox.json`. | | `autostart/{windows,darwin,linux}.ts` | Per-OS autostart: Scheduled Task / HKCU Run / `node-windows` service / launchd LaunchAgent / systemd `--user`. Selected via `autostart/index.ts`. | ### Identity derivation `hook.ts::deriveSessionName` builds the mailbox name from the SessionStart hook's stdin JSON (`session_id`, `cwd`): `-`. Project name is the git repo basename if inside a repo, else the cwd basename, sanitized (lowercased, non-alphanumerics → `-`, capped at 40 chars). Without a cwd it degrades to `claude-<8hex>`. This is how two parallel sessions in the same project stay distinct. ### Push delivery (`watch --block`) Long-poll on `GET /v1/watch?name=…&timeout=…`. The daemon parks the request on an in-memory waiter list keyed by mailbox; `send` wakes the first waiter atomically (FIFO winner across concurrent watchers). Exit codes: `0` delivered (or `Mailbox renamed to ''` on stdout), `2` daemon unreachable, `3` timeout. Push is **opt-in** — the plugin's `SessionStart` hook does *not* launch the watcher automatically; users invoke the `mailbox-collaborate` skill (or `/collaborate`) to enter collaboration mode. The pull path via `UserPromptSubmit` / `SubagentStop` hooks remains the always-on fallback. ### Plugin hooks (what runs without you doing anything) `plugin/hooks/hooks.json` wires five hooks to the installed `claude-mailbox` binary: - `SessionStart` → `claude-mailbox session-announce` (prints identity + active peers into context, registers session with daemon) - `UserPromptSubmit` → `claude-mailbox check --hook` (drains inbox, injects messages) - `SubagentStop` → `claude-mailbox check --hook` (same, when a subagent finishes) - `TaskCompleted` → `claude-mailbox check --hook` (same, when Claude marks a TaskCreate task completed — gives mid-run sync points between todo items) - `SessionEnd` → `claude-mailbox session-end` (deletes the session's mailbox if no pending messages — same semantics as the sweeper, just immediate; renamed mailboxes are preserved because the auto-derived name no longer exists) `install-hook` / `uninstall-hook` patch the same five events into `settings.json` for users not using the plugin. The manual installer is multi-event aware — adding/removing a hook in `plugin/hooks/hooks.json` should be mirrored by `MANAGED_HOOK_EVENTS` and `buildPluginHookCommands` in `hook.ts`. ## Conventions worth knowing - ES modules with `NodeNext` resolution — relative imports must use the `.js` extension even in `.ts` source (e.g. `import { … } from "./db.js"`). - `tsconfig.json` is strict including `noUnusedLocals` / `noUnusedParameters` — dead identifiers fail the build. - `node:sqlite` is used directly (no `better-sqlite3`) — Node 24+ requirement comes from this. - The daemon **never authenticates**: loopback bind + filesystem perms are the trust boundary. `X-Mailbox` header is identity, not auth — anything on loopback can claim any name. Don't add code that assumes otherwise. - `*.db`, `*.db-shm`, `*.db-wal` are gitignored — never commit a real mailbox DB. ## Release `node/package.json` is the published artifact (`@kuns/claude-mailbox`, registry `https://git.kuns.dev/api/packages/releases/npm/`). Version bumps land as `chore(release): X.Y.Z` commits (see git log). The plugin and the npm package version-lockstep — `mailbox-doctor` and `mailbox-update` slash commands run `npm install -g @kuns/claude-mailbox` against that registry.