Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d37d2419d6 | ||
|
|
ee0b72f43b | ||
|
|
d3abc762fd | ||
|
|
d0eb2af183 | ||
|
|
42237149a1 | ||
|
|
ac626f678b | ||
|
|
73a49e405f |
@@ -36,7 +36,7 @@ jobs:
|
|||||||
- name: Set package version
|
- name: Set package version
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ steps.ver.outputs.version }}
|
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: Install
|
- name: Install
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -44,9 +44,6 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Pack
|
- name: Pack
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ steps.ver.outputs.version }}
|
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:
|
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
|
/plugin install claude-mailbox@claude-mailbox
|
||||||
/claude-mailbox:mailbox-doctor
|
/claude-mailbox:mailbox-doctor
|
||||||
```
|
```
|
||||||
@@ -73,7 +73,7 @@ Then drop this into your project's `.mcp.json`:
|
|||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"mailbox": {
|
"mailbox": {
|
||||||
"type": "http",
|
"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):
|
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]
|
claude-mailbox uninstall-service [--purge]
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ claude-mailbox uninstall-autostart [--purge]
|
|||||||
|
|
||||||
| Platform | Default mechanism | `--service` mechanism |
|
| 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 |
|
| macOS | launchd LaunchAgent in `~/Library/LaunchAgents/` | n/a |
|
||||||
| Linux | systemd `--user` unit in `~/.config/systemd/user/` | 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>`.
|
`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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
4
node/package-lock.json
generated
4
node/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@kuns/claude-mailbox",
|
"name": "@kuns/claude-mailbox",
|
||||||
"version": "0.1.0",
|
"version": "1.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@kuns/claude-mailbox",
|
"name": "@kuns/claude-mailbox",
|
||||||
"version": "0.1.0",
|
"version": "1.2.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kuns/claude-mailbox",
|
"name": "@kuns/claude-mailbox",
|
||||||
"version": "0.1.0",
|
"version": "1.2.0",
|
||||||
"description": "Standalone MCP mail server that lets parallel Claude sessions coordinate with each other.",
|
"description": "Standalone MCP mail server that lets parallel Claude sessions coordinate with each other.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -1,10 +1,40 @@
|
|||||||
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { run, cliEntry, type AutostartManager, type AutostartInstallOpts } from "./index.js";
|
import { run, cliEntry, type AutostartManager, type AutostartInstallOpts } from "./index.js";
|
||||||
import { userConfigPath } from "../config.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 TASK_NAME = "ClaudeMailbox";
|
||||||
const SERVICE_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 {
|
function ensureConfigSeeded(opts: AutostartInstallOpts): string {
|
||||||
const path = userConfigPath();
|
const path = userConfigPath();
|
||||||
@@ -24,10 +54,14 @@ function buildServeCommand(): { node: string; script: string; configPath: string
|
|||||||
return { node, script, configPath: userConfigPath() };
|
return { node, script, configPath: userConfigPath() };
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduledTaskInstall(opts: AutostartInstallOpts): void {
|
function buildServeCommandString(configPath: string): string {
|
||||||
const configPath = ensureConfigSeeded(opts);
|
|
||||||
const { node, script } = buildServeCommand();
|
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", [
|
const r = run("schtasks.exe", [
|
||||||
"/Create",
|
"/Create",
|
||||||
"/SC",
|
"/SC",
|
||||||
@@ -40,28 +74,110 @@ function scheduledTaskInstall(opts: AutostartInstallOpts): void {
|
|||||||
"LIMITED",
|
"LIMITED",
|
||||||
"/F",
|
"/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) {
|
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]);
|
spawnRunKeyDaemon(configPath);
|
||||||
if (start.status !== 0) {
|
}
|
||||||
console.warn(`Task created but /Run returned ${start.status}: ${start.stderr.trim()}`);
|
|
||||||
|
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 {
|
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]);
|
run("schtasks.exe", ["/End", "/TN", TASK_NAME]);
|
||||||
const r = run("schtasks.exe", ["/Delete", "/TN", TASK_NAME, "/F"]);
|
const r = run("schtasks.exe", ["/Delete", "/TN", TASK_NAME, "/F"]);
|
||||||
if (r.status !== 0 && !/cannot find/i.test(r.stderr)) {
|
if (r.status !== 0 && !/cannot find/i.test(r.stderr) && !/does not exist/i.test(r.stderr)) {
|
||||||
throw new Error(`schtasks /Delete failed (exit ${r.status}): ${r.stderr || r.stdout}`);
|
// 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();
|
if (purge) purgeData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduledTaskStatus(): "Running" | "Stopped" | "NotInstalled" {
|
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"]);
|
const r = run("schtasks.exe", ["/Query", "/TN", TASK_NAME, "/FO", "LIST", "/V"]);
|
||||||
if (r.status !== 0) {
|
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";
|
return "Stopped";
|
||||||
}
|
}
|
||||||
if (/Status:\s*Running/i.test(r.stdout)) return "Running";
|
if (/Status:\s*Running/i.test(r.stdout)) return "Running";
|
||||||
@@ -69,11 +185,22 @@ function scheduledTaskStatus(): "Running" | "Stopped" | "NotInstalled" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scheduledTaskRun(): void {
|
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]);
|
const r = run("schtasks.exe", ["/Run", "/TN", TASK_NAME]);
|
||||||
if (r.status !== 0) throw new Error(`schtasks /Run failed: ${r.stderr || r.stdout}`);
|
if (r.status !== 0) throw new Error(`schtasks /Run failed: ${r.stderr || r.stdout}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduledTaskEnd(): void {
|
function scheduledTaskEnd(): void {
|
||||||
|
const mode = readActiveMode();
|
||||||
|
if (mode === "run-key") {
|
||||||
|
killRunKeyDaemon();
|
||||||
|
return;
|
||||||
|
}
|
||||||
run("schtasks.exe", ["/End", "/TN", TASK_NAME]);
|
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 { resolveConfig, baseUrl, DEFAULT_PORT } from "./config.js";
|
||||||
import { startServer } from "./server.js";
|
import { startServer } from "./server.js";
|
||||||
import { autostartManager } from "./autostart/index.js";
|
import { autostartManager } from "./autostart/index.js";
|
||||||
|
import { runStdioMcp } from "./mcp-stdio.js";
|
||||||
import {
|
import {
|
||||||
applyInstall,
|
applyInstall,
|
||||||
applyUninstall,
|
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(
|
async function callJson(
|
||||||
method: string,
|
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
|
program
|
||||||
.command("install-hook")
|
.command("install-hook")
|
||||||
.description(
|
.description(
|
||||||
@@ -328,11 +345,49 @@ program
|
|||||||
.option("--port <port>", "Port to listen on", (v) => parseInt(v, 10))
|
.option("--port <port>", "Port to listen on", (v) => parseInt(v, 10))
|
||||||
.option("--bind <address>", "Bind address")
|
.option("--bind <address>", "Bind address")
|
||||||
.option("--db-path <path>", "SQLite database path")
|
.option("--db-path <path>", "SQLite database path")
|
||||||
.action(async (opts: { service?: boolean; port?: number; bind?: string; dbPath?: string }) => {
|
.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");
|
const mgr = await autostartManager(opts.service ? "service" : "default");
|
||||||
await mgr.install({ port: opts.port, bind: opts.bind, dbPath: opts.dbPath });
|
await mgr.install({ port: opts.port, bind: opts.bind, dbPath: opts.dbPath });
|
||||||
console.log("Autostart installed.");
|
console.log("Autostart installed.");
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
program
|
program
|
||||||
.command("uninstall-autostart")
|
.command("uninstall-autostart")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|||||||
import { homedir } from "node:os";
|
import { homedir } from "node:os";
|
||||||
import { join, resolve } from "node:path";
|
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 const DEFAULT_BIND = "127.0.0.1";
|
||||||
|
|
||||||
export interface FileConfig {
|
export interface FileConfig {
|
||||||
|
|||||||
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");
|
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", () => {
|
it("non-hook mode errors out when no name resolved", () => {
|
||||||
const r = runCli(["check"], { env: { CLAUDE_MAILBOX_NAME: undefined } });
|
const r = runCli(["check"], { env: { CLAUDE_MAILBOX_NAME: undefined } });
|
||||||
expect(r.status).not.toBe(0);
|
expect(r.status).not.toBe(0);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"mailbox": {
|
"mailbox": {
|
||||||
"type": "http",
|
"type": "stdio",
|
||||||
"url": "http://127.0.0.1:47822/mcp"
|
"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)
|
## 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
|
/plugin install claude-mailbox@claude-mailbox
|
||||||
/claude-mailbox:mailbox-doctor
|
/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)
|
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
|
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`)
|
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
|
5. runs a self → self smoke test
|
||||||
|
|
||||||
@@ -44,7 +44,9 @@ Cost: one local HTTP round-trip per prompt + Node coldstart (~100ms on Windows).
|
|||||||
|
|
||||||
## MCP tools
|
## 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 |
|
| Tool | Required args | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
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 (binary install, port-conflict detection, daemon autostart, smoke test, optional base-prefix).
|
||||||
allowed-tools: Bash, Read, Edit, Write
|
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.
|
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`, `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 — daemon binary on PATH
|
||||||
|
|
||||||
@@ -27,7 +27,31 @@ Run: `claude-mailbox --version`
|
|||||||
|
|
||||||
After install, re-run `claude-mailbox --version`. If it still fails, stop and report.
|
After install, re-run `claude-mailbox --version`. If it still fails, stop and report.
|
||||||
|
|
||||||
## Step 2 — daemon autostart and running state
|
## Step 2 — 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 4.
|
||||||
|
- **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 3.
|
||||||
|
|
||||||
|
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 3 — daemon autostart and running state
|
||||||
|
|
||||||
Run: `claude-mailbox status`
|
Run: `claude-mailbox status`
|
||||||
|
|
||||||
@@ -35,51 +59,48 @@ Run: `claude-mailbox status`
|
|||||||
- `Stopped` → `claude-mailbox start`, re-check.
|
- `Stopped` → `claude-mailbox start`, re-check.
|
||||||
- `NotInstalled` → `claude-mailbox install-autostart`, then `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 4 — 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 5 — 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`."
|
Read `.claude/settings.json` and look for `env.CLAUDE_MAILBOX_NAME`.
|
||||||
- If unset → ✓ tell the user "Mailbox name will be auto-derived (`claude-<short_session_id>`)."
|
|
||||||
|
|
||||||
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:
|
On yes/explicit name: merge `env.CLAUDE_MAILBOX_NAME = <name>` into `.claude/settings.json`, preserving other keys. Mark `restart_needed = true`.
|
||||||
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`.
|
|
||||||
|
|
||||||
If they say no or skip → leave as-is.
|
## Step 6 — smoke test
|
||||||
|
|
||||||
## Step 5 — smoke test
|
Use two ephemeral names — we don't need the real session name here:
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
claude-mailbox send --from doctor-probe-a --to doctor-probe-b --body "ping from doctor"
|
claude-mailbox send --from doctor-probe-a --to doctor-probe-b --body "ping from doctor"
|
||||||
claude-mailbox check --name doctor-probe-b
|
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 2, 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 7 — summary
|
||||||
|
|
||||||
```
|
```
|
||||||
Claude-Mailbox doctor
|
Claude-Mailbox doctor
|
||||||
binary: <version>
|
binary: <version>
|
||||||
daemon: Running (and what you did, if anything)
|
daemon: Running (port: <port>, what you did if anything)
|
||||||
health: ok
|
health: ok
|
||||||
|
port conflict: none | resolved (moved from 37849 to <port>)
|
||||||
base prefix: <name from settings, or "auto-derived (anonymous)">
|
base prefix: <name from settings, or "auto-derived (anonymous)">
|
||||||
smoke test: passed | failed
|
smoke test: passed | failed
|
||||||
restart hint: yes if restart_needed, otherwise no
|
restart hint: yes if restart_needed, otherwise no
|
||||||
@@ -87,6 +108,6 @@ Claude-Mailbox doctor
|
|||||||
|
|
||||||
End with one of:
|
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 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 base prefix."
|
- 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>."
|
- Anything ✗ → "Setup incomplete: <first failure>."
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Print exactly this block, filling in each line:
|
|||||||
Claude-Mailbox status
|
Claude-Mailbox status
|
||||||
binary: <output of `claude-mailbox --version`, or "not installed">
|
binary: <output of `claude-mailbox --version`, or "not installed">
|
||||||
daemon: <output of `claude-mailbox status`>
|
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>
|
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">
|
pending: <integer count from `claude-mailbox peek --name <resolved-name>` if name is set, else "n/a">
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace ClaudeMailbox.Cli;
|
|||||||
|
|
||||||
public static class ClientCommands
|
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)
|
public static async Task<int> RunAsync(string[] args)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ public static class ServiceCommands
|
|||||||
if (!File.Exists(configPath))
|
if (!File.Exists(configPath))
|
||||||
{
|
{
|
||||||
var portStr = ClientCommands.GetOption(args, "--port");
|
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 bind = ClientCommands.GetOption(args, "--bind") ?? "127.0.0.1";
|
||||||
var dbPath = ClientCommands.GetOption(args, "--db-path") ?? defaultDbPath;
|
var dbPath = ClientCommands.GetOption(args, "--db-path") ?? defaultDbPath;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ namespace ClaudeMailbox.Config;
|
|||||||
|
|
||||||
public sealed class DaemonConfig
|
public sealed class DaemonConfig
|
||||||
{
|
{
|
||||||
public const int DefaultPort = 47822;
|
public const int DefaultPort = 37849;
|
||||||
public const string DefaultBindAddress = "127.0.0.1";
|
public const string DefaultBindAddress = "127.0.0.1";
|
||||||
|
|
||||||
public int Port { get; init; } = DefaultPort;
|
public int Port { get; init; } = DefaultPort;
|
||||||
|
|||||||
Reference in New Issue
Block a user