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:
Mika Kuns
2026-05-19 11:39:14 +02:00
parent c231f8c18c
commit 462d6561e1
9 changed files with 385 additions and 94 deletions

View File

@@ -2,6 +2,45 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
export interface HookStdinPayload {
session_id?: string;
hook_event_name?: string;
[key: string]: unknown;
}
export function parseHookStdin(raw: string | null | undefined): HookStdinPayload | null {
if (!raw || !raw.trim()) return null;
try {
const parsed = JSON.parse(raw) as unknown;
if (parsed && typeof parsed === "object") return parsed as HookStdinPayload;
return null;
} catch {
return null;
}
}
export function readStdinIfPiped(): string | null {
if (process.stdin.isTTY) return null;
try {
return readFileSync(0, "utf8");
} catch {
return null;
}
}
export function shortSessionId(sessionId: string): string {
const hex = sessionId.replace(/[^0-9a-fA-F]/g, "").toLowerCase();
if (hex.length >= 8) return hex.slice(0, 8);
return sessionId.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 8) || "00000000";
}
export function deriveSessionName(sessionId: string, base?: string | null): string {
const short = shortSessionId(sessionId);
const trimmed = (base ?? "").trim();
if (trimmed) return `${trimmed}-${short}`;
return `claude-${short}`;
}
export interface HookMessage {
id: number;
from: string;