Compare commits
10 Commits
v1.0.0
...
8832eab6c7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8832eab6c7 | ||
|
|
8747d638fb | ||
|
|
d456f29138 | ||
|
|
d37d2419d6 | ||
|
|
ee0b72f43b | ||
|
|
d3abc762fd | ||
|
|
d0eb2af183 | ||
|
|
42237149a1 | ||
|
|
ac626f678b | ||
|
|
73a49e405f |
@@ -36,7 +36,17 @@ jobs:
|
||||
- name: Set package version
|
||||
env:
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
run: npm version --no-git-tag-version "$VERSION"
|
||||
run: npm version --no-git-tag-version --allow-same-version "$VERSION"
|
||||
|
||||
- name: Sync plugin.json version
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
jq --arg v "$VERSION" '.version = $v' plugin/.claude-plugin/plugin.json > plugin/.claude-plugin/plugin.json.tmp
|
||||
mv plugin/.claude-plugin/plugin.json.tmp plugin/.claude-plugin/plugin.json
|
||||
cat plugin/.claude-plugin/plugin.json
|
||||
|
||||
- name: Install
|
||||
run: npm ci
|
||||
@@ -44,9 +54,6 @@ jobs:
|
||||
- name: Test
|
||||
run: npm test
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Pack
|
||||
env:
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
|
||||
10
README.md
10
README.md
@@ -15,7 +15,7 @@ Pick one path. Most users want path A.
|
||||
Inside Claude Code:
|
||||
|
||||
```
|
||||
/plugin marketplace add https://git.kuns.dev/releases/ClaudeMailbox
|
||||
/plugin marketplace add https://git.kuns.dev/releases/ClaudeMailbox.git
|
||||
/plugin install claude-mailbox@claude-mailbox
|
||||
/claude-mailbox:mailbox-doctor
|
||||
```
|
||||
@@ -73,7 +73,7 @@ Then drop this into your project's `.mcp.json`:
|
||||
"mcpServers": {
|
||||
"mailbox": {
|
||||
"type": "http",
|
||||
"url": "http://127.0.0.1:47822/mcp"
|
||||
"url": "http://127.0.0.1:37849/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ dotnet publish src/ClaudeMailbox -c Release -r win-x64 --self-contained -p:Publi
|
||||
Put the resulting `claude-mailbox.exe` on `PATH`. Windows-only `install-service` verbs (admin shell):
|
||||
|
||||
```
|
||||
claude-mailbox install-service [--port 47822] [--bind 127.0.0.1] [--db-path <path>]
|
||||
claude-mailbox install-service [--port 37849] [--bind 127.0.0.1] [--db-path <path>]
|
||||
claude-mailbox uninstall-service [--purge]
|
||||
```
|
||||
|
||||
@@ -127,7 +127,7 @@ claude-mailbox uninstall-autostart [--purge]
|
||||
|
||||
| Platform | Default mechanism | `--service` mechanism |
|
||||
|---|---|---|
|
||||
| Windows | Scheduled Task at logon (no admin) | Windows Service (admin, via `node-windows`) |
|
||||
| Windows | Scheduled Task at logon (no admin); falls back to HKCU Run-key if Group Policy blocks schtasks | Windows Service (admin, via `node-windows`) |
|
||||
| macOS | launchd LaunchAgent in `~/Library/LaunchAgents/` | n/a |
|
||||
| Linux | systemd `--user` unit in `~/.config/systemd/user/` | n/a |
|
||||
|
||||
@@ -193,7 +193,7 @@ CLI flag > mailbox.json > built-in defaults
|
||||
|
||||
`mailbox.json` is searched at `~/.claude-mailbox/mailbox.json` (per-user), and on Windows additionally at `%ProgramData%\ClaudeMailbox\mailbox.json` (machine-wide, written by `--service` install). Override with `--config <path>`.
|
||||
|
||||
Defaults: port `47822`, bind `127.0.0.1`, database at `~/.claude-mailbox/mailbox.db`.
|
||||
Defaults: port `37849`, bind `127.0.0.1`, database at `~/.claude-mailbox/mailbox.db`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -48,5 +48,5 @@ Cost: one local HTTP round-trip plus Node coldstart per prompt (~100ms on Window
|
||||
npm config set //git.kuns.dev/api/packages/releases/npm/:_authToken=<token>
|
||||
```
|
||||
|
||||
`gyp ERR! find VS` on Windows during install
|
||||
: `better-sqlite3` ships prebuilt binaries for current Node LTS versions. If yours isn't covered, npm falls back to building from source and needs the Visual Studio Build Tools. Either install them or pin to a Node version with a matching prebuild.
|
||||
`Cannot find module 'node:sqlite'` or similar
|
||||
: claude-mailbox uses Node's built-in `node:sqlite`, stable since Node 24. On Node 22.5–23.x it works only with `--experimental-sqlite`. Upgrade to Node 24 LTS or newer: `nvm install 24 && nvm use 24` (or `winget install OpenJS.NodeJS.LTS` on Windows).
|
||||
|
||||
2031
node/package-lock.json
generated
2031
node/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kuns/claude-mailbox",
|
||||
"version": "0.1.0",
|
||||
"version": "1.3.0",
|
||||
"description": "Standalone MCP mail server that lets parallel Claude sessions coordinate with each other.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -20,11 +20,10 @@
|
||||
"prepack": "npm run build"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=24"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"better-sqlite3": "^11.3.0",
|
||||
"commander": "^12.1.0",
|
||||
"fastify": "^5.0.0",
|
||||
"zod": "^3.25.0"
|
||||
@@ -33,10 +32,9 @@
|
||||
"node-windows": "^1.0.0-beta.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.11",
|
||||
"@types/node": "^22.7.4",
|
||||
"typescript": "^5.6.2",
|
||||
"vitest": "^2.1.1"
|
||||
"vitest": "^4.1.6"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
|
||||
@@ -1,10 +1,40 @@
|
||||
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { run, cliEntry, type AutostartManager, type AutostartInstallOpts } from "./index.js";
|
||||
import { userConfigPath } from "../config.js";
|
||||
|
||||
function markerPath(): string {
|
||||
return join(dirname(userConfigPath()), MARKER_FILE);
|
||||
}
|
||||
|
||||
function readActiveMode(): "task" | "run-key" | null {
|
||||
const path = markerPath();
|
||||
if (!existsSync(path)) return null;
|
||||
const raw = readFileSync(path, "utf8").trim();
|
||||
if (raw === "task" || raw === "run-key") return raw;
|
||||
return null;
|
||||
}
|
||||
|
||||
function writeActiveMode(mode: "task" | "run-key"): void {
|
||||
const path = markerPath();
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, mode, "utf8");
|
||||
}
|
||||
|
||||
function clearActiveMode(): void {
|
||||
const path = markerPath();
|
||||
if (existsSync(path)) rmSync(path, { force: true });
|
||||
}
|
||||
|
||||
function isAccessDenied(stderr: string): boolean {
|
||||
return /access is denied|0x80070005/i.test(stderr);
|
||||
}
|
||||
|
||||
const TASK_NAME = "ClaudeMailbox";
|
||||
const SERVICE_NAME = "ClaudeMailbox";
|
||||
const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
||||
const RUN_VALUE = "ClaudeMailbox";
|
||||
const MARKER_FILE = "autostart-mode";
|
||||
|
||||
function ensureConfigSeeded(opts: AutostartInstallOpts): string {
|
||||
const path = userConfigPath();
|
||||
@@ -24,10 +54,14 @@ function buildServeCommand(): { node: string; script: string; configPath: string
|
||||
return { node, script, configPath: userConfigPath() };
|
||||
}
|
||||
|
||||
function scheduledTaskInstall(opts: AutostartInstallOpts): void {
|
||||
const configPath = ensureConfigSeeded(opts);
|
||||
function buildServeCommandString(configPath: string): string {
|
||||
const { node, script } = buildServeCommand();
|
||||
const tr = `"${node}" "${script}" serve --config "${configPath}"`;
|
||||
return `"${node}" "${script}" serve --config "${configPath}"`;
|
||||
}
|
||||
|
||||
function tryScheduledTaskInstall(opts: AutostartInstallOpts): { ok: boolean; stderr: string } {
|
||||
const configPath = ensureConfigSeeded(opts);
|
||||
const tr = buildServeCommandString(configPath);
|
||||
const r = run("schtasks.exe", [
|
||||
"/Create",
|
||||
"/SC",
|
||||
@@ -40,28 +74,110 @@ function scheduledTaskInstall(opts: AutostartInstallOpts): void {
|
||||
"LIMITED",
|
||||
"/F",
|
||||
]);
|
||||
if (r.status !== 0) return { ok: false, stderr: r.stderr || r.stdout };
|
||||
run("schtasks.exe", ["/Run", "/TN", TASK_NAME]);
|
||||
return { ok: true, stderr: "" };
|
||||
}
|
||||
|
||||
function runKeyInstall(opts: AutostartInstallOpts): void {
|
||||
const configPath = ensureConfigSeeded(opts);
|
||||
const cmd = buildServeCommandString(configPath);
|
||||
const r = run("reg.exe", [
|
||||
"add",
|
||||
RUN_KEY,
|
||||
"/v",
|
||||
RUN_VALUE,
|
||||
"/t",
|
||||
"REG_SZ",
|
||||
"/d",
|
||||
cmd,
|
||||
"/f",
|
||||
]);
|
||||
if (r.status !== 0) {
|
||||
throw new Error(`schtasks /Create failed (exit ${r.status}): ${r.stderr || r.stdout}`);
|
||||
throw new Error(`reg add (HKCU Run) failed (exit ${r.status}): ${r.stderr || r.stdout}`);
|
||||
}
|
||||
const start = run("schtasks.exe", ["/Run", "/TN", TASK_NAME]);
|
||||
if (start.status !== 0) {
|
||||
console.warn(`Task created but /Run returned ${start.status}: ${start.stderr.trim()}`);
|
||||
spawnRunKeyDaemon(configPath);
|
||||
}
|
||||
|
||||
function spawnRunKeyDaemon(configPath: string): void {
|
||||
if (runKeyDaemonRunning()) return;
|
||||
const { node, script } = buildServeCommand();
|
||||
const ps = `Start-Process -WindowStyle Hidden -FilePath "${node}" -ArgumentList @('"${script}"','serve','--config','"${configPath}"')`;
|
||||
run("powershell.exe", ["-NoProfile", "-Command", ps]);
|
||||
}
|
||||
|
||||
function runKeyDaemonRunning(): boolean {
|
||||
const ps = `Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -like '*claude-mailbox*serve*' } | Select-Object -First 1 -ExpandProperty ProcessId`;
|
||||
const r = run("powershell.exe", ["-NoProfile", "-Command", ps]);
|
||||
return r.status === 0 && r.stdout.trim().length > 0;
|
||||
}
|
||||
|
||||
function killRunKeyDaemon(): void {
|
||||
const ps = `Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -like '*claude-mailbox*serve*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }`;
|
||||
run("powershell.exe", ["-NoProfile", "-Command", ps]);
|
||||
}
|
||||
|
||||
function runKeyUninstall(): void {
|
||||
const r = run("reg.exe", ["delete", RUN_KEY, "/v", RUN_VALUE, "/f"]);
|
||||
if (r.status !== 0 && !/unable to find/i.test(r.stderr)) {
|
||||
throw new Error(`reg delete failed (exit ${r.status}): ${r.stderr || r.stdout}`);
|
||||
}
|
||||
killRunKeyDaemon();
|
||||
}
|
||||
|
||||
function scheduledTaskInstall(opts: AutostartInstallOpts): void {
|
||||
const attempt = tryScheduledTaskInstall(opts);
|
||||
if (attempt.ok) {
|
||||
writeActiveMode("task");
|
||||
return;
|
||||
}
|
||||
if (isAccessDenied(attempt.stderr)) {
|
||||
console.warn(
|
||||
"schtasks /Create denied by Windows policy — falling back to HKCU Run-key autostart (per-user, no admin).",
|
||||
);
|
||||
runKeyInstall(opts);
|
||||
writeActiveMode("run-key");
|
||||
return;
|
||||
}
|
||||
throw new Error(`schtasks /Create failed: ${attempt.stderr}`);
|
||||
}
|
||||
|
||||
function scheduledTaskUninstall(purge: boolean): void {
|
||||
const mode = readActiveMode();
|
||||
if (mode === "run-key") {
|
||||
runKeyUninstall();
|
||||
clearActiveMode();
|
||||
if (purge) purgeData();
|
||||
return;
|
||||
}
|
||||
// Default to task uninstall, also clean up Run-key in case of mixed state
|
||||
run("schtasks.exe", ["/End", "/TN", TASK_NAME]);
|
||||
const r = run("schtasks.exe", ["/Delete", "/TN", TASK_NAME, "/F"]);
|
||||
if (r.status !== 0 && !/cannot find/i.test(r.stderr)) {
|
||||
throw new Error(`schtasks /Delete failed (exit ${r.status}): ${r.stderr || r.stdout}`);
|
||||
if (r.status !== 0 && !/cannot find/i.test(r.stderr) && !/does not exist/i.test(r.stderr)) {
|
||||
// Fall through — try Run-key cleanup anyway
|
||||
}
|
||||
// Best-effort Run-key cleanup
|
||||
run("reg.exe", ["delete", RUN_KEY, "/v", RUN_VALUE, "/f"]);
|
||||
killRunKeyDaemon();
|
||||
clearActiveMode();
|
||||
if (purge) purgeData();
|
||||
}
|
||||
|
||||
function scheduledTaskStatus(): "Running" | "Stopped" | "NotInstalled" {
|
||||
const mode = readActiveMode();
|
||||
if (mode === "run-key") {
|
||||
const r = run("reg.exe", ["query", RUN_KEY, "/v", RUN_VALUE]);
|
||||
if (r.status !== 0) return "NotInstalled";
|
||||
return runKeyDaemonRunning() ? "Running" : "Stopped";
|
||||
}
|
||||
const r = run("schtasks.exe", ["/Query", "/TN", TASK_NAME, "/FO", "LIST", "/V"]);
|
||||
if (r.status !== 0) {
|
||||
if (/cannot find/i.test(r.stderr) || /does not exist/i.test(r.stderr)) return "NotInstalled";
|
||||
if (/cannot find/i.test(r.stderr) || /does not exist/i.test(r.stderr)) {
|
||||
// Maybe a Run-key install happened without a marker (legacy / manual). Check reg.
|
||||
const reg = run("reg.exe", ["query", RUN_KEY, "/v", RUN_VALUE]);
|
||||
if (reg.status === 0) return runKeyDaemonRunning() ? "Running" : "Stopped";
|
||||
return "NotInstalled";
|
||||
}
|
||||
return "Stopped";
|
||||
}
|
||||
if (/Status:\s*Running/i.test(r.stdout)) return "Running";
|
||||
@@ -69,11 +185,22 @@ function scheduledTaskStatus(): "Running" | "Stopped" | "NotInstalled" {
|
||||
}
|
||||
|
||||
function scheduledTaskRun(): void {
|
||||
const mode = readActiveMode();
|
||||
if (mode === "run-key") {
|
||||
const cfgPath = userConfigPath();
|
||||
spawnRunKeyDaemon(cfgPath);
|
||||
return;
|
||||
}
|
||||
const r = run("schtasks.exe", ["/Run", "/TN", TASK_NAME]);
|
||||
if (r.status !== 0) throw new Error(`schtasks /Run failed: ${r.stderr || r.stdout}`);
|
||||
}
|
||||
|
||||
function scheduledTaskEnd(): void {
|
||||
const mode = readActiveMode();
|
||||
if (mode === "run-key") {
|
||||
killRunKeyDaemon();
|
||||
return;
|
||||
}
|
||||
run("schtasks.exe", ["/End", "/TN", TASK_NAME]);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
||||
import { resolveConfig, baseUrl, DEFAULT_PORT } from "./config.js";
|
||||
import { startServer } from "./server.js";
|
||||
import { autostartManager } from "./autostart/index.js";
|
||||
import { runStdioMcp } from "./mcp-stdio.js";
|
||||
import {
|
||||
applyInstall,
|
||||
applyUninstall,
|
||||
@@ -35,7 +36,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,
|
||||
@@ -265,6 +268,20 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command("mcp-stdio")
|
||||
.description(
|
||||
"Run a stdio MCP server that proxies tool calls to the local daemon's REST API. The daemon URL comes from $CLAUDE_MAILBOX_URL (default http://127.0.0.1:37849). Used by the Claude Code plugin's .mcp.json so the URL is configurable per machine without env-substitution in the URL field.",
|
||||
)
|
||||
.action(async () => {
|
||||
try {
|
||||
await runStdioMcp();
|
||||
} catch (err) {
|
||||
console.error(err instanceof Error ? err.message : String(err));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command("install-hook")
|
||||
.description(
|
||||
@@ -328,11 +345,49 @@ program
|
||||
.option("--port <port>", "Port to listen on", (v) => parseInt(v, 10))
|
||||
.option("--bind <address>", "Bind address")
|
||||
.option("--db-path <path>", "SQLite database path")
|
||||
.action(async (opts: { service?: boolean; port?: number; bind?: string; dbPath?: string }) => {
|
||||
const mgr = await autostartManager(opts.service ? "service" : "default");
|
||||
await mgr.install({ port: opts.port, bind: opts.bind, dbPath: opts.dbPath });
|
||||
console.log("Autostart installed.");
|
||||
});
|
||||
.option(
|
||||
"--skip-port-check",
|
||||
"Skip the pre-install probe for a foreign occupant on the daemon's port",
|
||||
)
|
||||
.action(
|
||||
async (opts: {
|
||||
service?: boolean;
|
||||
port?: number;
|
||||
bind?: string;
|
||||
dbPath?: string;
|
||||
skipPortCheck?: boolean;
|
||||
}) => {
|
||||
if (!opts.skipPortCheck) {
|
||||
const cfg = resolveConfig({ port: opts.port, bind: opts.bind, dbPath: opts.dbPath });
|
||||
const probeUrl = `http://${cfg.bind}:${cfg.port}/health`;
|
||||
try {
|
||||
const res = await fetch(probeUrl, { headers: { Accept: "application/json" } });
|
||||
const text = await res.text();
|
||||
let parsed: { status?: string; version?: string } | null = null;
|
||||
try {
|
||||
parsed = text.length ? (JSON.parse(text) as { status?: string; version?: string }) : null;
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
if (res.ok && parsed?.status === "ok" && parsed.version) {
|
||||
console.log(
|
||||
`Port ${cfg.port} already serves a claude-mailbox daemon (version ${parsed.version}). Autostart will manage that one.`,
|
||||
);
|
||||
} else {
|
||||
console.error(
|
||||
`Port ${cfg.port} is held by a non-claude-mailbox service (status ${res.status}). Pick a free port via \`--port <n>\` or write {"port": <n>} to ~/.claude-mailbox/mailbox.json. Use --skip-port-check to bypass.`,
|
||||
);
|
||||
process.exit(4);
|
||||
}
|
||||
} catch {
|
||||
// Connection refused or similar — port is free, proceed.
|
||||
}
|
||||
}
|
||||
const mgr = await autostartManager(opts.service ? "service" : "default");
|
||||
await mgr.install({ port: opts.port, bind: opts.bind, dbPath: opts.dbPath });
|
||||
console.log("Autostart installed.");
|
||||
},
|
||||
);
|
||||
|
||||
program
|
||||
.command("uninstall-autostart")
|
||||
|
||||
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
export const DEFAULT_PORT = 47822;
|
||||
export const DEFAULT_PORT = 37849;
|
||||
export const DEFAULT_BIND = "127.0.0.1";
|
||||
|
||||
export interface FileConfig {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { DatabaseSync, type StatementSync } from "node:sqlite";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
@@ -57,28 +57,44 @@ function parseDate(s: string | null | undefined): Date | null {
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
function runInTransaction<T>(db: DatabaseSync, fn: () => T): T {
|
||||
db.exec("BEGIN");
|
||||
try {
|
||||
const result = fn();
|
||||
db.exec("COMMIT");
|
||||
return result;
|
||||
} catch (err) {
|
||||
try {
|
||||
db.exec("ROLLBACK");
|
||||
} catch {
|
||||
// ignore: original error already on its way up
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export class MailboxStore {
|
||||
private readonly db: Database.Database;
|
||||
private readonly db: DatabaseSync;
|
||||
|
||||
private readonly stmts: {
|
||||
findMailbox: Database.Statement;
|
||||
insertMailbox: Database.Statement;
|
||||
touchMailbox: Database.Statement;
|
||||
listMailboxes: Database.Statement;
|
||||
insertMessage: Database.Statement;
|
||||
countPending: Database.Statement;
|
||||
oldestPending: Database.Statement;
|
||||
selectPending: Database.Statement;
|
||||
markDelivered: Database.Statement;
|
||||
pendingByRecipient: Database.Statement;
|
||||
findMailbox: StatementSync;
|
||||
insertMailbox: StatementSync;
|
||||
touchMailbox: StatementSync;
|
||||
listMailboxes: StatementSync;
|
||||
insertMessage: StatementSync;
|
||||
countPending: StatementSync;
|
||||
oldestPending: StatementSync;
|
||||
selectPending: StatementSync;
|
||||
markDelivered: StatementSync;
|
||||
pendingByRecipient: StatementSync;
|
||||
};
|
||||
|
||||
constructor(public readonly dbPath: string) {
|
||||
mkdirSync(dirname(dbPath), { recursive: true });
|
||||
this.db = new Database(dbPath);
|
||||
this.db.pragma("journal_mode = WAL");
|
||||
this.db.pragma("foreign_keys = ON");
|
||||
for (const sql of DDL_STATEMENTS) this.db.prepare(sql).run();
|
||||
this.db = new DatabaseSync(dbPath);
|
||||
this.db.exec("PRAGMA journal_mode = WAL");
|
||||
this.db.exec("PRAGMA foreign_keys = ON");
|
||||
for (const sql of DDL_STATEMENTS) this.db.exec(sql);
|
||||
|
||||
this.stmts = {
|
||||
findMailbox: this.db.prepare("SELECT * FROM mailboxes WHERE name = ?"),
|
||||
@@ -114,7 +130,7 @@ export class MailboxStore {
|
||||
|
||||
upsertMailbox(name: string): void {
|
||||
const now = nowIso();
|
||||
const existing = this.stmts.findMailbox.get(name) as MailboxRow | undefined;
|
||||
const existing = this.stmts.findMailbox.get(name) as unknown as MailboxRow | undefined;
|
||||
if (existing) {
|
||||
this.stmts.touchMailbox.run(now, name);
|
||||
} else {
|
||||
@@ -123,14 +139,13 @@ export class MailboxStore {
|
||||
}
|
||||
|
||||
send(from: string, to: string, body: string): { id: number; queuedAt: Date } {
|
||||
const tx = this.db.transaction(() => {
|
||||
return runInTransaction(this.db, () => {
|
||||
this.upsertMailbox(from);
|
||||
this.upsertMailbox(to);
|
||||
const createdAt = nowIso();
|
||||
const result = this.stmts.insertMessage.run(to, from, body, createdAt);
|
||||
return { id: Number(result.lastInsertRowid), queuedAt: new Date(createdAt) };
|
||||
});
|
||||
return tx();
|
||||
}
|
||||
|
||||
peek(name: string): InboxStatus {
|
||||
@@ -141,19 +156,18 @@ export class MailboxStore {
|
||||
}
|
||||
|
||||
checkInbox(name: string): MessageRow[] {
|
||||
const tx = this.db.transaction(() => {
|
||||
const pending = this.stmts.selectPending.all(name) as MessageRow[];
|
||||
return runInTransaction(this.db, () => {
|
||||
const pending = this.stmts.selectPending.all(name) as unknown as MessageRow[];
|
||||
if (pending.length > 0) {
|
||||
const ids = pending.map((m) => m.id);
|
||||
this.stmts.markDelivered.run(nowIso(), JSON.stringify(ids));
|
||||
}
|
||||
return pending;
|
||||
});
|
||||
return tx();
|
||||
}
|
||||
|
||||
listMailboxes(forName?: string): MailboxInfo[] {
|
||||
const rows = this.stmts.listMailboxes.all() as MailboxRow[];
|
||||
const rows = this.stmts.listMailboxes.all() as unknown as MailboxRow[];
|
||||
const pendingMap = new Map<string, number>();
|
||||
if (forName) {
|
||||
const counts = this.stmts.pendingByRecipient.all() as { to_mailbox: string; n: number }[];
|
||||
|
||||
165
node/src/mcp-stdio.ts
Normal file
165
node/src/mcp-stdio.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import { DEFAULT_PORT } from "./config.js";
|
||||
|
||||
const HARDCODED_DEFAULT_URL = `http://127.0.0.1:${DEFAULT_PORT}`;
|
||||
|
||||
function resolveDaemonUrl(): string {
|
||||
const env = (process.env["CLAUDE_MAILBOX_URL"] ?? "").trim();
|
||||
if (!env || env.includes("${")) return HARDCODED_DEFAULT_URL;
|
||||
return env.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
function requireIdentity(value: string | undefined, argName: "from" | "name"): string {
|
||||
const v = (value ?? "").trim();
|
||||
if (!v) {
|
||||
throw new Error(
|
||||
`Pass \`${argName}\` (your mailbox name from the SessionStart announcement).`,
|
||||
);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
async function rest(
|
||||
method: "GET" | "POST",
|
||||
url: string,
|
||||
init: { headers?: Record<string, string>; body?: unknown } = {},
|
||||
): Promise<unknown> {
|
||||
const headers: Record<string, string> = { Accept: "application/json", ...(init.headers ?? {}) };
|
||||
let body: string | undefined;
|
||||
if (init.body !== undefined) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
body = JSON.stringify(init.body);
|
||||
}
|
||||
const res = await fetch(url, { method, headers, body });
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
throw new Error(`${method} ${url} → ${res.status}: ${text}`);
|
||||
}
|
||||
return text.length ? JSON.parse(text) : null;
|
||||
}
|
||||
|
||||
export function buildStdioMcpServer(daemonUrl: string = resolveDaemonUrl()): McpServer {
|
||||
const server = new McpServer({ name: "claude-mailbox", version: "1.0.0" });
|
||||
|
||||
server.registerTool(
|
||||
"send",
|
||||
{
|
||||
title: "Send mail",
|
||||
description:
|
||||
"Send a message to another mailbox. Pass `from` with your own mailbox name (see the SessionStart announcement).",
|
||||
inputSchema: {
|
||||
to: z.string().describe("Name of the recipient mailbox."),
|
||||
body: z.string().describe("Message body (plain text or markdown)."),
|
||||
from: z
|
||||
.string()
|
||||
.describe(
|
||||
"Your mailbox name. Take it from the SessionStart announcement.",
|
||||
),
|
||||
},
|
||||
},
|
||||
async ({ to, body, from }) => {
|
||||
const sender = requireIdentity(from, "from");
|
||||
const out = (await rest("POST", `${daemonUrl}/v1/send`, {
|
||||
headers: { "X-Mailbox": sender },
|
||||
body: { to, body },
|
||||
})) as { id: number; queuedAt: string };
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(out) }],
|
||||
structuredContent: out,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"check_inbox",
|
||||
{
|
||||
title: "Check inbox",
|
||||
description:
|
||||
"Pull all undelivered messages for your mailbox and mark them delivered. Pass `name` with your own mailbox name.",
|
||||
inputSchema: {
|
||||
name: z
|
||||
.string()
|
||||
.describe(
|
||||
"Your mailbox name. Take it from the SessionStart announcement.",
|
||||
),
|
||||
},
|
||||
},
|
||||
async ({ name }) => {
|
||||
const me = requireIdentity(name, "name");
|
||||
const messages = (await rest(
|
||||
"POST",
|
||||
`${daemonUrl}/v1/check-inbox?name=${encodeURIComponent(me)}`,
|
||||
{ headers: { "X-Mailbox": me } },
|
||||
)) as { id: number; from: string; body: string; sentAt: string }[];
|
||||
const arr = Array.isArray(messages) ? messages : [];
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(arr) }],
|
||||
structuredContent: { messages: arr },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"peek_inbox",
|
||||
{
|
||||
title: "Peek inbox",
|
||||
description:
|
||||
"Non-consuming check of your mailbox. Returns pending count and oldest pending timestamp. Pass `name` with your own mailbox name.",
|
||||
inputSchema: {
|
||||
name: z
|
||||
.string()
|
||||
.describe(
|
||||
"Your mailbox name. Take it from the SessionStart announcement.",
|
||||
),
|
||||
},
|
||||
},
|
||||
async ({ name }) => {
|
||||
const me = requireIdentity(name, "name");
|
||||
const out = (await rest("GET", `${daemonUrl}/v1/peek?name=${encodeURIComponent(me)}`)) as {
|
||||
pending: number;
|
||||
oldestAt: string | null;
|
||||
};
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(out) }],
|
||||
structuredContent: out,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"list_mailboxes",
|
||||
{
|
||||
title: "List mailboxes",
|
||||
description:
|
||||
"Discover known mailboxes and how many messages each has waiting for you. Pass `name` with your own mailbox name.",
|
||||
inputSchema: {
|
||||
name: z
|
||||
.string()
|
||||
.describe(
|
||||
"Your mailbox name. Take it from the SessionStart announcement.",
|
||||
),
|
||||
},
|
||||
},
|
||||
async ({ name }) => {
|
||||
const me = requireIdentity(name, "name");
|
||||
const list = (await rest("GET", `${daemonUrl}/v1/list`, {
|
||||
headers: { "X-Mailbox": me },
|
||||
})) as { name: string; lastSeenAt: string; pendingForYou: number }[];
|
||||
const arr = Array.isArray(list) ? list : [];
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(arr) }],
|
||||
structuredContent: { mailboxes: arr },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
export async function runStdioMcp(): Promise<void> {
|
||||
const server = buildStdioMcpServer();
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
}
|
||||
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"name": "claude-mailbox",
|
||||
"version": "0.1.0",
|
||||
"description": "Auto-checks the local Claude-Mailbox daemon before every prompt and injects pending messages into the conversation context.",
|
||||
"version": "1.3.0",
|
||||
"description": "Auto-checks the local Claude-Mailbox daemon before every prompt and after each subagent run, and injects pending messages into the conversation context.",
|
||||
"author": {
|
||||
"name": "Mika Kuns"
|
||||
},
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"mailbox": {
|
||||
"type": "http",
|
||||
"url": "http://127.0.0.1:47822/mcp"
|
||||
"type": "stdio",
|
||||
"command": "claude-mailbox",
|
||||
"args": ["mcp-stdio"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ Lets Claude Code pull unread messages from a local `claude-mailbox` daemon befor
|
||||
## Setup (three prompts, all inside Claude Code)
|
||||
|
||||
```
|
||||
/plugin marketplace add https://git.kuns.dev/releases/ClaudeMailbox
|
||||
/plugin marketplace add https://git.kuns.dev/releases/ClaudeMailbox.git
|
||||
/plugin install claude-mailbox@claude-mailbox
|
||||
/claude-mailbox:mailbox-doctor
|
||||
```
|
||||
@@ -14,7 +14,7 @@ The doctor walks the rest:
|
||||
|
||||
1. installs the `claude-mailbox` binary via `npm install -g @kuns/claude-mailbox` if missing (asks first)
|
||||
2. registers the daemon for autostart and starts it if needed
|
||||
3. health-probes `http://127.0.0.1:47822/health`
|
||||
3. health-probes `http://127.0.0.1:37849/health`
|
||||
4. optionally lets you set a **base prefix** (e.g., `backend`) — without one, mailbox names are anonymous (`claude-XXXXXXXX`)
|
||||
5. runs a self → self smoke test
|
||||
|
||||
@@ -39,12 +39,15 @@ The `SessionStart` hook announces the current session's mailbox name in the conv
|
||||
|---|---|---|
|
||||
| `SessionStart` | `claude-mailbox session-announce` | Registers the session with the daemon, then prints (a) this session's mailbox name, (b) the exact `from` / `name` args to pass to MCP tools, and (c) a list of other mailboxes active in the last hour — so Claude knows who's around without needing to call `list_mailboxes` first. |
|
||||
| `UserPromptSubmit` | `claude-mailbox check --hook` | Pulls unread messages for the session's mailbox and injects them as context. Silent on empty inbox; emits a one-line setup hint when the daemon is unreachable. |
|
||||
| `SubagentStop` | `claude-mailbox check --hook` | Same as `UserPromptSubmit`, but fires when a subagent finishes (Task tool). Lets the parent see peer messages that arrived during a long-running subagent run, instead of waiting until the next user prompt. |
|
||||
|
||||
Cost: one local HTTP round-trip per prompt + Node coldstart (~100ms on Windows).
|
||||
Cost: one local HTTP round-trip per prompt and per subagent stop + Node coldstart (~100ms on Windows).
|
||||
|
||||
## MCP tools
|
||||
|
||||
The plugin also ships a `.mcp.json` so Claude has direct access to the mailbox via tool calls. Because the X-Mailbox header would be the same for two parallel sessions sharing one `.mcp.json`, **each MCP tool takes the caller's mailbox name as an explicit argument** (from the SessionStart announcement):
|
||||
The plugin ships a `.mcp.json` that spawns a **stdio MCP wrapper** (`claude-mailbox mcp-stdio`) so the daemon URL is configurable per machine via the `CLAUDE_MAILBOX_URL` env var (Claude Code doesn't yet support env substitution in HTTP MCP URLs — see issue #46889). The wrapper proxies tool calls to the daemon's REST API.
|
||||
|
||||
Each MCP tool takes the caller's mailbox name as an explicit argument (from the SessionStart announcement):
|
||||
|
||||
| Tool | Required args | Purpose |
|
||||
|---|---|---|
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
---
|
||||
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 (Node version, 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`, `node`, `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
|
||||
## Step 1 — Node.js version
|
||||
|
||||
Run: `node --version`
|
||||
|
||||
claude-mailbox uses Node's built-in `node:sqlite` and therefore requires **Node 24 or newer**. Parse the major version from the output.
|
||||
|
||||
- **Major ≥ 24** → ✓ record the version, continue.
|
||||
- **Major == 22 or 23** → ✗ Stop. `node:sqlite` is experimental on these and requires `--experimental-sqlite`. Print:
|
||||
> Found Node `<X.Y.Z>`. claude-mailbox needs Node 24 LTS or newer. Install via `nvm install 24 && nvm use 24` (or `nvs` / `winget install OpenJS.NodeJS.LTS` on Windows), then re-run the doctor.
|
||||
- **Major < 22** → ✗ Stop with the same message; this Node is end-of-life.
|
||||
- **Major ≥ 26** with `better-sqlite3` still installed globally from a previous version → just note: "Node `<X.Y.Z>` is fine for the current claude-mailbox (no native deps); ignore any old `better-sqlite3` build warnings from a prior install."
|
||||
|
||||
If `node --version` itself fails (`command not found`), stop and tell the user to install Node 24+ first.
|
||||
|
||||
## Step 2 — daemon binary on PATH
|
||||
|
||||
Run: `claude-mailbox --version`
|
||||
|
||||
@@ -27,7 +41,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 3 — port-conflict check (before autostart!)
|
||||
|
||||
Default port is 37849. Probe whether anything is already on it:
|
||||
|
||||
```
|
||||
curl -sf http://127.0.0.1:37849/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 5.
|
||||
- **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 37849.
|
||||
- **Connection refused** → port is free, ✓ continue to Step 4.
|
||||
|
||||
If port conflict detected:
|
||||
1. Tell the user which process holds the port (Windows: `Get-NetTCPConnection -LocalPort 37849 | Select-Object OwningProcess`, then `Get-Process -Id <pid>`; macOS/Linux: `lsof -i :37849`).
|
||||
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 4 — daemon autostart and running state
|
||||
|
||||
Run: `claude-mailbox status`
|
||||
|
||||
@@ -35,51 +73,49 @@ 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.
|
||||
**Behavior on `install-autostart`:** The CLI tries a Scheduled Task first (`schtasks /RL LIMITED`, no admin). If Windows Group Policy returns "Access is denied", it falls back transparently to an `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` registry entry plus a hidden `node serve` process — same per-user persistence, no admin needed. The chosen mechanism is recorded in `~/.claude-mailbox/autostart-mode` and respected by `status`/`start`/`stop`/`uninstall-autostart`.
|
||||
|
||||
## Step 3 — health probe
|
||||
If `install-autostart` still fails after both attempts (very rare — would mean both `schtasks` and `reg add` are blocked), stop and report what `status` and `start` printed.
|
||||
|
||||
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 5 — health probe
|
||||
|
||||
## Step 4 — mailbox identity
|
||||
Hit `http://127.0.0.1:<port>/health` (use the configured port, not necessarily 37849). 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 6 — 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 7 — 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 3, 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 8 — summary
|
||||
|
||||
```
|
||||
Claude-Mailbox doctor
|
||||
node: <version>
|
||||
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 37849 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 +123,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>."
|
||||
|
||||
@@ -11,7 +11,7 @@ Print exactly this block, filling in each line:
|
||||
Claude-Mailbox status
|
||||
binary: <output of `claude-mailbox --version`, or "not installed">
|
||||
daemon: <output of `claude-mailbox status`>
|
||||
health: <"ok" if GET http://127.0.0.1:47822/health returns 200, else "unreachable">
|
||||
health: <"ok" if GET http://127.0.0.1:37849/health returns 200, else "unreachable">
|
||||
mailbox name: <value of env.CLAUDE_MAILBOX_NAME in ./.claude/settings.json, or "unset"; also note if ~/.claude/settings.json has a value>
|
||||
pending: <integer count from `claude-mailbox peek --name <resolved-name>` if name is set, else "n/a">
|
||||
```
|
||||
|
||||
@@ -19,6 +19,16 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SubagentStop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "claude-mailbox check --hook"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace ClaudeMailbox.Cli;
|
||||
|
||||
public static class ClientCommands
|
||||
{
|
||||
private const string DefaultUrl = "http://127.0.0.1:47822";
|
||||
private const string DefaultUrl = "http://127.0.0.1:37849";
|
||||
|
||||
public static async Task<int> RunAsync(string[] args)
|
||||
{
|
||||
|
||||
@@ -61,7 +61,7 @@ public static class ServiceCommands
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
var portStr = ClientCommands.GetOption(args, "--port");
|
||||
var port = int.TryParse(portStr, out var p) ? p : 47822;
|
||||
var port = int.TryParse(portStr, out var p) ? p : 37849;
|
||||
var bind = ClientCommands.GetOption(args, "--bind") ?? "127.0.0.1";
|
||||
var dbPath = ClientCommands.GetOption(args, "--db-path") ?? defaultDbPath;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ namespace ClaudeMailbox.Config;
|
||||
|
||||
public sealed class DaemonConfig
|
||||
{
|
||||
public const int DefaultPort = 47822;
|
||||
public const int DefaultPort = 37849;
|
||||
public const string DefaultBindAddress = "127.0.0.1";
|
||||
|
||||
public int Port { get; init; } = DefaultPort;
|
||||
|
||||
Reference in New Issue
Block a user