From fed74dc8f0d021ecfea7deb10e320ae33092740e Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Wed, 27 May 2026 13:16:19 +0200 Subject: [PATCH] docs: document TaskCompleted hook in CLAUDE.md --- CLAUDE.md | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8d4d80c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,109 @@ +# 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.