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
This commit is contained in:
@@ -5,14 +5,14 @@ import { resolve } from "node:path";
|
||||
|
||||
const cliPath = resolve(__dirname, "..", "dist", "cli.js");
|
||||
|
||||
function runCli(args: string[], env: Record<string, string | undefined> = {}): {
|
||||
status: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
} {
|
||||
function runCli(
|
||||
args: string[],
|
||||
opts: { env?: Record<string, string | undefined>; stdin?: string } = {},
|
||||
): { status: number; stdout: string; stderr: string } {
|
||||
const r = spawnSync(process.execPath, [cliPath, ...args], {
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, ...env },
|
||||
env: { ...process.env, ...(opts.env ?? {}) },
|
||||
input: opts.stdin,
|
||||
});
|
||||
return {
|
||||
status: r.status ?? -1,
|
||||
@@ -21,6 +21,13 @@ function runCli(args: string[], env: Record<string, string | undefined> = {}): {
|
||||
};
|
||||
}
|
||||
|
||||
const HOOK_STDIN = JSON.stringify({
|
||||
session_id: "abc12345-de67-89f0-1234-567890abcdef",
|
||||
hook_event_name: "UserPromptSubmit",
|
||||
cwd: "/tmp",
|
||||
prompt: "test",
|
||||
});
|
||||
|
||||
describe("`check --hook` CLI behavior", () => {
|
||||
beforeAll(() => {
|
||||
if (!existsSync(cliPath)) {
|
||||
@@ -28,36 +35,87 @@ describe("`check --hook` CLI behavior", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("exits 0 silently when no name resolved (no --name, no env)", () => {
|
||||
const r = runCli(["check", "--hook"], { CLAUDE_MAILBOX_NAME: undefined });
|
||||
it("exits 0 silently when no stdin, no --name, no env", () => {
|
||||
const r = runCli(["check", "--hook"], { env: { CLAUDE_MAILBOX_NAME: undefined } });
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toBe("");
|
||||
expect(r.stderr).toBe("");
|
||||
});
|
||||
|
||||
it("emits daemon-not-reachable hint when name is set but daemon is down", () => {
|
||||
const r = runCli(
|
||||
["check", "--hook", "--url", "http://127.0.0.1:1"],
|
||||
{ CLAUDE_MAILBOX_NAME: "alice" },
|
||||
);
|
||||
it("derives session-id-based name from stdin and emits daemon hint when down", () => {
|
||||
const r = runCli(["check", "--hook", "--url", "http://127.0.0.1:1"], {
|
||||
env: { CLAUDE_MAILBOX_NAME: undefined },
|
||||
stdin: HOOK_STDIN,
|
||||
});
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toContain("[Claude-Mailbox] Daemon not reachable");
|
||||
expect(r.stdout).toContain("http://127.0.0.1:1");
|
||||
expect(r.stdout).toContain("claude-mailbox install-autostart");
|
||||
});
|
||||
|
||||
it("`--name` arg wins over CLAUDE_MAILBOX_NAME env (visible via hint URL/contents)", () => {
|
||||
it("uses base prefix from CLAUDE_MAILBOX_NAME when both env and stdin present", () => {
|
||||
// We can't directly assert the name from --hook output (it's only in the unreachable hint URL).
|
||||
// The hint always contains the URL we passed, so this just confirms the path runs without error.
|
||||
const r = runCli(["check", "--hook", "--url", "http://127.0.0.1:1"], {
|
||||
env: { CLAUDE_MAILBOX_NAME: "backend" },
|
||||
stdin: HOOK_STDIN,
|
||||
});
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toContain("[Claude-Mailbox] Daemon not reachable");
|
||||
});
|
||||
|
||||
it("explicit --name overrides session-id derivation", () => {
|
||||
const r = runCli(
|
||||
["check", "--hook", "--name", "bob", "--url", "http://127.0.0.1:1"],
|
||||
{ CLAUDE_MAILBOX_NAME: "alice" },
|
||||
["check", "--hook", "--name", "explicit", "--url", "http://127.0.0.1:1"],
|
||||
{ env: { CLAUDE_MAILBOX_NAME: "ignored" }, stdin: HOOK_STDIN },
|
||||
);
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toContain("[Claude-Mailbox] Daemon not reachable");
|
||||
});
|
||||
|
||||
it("non-hook mode errors out when no name resolved", () => {
|
||||
const r = runCli(["check"], { CLAUDE_MAILBOX_NAME: undefined });
|
||||
const r = runCli(["check"], { env: { CLAUDE_MAILBOX_NAME: undefined } });
|
||||
expect(r.status).not.toBe(0);
|
||||
expect(r.stderr).toContain("CLAUDE_MAILBOX_NAME");
|
||||
});
|
||||
});
|
||||
|
||||
describe("`session-announce` CLI behavior", () => {
|
||||
beforeAll(() => {
|
||||
if (!existsSync(cliPath)) {
|
||||
throw new Error(`CLI not built. Run \`npm run build\` first. Missing: ${cliPath}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("prints the derived mailbox name from a SessionStart payload", () => {
|
||||
const r = runCli(["session-announce"], {
|
||||
env: { CLAUDE_MAILBOX_NAME: undefined },
|
||||
stdin: HOOK_STDIN,
|
||||
});
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toContain("`claude-abc12345`");
|
||||
expect(r.stdout).toContain("claude-mailbox send");
|
||||
});
|
||||
|
||||
it("uses base prefix when set", () => {
|
||||
const r = runCli(["session-announce"], {
|
||||
env: { CLAUDE_MAILBOX_NAME: "backend" },
|
||||
stdin: HOOK_STDIN,
|
||||
});
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toContain("`backend-abc12345`");
|
||||
});
|
||||
|
||||
it("stays silent when no session_id in stdin", () => {
|
||||
const r = runCli(["session-announce"], {
|
||||
env: { CLAUDE_MAILBOX_NAME: undefined },
|
||||
stdin: JSON.stringify({ hook_event_name: "SessionStart" }),
|
||||
});
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toBe("");
|
||||
});
|
||||
|
||||
it("stays silent when no stdin at all", () => {
|
||||
const r = runCli(["session-announce"], { env: { CLAUDE_MAILBOX_NAME: undefined } });
|
||||
expect(r.status).toBe(0);
|
||||
expect(r.stdout).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user