@sleep2agi/commhub-server 0.5.0-preview.24 → 0.5.0-preview.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sleep2agi/commhub-server",
3
- "version": "0.5.0-preview.24",
4
- "description": "CommHub MCP Server AI Agent communication hub with SSE push, MCP protocol, and REST API",
3
+ "version": "0.5.0-preview.25",
4
+ "description": "CommHub Server \u2014 AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and 18 MCP tools.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "bin": {
@@ -21,10 +21,11 @@
21
21
  "agent",
22
22
  "ai",
23
23
  "sse",
24
- "claude",
24
+ "server",
25
25
  "orchestration",
26
- "communication",
27
- "hub"
26
+ "multi-network",
27
+ "auth",
28
+ "license"
28
29
  ],
29
30
  "author": "sleep2agi",
30
31
  "license": "MIT",
@@ -39,4 +40,4 @@
39
40
  "dependencies": {
40
41
  "@modelcontextprotocol/sdk": "^1.12.0"
41
42
  }
42
- }
43
+ }
package/src/auth.ts CHANGED
@@ -23,7 +23,7 @@ export function register(username: string, password: string, email?: string, dis
23
23
  if (!password || password.length < 6) return { ok: false, error: "password must be at least 6 characters" };
24
24
  if (!/^[a-zA-Z0-9_\-\u4e00-\u9fff]+$/.test(username)) return { ok: false, error: "username contains invalid characters" };
25
25
 
26
- const existing = db.query<any, [string]>("SELECT user_id FROM users WHERE username = ?1").get(username);
26
+ const existing = db.get<any>("SELECT user_id FROM users WHERE username = ?1", username);
27
27
  if (existing) return { ok: false, error: "username already taken" };
28
28
 
29
29
  const userId = generateId("u");
@@ -57,17 +57,17 @@ export function register(username: string, password: string, email?: string, dis
57
57
  }
58
58
 
59
59
  export function login(username: string, password: string): AuthResult {
60
- const user = db.query<any, [string]>(
61
- "SELECT user_id, username, password_hash, display_name, email, role FROM users WHERE username = ?1"
62
- ).get(username);
60
+ const user = db.get<any>(
61
+ "SELECT user_id, username, password_hash, display_name, email, role FROM users WHERE username = ?1",
62
+ username);
63
63
 
64
64
  if (!user) return { ok: false, error: "invalid username or password" };
65
65
  if (user.password_hash !== hashPassword(password)) return { ok: false, error: "invalid username or password" };
66
66
 
67
67
  // Find or create token
68
- let tokenRow = db.query<any, [string]>(
69
- "SELECT token_id FROM api_tokens WHERE user_id = ?1 ORDER BY created_at DESC LIMIT 1"
70
- ).get(user.user_id);
68
+ let tokenRow = db.get<any>(
69
+ "SELECT token_id FROM api_tokens WHERE user_id = ?1 ORDER BY created_at DESC LIMIT 1",
70
+ user.user_id);
71
71
 
72
72
  let token: string;
73
73
  if (tokenRow) {
@@ -78,9 +78,9 @@ export function login(username: string, password: string): AuthResult {
78
78
  } else {
79
79
  token = generateToken();
80
80
  const tokenId = generateId("tok");
81
- const networkId = db.query<any, [string]>(
82
- "SELECT network_id FROM networks WHERE owner_id = ?1 LIMIT 1"
83
- ).get(user.user_id)?.network_id;
81
+ const networkId = db.get<any>(
82
+ "SELECT network_id FROM networks WHERE owner_id = ?1 LIMIT 1",
83
+ user.user_id)?.network_id;
84
84
  db.run(
85
85
  "INSERT INTO api_tokens (token_id, token_hash, user_id, network_id, name) VALUES (?1, ?2, ?3, ?4, ?5)",
86
86
  [tokenId, hashToken(token), user.user_id, networkId || null, "login"]
@@ -96,11 +96,11 @@ export function login(username: string, password: string): AuthResult {
96
96
 
97
97
  export function resolveToken(token: string): { user: AuthUser; networkId: string | null } | null {
98
98
  const tHash = hashToken(token);
99
- const row = db.query<any, [string]>(
99
+ const row = db.get<any>(
100
100
  `SELECT t.user_id, t.network_id, t.scope, u.username, u.display_name, u.email, u.role
101
101
  FROM api_tokens t JOIN users u ON t.user_id = u.user_id
102
- WHERE t.token_hash = ?1 AND (t.expires_at IS NULL OR t.expires_at > datetime('now'))`
103
- ).get(tHash);
102
+ WHERE t.token_hash = ?1 AND (t.expires_at IS NULL OR t.expires_at > datetime('now'))`,
103
+ tHash);
104
104
 
105
105
  if (!row) return null;
106
106
 
@@ -114,15 +114,15 @@ export function resolveToken(token: string): { user: AuthUser; networkId: string
114
114
  }
115
115
 
116
116
  export function getUserNetworks(userId: string) {
117
- return db.query<any, [string]>(
118
- "SELECT * FROM networks WHERE owner_id = ?1 ORDER BY created_at"
119
- ).all(userId);
117
+ return db.all<any>(
118
+ "SELECT * FROM networks WHERE owner_id = ?1 ORDER BY created_at",
119
+ userId);
120
120
  }
121
121
 
122
122
  export function createNetwork(userId: string, name: string, description?: string) {
123
- const existing = db.query<any, [string, string]>(
124
- "SELECT network_id FROM networks WHERE owner_id = ?1 AND network_name = ?2"
125
- ).get(userId, name);
123
+ const existing = db.get<any>(
124
+ "SELECT network_id FROM networks WHERE owner_id = ?1 AND network_name = ?2",
125
+ userId, name);
126
126
  if (existing) return { ok: false, error: "network name already exists" };
127
127
 
128
128
  const networkId = generateId("net");
@@ -134,27 +134,27 @@ export function createNetwork(userId: string, name: string, description?: string
134
134
  }
135
135
 
136
136
  export function listTokens(userId: string) {
137
- return db.query<any, [string]>(
138
- "SELECT token_id, name, scope, network_id, last_used_at, created_at FROM api_tokens WHERE user_id = ?1 ORDER BY created_at DESC"
139
- ).all(userId);
137
+ return db.all<any>(
138
+ "SELECT token_id, name, scope, network_id, last_used_at, created_at FROM api_tokens WHERE user_id = ?1 ORDER BY created_at DESC",
139
+ userId);
140
140
  }
141
141
 
142
142
  export function renameNetwork(userId: string, networkId: string, newName: string): { ok: boolean; error?: string } {
143
- const net = db.query<any, [string]>("SELECT * FROM networks WHERE network_id = ?1").get(networkId);
143
+ const net = db.get<any>("SELECT * FROM networks WHERE network_id = ?1", networkId);
144
144
  if (!net) return { ok: false, error: "network not found" };
145
145
  if (net.owner_id !== userId) return { ok: false, error: "not your network" };
146
- const dup = db.query<any, [string, string]>("SELECT network_id FROM networks WHERE owner_id = ?1 AND network_name = ?2").get(userId, newName);
146
+ const dup = db.get<any>("SELECT network_id FROM networks WHERE owner_id = ?1 AND network_name = ?2", userId, newName);
147
147
  if (dup) return { ok: false, error: "name already taken" };
148
148
  db.run("UPDATE networks SET network_name = ?1, updated_at = datetime('now') WHERE network_id = ?2", [newName, networkId]);
149
149
  return { ok: true };
150
150
  }
151
151
 
152
152
  export function deleteNetwork(userId: string, networkId: string): { ok: boolean; error?: string } {
153
- const net = db.query<any, [string]>("SELECT * FROM networks WHERE network_id = ?1").get(networkId);
153
+ const net = db.get<any>("SELECT * FROM networks WHERE network_id = ?1", networkId);
154
154
  if (!net) return { ok: false, error: "network not found" };
155
155
  if (net.owner_id !== userId) return { ok: false, error: "not your network" };
156
156
  // Check if any sessions/tasks still reference this network
157
- const sessions = db.query<{ cnt: number }, [string]>("SELECT COUNT(*) as cnt FROM sessions WHERE network_id = ?1").get(networkId);
157
+ const sessions = db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM sessions WHERE network_id = ?1", networkId);
158
158
  if (sessions && sessions.cnt > 0) return { ok: false, error: `network has ${sessions.cnt} active session(s) — stop them first` };
159
159
  db.run("DELETE FROM networks WHERE network_id = ?1 AND owner_id = ?2", [networkId, userId]);
160
160
  return { ok: true };
@@ -177,7 +177,7 @@ export function revokeToken(userId: string, tokenId: string): { ok: boolean; err
177
177
 
178
178
  export function changePassword(userId: string, oldPassword: string, newPassword: string): { ok: boolean; error?: string } {
179
179
  if (!newPassword || newPassword.length < 6) return { ok: false, error: "new password must be at least 6 characters" };
180
- const user = db.query<any, [string]>("SELECT password_hash FROM users WHERE user_id = ?1").get(userId);
180
+ const user = db.get<any>("SELECT password_hash FROM users WHERE user_id = ?1", userId);
181
181
  if (!user) return { ok: false, error: "user not found" };
182
182
  if (user.password_hash !== hashPassword(oldPassword)) return { ok: false, error: "incorrect current password" };
183
183
  db.run("UPDATE users SET password_hash = ?1, updated_at = datetime('now') WHERE user_id = ?2", [hashPassword(newPassword), userId]);
package/src/db-adapter.ts CHANGED
@@ -1,12 +1,19 @@
1
1
  /**
2
- * Database Adapter Interface async-first, supports SQLite and PostgreSQL
2
+ * Database Adapter — supports SQLite and PostgreSQL
3
3
  *
4
- * SQLite adapter: wraps bun:sqlite sync calls in Promise (zero overhead)
5
- * PostgreSQL adapter: uses bun:sql native async
4
+ * SQLite adapter: wraps bun:sqlite (sync)
5
+ * PostgreSQL adapter: wraps pg Pool (async, bridged to sync interface via blocking)
6
6
  *
7
- * All callers use await — sync SQLite just resolves immediately.
7
+ * Key design: callers write SQLite-style SQL. PgAdapter auto-translates:
8
+ * - ?1, ?2 → $1, $2
9
+ * - datetime('now') → NOW()
10
+ * - datetime('now', '+N seconds') → NOW() + INTERVAL 'N seconds'
11
+ * - INTEGER PRIMARY KEY AUTOINCREMENT → SERIAL PRIMARY KEY
12
+ * - ON CONFLICT(col) DO UPDATE SET → ON CONFLICT(col) DO UPDATE SET (same syntax)
8
13
  */
9
14
 
15
+ import { Database } from "bun:sqlite";
16
+
10
17
  export interface QueryResult {
11
18
  changes: number;
12
19
  }
@@ -31,42 +38,212 @@ export interface DbAdapter {
31
38
  close(): void;
32
39
 
33
40
  /** Dialect identifier */
34
- readonly dialect: 'sqlite' | 'postgres';
41
+ readonly dialect: "sqlite" | "postgres";
42
+ }
43
+
44
+ // ════════════════════════════════════════════
45
+ // SQLite Adapter (bun:sqlite, sync)
46
+ // ════════════════════════════════════════════
47
+
48
+ export class SQLiteAdapter implements DbAdapter {
49
+ readonly dialect = "sqlite" as const;
50
+ constructor(private readonly rawDb: Database) {}
51
+
52
+ run(sql: string, params?: any[]): QueryResult {
53
+ return this.rawDb.run(sql, params as any);
54
+ }
55
+
56
+ get<T = any>(sql: string, ...params: any[]): T | null {
57
+ return this.rawDb.query<T, any[]>(sql).get(...params) ?? null;
58
+ }
59
+
60
+ all<T = any>(sql: string, ...params: any[]): T[] {
61
+ return this.rawDb.query<T, any[]>(sql).all(...params);
62
+ }
63
+
64
+ exec(sql: string): void {
65
+ this.rawDb.exec(sql);
66
+ }
67
+
68
+ transaction<T>(fn: () => T): T {
69
+ return this.rawDb.transaction(fn)();
70
+ }
71
+
72
+ close(): void {
73
+ this.rawDb.close();
74
+ }
35
75
  }
36
76
 
77
+ // ════════════════════════════════════════════
78
+ // PostgreSQL Adapter (pg Pool, sync bridge)
79
+ // ════════════════════════════════════════════
80
+
37
81
  /**
38
- * Phase 1 strategy:
39
- *
40
- * Current code is sync (bun:sqlite). We keep it sync for now.
41
- * All DB access goes through the adapter interface above.
42
- *
43
- * When we add PostgreSQL (Phase 2), the adapter interface
44
- * will change to async. At that point we'll update callers
45
- * in a single pass. The unified call sites from Phase 1
46
- * make that pass mechanical rather than archaeological.
82
+ * Translate SQLite-style SQL to PostgreSQL.
83
+ * Called on every query — must be fast (simple regex, no parsing).
84
+ */
85
+ export function sqliteToPostgres(sql: string): string {
86
+ let s = sql;
87
+ // ── datetime translations (before ?N→$N to handle datetime('now', ?N)) ──
88
+ // datetime('now', ?N) NOW() + $N::INTERVAL (param contains "+3600 seconds")
89
+ s = s.replace(/datetime\s*\(\s*'now'\s*,\s*\?(\d+)\s*\)/gi, (_, n) => {
90
+ return `NOW() + $${n}::INTERVAL`;
91
+ });
92
+ // datetime('now', '+N seconds') → NOW() + INTERVAL 'N seconds'
93
+ s = s.replace(/datetime\s*\(\s*'now'\s*,\s*'([^']+)'\s*\)/gi, (_, offset) => {
94
+ return `NOW() + INTERVAL '${offset.replace(/^\+/, "")}'`;
95
+ });
96
+ // datetime('now') → NOW()
97
+ s = s.replace(/datetime\s*\(\s*'now'\s*\)/gi, "NOW()");
98
+ // TEXT NOT NULL DEFAULT (datetime('now')) → TIMESTAMP NOT NULL DEFAULT NOW()
99
+ s = s.replace(/TEXT\s+NOT\s+NULL\s+DEFAULT\s+\(NOW\(\)\)/gi, "TIMESTAMP NOT NULL DEFAULT NOW()");
100
+ // ── Parameter placeholders ──
101
+ // ?1, ?2 → $1, $2 (positional params)
102
+ s = s.replace(/\?(\d+)/g, (_, n) => `$${n}`);
103
+ // Unindexed ? → $N (sequential)
104
+ let idx = 0;
105
+ s = s.replace(/\?(?!\d)/g, () => `$${++idx}`);
106
+ // ── DDL translations ──
107
+ // INTEGER PRIMARY KEY AUTOINCREMENT → SERIAL PRIMARY KEY
108
+ s = s.replace(/INTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT/gi, "SERIAL PRIMARY KEY");
109
+ return s;
110
+ }
111
+
112
+ /**
113
+ * PostgreSQL adapter using pg Pool.
47
114
  *
48
- * Why not async-first now?
49
- * - bun:sqlite is sync, wrapping in Promise adds noise
50
- * - All MCP tool handlers are already async, so the future
51
- * migration is: db.run() → await db.run(), straightforward
52
- * - 750+ lines of tools.ts would need gratuitous await for zero benefit today
115
+ * Design: uses synchronous blocking via Bun's subprocess or
116
+ * deasync-style approach. Since Bun MCP handlers are async,
117
+ * we store a pool and use blocking queries via pg's synchronous mode.
53
118
  *
54
- * The contract: every DB call goes through adapter methods,
55
- * never through raw db.query() or db.run() on the bun:sqlite object.
56
- * This is what makes Phase 2 feasible.
119
+ * NOTE: This adapter uses `pg` npm package in synchronous mode.
120
+ * For production, the interface should be async. This sync bridge
121
+ * works for the current codebase where MCP handlers are async
122
+ * but DB calls are sync within them.
57
123
  */
124
+ export class PgAdapter implements DbAdapter {
125
+ readonly dialect = "postgres" as const;
126
+ private pool: any; // pg.Pool
127
+ private client: any; // dedicated sync client
58
128
 
59
- /** SQL helpers for cross-dialect compatibility */
60
- export function sqlNow(dialect: 'sqlite' | 'postgres'): string {
61
- return dialect === 'postgres' ? 'NOW()' : "datetime('now')";
62
- }
129
+ constructor(connectionString: string) {
130
+ // Dynamic import of pg only loaded when DATABASE_URL is set
131
+ try {
132
+ const pg = require("pg");
133
+ this.pool = new pg.Pool({ connectionString, max: 10 });
134
+ // Get a dedicated client for sync operations
135
+ // We use execSync pattern for blocking
136
+ } catch (e) {
137
+ throw new Error(
138
+ "PostgreSQL support requires 'pg' package. Install with: bun add pg\n" +
139
+ ` Original error: ${(e as Error).message}`
140
+ );
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Execute a blocking query against PG.
146
+ * Uses Bun's ability to block on promises in sync context.
147
+ */
148
+ private querySync(sql: string, params?: any[]): any {
149
+ const pgSql = sqliteToPostgres(sql);
150
+ // Bun supports top-level await and can block — use a shared promise pattern
151
+ // For sync bridge, we use a worker or subprocess
152
+ // Simplest approach: use pg's synchronous query via dedicated connection
153
+ const result = this._blockingQuery(pgSql, params);
154
+ return result;
155
+ }
156
+
157
+ private _blockingQuery(sql: string, params?: any[]): any {
158
+ // Use Bun.spawnSync to run a node script that executes the query
159
+ // This is a pragmatic sync bridge until we migrate to async interface
160
+ const script = `
161
+ const { Pool } = require('pg');
162
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
163
+ pool.query(${JSON.stringify(sql)}, ${JSON.stringify(params || [])})
164
+ .then(r => { process.stdout.write(JSON.stringify({ rows: r.rows, rowCount: r.rowCount })); pool.end(); })
165
+ .catch(e => { process.stdout.write(JSON.stringify({ error: e.message })); pool.end(); process.exit(1); });
166
+ `;
167
+ const proc = Bun.spawnSync(["node", "-e", script], {
168
+ env: { ...process.env },
169
+ stdout: "pipe",
170
+ stderr: "pipe",
171
+ });
172
+ const out = proc.stdout.toString().trim();
173
+ if (proc.exitCode !== 0) {
174
+ const errOut = proc.stderr.toString().trim();
175
+ throw new Error(`PG query failed: ${errOut || out}`);
176
+ }
177
+ return JSON.parse(out);
178
+ }
179
+
180
+ run(sql: string, params?: any[]): QueryResult {
181
+ if (sql.trim().toUpperCase().startsWith("PRAGMA")) {
182
+ return { changes: 0 }; // Skip SQLite PRAGMAs
183
+ }
184
+ const result = this.querySync(sql, params);
185
+ return { changes: result.rowCount ?? 0 };
186
+ }
63
187
 
64
- export function sqlAddSeconds(dialect: 'sqlite' | 'postgres', seconds: number | string): string {
65
- return dialect === 'postgres'
66
- ? `NOW() + INTERVAL '${seconds} seconds'`
67
- : `datetime('now', '+${seconds} seconds')`;
188
+ get<T = any>(sql: string, ...params: any[]): T | null {
189
+ const result = this.querySync(sql, params.length > 0 ? params : undefined);
190
+ return (result.rows?.[0] as T) ?? null;
191
+ }
192
+
193
+ all<T = any>(sql: string, ...params: any[]): T[] {
194
+ const result = this.querySync(sql, params.length > 0 ? params : undefined);
195
+ return (result.rows as T[]) ?? [];
196
+ }
197
+
198
+ exec(sql: string): void {
199
+ if (sql.trim().toUpperCase().startsWith("PRAGMA")) return; // Skip
200
+ // Split multiple statements and execute each
201
+ const statements = sql.split(";").map(s => s.trim()).filter(s => s.length > 0);
202
+ for (const stmt of statements) {
203
+ this.querySync(stmt);
204
+ }
205
+ }
206
+
207
+ transaction<T>(fn: () => T): T {
208
+ this.querySync("BEGIN");
209
+ try {
210
+ const result = fn();
211
+ this.querySync("COMMIT");
212
+ return result;
213
+ } catch (e) {
214
+ try { this.querySync("ROLLBACK"); } catch {}
215
+ throw e;
216
+ }
217
+ }
218
+
219
+ close(): void {
220
+ try { this.pool?.end(); } catch {}
221
+ }
68
222
  }
69
223
 
70
- export function sqlPlaceholder(dialect: 'sqlite' | 'postgres', index: number): string {
71
- return dialect === 'postgres' ? `$${index}` : `?${index}`;
224
+ // ════════════════════════════════════════════
225
+ // Factory
226
+ // ════════════════════════════════════════════
227
+
228
+ /**
229
+ * Create the appropriate adapter based on environment.
230
+ * - DATABASE_URL starts with "postgres://" → PgAdapter
231
+ * - Otherwise → SQLiteAdapter with COMMHUB_DB or default path
232
+ */
233
+ export function createAdapter(): DbAdapter {
234
+ const dbUrl = process.env.DATABASE_URL;
235
+ if (dbUrl && (dbUrl.startsWith("postgres://") || dbUrl.startsWith("postgresql://"))) {
236
+ console.log("[commhub] database: PostgreSQL");
237
+ return new PgAdapter(dbUrl);
238
+ }
239
+ // Default: SQLite
240
+ const { mkdirSync } = require("fs");
241
+ const { dirname } = require("path");
242
+ const dbPath = process.env.COMMHUB_DB || `${process.env.HOME}/.commhub/commhub.db`;
243
+ mkdirSync(dirname(dbPath), { recursive: true });
244
+ console.log(`[commhub] database: ${dbPath}`);
245
+ const rawDb = new Database(dbPath);
246
+ rawDb.exec("PRAGMA journal_mode=WAL");
247
+ rawDb.exec("PRAGMA busy_timeout=5000");
248
+ return new SQLiteAdapter(rawDb);
72
249
  }
package/src/db.ts CHANGED
@@ -1,14 +1,6 @@
1
- import { Database } from "bun:sqlite";
2
- import { mkdirSync } from "fs";
3
- import { dirname } from "path";
1
+ import { createAdapter, type DbAdapter } from "./db-adapter";
4
2
 
5
- const DB_PATH = process.env.COMMHUB_DB || `${process.env.HOME}/.commhub/commhub.db`;
6
- mkdirSync(dirname(DB_PATH), { recursive: true });
7
-
8
- console.log(`[commhub] database: ${DB_PATH}`);
9
- export const db = new Database(DB_PATH);
10
- db.exec("PRAGMA journal_mode=WAL");
11
- db.exec("PRAGMA busy_timeout=5000");
3
+ export const db: DbAdapter = createAdapter();
12
4
 
13
5
  // Schema
14
6
  db.exec(`
@@ -238,7 +230,7 @@ db.exec(`
238
230
  `);
239
231
 
240
232
  // Auto-create trial license on first run
241
- const existingLicense = db.query<any, []>("SELECT id FROM licenses LIMIT 1").get();
233
+ const existingLicense = db.get<any>("SELECT id FROM licenses LIMIT 1");
242
234
  if (!existingLicense) {
243
235
  const trialId = crypto.randomUUID().replace(/-/g, "").slice(0, 12);
244
236
  db.run(
package/src/index.ts CHANGED
@@ -128,9 +128,8 @@ setInterval(() => {
128
128
  if (result.changes > 0) {
129
129
  console.log(`[patrol] expired ${result.changes} stale task(s)`);
130
130
  // Log events for expired tasks
131
- const expired = db.query<{ task_id: string }, []>(
132
- "SELECT task_id FROM tasks WHERE status = 'expired' AND completed_at >= datetime('now', '-1 minute')"
133
- ).all();
131
+ const expired = db.all<{ task_id: string }>(
132
+ "SELECT task_id FROM tasks WHERE status = 'expired' AND completed_at >= datetime('now', '-1 minute')");
134
133
  for (const t of expired) logTaskEvent(t.task_id, null, "expired", "patrol");
135
134
  }
136
135
  } catch {}
@@ -188,7 +187,7 @@ Bun.serve({
188
187
 
189
188
  // ── V3: License endpoints ──
190
189
  if (url.pathname === "/api/license" && req.method === "GET") {
191
- const license = db.query<any, []>("SELECT * FROM licenses ORDER BY created_at LIMIT 1").get();
190
+ const license = db.get<any>("SELECT * FROM licenses ORDER BY created_at LIMIT 1");
192
191
  if (!license) return withCors(req, Response.json({ ok: true, status: "no_license" }));
193
192
  const now = new Date().toISOString().replace("T", " ").slice(0, 19);
194
193
  const expired = license.expires_at && license.expires_at < now;
@@ -283,7 +282,7 @@ Bun.serve({
283
282
  db.run(`UPDATE users SET ${updates.join(", ")} WHERE user_id = ?${params.length}`, params);
284
283
  }
285
284
  // Re-fetch
286
- const user = db.query<any, [string]>("SELECT user_id, username, display_name, email, role FROM users WHERE user_id = ?1").get(resolved.user.user_id);
285
+ const user = db.get<any>("SELECT user_id, username, display_name, email, role FROM users WHERE user_id = ?1", resolved.user.user_id);
287
286
  return withCors(req, Response.json({ ok: true, user }));
288
287
  } catch (e: any) {
289
288
  return withCors(req, Response.json({ ok: false, error: e.message }, { status: 400 }));
@@ -373,7 +372,7 @@ Bun.serve({
373
372
  if (!resolved || resolved.user.role !== "admin") {
374
373
  return withCors(req, Response.json({ ok: false, error: "admin required" }, { status: 403 }));
375
374
  }
376
- const users = db.query("SELECT user_id, username, display_name, email, role, created_at FROM users ORDER BY created_at").all();
375
+ const users = db.all("SELECT user_id, username, display_name, email, role, created_at FROM users ORDER BY created_at");
377
376
  return withCors(req, Response.json({ ok: true, users }));
378
377
  }
379
378
 
@@ -384,16 +383,16 @@ Bun.serve({
384
383
  const resolved = resolveToken(token);
385
384
  if (!resolved) return withCors(req, Response.json({ ok: false, error: "invalid token" }, { status: 401 }));
386
385
  const networkId = netDetailMatch[1];
387
- const network = db.query<any, [string]>("SELECT * FROM networks WHERE network_id = ?1").get(networkId);
386
+ const network = db.get<any>("SELECT * FROM networks WHERE network_id = ?1", networkId);
388
387
  if (!network) return withCors(req, Response.json({ ok: false, error: "network not found" }, { status: 404 }));
389
388
  // Ownership check: only owner or admin can view
390
389
  if (network.owner_id !== resolved.user.user_id && resolved.user.role !== "admin") {
391
390
  return withCors(req, Response.json({ ok: false, error: "access denied" }, { status: 403 }));
392
391
  }
393
392
  // Get network stats
394
- const nodeCount = db.query<{ cnt: number }, [string]>("SELECT COUNT(*) as cnt FROM nodes WHERE network_id = ?1").get(networkId);
395
- const sessionCount = db.query<{ cnt: number }, [string]>("SELECT COUNT(*) as cnt FROM sessions WHERE network_id = ?1").get(networkId);
396
- const taskStats = db.query<any, [string]>("SELECT status, COUNT(*) as count FROM tasks WHERE network_id = ?1 GROUP BY status").all(networkId);
393
+ const nodeCount = db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM nodes WHERE network_id = ?1", networkId);
394
+ const sessionCount = db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM sessions WHERE network_id = ?1", networkId);
395
+ const taskStats = db.all<any>("SELECT status, COUNT(*) as count FROM tasks WHERE network_id = ?1 GROUP BY status", networkId);
397
396
  return withCors(req, Response.json({
398
397
  ok: true, network,
399
398
  stats: { nodes: nodeCount?.cnt || 0, sessions: sessionCount?.cnt || 0, tasks: taskStats },
@@ -430,9 +429,9 @@ Bun.serve({
430
429
 
431
430
  // ── REST: health (public, no auth) ──
432
431
  if (url.pathname === "/health") {
433
- const count = db.query<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM sessions").get();
432
+ const count = db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM sessions");
434
433
  const sse = getSSEStats();
435
- const license = db.query<any, []>("SELECT type, expires_at FROM licenses LIMIT 1").get();
434
+ const license = db.get<any>("SELECT type, expires_at FROM licenses LIMIT 1");
436
435
  return withCors(req, Response.json({
437
436
  ok: true,
438
437
  version: "1.0.0-preview",
@@ -461,7 +460,7 @@ Bun.serve({
461
460
  const sql = netFilter
462
461
  ? "SELECT * FROM sessions WHERE network_id = ?1 ORDER BY updated_at DESC"
463
462
  : "SELECT * FROM sessions ORDER BY updated_at DESC";
464
- const sessions = netFilter ? db.query(sql).all(netFilter) : db.query(sql).all();
463
+ const sessions = netFilter ? db.all(sql, netFilter) : db.all(sql);
465
464
  return withCors(req, Response.json({ ok: true, sessions }));
466
465
  }
467
466
 
@@ -486,9 +485,9 @@ Bun.serve({
486
485
  [id, body.alias, body.priority, body.task, fromSession]
487
486
  );
488
487
  // SSE push: 秒达
489
- const pending = db.query<{ cnt: number }, [string]>(
490
- "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0"
491
- ).get(body.alias);
488
+ const pending = db.get<{ cnt: number }>(
489
+ "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0",
490
+ body.alias);
492
491
  pushEvent(body.alias, { type: "new_task", inbox_count: pending?.cnt ?? 1, priority: body.priority, from: fromSession });
493
492
  return withCors(req, Response.json({ ok: true, message_id: id }));
494
493
  }
@@ -510,7 +509,7 @@ Bun.serve({
510
509
  const params: any[] = [];
511
510
  if (body.filter_server) { sql += " AND server = ?"; params.push(body.filter_server); }
512
511
  if (body.filter_status) { sql += " AND status = ?"; params.push(body.filter_status); }
513
- const targets = db.query<{ alias: string }, any[]>(sql).all(...params);
512
+ const targets = db.all<{ alias: string }>(sql, ...params);
514
513
  const ids: string[] = [];
515
514
  for (const t of targets) {
516
515
  const id = crypto.randomUUID();
@@ -577,9 +576,9 @@ Bun.serve({
577
576
  if (url.pathname === "/api/messages") {
578
577
  const limit = Number(url.searchParams.get("limit")) || 100;
579
578
  const since = url.searchParams.get("since") ?? new Date(Date.now() - 3600000).toISOString().replace("T", " ").slice(0, 19);
580
- const rows = db.query(
581
- "SELECT id, session_name as to_alias, from_session as from_alias, type, priority, content, created_at FROM inbox WHERE created_at >= ?1 ORDER BY created_at DESC LIMIT ?2"
582
- ).all(since, limit);
579
+ const rows = db.all(
580
+ "SELECT id, session_name as to_alias, from_session as from_alias, type, priority, content, created_at FROM inbox WHERE created_at >= ?1 ORDER BY created_at DESC LIMIT ?2",
581
+ since, limit);
583
582
  return withCors(req, Response.json({ ok: true, messages: rows }));
584
583
  }
585
584
 
@@ -588,20 +587,20 @@ Bun.serve({
588
587
  const n = url.searchParams.get("network_id");
589
588
  // Parameterized queries to prevent SQL injection
590
589
  const taskStats = n
591
- ? db.query<any, [string]>("SELECT status, COUNT(*) as count FROM tasks WHERE network_id = ?1 GROUP BY status").all(n)
592
- : db.query<any, []>("SELECT status, COUNT(*) as count FROM tasks GROUP BY status").all();
590
+ ? db.all<any>("SELECT status, COUNT(*) as count FROM tasks WHERE network_id = ?1 GROUP BY status", n)
591
+ : db.all<any>("SELECT status, COUNT(*) as count FROM tasks GROUP BY status");
593
592
  const sessionStats = n
594
- ? db.query<any, [string]>("SELECT status, COUNT(*) as count FROM sessions WHERE network_id = ?1 GROUP BY status").all(n)
595
- : db.query<any, []>("SELECT status, COUNT(*) as count FROM sessions GROUP BY status").all();
593
+ ? db.all<any>("SELECT status, COUNT(*) as count FROM sessions WHERE network_id = ?1 GROUP BY status", n)
594
+ : db.all<any>("SELECT status, COUNT(*) as count FROM sessions GROUP BY status");
596
595
  const totalTasks = n
597
- ? db.query<{ cnt: number }, [string]>("SELECT COUNT(*) as cnt FROM tasks WHERE network_id = ?1").get(n)
598
- : db.query<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM tasks").get();
596
+ ? db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM tasks WHERE network_id = ?1", n)
597
+ : db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM tasks");
599
598
  const totalNodes = n
600
- ? db.query<{ cnt: number }, [string]>("SELECT COUNT(*) as cnt FROM nodes WHERE network_id = ?1").get(n)
601
- : db.query<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM nodes").get();
599
+ ? db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM nodes WHERE network_id = ?1", n)
600
+ : db.get<{ cnt: number }>("SELECT COUNT(*) as cnt FROM nodes");
602
601
  const recentTasks = n
603
- ? db.query<any, [string]>("SELECT task_id, from_name, to_name, status, created_at FROM tasks WHERE network_id = ?1 ORDER BY created_at DESC LIMIT 5").all(n)
604
- : db.query<any, []>("SELECT task_id, from_name, to_name, status, created_at FROM tasks ORDER BY created_at DESC LIMIT 5").all();
602
+ ? db.all<any>("SELECT task_id, from_name, to_name, status, created_at FROM tasks WHERE network_id = ?1 ORDER BY created_at DESC LIMIT 5", n)
603
+ : db.all<any>("SELECT task_id, from_name, to_name, status, created_at FROM tasks ORDER BY created_at DESC LIMIT 5");
605
604
  return withCors(req, Response.json({
606
605
  ok: true,
607
606
  network_id: n || null,
@@ -629,7 +628,7 @@ Bun.serve({
629
628
  if (userId && resolved.user.role === "admin") { sql += ` AND user_id = ?${params.length + 1}`; params.push(userId); }
630
629
  sql += ` ORDER BY created_at DESC LIMIT ?${params.length + 1}`;
631
630
  params.push(limit);
632
- const logs = db.query(sql).all(...params);
631
+ const logs = db.all(sql, ...params);
633
632
  return withCors(req, Response.json({ ok: true, logs, count: logs.length }));
634
633
  }
635
634
 
@@ -642,7 +641,7 @@ Bun.serve({
642
641
  if (taskId) { sql += " WHERE task_id = ?1"; params.push(taskId); }
643
642
  sql += " ORDER BY created_at DESC LIMIT ?";
644
643
  params.push(limit);
645
- const rows = db.query(sql).all(...params);
644
+ const rows = db.all(sql, ...params);
646
645
  return withCors(req, Response.json({ ok: true, events: rows, count: rows.length }));
647
646
  }
648
647
 
@@ -657,7 +656,7 @@ Bun.serve({
657
656
  if (nodeId) { sql += ` AND node_id = ?${params.length + 1}`; params.push(nodeId); }
658
657
  if (alias) { sql += ` AND alias = ?${params.length + 1}`; params.push(alias); }
659
658
  sql += " ORDER BY updated_at DESC";
660
- const rows = db.query(sql).all(...params);
659
+ const rows = db.all(sql, ...params);
661
660
  return withCors(req, Response.json({ ok: true, nodes: rows, count: rows.length }));
662
661
  }
663
662
 
@@ -680,17 +679,17 @@ Bun.serve({
680
679
  sql += ` ORDER BY created_at DESC LIMIT ?${params.length + 1}`;
681
680
  params.push(limit);
682
681
 
683
- const rows = db.query(sql).all(...params);
682
+ const rows = db.all(sql, ...params);
684
683
  const stats = netFilter
685
- ? db.query<any, [string]>("SELECT status, COUNT(*) as count FROM tasks WHERE network_id = ?1 GROUP BY status").all(netFilter)
686
- : db.query<any, []>("SELECT status, COUNT(*) as count FROM tasks GROUP BY status").all();
684
+ ? db.all<any>("SELECT status, COUNT(*) as count FROM tasks WHERE network_id = ?1 GROUP BY status", netFilter)
685
+ : db.all<any>("SELECT status, COUNT(*) as count FROM tasks GROUP BY status");
687
686
  return withCors(req, Response.json({ ok: true, tasks: rows, count: rows.length, stats }));
688
687
  }
689
688
 
690
689
  // ── REST: recent completions ──
691
690
  if (url.pathname === "/api/completions") {
692
691
  const since = url.searchParams.get("since") ?? new Date(Date.now() - 86400000).toISOString();
693
- const rows = db.query("SELECT * FROM completions WHERE completed_at >= ?1 ORDER BY completed_at DESC LIMIT 100").all(since);
692
+ const rows = db.all("SELECT * FROM completions WHERE completed_at >= ?1 ORDER BY completed_at DESC LIMIT 100", since);
694
693
  return withCors(req, Response.json({ ok: true, completions: rows }));
695
694
  }
696
695
 
package/src/tools.ts CHANGED
@@ -45,9 +45,8 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
45
45
  console.log(`[${ts()}] ${alias} (${resume_id.slice(0, 8)}) → report_status: ${status}${task ? " | " + task.slice(0, 60) : ""}${effectiveNetId ? " [net]" : ""}`);
46
46
  const trimmedOutput = output?.slice(0, 4000);
47
47
 
48
- try {
49
- db.run("BEGIN IMMEDIATE");
50
- // Only delete same-alias sessions within the same network (prevent cross-network alias conflict)
48
+ db.transaction(() => {
49
+ // Only delete same-alias sessions within the same network
51
50
  if (effectiveNetId) {
52
51
  db.run("DELETE FROM sessions WHERE alias = ?1 AND resume_id != ?2 AND network_id = ?3", [alias, resume_id, effectiveNetId]);
53
52
  } else {
@@ -57,33 +56,19 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
57
56
  `INSERT INTO sessions (resume_id, alias, tmux_name, server, ip, hostname, agent, project_dir, version, status, task, output, progress, score, node_id, session_id, config_path, channels, network_id, last_seen_at, updated_at)
58
57
  VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, datetime('now'), datetime('now'))
59
58
  ON CONFLICT(resume_id) DO UPDATE SET
60
- alias = COALESCE(?2, sessions.alias),
61
- tmux_name = COALESCE(?3, sessions.tmux_name),
62
- server = COALESCE(?4, sessions.server),
63
- ip = COALESCE(?5, sessions.ip),
64
- hostname = COALESCE(?6, sessions.hostname),
65
- agent = COALESCE(?7, sessions.agent),
66
- project_dir = COALESCE(?8, sessions.project_dir),
67
- version = COALESCE(?9, sessions.version),
68
- status = ?10,
69
- task = COALESCE(?11, sessions.task),
70
- output = COALESCE(?12, sessions.output),
71
- progress = COALESCE(?13, sessions.progress),
72
- score = COALESCE(?14, sessions.score),
73
- node_id = COALESCE(?15, sessions.node_id),
74
- session_id = COALESCE(?16, sessions.session_id),
75
- config_path = COALESCE(?17, sessions.config_path),
76
- channels = COALESCE(?18, sessions.channels),
77
- network_id = COALESCE(?19, sessions.network_id),
78
- last_seen_at = datetime('now'),
79
- updated_at = datetime('now')`,
59
+ alias = COALESCE(?2, sessions.alias), tmux_name = COALESCE(?3, sessions.tmux_name),
60
+ server = COALESCE(?4, sessions.server), ip = COALESCE(?5, sessions.ip),
61
+ hostname = COALESCE(?6, sessions.hostname), agent = COALESCE(?7, sessions.agent),
62
+ project_dir = COALESCE(?8, sessions.project_dir), version = COALESCE(?9, sessions.version),
63
+ status = ?10, task = COALESCE(?11, sessions.task),
64
+ output = COALESCE(?12, sessions.output), progress = COALESCE(?13, sessions.progress),
65
+ score = COALESCE(?14, sessions.score), node_id = COALESCE(?15, sessions.node_id),
66
+ session_id = COALESCE(?16, sessions.session_id), config_path = COALESCE(?17, sessions.config_path),
67
+ channels = COALESCE(?18, sessions.channels), network_id = COALESCE(?19, sessions.network_id),
68
+ last_seen_at = datetime('now'), updated_at = datetime('now')`,
80
69
  [resume_id, alias, tmux ?? null, srv ?? null, clientIP ?? null, hn ?? null, ag ?? null, pd ?? null, ver ?? null, status, task ?? null, trimmedOutput ?? null, progress ?? null, score ?? null, node_id ?? null, session_id ?? null, config_path ?? null, channels ?? null, netId ?? null]
81
70
  );
82
- db.run("COMMIT");
83
- } catch (e) {
84
- try { db.run("ROLLBACK"); } catch {}
85
- throw e;
86
- }
71
+ });
87
72
 
88
73
  // V2: sync tasks table — report_status(working) → tasks.running
89
74
  if (status === "working" && task) {
@@ -95,9 +80,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
95
80
  );
96
81
  if (runResult.changes > 0) {
97
82
  // Find task_id for logging
98
- const t = db.query<{ task_id: string }, [string, string]>(
99
- "SELECT task_id FROM tasks WHERE to_name = ?1 AND content = ?2 AND status = 'running' ORDER BY started_at DESC LIMIT 1"
100
- ).get(alias, task);
83
+ const t = db.get<{ task_id: string }>(
84
+ "SELECT task_id FROM tasks WHERE to_name = ?1 AND content = ?2 AND status = 'running' ORDER BY started_at DESC LIMIT 1",
85
+ alias, task);
101
86
  if (t) logTaskEvent(t.task_id, null, "running", alias);
102
87
  }
103
88
  } catch {}
@@ -127,9 +112,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
127
112
  }
128
113
 
129
114
  // inbox uses alias for routing
130
- const row = db.query<{ cnt: number }, [string]>(
131
- "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0"
132
- ).get(alias);
115
+ const row = db.get<{ cnt: number }>(
116
+ "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0",
117
+ alias);
133
118
 
134
119
  return {
135
120
  content: [
@@ -161,51 +146,40 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
161
146
  async ({ alias, task, result, artifacts, score, duration_minutes }) => {
162
147
  console.log(`[${ts()}] ${alias} → report_completion: ${task.slice(0, 60)}`);
163
148
  const id = uuidv4();
164
- try {
165
- db.run("BEGIN IMMEDIATE");
149
+ const taskUpdateChanges = db.transaction(() => {
166
150
  db.run(
167
151
  `INSERT INTO completions (id, session_name, task, result, artifacts, score, duration_minutes)
168
152
  VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)`,
169
153
  [id, alias, task, result, artifacts ? JSON.stringify(artifacts) : null, score ?? null, duration_minutes ?? null]
170
154
  );
171
-
172
155
  db.run(
173
156
  `UPDATE sessions SET status = 'idle', task = NULL, progress = 0, updated_at = datetime('now')
174
157
  WHERE alias = ?1`,
175
158
  [alias]
176
159
  );
177
-
178
160
  // V2: sync tasks table — try by task_id first, then by content
179
- const taskUpdate = db.run(
161
+ const tu = db.run(
180
162
  `UPDATE tasks SET status = 'replied', result = ?1, completed_at = datetime('now')
181
163
  WHERE task_id = ?2 AND status IN ('delivered', 'acked', 'running')`,
182
164
  [result.slice(0, 4000), task]
183
165
  );
184
- if (taskUpdate.changes === 0) {
185
- // fallback: match most recent task by to_name + content (legacy path)
186
- const match = db.query<{ task_id: string }, [string, string]>(
166
+ if (tu.changes === 0) {
167
+ const match = db.get<{ task_id: string }>(
187
168
  `SELECT task_id FROM tasks WHERE to_name = ?1 AND content = ?2
188
- AND status IN ('delivered', 'acked', 'running') ORDER BY created_at DESC LIMIT 1`
189
- ).get(alias, task);
169
+ AND status IN ('delivered', 'acked', 'running') ORDER BY created_at DESC LIMIT 1`,
170
+ alias, task);
190
171
  if (match) {
191
- db.run(
192
- `UPDATE tasks SET status = 'replied', result = ?1, completed_at = datetime('now')
193
- WHERE task_id = ?2`,
194
- [result.slice(0, 4000), match.task_id]
195
- );
172
+ db.run(`UPDATE tasks SET status = 'replied', result = ?1, completed_at = datetime('now') WHERE task_id = ?2`,
173
+ [result.slice(0, 4000), match.task_id]);
196
174
  }
197
175
  }
198
-
199
- db.run("COMMIT");
200
- // Log event after commit
201
- const updatedTaskId = taskUpdate.changes > 0 ? task : (db.query<{ task_id: string }, [string]>(
202
- "SELECT task_id FROM tasks WHERE to_name = ?1 AND status = 'replied' ORDER BY completed_at DESC LIMIT 1"
203
- ).get(alias)?.task_id);
204
- if (updatedTaskId) logTaskEvent(updatedTaskId, null, "replied", alias, "report_completion");
205
- } catch (e) {
206
- try { db.run("ROLLBACK"); } catch {}
207
- throw e;
208
- }
176
+ return tu.changes;
177
+ });
178
+ // Log event after transaction
179
+ const updatedTaskId = taskUpdateChanges > 0 ? task : (db.get<{ task_id: string }>(
180
+ "SELECT task_id FROM tasks WHERE to_name = ?1 AND status = 'replied' ORDER BY completed_at DESC LIMIT 1",
181
+ alias)?.task_id);
182
+ if (updatedTaskId) logTaskEvent(updatedTaskId, null, "replied", alias, "report_completion");
209
183
 
210
184
  return {
211
185
  content: [{ type: "text" as const, text: JSON.stringify({ ok: true, completion_id: id }) }],
@@ -221,16 +195,16 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
221
195
  limit: z.number().min(1).max(100).optional().default(10),
222
196
  },
223
197
  async ({ alias, limit }) => {
224
- const rows0 = db.query<{ cnt: number }, [string]>(
225
- "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0"
226
- ).get(alias);
198
+ const rows0 = db.get<{ cnt: number }>(
199
+ "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0",
200
+ alias);
227
201
  console.log(`[${ts()}] ${alias} → get_inbox: ${rows0?.cnt ?? 0} pending messages`);
228
- const rows = db.query<any, [string, number]>(
202
+ const rows = db.all(
229
203
  `SELECT id, type, priority, content, context, from_session, created_at
230
204
  FROM inbox WHERE session_name = ?1 AND acked = 0
231
205
  ORDER BY CASE priority WHEN 'high' THEN 0 WHEN 'normal' THEN 1 ELSE 2 END, created_at
232
- LIMIT ?2`
233
- ).all(alias, limit);
206
+ LIMIT ?2`,
207
+ alias, limit);
234
208
 
235
209
  return {
236
210
  content: [{ type: "text" as const, text: JSON.stringify({ ok: true, messages: rows }) }],
@@ -294,12 +268,11 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
294
268
  if (filter_status) { sql += " AND status = ?"; params.push(filter_status); }
295
269
  if (filter_server) { sql += " AND server = ?"; params.push(filter_server); }
296
270
  sql += " ORDER BY updated_at DESC";
297
- return db.query(sql).all(...params);
298
- })();
271
+ return db.all(sql, ...params);
272
+ });
299
273
 
300
- const summary = db.query<any, []>(
301
- "SELECT status, COUNT(*) as count FROM sessions GROUP BY status"
302
- ).all();
274
+ const summary = db.all(
275
+ "SELECT status, COUNT(*) as count FROM sessions GROUP BY status");
303
276
 
304
277
  return {
305
278
  content: [
@@ -318,13 +291,13 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
318
291
  { alias: z.string().min(1).max(200).describe("Session alias") },
319
292
  async ({ alias }) => {
320
293
  console.log(`[${ts()}] hub → get_session_status: ${alias}`);
321
- const session = db.query("SELECT * FROM sessions WHERE alias = ?1").get(alias);
322
- const pending = db.query<{ cnt: number }, [string]>(
323
- "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0"
324
- ).get(alias);
325
- const recent = db.query(
326
- "SELECT * FROM completions WHERE session_name = ?1 ORDER BY completed_at DESC LIMIT 5"
327
- ).all(alias);
294
+ const session = db.get("SELECT * FROM sessions WHERE alias = ?1", alias);
295
+ const pending = db.get<{ cnt: number }>(
296
+ "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0",
297
+ alias);
298
+ const recent = db.all(
299
+ "SELECT * FROM completions WHERE session_name = ?1 ORDER BY completed_at DESC LIMIT 5",
300
+ alias);
328
301
 
329
302
  return {
330
303
  content: [
@@ -353,7 +326,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
353
326
  const effectiveNetId = getNetworkId(netId);
354
327
 
355
328
  // License check
356
- const license = db.query<any, []>("SELECT type, expires_at FROM licenses ORDER BY created_at LIMIT 1").get();
329
+ const license = db.get<any>("SELECT type, expires_at FROM licenses ORDER BY created_at LIMIT 1");
357
330
  if (license?.expires_at) {
358
331
  const now = new Date().toISOString().replace("T", " ").slice(0, 19);
359
332
  if (license.expires_at < now) {
@@ -367,8 +340,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
367
340
  console.log(`[${ts()}] ${from_session} → send_task → ${alias}: ${task.slice(0, 60)}${priority === "high" ? " [HIGH]" : ""}`);
368
341
  const id = uuidv4();
369
342
  // 事务:inbox + tasks 双写
370
- try {
371
- db.run("BEGIN IMMEDIATE");
343
+ db.transaction(() => {
372
344
  db.run(
373
345
  `INSERT INTO inbox (id, session_name, type, priority, content, context, from_session, requires_response, network_id)
374
346
  VALUES (?1, ?2, 'task', ?3, ?4, ?5, ?6, 'reply', ?7)`,
@@ -379,19 +351,15 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
379
351
  VALUES (?1, ?2, ?3, ?4, 'delivered', ?5, 'reply', datetime('now'), datetime('now'), datetime('now', ?6), ?7)`,
380
352
  [id, from_session, alias, priority, task, `+${ttl_seconds || 3600} seconds`, effectiveNetId]
381
353
  );
382
- db.run("COMMIT");
383
- logTaskEvent(id, null, "delivered", from_session, `→ ${alias}`);
384
- } catch (e) {
385
- try { db.run("ROLLBACK"); } catch {}
386
- throw e;
387
- }
354
+ });
355
+ logTaskEvent(id, null, "delivered", from_session, `→ ${alias}`);
388
356
 
389
- const session = db.query<any, [string]>("SELECT status FROM sessions WHERE alias = ?1").get(alias);
357
+ const session = db.get<any>("SELECT status FROM sessions WHERE alias = ?1", alias);
390
358
 
391
359
  // SSE push by alias
392
- const pending = db.query<{ cnt: number }, [string]>(
393
- "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0"
394
- ).get(alias);
360
+ const pending = db.get<{ cnt: number }>(
361
+ "SELECT COUNT(*) as cnt FROM inbox WHERE session_name = ?1 AND acked = 0",
362
+ alias);
395
363
  pushEvent(alias, { type: "new_task", inbox_count: pending?.cnt ?? 1, priority, from: from_session });
396
364
 
397
365
  return {
@@ -426,7 +394,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
426
394
  [id, alias, message, from_session]
427
395
  );
428
396
 
429
- const session = db.query<any, [string]>("SELECT status FROM sessions WHERE alias = ?1").get(alias);
397
+ const session = db.get<any>("SELECT status FROM sessions WHERE alias = ?1", alias);
430
398
 
431
399
  pushEvent(alias, { type: "new_message", message, from: from_session, message_id: id });
432
400
 
@@ -459,9 +427,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
459
427
  async ({ alias, text, in_reply_to, status: replyStatus, from_session }) => {
460
428
  console.log(`[${ts()}] ${from_session} → send_reply (${replyStatus}) → ${alias}: ${text.slice(0, 60)}`);
461
429
  const id = uuidv4();
462
- let replyLogged = false;
463
- try {
464
- db.run("BEGIN IMMEDIATE");
430
+ const replyLogged = db.transaction(() => {
465
431
  db.run(
466
432
  `INSERT INTO inbox (id, session_name, type, priority, content, from_session, in_reply_to, requires_response)
467
433
  VALUES (?1, ?2, 'reply', 'normal', ?3, ?4, ?5, 'none')`,
@@ -477,20 +443,17 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
477
443
  );
478
444
  if (result.changes === 0) {
479
445
  console.log(`[${ts()}] ⚠ send_reply: task ${in_reply_to?.slice(0, 8)} not found or already terminal`);
480
- } else {
481
- replyLogged = true;
446
+ return false;
482
447
  }
448
+ return true;
483
449
  }
484
- db.run("COMMIT");
485
- } catch (e) {
486
- try { db.run("ROLLBACK"); } catch {}
487
- throw e;
488
- }
450
+ return false;
451
+ });
489
452
 
490
453
  // Log event after commit (outside transaction)
491
454
  if (replyLogged && in_reply_to) logTaskEvent(in_reply_to, null, replyStatus, from_session, text.slice(0, 200));
492
455
 
493
- const session = db.query<any, [string]>("SELECT status FROM sessions WHERE alias = ?1").get(alias);
456
+ const session = db.get<any>("SELECT status FROM sessions WHERE alias = ?1", alias);
494
457
  pushEvent(alias, { type: "new_reply", from: from_session, message_id: id, in_reply_to, status: replyStatus });
495
458
 
496
459
  return {
@@ -537,17 +500,14 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
537
500
  async ({ task_id, from_session }) => {
538
501
  console.log(`[${ts()}] ${from_session} → retry_task → ${task_id.slice(0, 8)}`);
539
502
  // Find the original task
540
- const task = db.query<any, [string]>(
541
- "SELECT * FROM tasks WHERE task_id = ?1"
542
- ).get(task_id);
503
+ const task = db.get<any>("SELECT * FROM tasks WHERE task_id = ?1", task_id);
543
504
  if (!task) {
544
505
  return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "task not found" }) }] };
545
506
  }
546
507
  if (!["failed", "expired", "cancelled"].includes(task.status)) {
547
508
  return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: `task status is ${task.status}, not retryable` }) }] };
548
509
  }
549
- try {
550
- db.run("BEGIN IMMEDIATE");
510
+ db.transaction(() => {
551
511
  // Reset task status
552
512
  db.run(
553
513
  `UPDATE tasks SET status = 'delivered', result = NULL, completed_at = NULL, started_at = NULL, delivered_at = datetime('now'), expires_at = datetime('now', '+1 hour')
@@ -561,12 +521,8 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
561
521
  VALUES (?1, ?2, 'task', ?3, ?4, ?5, 'reply')`,
562
522
  [retryInboxId, task.to_name, task.priority, task.content, from_session]
563
523
  );
564
- db.run("COMMIT");
565
- logTaskEvent(task_id, task.status, "delivered", from_session, "retry");
566
- } catch (e) {
567
- try { db.run("ROLLBACK"); } catch {}
568
- throw e;
569
- }
524
+ });
525
+ logTaskEvent(task_id, task.status, "delivered", from_session, "retry");
570
526
  // SSE push
571
527
  pushEvent(task.to_name, { type: "new_task", inbox_count: 1, priority: task.priority, from: from_session });
572
528
  return {
@@ -583,7 +539,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
583
539
  task_id: z.string().min(1).max(200).describe("Task ID to query"),
584
540
  },
585
541
  async ({ task_id }) => {
586
- const task = db.query<any, [string]>("SELECT * FROM tasks WHERE task_id = ?1").get(task_id);
542
+ const task = db.get<any>("SELECT * FROM tasks WHERE task_id = ?1", task_id);
587
543
  return {
588
544
  content: [{
589
545
  type: "text" as const,
@@ -614,12 +570,11 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
614
570
  if (from_name) { sql += ` AND from_name = ?${params.length + 1}`; params.push(from_name); }
615
571
  sql += ` ORDER BY created_at DESC LIMIT ?${params.length + 1}`;
616
572
  params.push(limit);
617
- const tasks = db.query(sql).all(...params);
573
+ const tasks = db.all(sql, ...params);
618
574
 
619
575
  // Stats
620
- const stats = db.query<any, []>(
621
- "SELECT status, COUNT(*) as count FROM tasks GROUP BY status"
622
- ).all();
576
+ const stats = db.all(
577
+ "SELECT status, COUNT(*) as count FROM tasks GROUP BY status");
623
578
 
624
579
  return {
625
580
  content: [{
@@ -668,26 +623,21 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
668
623
  },
669
624
  async ({ task_id, new_alias, from_session }) => {
670
625
  console.log(`[${ts()}] ${from_session} → reassign_task → ${task_id.slice(0, 8)} → ${new_alias}`);
671
- const task = db.query<any, [string]>("SELECT * FROM tasks WHERE task_id = ?1").get(task_id);
626
+ const task = db.get<any>("SELECT * FROM tasks WHERE task_id = ?1", task_id);
672
627
  if (!task) return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: "task not found" }) }] };
673
628
  if (["replied", "failed", "cancelled", "expired"].includes(task.status)) {
674
629
  return { content: [{ type: "text" as const, text: JSON.stringify({ ok: false, error: `task is terminal (${task.status})` }) }] };
675
630
  }
676
631
  const oldAlias = task.to_name;
677
- try {
678
- db.run("BEGIN IMMEDIATE");
632
+ db.transaction(() => {
679
633
  // Ack old inbox to prevent original agent from picking it up
680
634
  db.run("UPDATE inbox SET acked = 1 WHERE id = ?1 AND acked = 0", [task_id]);
681
635
  db.run("UPDATE tasks SET to_name = ?1, status = 'delivered', started_at = NULL, delivered_at = datetime('now') WHERE task_id = ?2", [new_alias, task_id]);
682
636
  const newInboxId = uuidv4();
683
637
  db.run("INSERT INTO inbox (id, session_name, type, priority, content, from_session, requires_response) VALUES (?1, ?2, 'task', ?3, ?4, ?5, 'reply')",
684
638
  [newInboxId, new_alias, task.priority, task.content, from_session]);
685
- db.run("COMMIT");
686
- logTaskEvent(task_id, task.status, "delivered", from_session, `reassign: ${oldAlias} → ${new_alias}`);
687
- } catch (e) {
688
- try { db.run("ROLLBACK"); } catch {}
689
- throw e;
690
- }
639
+ });
640
+ logTaskEvent(task_id, task.status, "delivered", from_session, `reassign: ${oldAlias} → ${new_alias}`);
691
641
  pushEvent(new_alias, { type: "new_task", inbox_count: 1, priority: task.priority, from: from_session });
692
642
  return { content: [{ type: "text" as const, text: JSON.stringify({ ok: true, task_id, reassigned_from: oldAlias, reassigned_to: new_alias }) }] };
693
643
  }
@@ -710,7 +660,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
710
660
  if (filter_server) { sql += " AND server = ?"; params.push(filter_server); }
711
661
  if (filter_status) { sql += " AND status = ?"; params.push(filter_status); }
712
662
 
713
- const targets = db.query<{ alias: string }, any[]>(sql).all(...params);
663
+ const targets = db.all<{ alias: string }>(sql, ...params);
714
664
  const ids: string[] = [];
715
665
 
716
666
  for (const t of targets) {
@@ -759,7 +709,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
759
709
  sql += ` ORDER BY completed_at DESC LIMIT ?${paramIdx}`;
760
710
  params.push(limit);
761
711
 
762
- const rows = db.query(sql).all(...params);
712
+ const rows = db.all(sql, ...params);
763
713
  return {
764
714
  content: [{ type: "text" as const, text: JSON.stringify({ ok: true, completions: rows }) }],
765
715
  };