Compare commits
2 Commits
d456f29138
...
8832eab6c7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8832eab6c7 | ||
|
|
8747d638fb |
@@ -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": "1.2.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,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 }[];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "claude-mailbox",
|
||||
"version": "1.2.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"
|
||||
},
|
||||
|
||||
@@ -39,8 +39,9 @@ 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
|
||||
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
---
|
||||
description: Diagnose and auto-fix the Claude-Mailbox setup (binary install, port-conflict detection, 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` and `mailbox.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,7 @@ Run: `claude-mailbox --version`
|
||||
|
||||
After install, re-run `claude-mailbox --version`. If it still fails, stop and report.
|
||||
|
||||
## Step 2 — port-conflict check (before autostart!)
|
||||
## Step 3 — port-conflict check (before autostart!)
|
||||
|
||||
Default port is 37849. Probe whether anything is already on it:
|
||||
|
||||
@@ -35,10 +49,10 @@ 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 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 3.
|
||||
- **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`).
|
||||
@@ -51,7 +65,7 @@ If port conflict detected:
|
||||
Merge into existing env, preserving other keys.
|
||||
5. Mark `restart_needed = true`.
|
||||
|
||||
## Step 3 — daemon autostart and running state
|
||||
## Step 4 — daemon autostart and running state
|
||||
|
||||
Run: `claude-mailbox status`
|
||||
|
||||
@@ -63,11 +77,11 @@ Run: `claude-mailbox status`
|
||||
|
||||
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.
|
||||
|
||||
## Step 4 — health probe
|
||||
## Step 5 — health probe
|
||||
|
||||
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 6 — mailbox identity (base prefix)
|
||||
|
||||
**No prompt by default.** Each Claude Code session gets a unique mailbox name auto-derived from its `session_id` (e.g., `claude-a8b3c1d2`).
|
||||
|
||||
@@ -80,7 +94,7 @@ Ask once: *"Want to flavor your mailbox names with a memorable prefix (e.g., `ba
|
||||
|
||||
On yes/explicit name: merge `env.CLAUDE_MAILBOX_NAME = <name>` into `.claude/settings.json`, preserving other keys. Mark `restart_needed = true`.
|
||||
|
||||
## Step 6 — smoke test
|
||||
## Step 7 — smoke test
|
||||
|
||||
Use two ephemeral names — we don't need the real session name here:
|
||||
|
||||
@@ -89,14 +103,15 @@ claude-mailbox send --from doctor-probe-a --to doctor-probe-b --body "ping from
|
||||
claude-mailbox check --name doctor-probe-b
|
||||
```
|
||||
|
||||
(If the port was changed in Step 2, pass `--url http://127.0.0.1:<port>` to both.)
|
||||
(If the port was changed in Step 3, pass `--url http://127.0.0.1:<port>` to both.)
|
||||
|
||||
The `check` output must be a JSON array with one message: `from: doctor-probe-a`, body matches. ✓ on success, ✗ otherwise.
|
||||
|
||||
## Step 7 — summary
|
||||
## Step 8 — summary
|
||||
|
||||
```
|
||||
Claude-Mailbox doctor
|
||||
node: <version>
|
||||
binary: <version>
|
||||
daemon: Running (port: <port>, what you did if anything)
|
||||
health: ok
|
||||
|
||||
@@ -19,6 +19,16 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SubagentStop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "claude-mailbox check --hook"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user