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:
@@ -2,15 +2,18 @@ import { describe, it, expect } from "vitest";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import {
|
||||
applyInstall,
|
||||
applyUninstall,
|
||||
buildHookCommand,
|
||||
deriveProjectName,
|
||||
deriveSessionName,
|
||||
formatActivePeerList,
|
||||
formatMessagesForHook,
|
||||
parseHookStdin,
|
||||
readSettings,
|
||||
sanitizeProjectName,
|
||||
shortSessionId,
|
||||
writeSettings,
|
||||
type PeerEntry,
|
||||
@@ -205,7 +208,7 @@ describe("parseHookStdin", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("shortSessionId / deriveSessionName", () => {
|
||||
describe("shortSessionId", () => {
|
||||
it("takes first 8 hex chars from a UUID", () => {
|
||||
expect(shortSessionId("abc12345-de67-89f0-1234-567890abcdef")).toBe("abc12345");
|
||||
});
|
||||
@@ -217,26 +220,73 @@ describe("shortSessionId / deriveSessionName", () => {
|
||||
it("falls back to a sanitized prefix for non-hex ids", () => {
|
||||
expect(shortSessionId("session-Test123")).toBe("sessiont");
|
||||
});
|
||||
});
|
||||
|
||||
it("derives anonymous name when no base", () => {
|
||||
expect(deriveSessionName("abc12345-de67-89f0-1234-567890abcdef")).toBe("claude-abc12345");
|
||||
describe("sanitizeProjectName", () => {
|
||||
it("lowercases and replaces non-alnum with dashes", () => {
|
||||
expect(sanitizeProjectName("My Project!")).toBe("my-project");
|
||||
});
|
||||
|
||||
it("prepends base prefix when given", () => {
|
||||
expect(deriveSessionName("abc12345-de67-89f0-1234-567890abcdef", "backend")).toBe(
|
||||
"backend-abc12345",
|
||||
);
|
||||
it("collapses runs of separators", () => {
|
||||
expect(sanitizeProjectName("foo __ bar")).toBe("foo-bar");
|
||||
});
|
||||
|
||||
it("treats whitespace-only base as no base", () => {
|
||||
expect(deriveSessionName("abc12345-de67-89f0-1234-567890abcdef", " ")).toBe(
|
||||
"claude-abc12345",
|
||||
);
|
||||
it("trims leading/trailing dashes", () => {
|
||||
expect(sanitizeProjectName("--foo--")).toBe("foo");
|
||||
});
|
||||
|
||||
it("derives different names for different sessions with the same base", () => {
|
||||
const a = deriveSessionName("aaaa1111-de67-89f0-1234-567890abcdef", "shared");
|
||||
const b = deriveSessionName("bbbb2222-de67-89f0-1234-567890abcdef", "shared");
|
||||
it("returns empty for purely non-alnum input", () => {
|
||||
expect(sanitizeProjectName("---")).toBe("");
|
||||
expect(sanitizeProjectName("")).toBe("");
|
||||
expect(sanitizeProjectName(null)).toBe("");
|
||||
expect(sanitizeProjectName(undefined)).toBe("");
|
||||
});
|
||||
|
||||
it("caps long names", () => {
|
||||
const out = sanitizeProjectName("a".repeat(120));
|
||||
expect(out.length).toBeLessThanOrEqual(40);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deriveProjectName", () => {
|
||||
it("uses cwd basename when not in a git repo", () => {
|
||||
// tmpdir is virtually never inside a git repo; basename is platform-dependent.
|
||||
const got = deriveProjectName(tmpdir());
|
||||
expect(got).toMatch(/^[a-z0-9-]+$/);
|
||||
});
|
||||
|
||||
it("falls back to 'claude' when cwd is empty", () => {
|
||||
expect(deriveProjectName("")).toBe("claude");
|
||||
expect(deriveProjectName(null)).toBe("claude");
|
||||
expect(deriveProjectName(undefined)).toBe("claude");
|
||||
});
|
||||
|
||||
it("uses git toplevel basename when called from inside a repo", () => {
|
||||
// The test harness itself runs inside the claude-mailbox checkout.
|
||||
let inRepo = false;
|
||||
try {
|
||||
execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: "pipe" });
|
||||
inRepo = true;
|
||||
} catch {
|
||||
inRepo = false;
|
||||
}
|
||||
if (!inRepo) return; // CI without git in PATH — skip.
|
||||
const got = deriveProjectName(process.cwd());
|
||||
// Anywhere in the repo, we should resolve to the repo's basename — sanitized.
|
||||
expect(got).toMatch(/^[a-z0-9-]+$/);
|
||||
expect(got.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deriveSessionName", () => {
|
||||
it("composes <project>-<short>", () => {
|
||||
const got = deriveSessionName("abc12345-de67-89f0-1234-567890abcdef", "");
|
||||
expect(got).toBe("claude-abc12345");
|
||||
});
|
||||
|
||||
it("derives different names for different sessions in the same project", () => {
|
||||
const a = deriveSessionName("aaaa1111-de67-89f0-1234-567890abcdef", "");
|
||||
const b = deriveSessionName("bbbb2222-de67-89f0-1234-567890abcdef", "");
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user