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>
This commit is contained in:
Mika Kuns
2026-05-20 13:14:15 +02:00
parent 8832eab6c7
commit b10ac36ed0
14 changed files with 441 additions and 95 deletions

View File

@@ -94,6 +94,63 @@ describe("REST surface", () => {
expect(wrong.status).toBe(403);
});
it("POST /v1/rename transfers pending messages and exposes the new name", async () => {
// alice sends to bob-old.
await call("POST", "/v1/send", {
headers: { "X-Mailbox": "alice" },
body: { to: "bob-old", body: "hi old bob" },
});
const rename = await call("POST", "/v1/rename", {
headers: { "X-Mailbox": "bob-old" },
body: { to: "bob-new" },
});
expect(rename.status).toBe(200);
expect(rename.body).toMatchObject({
from: "bob-old",
to: "bob-new",
messagesTransferred: 1,
});
// Peek under new name shows the pending msg; old name is empty.
const peekNew = await call("GET", "/v1/peek?name=bob-new");
expect(peekNew.body).toMatchObject({ pending: 1 });
const peekOld = await call("GET", "/v1/peek?name=bob-old");
expect(peekOld.body).toMatchObject({ pending: 0 });
// check-inbox under new name pulls the message.
const check = await call("POST", "/v1/check-inbox?name=bob-new", {
headers: { "X-Mailbox": "bob-new" },
});
const arr = check.body as Array<{ from: string; body: string }>;
expect(arr).toHaveLength(1);
expect(arr[0]!.body).toBe("hi old bob");
});
it("POST /v1/rename returns 409 when target name is taken", async () => {
await call("POST", "/v1/send", {
headers: { "X-Mailbox": "alice" },
body: { to: "bob", body: "x" },
});
// 'taken' already exists thanks to upsert on X-Mailbox.
const r = await call("POST", "/v1/rename", {
headers: { "X-Mailbox": "bob" },
body: { to: "alice" },
});
expect(r.status).toBe(409);
expect(r.body).toMatchObject({ reason: "target-exists" });
});
it("POST /v1/rename requires X-Mailbox and body.to", async () => {
const missingHeader = await call("POST", "/v1/rename", { body: { to: "x" } });
expect(missingHeader.status).toBe(400);
const missingTo = await call("POST", "/v1/rename", {
headers: { "X-Mailbox": "alice" },
body: {},
});
expect(missingTo.status).toBe(400);
});
it("/v1/list and /v1/peek are anonymous", async () => {
await call("POST", "/v1/send", {
headers: { "X-Mailbox": "alice" },