From ac626f678b66dce16393bdad6100de859cfcb618 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Tue, 19 May 2026 13:30:51 +0200 Subject: [PATCH] fix(cli,plugin): CLAUDE_MAILBOX_URL env override + port-conflict-aware doctor 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. --- node/src/cli.ts | 4 +- node/tests/cli-hook.test.ts | 9 ++++ plugin/commands/mailbox-doctor.md | 77 ++++++++++++++++++++----------- 3 files changed, 62 insertions(+), 28 deletions(-) diff --git a/node/src/cli.ts b/node/src/cli.ts index e7341b1..94919e9 100644 --- a/node/src/cli.ts +++ b/node/src/cli.ts @@ -35,7 +35,9 @@ function readVersion(): string { } } -const DEFAULT_URL = `http://127.0.0.1:${DEFAULT_PORT}`; +const HARDCODED_DEFAULT_URL = `http://127.0.0.1:${DEFAULT_PORT}`; +const ENV_URL = (process.env["CLAUDE_MAILBOX_URL"] ?? "").trim(); +const DEFAULT_URL = ENV_URL || HARDCODED_DEFAULT_URL; async function callJson( method: string, diff --git a/node/tests/cli-hook.test.ts b/node/tests/cli-hook.test.ts index 16ac64f..04dec09 100644 --- a/node/tests/cli-hook.test.ts +++ b/node/tests/cli-hook.test.ts @@ -71,6 +71,15 @@ describe("`check --hook` CLI behavior", () => { expect(r.stdout).toContain("[Claude-Mailbox] Daemon not reachable"); }); + it("uses CLAUDE_MAILBOX_URL env as default base URL when --url is not given", () => { + const r = runCli(["check", "--hook"], { + env: { CLAUDE_MAILBOX_NAME: undefined, CLAUDE_MAILBOX_URL: "http://127.0.0.1:1" }, + stdin: HOOK_STDIN, + }); + expect(r.status).toBe(0); + expect(r.stdout).toContain("[Claude-Mailbox] Daemon not reachable at http://127.0.0.1:1"); + }); + it("non-hook mode errors out when no name resolved", () => { const r = runCli(["check"], { env: { CLAUDE_MAILBOX_NAME: undefined } }); expect(r.status).not.toBe(0); diff --git a/plugin/commands/mailbox-doctor.md b/plugin/commands/mailbox-doctor.md index c84fdbc..5bcfd4d 100644 --- a/plugin/commands/mailbox-doctor.md +++ b/plugin/commands/mailbox-doctor.md @@ -1,11 +1,11 @@ --- -description: Diagnose and auto-fix the Claude-Mailbox setup (binary install, daemon autostart, smoke test, optional base-prefix). +description: Diagnose and auto-fix the Claude-Mailbox setup (binary install, port-conflict detection, daemon autostart, smoke test, optional base-prefix). allowed-tools: Bash, Read, Edit, Write --- You are running the **Claude-Mailbox doctor**. Walk through these checks in order. After each step, print a one-line `✓` / `✗` with the action you took. End with a summary block. -Use `Bash` only for `claude-mailbox` subcommands, `npm`, `where`/`which`, and HTTP probes. Use `Read`/`Edit`/`Write` for `.claude/settings.json`. Never run `sudo` automatically — if elevation is needed, stop and ask. +Use `Bash` only for `claude-mailbox` subcommands, `npm`, `where`/`which`, and HTTP probes. Use `Read`/`Edit`/`Write` for `.claude/settings.json` and `mailbox.json`. Never run `sudo` automatically — if elevation is needed, stop and ask. ## Step 1 — daemon binary on PATH @@ -27,7 +27,31 @@ Run: `claude-mailbox --version` After install, re-run `claude-mailbox --version`. If it still fails, stop and report. -## Step 2 — daemon autostart and running state +## Step 2 — port-conflict check (before autostart!) + +Default port is 47822. Probe whether anything is already on it: + +``` +curl -sf http://127.0.0.1:47822/health +``` + +- **Returns a JSON body with `"status":"ok"` and a `version` field that matches `claude-mailbox --version`** → it's already our daemon, ✓ skip to Step 4. +- **Returns 200 with `"status":"ok"` but a different `version`** → it's an older claude-mailbox; treat as running, ✓. +- **Returns non-200, non-JSON, or any other foreign response** → **port conflict**. Some other process owns 47822. +- **Connection refused** → port is free, ✓ continue to Step 3. + +If port conflict detected: +1. Tell the user which process holds the port (Windows: `Get-NetTCPConnection -LocalPort 47822 | Select-Object OwningProcess`, then `Get-Process -Id `; macOS/Linux: `lsof -i :47822`). +2. Pick a free port. Default suggestion: **47900**. Verify it's free: `curl -sf http://127.0.0.1:47900/health` should fail with connection refused. +3. Read `~/.claude-mailbox/mailbox.json` (create empty `{}` if missing) and merge `{"port": }`. Write back. +4. Also write the override into `.claude/settings.json` env so the plugin's hooks find the right URL: + ```json + "env": { "CLAUDE_MAILBOX_URL": "http://127.0.0.1:" } + ``` + Merge into existing env, preserving other keys. +5. Mark `restart_needed = true`. + +## Step 3 — daemon autostart and running state Run: `claude-mailbox status` @@ -35,51 +59,50 @@ Run: `claude-mailbox status` - `Stopped` → `claude-mailbox start`, re-check. - `NotInstalled` → `claude-mailbox install-autostart`, then `claude-mailbox start`, re-check. -If status doesn't reach `Running`, stop and report. +**If `install-autostart` fails with "Access is denied" on Windows:** Group Policy may block non-admin `schtasks /Create`. Two fallbacks: +1. Tell the user to run `claude-mailbox install-autostart` from an elevated PowerShell themselves (one-time). +2. For this session, run `claude-mailbox serve` as a background process so the rest of the doctor's checks can pass — the daemon won't survive logoff, but that's fine for verification. -## Step 3 — health probe +If status doesn't reach `Running` after the fallback, stop and report. -Hit `http://127.0.0.1:47822/health`. Expect a JSON body with `"status":"ok"`. If unreachable, stop and report — the daemon claims it's running but isn't accepting connections. +## Step 4 — health probe -## Step 4 — mailbox identity +Hit `http://127.0.0.1:/health` (use the configured port, not necessarily 47822). Expect a JSON body with `"status":"ok"` AND a `version` matching `claude-mailbox --version`. If unreachable or version mismatch, stop and report. -**No prompt by default.** Each Claude Code session now gets a unique mailbox name auto-derived from its `session_id` (e.g., `claude-a8b3c1d2`), so two parallel sessions can never collide. +## Step 5 — mailbox identity (base prefix) -Read `.claude/settings.json` in the current working directory and look for `env.CLAUDE_MAILBOX_NAME`. +**No prompt by default.** Each Claude Code session gets a unique mailbox name auto-derived from its `session_id` (e.g., `claude-a8b3c1d2`). -- If set → ✓ this is a **base prefix**. The real name will be `-`. Tell the user "Mailbox prefix is set to `X`." -- If unset → ✓ tell the user "Mailbox name will be auto-derived (`claude-`)." +Read `.claude/settings.json` and look for `env.CLAUDE_MAILBOX_NAME`. -Then **ask** the user (one question, not a deep prompt): +- If set → ✓ "Mailbox prefix is ``." (real name will be `-`). +- If unset → ✓ "Mailbox name will be auto-derived (`claude-`)." -> "Want to flavor your mailbox names with a memorable prefix like `backend` or `frontend`? (yes / no / change to ``)" +Ask once: *"Want to flavor your mailbox names with a memorable prefix (e.g., `backend`, `frontend`)? (yes / no / ``)"* -If they say yes or give a value: -1. Read `.claude/settings.json` (empty `{}` if missing). -2. Merge `env.CLAUDE_MAILBOX_NAME` = chosen value, preserving anything else. -3. Write back with 2-space indentation. -4. Mark this as `restart_needed = true`. +On yes/explicit name: merge `env.CLAUDE_MAILBOX_NAME = ` into `.claude/settings.json`, preserving other keys. Mark `restart_needed = true`. -If they say no or skip → leave as-is. +## Step 6 — smoke test -## Step 5 — smoke test - -Use two ephemeral names (`doctor-probe-a` / `doctor-probe-b`) — we don't need the real session name here, we just need to prove the daemon round-trips: +Use two ephemeral names — we don't need the real session name here: ``` claude-mailbox send --from doctor-probe-a --to doctor-probe-b --body "ping from doctor" claude-mailbox check --name doctor-probe-b ``` -The `check` output must be a JSON array with one message: `from: doctor-probe-a`, body matches. If yes ✓. If no ✗ and report what came back. +(If the port was changed in Step 2, pass `--url http://127.0.0.1:` to both.) -## Step 6 — summary +The `check` output must be a JSON array with one message: `from: doctor-probe-a`, body matches. ✓ on success, ✗ otherwise. + +## Step 7 — summary ``` Claude-Mailbox doctor binary: - daemon: Running (and what you did, if anything) + daemon: Running (port: , what you did if anything) health: ok + port conflict: none | resolved (moved from 47822 to ) base prefix: smoke test: passed | failed restart hint: yes if restart_needed, otherwise no @@ -87,6 +110,6 @@ Claude-Mailbox doctor End with one of: -- All ✓ and no restart needed → "Setup is healthy. Your mailbox name this session will be revealed by the SessionStart hook on next session start (or run `claude-mailbox list` to see active mailboxes)." -- All ✓ and restart needed → "Restart Claude Code (or open a new session) so the SessionStart hook picks up the new base prefix." +- All ✓ and no restart needed → "Setup is healthy. Your mailbox name this session will be revealed by the SessionStart hook on next session start." +- All ✓ and restart needed → "Restart Claude Code (or open a new session) so the SessionStart hook picks up the new env values." - Anything ✗ → "Setup incomplete: ."