3 Commits

Author SHA1 Message Date
Mika Kuns
d456f29138 fix(plugin): sync plugin.json version with releases
All checks were successful
Release / release (push) Successful in 8s
Release (Node) / release (push) Successful in 12s
plugin.json was stuck at 0.1.0 across every release, so Claude Code's
per-version plugin cache never refreshed and clients kept running the
original .mcp.json (http://127.0.0.1:47822/mcp). Bump to 1.2.0 to match
the node package and add a release-workflow step that derives plugin.json
from the tag on every future release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:51:34 +02:00
Mika Kuns
d37d2419d6 feat(cli): pre-install port check in install-autostart
All checks were successful
CI (Node) / build-test (push) Successful in 9s
Release / release (push) Successful in 8s
Release (Node) / release (push) Successful in 12s
Before registering the Scheduled Task / Run-key / launchd / systemd unit,
probe /health on the resolved port. If a non-claude-mailbox service
answers, refuse with a helpful hint (`--port <n>` or mailbox.json) so
users don't end up with autostart firing against an occupied port.
Pass --skip-port-check to bypass.

The doctor already had this logic in Step 2; now standalone
install-autostart invocations are protected too.
2026-05-19 14:09:21 +02:00
Mika Kuns
ee0b72f43b feat: change default port from 47822 to 37849 (v1.2.0)
All checks were successful
CI (Node) / build-test (push) Successful in 10s
CI (.NET) / build (push) Successful in 11s
47822 collided with ClaudeDo.Worker.exe on at least one user's machine.
37849 is high, registered to nobody, and avoids the prior conflict.
Both the Node port and the .NET port move together (still
wire-compatible). Defaults change only — if a user has a custom port
in mailbox.json, that stays.
2026-05-19 14:07:56 +02:00
13 changed files with 73 additions and 25 deletions

View File

@@ -38,6 +38,16 @@ jobs:
VERSION: ${{ steps.ver.outputs.version }} VERSION: ${{ steps.ver.outputs.version }}
run: npm version --no-git-tag-version --allow-same-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 - name: Install
run: npm ci run: npm ci

View File

@@ -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]
``` ```
@@ -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`.
--- ---

View File

@@ -1,12 +1,12 @@
{ {
"name": "@kuns/claude-mailbox", "name": "@kuns/claude-mailbox",
"version": "1.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": "1.1.0", "version": "1.2.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0", "@modelcontextprotocol/sdk": "^1.29.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@kuns/claude-mailbox", "name": "@kuns/claude-mailbox",
"version": "1.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": {

View File

@@ -271,7 +271,7 @@ program
program program
.command("mcp-stdio") .command("mcp-stdio")
.description( .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:47822). Used by the Claude Code plugin's .mcp.json so the URL is configurable per machine without env-substitution in the URL field.", "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 () => { .action(async () => {
try { try {
@@ -345,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")

View File

@@ -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 {

View File

@@ -1,6 +1,6 @@
{ {
"name": "claude-mailbox", "name": "claude-mailbox",
"version": "0.1.0", "version": "1.2.0",
"description": "Auto-checks the local Claude-Mailbox daemon before every prompt and injects pending messages into the conversation context.", "description": "Auto-checks the local Claude-Mailbox daemon before every prompt and injects pending messages into the conversation context.",
"author": { "author": {
"name": "Mika Kuns" "name": "Mika Kuns"

View File

@@ -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

View File

@@ -29,19 +29,19 @@ Run: `claude-mailbox --version`
## Step 2 — port-conflict check (before autostart!) ## Step 2 — port-conflict check (before autostart!)
Default port is 47822. Probe whether anything is already on it: Default port is 37849. Probe whether anything is already on it:
``` ```
curl -sf http://127.0.0.1:47822/health 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 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 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 47822. - **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. - **Connection refused** → port is free, ✓ continue to Step 3.
If port conflict detected: If port conflict detected:
1. Tell the user which process holds the port (Windows: `Get-NetTCPConnection -LocalPort 47822 | Select-Object OwningProcess`, then `Get-Process -Id <pid>`; macOS/Linux: `lsof -i :47822`). 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. 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. 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: 4. Also write the override into `.claude/settings.json` env so the plugin's hooks find the right URL:
@@ -65,7 +65,7 @@ If `install-autostart` still fails after both attempts (very rare — would mean
## Step 4 — health probe ## Step 4 — health probe
Hit `http://127.0.0.1:<port>/health` (use the configured port, not necessarily 47822). Expect a JSON body with `"status":"ok"` AND a `version` matching `claude-mailbox --version`. If unreachable or version mismatch, stop and report. 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.
## Step 5 — mailbox identity (base prefix) ## Step 5 — mailbox identity (base prefix)
@@ -100,7 +100,7 @@ Claude-Mailbox doctor
binary: <version> binary: <version>
daemon: Running (port: <port>, what you did if anything) daemon: Running (port: <port>, what you did if anything)
health: ok health: ok
port conflict: none | resolved (moved from 47822 to <port>) 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

View File

@@ -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">
``` ```

View File

@@ -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)
{ {

View File

@@ -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;

View File

@@ -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;