fix(cli,plugin): CLAUDE_MAILBOX_URL env override + port-conflict-aware doctor
All checks were successful
CI (Node) / build-test (push) Successful in 9s
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.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 <pid>`; 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": <chosen>}`. 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:<chosen>" }
|
||||
```
|
||||
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:<port>/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 `<base>-<short_session_id>`. Tell the user "Mailbox prefix is set to `X`."
|
||||
- If unset → ✓ tell the user "Mailbox name will be auto-derived (`claude-<short_session_id>`)."
|
||||
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 `<X>`." (real name will be `<X>-<short_session_id>`).
|
||||
- If unset → ✓ "Mailbox name will be auto-derived (`claude-<short_session_id>`)."
|
||||
|
||||
> "Want to flavor your mailbox names with a memorable prefix like `backend` or `frontend`? (yes / no / change to `<x>`)"
|
||||
Ask once: *"Want to flavor your mailbox names with a memorable prefix (e.g., `backend`, `frontend`)? (yes / no / `<name>`)"*
|
||||
|
||||
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 = <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:<port>` 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: <version>
|
||||
daemon: Running (and what you did, if anything)
|
||||
daemon: Running (port: <port>, what you did if anything)
|
||||
health: ok
|
||||
port conflict: none | resolved (moved from 47822 to <port>)
|
||||
base prefix: <name from settings, or "auto-derived (anonymous)">
|
||||
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: <first failure>."
|
||||
|
||||
Reference in New Issue
Block a user