Commit Graph

35 Commits

Author SHA1 Message Date
Mika Kuns
c1fc863047 docs(db): mark waiterCount as @internal test helper
Final review flagged that waiterCount looks like a public API but is
only called from the rename-watch flake fix test. The behavior is fine
to keep public (in-memory, safe), but the docstring now matches the
intent so future API-surface scans don't load-bear on it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:45:47 +02:00
Mika Kuns
8c8be67a98 docs: document watch --block push delivery and bootstrap behavior
Adds a Push delivery (watch) section to the root README with exit-code
table, cross-process semantics, and the active-vs-idle latency caveat
that came out of the empirical Claude Code BashOutput test. Adds a brief
reference + cross-link in node/README.md, and notes the SessionStart
bootstrap behavior in plugin/README.md alongside the existing hook
table. Adds /v1/watch to the REST surface table and the watch verb to
the CLI listing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:42:52 +02:00
Mika Kuns
307e15b05b fix(hook): suppress peer list when daemon is unreachable
Fold daemonError into SessionAnnounceOptions so the helper owns the
mutually-exclusive choice between peer list and daemon-down hint. Before
this fix, a session-announce against an unreachable daemon emitted both
"No other mailboxes seen within the last 60 minutes (0 total registered)."
(misleading — the daemon was never asked) AND the daemon-unreachable hint.
Now only the hint appears when the daemon is down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:41:24 +02:00
Mika Kuns
efdc752890 feat(hook): nudge Claude to start watch --block as background bash on session start
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:37:40 +02:00
Mika Kuns
9f8c1d9e9d test(cli): fix flaky rename watch test with deterministic waiter polling
The rename test relied on a fixed 300ms setTimeout to fire after the CLI
subprocess had registered its waiter — adequate in isolation but flaky
under full-suite load on Windows (CLI spawn + first HTTP request can
exceed 300ms). Add a tiny public MailboxStore.waiterCount(name) helper
so the test can poll until the waiter is actually registered before
triggering the rename. Also tighten the missing-name assertion from
not-zero to the contract-exact exit code 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:34:47 +02:00
Mika Kuns
1c2c1d2f7e feat(cli): add watch --block subcommand with documented exit codes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:29:38 +02:00
Mika Kuns
bc53daf6e6 fix(server): set explicit connectionTimeout to bound long-poll sockets
Fastify's default connectionTimeout is 0 (no timeout). With /v1/watch
holding requests open for up to 300s, an OS-level cap prevents a stuck
socket from persisting forever even if app-level cleanup misses a case.
Set just above the watch max so a healthy long-poll never races the
socket timeout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:24:26 +02:00
Mika Kuns
8169ebf4fe refactor(server): clean up close listener and avoid writing 499 to dead socket
Wrap the long-poll handler in try/finally to detach the req.raw close
listener on every resolution path, and use reply.hijack() on the aborted
branch so Fastify does not attempt to write to a socket that's already
closed (which would otherwise emit per-disconnect log noise once the
watcher relaunch loop is busy). Behavior on the wire is unchanged for
the four documented status codes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:23:48 +02:00
Mika Kuns
b05e6f2bd7 feat(server): add GET /v1/watch long-poll endpoint with abort + rename handling
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:19:11 +02:00
Mika Kuns
b74e969229 test(db): add abort-racing-send + rejectAllWaiters coverage; document notifyOneWaiter invariant
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:15:39 +02:00
Mika Kuns
31584fe623 feat(db): add waitForMessage with FIFO single-delivery and rename signaling
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:10:17 +02:00
Mika Kuns
407f3a8f16 chore(repo): drop .NET implementation in favor of npm-only
The Node port (@kuns/claude-mailbox) is the recommended runtime and
covers all supported features. Maintaining a wire-compatible .NET twin
adds dual-impl tax with no remaining users, so remove src/, tests/,
solution + MSBuild files, NuGet config, and both .NET CI workflows.

Docs updated: README "Path C" build-from-source section dropped,
architecture diagram + Development section simplified, and node/README
no longer subtitles itself as a port of the .NET daemon.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:35:31 +02:00
Mika Kuns
75a180279e chore(release): 1.5.2
All checks were successful
Release / release (push) Successful in 7s
CI (Node) / build-test (push) Successful in 9s
Release (Node) / release (push) Successful in 11s
2026-05-20 14:41:56 +02:00
Mika Kuns
f4539eb2c9 chore(release): 1.5.1
All checks were successful
Release / release (push) Successful in 8s
Release (Node) / release (push) Successful in 10s
2026-05-20 14:09:19 +02:00
Mika Kuns
4b93641cf4 fix(server): register onClose hook before app.listen
Fastify forbids addHook after the instance is listening, so the
sweep-timer cleanup hook from 1.5.0 threw on every `serve` startup
and crashed the daemon. Register the hook first, then start
listening, and assign the timer through a ref.
2026-05-20 14:09:11 +02:00
Mika Kuns
2cadc3a867 chore(release): 1.5.0
All checks were successful
CI (Node) / build-test (push) Successful in 8s
Release / release (push) Successful in 8s
Release (Node) / release (push) Successful in 12s
2026-05-20 13:54:43 +02:00
Mika Kuns
0c06e2cf4b feat(cleanup): hide and prune stale mailboxes
Mailbox listings grew unbounded as old sessions ended without
unregistering. This adds two layers of cleanup, configurable via
mailbox.json or `serve` flags:

- Lazy filter: list responses (REST /v1/list, MCP list_mailboxes)
  drop mailboxes idle longer than hideAfterMinutes (default 24h),
  while always keeping the caller and any sender with messages
  pending for them.
- Background sweep: startServer runs an initial prune on boot and
  schedules an unref'd interval timer that hard-deletes mailboxes
  idle longer than deleteAfterMinutes (default 7d) which have no
  pending messages, and wipes their delivered history.
2026-05-20 13:54:03 +02:00
Mika Kuns
06a2ea6b7b chore(release): 1.4.1
All checks were successful
Release / release (push) Successful in 7s
CI (Node) / build-test (push) Successful in 8s
Release (Node) / release (push) Successful in 11s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 13:34:34 +02:00
Mika Kuns
01c22ff9a3 fix(cli): lazy-load server module so non-serve commands skip node:sqlite
All checks were successful
CI (Node) / build-test (push) Successful in 8s
Importing server.js statically also imports db.ts, which pulls in
node:sqlite at startup. On Linux that emits an ExperimentalWarning to
stderr for every CLI invocation -- visible to users running the hook on
every prompt. Defer the server import into the serve action so check
--hook / session-announce / send / peek / list never touch sqlite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 13:30:04 +02:00
Mika Kuns
7b65545600 chore(release): 1.4.0
Some checks failed
CI (Node) / build-test (push) Failing after 7s
Release / release (push) Successful in 7s
Release (Node) / release (push) Failing after 8s
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 13:14:22 +02:00
Mika Kuns
b10ac36ed0 feat(naming)!: auto-derive mailbox name from project + runtime rename
Mailbox names are now built as <project>-<session-short>, where <project>
is the sanitized git-repo basename (or cwd basename) — no more env-var
prefix step. Sessions can re-tag themselves at runtime via the new
mcp__mailbox__rename tool (POST /v1/rename), which transfers all
pending messages to the new name in a single transaction. Peers using
the old name re-discover via list_mailboxes.

BREAKING: \$CLAUDE_MAILBOX_NAME is no longer read. Existing setups that
relied on the env-var prefix should remove it from .claude/settings.json;
the prefix now comes from the working directory automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 13:14:15 +02:00
Mika Kuns
8832eab6c7 refactor(node): migrate from better-sqlite3 to node:sqlite, require Node 24+
Some checks failed
CI (Node) / build-test (push) Failing after 8s
Native binding caused install pain on every new Node major (no prebuilts +
node-gyp needs VS+Windows SDK to fall back). For this project's workload
(a few ops/day, no advanced SQLite features) better-sqlite3's perf edge is
irrelevant — node:sqlite's bundled, ABI-stable sync API is the better fit.

- db.ts: DatabaseSync, db.exec("PRAGMA …"), explicit BEGIN/COMMIT helper to
  replace db.transaction(); row casts go through unknown because node:sqlite
  returns Record<string, SQLOutputValue>.
- package.json: drop better-sqlite3 + @types/better-sqlite3, bump
  engines.node to >=24, vitest 2 → 4 (2.x couldn't resolve `node:sqlite`).
- mailbox-doctor: add Step 1 that enforces Node ≥24 with a concrete fix
  message, renumbers downstream steps.

Node 1.2.0 → 1.3.0. 35 transitive packages removed from the lockfile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:07:21 +02:00
Mika Kuns
d37d2419d6 feat(cli): pre-install port check in install-autostart
All checks were successful
CI (Node) / build-test (push) Successful in 9s
Release / release (push) Successful in 8s
Release (Node) / release (push) Successful in 12s
Before registering the Scheduled Task / Run-key / launchd / systemd unit,
probe /health on the resolved port. If a non-claude-mailbox service
answers, refuse with a helpful hint (`--port <n>` or mailbox.json) so
users don't end up with autostart firing against an occupied port.
Pass --skip-port-check to bypass.

The doctor already had this logic in Step 2; now standalone
install-autostart invocations are protected too.
2026-05-19 14:09:21 +02:00
Mika Kuns
ee0b72f43b feat: change default port from 47822 to 37849 (v1.2.0)
All checks were successful
CI (Node) / build-test (push) Successful in 10s
CI (.NET) / build (push) Successful in 11s
47822 collided with ClaudeDo.Worker.exe on at least one user's machine.
37849 is high, registered to nobody, and avoids the prior conflict.
Both the Node port and the .NET port move together (still
wire-compatible). Defaults change only — if a user has a custom port
in mailbox.json, that stays.
2026-05-19 14:07:56 +02:00
Mika Kuns
d0eb2af183 chore(node): release v1.1.0
Some checks failed
CI (Node) / build-test (push) Successful in 9s
Release / release (push) Successful in 6s
Release (Node) / release (push) Failing after 4s
2026-05-19 13:53:34 +02:00
Mika Kuns
42237149a1 feat: stdio MCP wrapper + Windows Run-key autostart fallback (v1.0.1)
Some checks failed
CI (Node) / build-test (push) Successful in 9s
Release (Node) / release (push) Failing after 1s
Release / release (push) Successful in 7s
Two production-readiness fixes so colleagues can install cleanly:

1. Plugin's MCP server now spawns `claude-mailbox mcp-stdio`, a small
   stdio MCP wrapper that proxies tool calls to the daemon's REST API.
   Claude Code does not support env-var substitution in HTTP MCP `url`
   fields (issue #46889), so the wrapper is the only way to make the
   daemon URL configurable per machine via CLAUDE_MAILBOX_URL.

2. Windows `install-autostart` now falls back from `schtasks /Create`
   to an HKCU\Software\Microsoft\Windows\CurrentVersion\Run entry
   when Group Policy blocks the Scheduled Task path. Both modes are
   per-user, no admin, persist across logoffs. The chosen mode is
   recorded in ~/.claude-mailbox/autostart-mode so status/start/stop/
   uninstall-autostart pick the right cleanup path.

Also bumps the npm version to 1.0.1 to align with the published 1.0.0
plus this patch.
2026-05-19 13:43:55 +02:00
Mika Kuns
ac626f678b fix(cli,plugin): CLAUDE_MAILBOX_URL env override + port-conflict-aware doctor
All checks were successful
CI (Node) / build-test (push) Successful in 9s
The plugin's UserPromptSubmit and SessionStart hooks call `claude-mailbox`
with no --url flag, so they previously always hit the hardcoded
http://127.0.0.1:47822/mcp default. If port 47822 was held by another
local service (e.g. ClaudeDo), the daemon couldn't bind there and every
hook was talking to the wrong process.

CLI default for --url now resolves to $CLAUDE_MAILBOX_URL when set,
falling back to http://127.0.0.1:47822. Doctor gained a Step 2 that
probes /health on 47822, identifies foreign occupants, picks a free
port, writes both ~/.claude-mailbox/mailbox.json and the
CLAUDE_MAILBOX_URL entry in .claude/settings.json env so the hooks
follow along automatically.

Also adds a fallback hint when Windows schtasks /Create fails with
Access is denied (Group Policy restricts non-admin task creation): run
install-autostart from an elevated shell, or accept an ephemeral serve
for the current session.
2026-05-19 13:30:51 +02:00
Mika Kuns
43e8d0d4ca fix(ci): add pretest hook so vitest sees built dist/cli.js
All checks were successful
CI (Node) / build-test (push) Successful in 8s
Release / release (push) Successful in 8s
Release (Node) / release (push) Successful in 13s
2026-05-19 13:12:59 +02:00
Mika Kuns
19d7a591df chore(node): release v0.1.0 2026-05-19 12:01:41 +02:00
Mika Kuns
48b6ba6452 feat(plugin): SessionStart hook discovers and announces active peers
session-announce now calls /v1/list with the session's X-Mailbox header,
which both registers the session with the daemon and returns all known
mailboxes in one round-trip. The output appends an "Active peers" block
listing mailboxes seen within the last hour (configurable via
--peer-window-minutes), capped at 10 entries by default. Self is
filtered out; the list is sorted most-recent-first.

So when the user says "I started a second session, coordinate with it",
Claude already has the peer's mailbox name in context — no manual
list_mailboxes call needed.

The peer-formatting logic is extracted into formatActivePeerList for
unit testing; CLI tests now pin --url to an unreachable port to keep
assertions stable on machines that have a real daemon running.
2026-05-19 11:59:31 +02:00
Mika Kuns
9fd321043f feat(mcp): identity-via-arg + plugin ships .mcp.json
MCP tools (send/check_inbox/peek_inbox/list_mailboxes) now accept the
caller's mailbox name as an explicit argument (from/name), falling back
to the X-Mailbox header for legacy single-session HTTP setups. This
unblocks multi-session coordination through a shared .mcp.json — each
Claude session passes its own session-derived name on every call,
instead of relying on a single transport header that all sessions
would share.

The plugin now ships .mcp.json (no header), and the SessionStart
announcement spells out the exact args to pass to each mcp__mailbox__*
tool so Claude wires it up automatically.
2026-05-19 11:50:58 +02:00
Mika Kuns
462d6561e1 feat(plugin): per-session mailbox identity + mailbox-update command
The hook now derives a unique mailbox name from the session_id supplied
on hook stdin, so two parallel Claude Code sessions in the same project
get distinct mailboxes (e.g. `claude-a8b3c1d2`, `claude-d4e5f6a7`)
instead of colliding on a shared env value. An optional
CLAUDE_MAILBOX_NAME base prefix flavors the names as `<base>-<sid>`.

Adds:
- `claude-mailbox session-announce` subcommand for the new SessionStart
  hook, which prints the current session's mailbox name to context
- `/claude-mailbox:mailbox-update` slash command for `npm update` +
  daemon restart
- stdin parsing helpers (parseHookStdin, deriveSessionName) with unit
  tests; the doctor no longer needs a mandatory name prompt
2026-05-19 11:39:14 +02:00
Mika Kuns
5c5843e62d feat(plugin): ship Claude Code plugin + marketplace manifest
Adds a /plugin marketplace at the repo root and a `claude-mailbox` plugin under
plugin/ that wires the UserPromptSubmit hook without needing the per-user
`install-hook` step. The hook command (`claude-mailbox check --hook`) now reads
the mailbox name from $CLAUDE_MAILBOX_NAME when --name is omitted and emits a
one-line setup hint when the daemon is unreachable, so a missing daemon is loud
instead of invisible.

The plugin only contains the Claude Code glue — the daemon binary is still a
separate prerequisite (`npm i -g @kuns/claude-mailbox` + install-autostart),
and the plugin/README plus main README spell out the three-step setup.
2026-05-19 10:49:36 +02:00
Mika Kuns
66967167bc feat(node): add Claude Code UserPromptSubmit hook for auto inbox-check
Adds `install-hook` / `uninstall-hook` subcommands that idempotently patch
~/.claude/settings.json (or .claude/settings.json with --project), plus a
`--hook` flag on `check` that emits human-readable output and stays silent
on empty inbox or unreachable daemon.
2026-05-19 10:09:30 +02:00
mika kuns
05d87d2aa7 feat(node): add TypeScript sibling project for npm-based install
Some checks failed
CI (Node) / build-test (push) Successful in 6s
CI (.NET) / build (push) Successful in 10s
Release / release (push) Successful in 8s
Release (Node) / release (push) Failing after 10s
Introduces @kuns/claude-mailbox under node/, a wire-compatible TypeScript
port of the .NET daemon that distributes via the public Gitea npm registry.
The .NET project stays in src/ClaudeMailbox/ untouched; users pick whichever
flavor they prefer.

- node/ project: fastify + @modelcontextprotocol/sdk StreamableHTTPServerTransport
  + better-sqlite3, schema and wire surface match the C# version (port 47822,
  X-Mailbox header, MCP tool names, snake_case SQLite columns)
- Cross-platform autostart: Scheduled Task (Win, no admin) / Windows Service
  (Win, --service) / launchd (mac) / systemd --user (linux)
- 9/9 vitest tests pass; end-to-end /health + send/check round-trip verified
- CI split: existing ci.yml/release.yml renamed to *-dotnet.yml with path
  filters, new ci-node.yml and release-node.yml publish to Gitea npm registry
- install.ps1 / install.sh bootstrap one-liners at repo root; homebrew/
  contains a tap formula template
- README install section reordered: npm path primary, dotnet publish secondary

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 14:06:46 +02:00