refactor(node): migrate from better-sqlite3 to node:sqlite, require Node 24+
Some checks failed
CI (Node) / build-test (push) Failing after 8s
Some checks failed
CI (Node) / build-test (push) Failing after 8s
Native binding caused install pain on every new Node major (no prebuilts +
node-gyp needs VS+Windows SDK to fall back). For this project's workload
(a few ops/day, no advanced SQLite features) better-sqlite3's perf edge is
irrelevant — node:sqlite's bundled, ABI-stable sync API is the better fit.
- db.ts: DatabaseSync, db.exec("PRAGMA …"), explicit BEGIN/COMMIT helper to
replace db.transaction(); row casts go through unknown because node:sqlite
returns Record<string, SQLOutputValue>.
- package.json: drop better-sqlite3 + @types/better-sqlite3, bump
engines.node to >=24, vitest 2 → 4 (2.x couldn't resolve `node:sqlite`).
- mailbox-doctor: add Step 1 that enforces Node ≥24 with a concrete fix
message, renumbers downstream steps.
Node 1.2.0 → 1.3.0. 35 transitive packages removed from the lockfile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
npm config set //git.kuns.dev/api/packages/releases/npm/:_authToken=<token>
|
||||||
```
|
```
|
||||||
|
|
||||||
`gyp ERR! find VS` on Windows during install
|
`Cannot find module 'node:sqlite'` or similar
|
||||||
: `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.
|
: 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",
|
"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.",
|
"description": "Standalone MCP mail server that lets parallel Claude sessions coordinate with each other.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -20,11 +20,10 @@
|
|||||||
"prepack": "npm run build"
|
"prepack": "npm run build"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=24"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"better-sqlite3": "^11.3.0",
|
|
||||||
"commander": "^12.1.0",
|
"commander": "^12.1.0",
|
||||||
"fastify": "^5.0.0",
|
"fastify": "^5.0.0",
|
||||||
"zod": "^3.25.0"
|
"zod": "^3.25.0"
|
||||||
@@ -33,10 +32,9 @@
|
|||||||
"node-windows": "^1.0.0-beta.8"
|
"node-windows": "^1.0.0-beta.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.11",
|
|
||||||
"@types/node": "^22.7.4",
|
"@types/node": "^22.7.4",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vitest": "^2.1.1"
|
"vitest": "^4.1.6"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"mcp",
|
"mcp",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Database from "better-sqlite3";
|
import { DatabaseSync, type StatementSync } from "node:sqlite";
|
||||||
import { mkdirSync } from "node:fs";
|
import { mkdirSync } from "node:fs";
|
||||||
import { dirname } from "node:path";
|
import { dirname } from "node:path";
|
||||||
|
|
||||||
@@ -57,28 +57,44 @@ function parseDate(s: string | null | undefined): Date | null {
|
|||||||
return isNaN(d.getTime()) ? null : d;
|
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 {
|
export class MailboxStore {
|
||||||
private readonly db: Database.Database;
|
private readonly db: DatabaseSync;
|
||||||
|
|
||||||
private readonly stmts: {
|
private readonly stmts: {
|
||||||
findMailbox: Database.Statement;
|
findMailbox: StatementSync;
|
||||||
insertMailbox: Database.Statement;
|
insertMailbox: StatementSync;
|
||||||
touchMailbox: Database.Statement;
|
touchMailbox: StatementSync;
|
||||||
listMailboxes: Database.Statement;
|
listMailboxes: StatementSync;
|
||||||
insertMessage: Database.Statement;
|
insertMessage: StatementSync;
|
||||||
countPending: Database.Statement;
|
countPending: StatementSync;
|
||||||
oldestPending: Database.Statement;
|
oldestPending: StatementSync;
|
||||||
selectPending: Database.Statement;
|
selectPending: StatementSync;
|
||||||
markDelivered: Database.Statement;
|
markDelivered: StatementSync;
|
||||||
pendingByRecipient: Database.Statement;
|
pendingByRecipient: StatementSync;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(public readonly dbPath: string) {
|
constructor(public readonly dbPath: string) {
|
||||||
mkdirSync(dirname(dbPath), { recursive: true });
|
mkdirSync(dirname(dbPath), { recursive: true });
|
||||||
this.db = new Database(dbPath);
|
this.db = new DatabaseSync(dbPath);
|
||||||
this.db.pragma("journal_mode = WAL");
|
this.db.exec("PRAGMA journal_mode = WAL");
|
||||||
this.db.pragma("foreign_keys = ON");
|
this.db.exec("PRAGMA foreign_keys = ON");
|
||||||
for (const sql of DDL_STATEMENTS) this.db.prepare(sql).run();
|
for (const sql of DDL_STATEMENTS) this.db.exec(sql);
|
||||||
|
|
||||||
this.stmts = {
|
this.stmts = {
|
||||||
findMailbox: this.db.prepare("SELECT * FROM mailboxes WHERE name = ?"),
|
findMailbox: this.db.prepare("SELECT * FROM mailboxes WHERE name = ?"),
|
||||||
@@ -114,7 +130,7 @@ export class MailboxStore {
|
|||||||
|
|
||||||
upsertMailbox(name: string): void {
|
upsertMailbox(name: string): void {
|
||||||
const now = nowIso();
|
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) {
|
if (existing) {
|
||||||
this.stmts.touchMailbox.run(now, name);
|
this.stmts.touchMailbox.run(now, name);
|
||||||
} else {
|
} else {
|
||||||
@@ -123,14 +139,13 @@ export class MailboxStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
send(from: string, to: string, body: string): { id: number; queuedAt: Date } {
|
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(from);
|
||||||
this.upsertMailbox(to);
|
this.upsertMailbox(to);
|
||||||
const createdAt = nowIso();
|
const createdAt = nowIso();
|
||||||
const result = this.stmts.insertMessage.run(to, from, body, createdAt);
|
const result = this.stmts.insertMessage.run(to, from, body, createdAt);
|
||||||
return { id: Number(result.lastInsertRowid), queuedAt: new Date(createdAt) };
|
return { id: Number(result.lastInsertRowid), queuedAt: new Date(createdAt) };
|
||||||
});
|
});
|
||||||
return tx();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
peek(name: string): InboxStatus {
|
peek(name: string): InboxStatus {
|
||||||
@@ -141,19 +156,18 @@ export class MailboxStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkInbox(name: string): MessageRow[] {
|
checkInbox(name: string): MessageRow[] {
|
||||||
const tx = this.db.transaction(() => {
|
return runInTransaction(this.db, () => {
|
||||||
const pending = this.stmts.selectPending.all(name) as MessageRow[];
|
const pending = this.stmts.selectPending.all(name) as unknown as MessageRow[];
|
||||||
if (pending.length > 0) {
|
if (pending.length > 0) {
|
||||||
const ids = pending.map((m) => m.id);
|
const ids = pending.map((m) => m.id);
|
||||||
this.stmts.markDelivered.run(nowIso(), JSON.stringify(ids));
|
this.stmts.markDelivered.run(nowIso(), JSON.stringify(ids));
|
||||||
}
|
}
|
||||||
return pending;
|
return pending;
|
||||||
});
|
});
|
||||||
return tx();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
listMailboxes(forName?: string): MailboxInfo[] {
|
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>();
|
const pendingMap = new Map<string, number>();
|
||||||
if (forName) {
|
if (forName) {
|
||||||
const counts = this.stmts.pendingByRecipient.all() as { to_mailbox: string; n: number }[];
|
const counts = this.stmts.pendingByRecipient.all() as { to_mailbox: string; n: number }[];
|
||||||
|
|||||||
@@ -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
|
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` 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`
|
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.
|
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:
|
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
|
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 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.
|
- **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:
|
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`).
|
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.
|
Merge into existing env, preserving other keys.
|
||||||
5. Mark `restart_needed = true`.
|
5. Mark `restart_needed = true`.
|
||||||
|
|
||||||
## Step 3 — daemon autostart and running state
|
## Step 4 — daemon autostart and running state
|
||||||
|
|
||||||
Run: `claude-mailbox status`
|
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.
|
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.
|
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`).
|
**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`.
|
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:
|
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
|
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.
|
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
|
Claude-Mailbox doctor
|
||||||
|
node: <version>
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user