7.3 KiB
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, slashcommands/, themailbox-collaborateskill). 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/:
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 <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 (<project>-<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): <sanitized-project>-<first-8-hex-of-session-id>. 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 '<new>' 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
NodeNextresolution — relative imports must use the.jsextension even in.tssource (e.g.import { … } from "./db.js"). tsconfig.jsonis strict includingnoUnusedLocals/noUnusedParameters— dead identifiers fail the build.node:sqliteis used directly (nobetter-sqlite3) — Node 24+ requirement comes from this.- The daemon never authenticates: loopback bind + filesystem perms are the trust boundary.
X-Mailboxheader is identity, not auth — anything on loopback can claim any name. Don't add code that assumes otherwise. *.db,*.db-shm,*.db-walare 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.