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>
|
||||
```
|
||||
|
||||
`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 }[];
|
||||
|
||||
Reference in New Issue
Block a user