@clawchatsai/connector 0.0.41 → 0.0.43

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +139 -90
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawchatsai/connector",
3
- "version": "0.0.41",
3
+ "version": "0.0.43",
4
4
  "type": "module",
5
5
  "description": "ClawChats OpenClaw plugin — P2P tunnel + local API bridge",
6
6
  "main": "dist/index.js",
package/server.js CHANGED
@@ -334,14 +334,14 @@ function getActiveDb() {
334
334
  return getDb(getWorkspaces().active);
335
335
  }
336
336
 
337
- let _globalDb = null;
338
- function getGlobalDb() {
339
- if (_globalDb) return _globalDb;
340
- fs.mkdirSync(DATA_DIR, { recursive: true });
341
- const dbPath = path.join(DATA_DIR, 'global.db');
342
- _globalDb = new Database(dbPath);
343
- _globalDb.pragma('journal_mode = WAL');
344
- _globalDb.exec(`
337
+ const _globalDbCache = new Map(); // keyed by resolved dbPath
338
+ function getGlobalDb(dataDir = DATA_DIR) {
339
+ const dbPath = path.join(dataDir, 'global.db');
340
+ if (_globalDbCache.has(dbPath)) return _globalDbCache.get(dbPath);
341
+ fs.mkdirSync(dataDir, { recursive: true });
342
+ const db = new Database(dbPath);
343
+ db.pragma('journal_mode = WAL');
344
+ db.exec(`
345
345
  CREATE TABLE IF NOT EXISTS custom_emojis (
346
346
  name TEXT NOT NULL,
347
347
  pack TEXT NOT NULL DEFAULT 'slackmojis',
@@ -351,7 +351,8 @@ function getGlobalDb() {
351
351
  PRIMARY KEY (name, pack)
352
352
  )
353
353
  `);
354
- return _globalDb;
354
+ _globalDbCache.set(dbPath, db);
355
+ return db;
355
356
  }
356
357
 
357
358
  function closeDb(workspaceName) {
@@ -363,11 +364,40 @@ function closeDb(workspaceName) {
363
364
  }
364
365
 
365
366
  function closeAllDbs() {
366
- for (const [name, db] of dbCache) {
367
- db.close();
368
- }
367
+ for (const [, db] of dbCache) db.close();
369
368
  dbCache.clear();
370
- if (_globalDb) { _globalDb.close(); _globalDb = null; }
369
+ for (const [, db] of _globalDbCache) db.close();
370
+ _globalDbCache.clear();
371
+ }
372
+
373
+ function _createFtsTables(db) {
374
+ db.exec(`
375
+ CREATE VIRTUAL TABLE messages_fts USING fts5(
376
+ content,
377
+ content=messages,
378
+ content_rowid=rowid,
379
+ tokenize='porter unicode61'
380
+ );
381
+ CREATE TRIGGER messages_ai AFTER INSERT ON messages BEGIN
382
+ INSERT INTO messages_fts(rowid, content) VALUES (new.rowid, new.content);
383
+ END;
384
+ CREATE TRIGGER messages_ad AFTER DELETE ON messages BEGIN
385
+ INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', old.rowid, old.content);
386
+ END;
387
+ CREATE TRIGGER messages_au AFTER UPDATE ON messages BEGIN
388
+ INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', old.rowid, old.content);
389
+ INSERT INTO messages_fts(rowid, content) VALUES (new.rowid, new.content);
390
+ END;
391
+ `);
392
+ }
393
+
394
+ function _dropFtsTables(db) {
395
+ db.exec(`
396
+ DROP TABLE IF EXISTS messages_fts;
397
+ DROP TRIGGER IF EXISTS messages_ai;
398
+ DROP TRIGGER IF EXISTS messages_ad;
399
+ DROP TRIGGER IF EXISTS messages_au;
400
+ `);
371
401
  }
372
402
 
373
403
  function migrate(db) {
@@ -428,30 +458,29 @@ function migrate(db) {
428
458
  db.exec('CREATE INDEX IF NOT EXISTS idx_unread_thread ON unread_messages(thread_id)');
429
459
 
430
460
  // FTS5 table — CREATE VIRTUAL TABLE doesn't support IF NOT EXISTS in all versions,
431
- // so check if it exists first
461
+ // so check existence first, then verify integrity and auto-repair if corrupted.
432
462
  const hasFts = db.prepare(
433
463
  "SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts'"
434
464
  ).get();
435
465
  if (!hasFts) {
436
- db.exec(`
437
- CREATE VIRTUAL TABLE messages_fts USING fts5(
438
- content,
439
- content=messages,
440
- content_rowid=rowid,
441
- tokenize='porter unicode61'
442
- );
443
-
444
- CREATE TRIGGER messages_ai AFTER INSERT ON messages BEGIN
445
- INSERT INTO messages_fts(rowid, content) VALUES (new.rowid, new.content);
446
- END;
447
- CREATE TRIGGER messages_ad AFTER DELETE ON messages BEGIN
448
- INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', old.rowid, old.content);
449
- END;
450
- CREATE TRIGGER messages_au AFTER UPDATE ON messages BEGIN
451
- INSERT INTO messages_fts(messages_fts, rowid, content) VALUES('delete', old.rowid, old.content);
452
- INSERT INTO messages_fts(rowid, content) VALUES (new.rowid, new.content);
453
- END;
454
- `);
466
+ _createFtsTables(db);
467
+ } else {
468
+ // Integrity check: corruption causes all message writes to 500.
469
+ // Attempt rebuild first; if that fails, drop entirely for graceful degradation
470
+ // (messages still save, search returns empty until next restart recreates the table).
471
+ try {
472
+ db.prepare("INSERT INTO messages_fts(messages_fts) VALUES('integrity-check')").run();
473
+ } catch (err) {
474
+ console.warn('[DB] messages_fts integrity check failed, attempting rebuild:', err.message);
475
+ try {
476
+ db.prepare("INSERT INTO messages_fts(messages_fts) VALUES('rebuild')").run();
477
+ console.log('[DB] messages_fts rebuilt successfully search index restored');
478
+ } catch (rebuildErr) {
479
+ console.error('[DB] messages_fts rebuild failed, dropping FTS for graceful degradation:', rebuildErr.message);
480
+ _dropFtsTables(db);
481
+ // On the next gateway restart the table will be recreated fresh via the !hasFts path
482
+ }
483
+ }
455
484
  }
456
485
  }
457
486
 
@@ -879,24 +908,29 @@ function handleGetThreads(req, res, params, query) {
879
908
  let threads, total;
880
909
  if (search) {
881
910
  // FTS5 search across messages, return matching thread IDs
882
- const ftsQuery = `
883
- SELECT DISTINCT m.thread_id
884
- FROM messages m
885
- JOIN messages_fts ON messages_fts.rowid = m.rowid
886
- WHERE messages_fts MATCH ?
887
- `;
888
- const matchingIds = db.prepare(ftsQuery).all(search).map(r => r.thread_id);
889
- if (matchingIds.length === 0) {
911
+ try {
912
+ const ftsQuery = `
913
+ SELECT DISTINCT m.thread_id
914
+ FROM messages m
915
+ JOIN messages_fts ON messages_fts.rowid = m.rowid
916
+ WHERE messages_fts MATCH ?
917
+ `;
918
+ const matchingIds = db.prepare(ftsQuery).all(search).map(r => r.thread_id);
919
+ if (matchingIds.length === 0) {
920
+ return send(res, 200, { threads: [], total: 0, page });
921
+ }
922
+ const placeholders = matchingIds.map(() => '?').join(',');
923
+ total = db.prepare(
924
+ `SELECT COUNT(*) as c FROM threads WHERE id IN (${placeholders})`
925
+ ).get(...matchingIds).c;
926
+ threads = db.prepare(
927
+ `SELECT t.*, (SELECT MAX(m.timestamp) FROM messages m WHERE m.thread_id = t.id) as last_message_at
928
+ FROM threads t WHERE t.id IN (${placeholders}) ORDER BY t.pinned DESC, t.sort_order DESC, t.updated_at DESC LIMIT ? OFFSET ?`
929
+ ).all(...matchingIds, limit, offset);
930
+ } catch (ftsErr) {
931
+ console.warn('[DB] FTS thread search failed, returning empty results:', ftsErr.message);
890
932
  return send(res, 200, { threads: [], total: 0, page });
891
933
  }
892
- const placeholders = matchingIds.map(() => '?').join(',');
893
- total = db.prepare(
894
- `SELECT COUNT(*) as c FROM threads WHERE id IN (${placeholders})`
895
- ).get(...matchingIds).c;
896
- threads = db.prepare(
897
- `SELECT t.*, (SELECT MAX(m.timestamp) FROM messages m WHERE m.thread_id = t.id) as last_message_at
898
- FROM threads t WHERE t.id IN (${placeholders}) ORDER BY t.pinned DESC, t.sort_order DESC, t.updated_at DESC LIMIT ? OFFSET ?`
899
- ).all(...matchingIds, limit, offset);
900
934
  } else {
901
935
  total = db.prepare('SELECT COUNT(*) as c FROM threads').get().c;
902
936
  threads = db.prepare(
@@ -1183,29 +1217,34 @@ function handleSearch(req, res, params, query) {
1183
1217
  const offset = (page - 1) * limit;
1184
1218
 
1185
1219
  // FTS5 search with snippet
1186
- const results = db.prepare(`
1187
- SELECT
1188
- m.id as messageId,
1189
- m.thread_id as threadId,
1190
- t.title as threadTitle,
1191
- m.role,
1192
- snippet(messages_fts, 0, '<mark>', '</mark>', '...', 40) as content,
1193
- m.timestamp
1194
- FROM messages_fts
1195
- JOIN messages m ON messages_fts.rowid = m.rowid
1196
- JOIN threads t ON m.thread_id = t.id
1197
- WHERE messages_fts MATCH ?
1198
- ORDER BY rank
1199
- LIMIT ? OFFSET ?
1200
- `).all(q, limit, offset);
1201
-
1202
- const totalRow = db.prepare(`
1203
- SELECT COUNT(*) as c
1204
- FROM messages_fts
1205
- WHERE messages_fts MATCH ?
1206
- `).get(q);
1207
-
1208
- send(res, 200, { results, total: totalRow.c });
1220
+ try {
1221
+ const results = db.prepare(`
1222
+ SELECT
1223
+ m.id as messageId,
1224
+ m.thread_id as threadId,
1225
+ t.title as threadTitle,
1226
+ m.role,
1227
+ snippet(messages_fts, 0, '<mark>', '</mark>', '...', 40) as content,
1228
+ m.timestamp
1229
+ FROM messages_fts
1230
+ JOIN messages m ON messages_fts.rowid = m.rowid
1231
+ JOIN threads t ON m.thread_id = t.id
1232
+ WHERE messages_fts MATCH ?
1233
+ ORDER BY rank
1234
+ LIMIT ? OFFSET ?
1235
+ `).all(q, limit, offset);
1236
+
1237
+ const totalRow = db.prepare(`
1238
+ SELECT COUNT(*) as c
1239
+ FROM messages_fts
1240
+ WHERE messages_fts MATCH ?
1241
+ `).get(q);
1242
+
1243
+ send(res, 200, { results, total: totalRow.c });
1244
+ } catch (ftsErr) {
1245
+ console.warn('[DB] FTS message search failed, returning empty results:', ftsErr.message);
1246
+ send(res, 200, { results: [], total: 0 });
1247
+ }
1209
1248
  }
1210
1249
 
1211
1250
  // --- Export ---
@@ -3485,12 +3524,17 @@ export function createApp(config = {}) {
3485
3524
  const search = query.search || '';
3486
3525
  let threads, total;
3487
3526
  if (search) {
3488
- const ftsQuery = `SELECT DISTINCT m.thread_id FROM messages m JOIN messages_fts ON messages_fts.rowid = m.rowid WHERE messages_fts MATCH ?`;
3489
- const matchingIds = db.prepare(ftsQuery).all(search).map(r => r.thread_id);
3490
- if (matchingIds.length === 0) return send(res, 200, { threads: [], total: 0, page });
3491
- const placeholders = matchingIds.map(() => '?').join(',');
3492
- total = db.prepare(`SELECT COUNT(*) as c FROM threads WHERE id IN (${placeholders})`).get(...matchingIds).c;
3493
- threads = db.prepare(`SELECT * FROM threads WHERE id IN (${placeholders}) ORDER BY pinned DESC, sort_order DESC, updated_at DESC LIMIT ? OFFSET ?`).all(...matchingIds, limit, offset);
3527
+ try {
3528
+ const ftsQuery = `SELECT DISTINCT m.thread_id FROM messages m JOIN messages_fts ON messages_fts.rowid = m.rowid WHERE messages_fts MATCH ?`;
3529
+ const matchingIds = db.prepare(ftsQuery).all(search).map(r => r.thread_id);
3530
+ if (matchingIds.length === 0) return send(res, 200, { threads: [], total: 0, page });
3531
+ const placeholders = matchingIds.map(() => '?').join(',');
3532
+ total = db.prepare(`SELECT COUNT(*) as c FROM threads WHERE id IN (${placeholders})`).get(...matchingIds).c;
3533
+ threads = db.prepare(`SELECT * FROM threads WHERE id IN (${placeholders}) ORDER BY pinned DESC, sort_order DESC, updated_at DESC LIMIT ? OFFSET ?`).all(...matchingIds, limit, offset);
3534
+ } catch (ftsErr) {
3535
+ console.warn('[DB] FTS thread search failed, returning empty results:', ftsErr.message);
3536
+ return send(res, 200, { threads: [], total: 0, page });
3537
+ }
3494
3538
  } else {
3495
3539
  total = db.prepare('SELECT COUNT(*) as c FROM threads').get().c;
3496
3540
  threads = db.prepare('SELECT * FROM threads ORDER BY pinned DESC, sort_order DESC, updated_at DESC LIMIT ? OFFSET ?').all(limit, offset);
@@ -3697,14 +3741,19 @@ export function createApp(config = {}) {
3697
3741
  const page = parseInt(query.page || '1', 10);
3698
3742
  const limit = Math.min(parseInt(query.limit || '20', 10), 100);
3699
3743
  const offset = (page - 1) * limit;
3700
- const results = db.prepare(`
3701
- SELECT m.id as messageId, m.thread_id as threadId, t.title as threadTitle, m.role,
3702
- snippet(messages_fts, 0, '<mark>', '</mark>', '...', 40) as content, m.timestamp
3703
- FROM messages_fts JOIN messages m ON messages_fts.rowid = m.rowid JOIN threads t ON m.thread_id = t.id
3704
- WHERE messages_fts MATCH ? ORDER BY rank LIMIT ? OFFSET ?
3705
- `).all(q, limit, offset);
3706
- const totalRow = db.prepare(`SELECT COUNT(*) as c FROM messages_fts WHERE messages_fts MATCH ?`).get(q);
3707
- send(res, 200, { results, total: totalRow.c });
3744
+ try {
3745
+ const results = db.prepare(`
3746
+ SELECT m.id as messageId, m.thread_id as threadId, t.title as threadTitle, m.role,
3747
+ snippet(messages_fts, 0, '<mark>', '</mark>', '...', 40) as content, m.timestamp
3748
+ FROM messages_fts JOIN messages m ON messages_fts.rowid = m.rowid JOIN threads t ON m.thread_id = t.id
3749
+ WHERE messages_fts MATCH ? ORDER BY rank LIMIT ? OFFSET ?
3750
+ `).all(q, limit, offset);
3751
+ const totalRow = db.prepare(`SELECT COUNT(*) as c FROM messages_fts WHERE messages_fts MATCH ?`).get(q);
3752
+ send(res, 200, { results, total: totalRow.c });
3753
+ } catch (ftsErr) {
3754
+ console.warn('[DB] FTS message search failed, returning empty results:', ftsErr.message);
3755
+ send(res, 200, { results: [], total: 0 });
3756
+ }
3708
3757
  }
3709
3758
 
3710
3759
  function _handleExport(req, res) {
@@ -4368,7 +4417,7 @@ export function createApp(config = {}) {
4368
4417
  // Custom emoji listing (no auth)
4369
4418
  if (method === 'GET' && urlPath === '/api/emoji') {
4370
4419
  try {
4371
- const db = getGlobalDb();
4420
+ const db = getGlobalDb(_DATA_DIR);
4372
4421
  const rows = db.prepare('SELECT name, pack, url, mime_type FROM custom_emojis ORDER BY created_at DESC').all();
4373
4422
  res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300' });
4374
4423
  return res.end(JSON.stringify(rows));
@@ -4412,7 +4461,7 @@ export function createApp(config = {}) {
4412
4461
  if (urlLower.endsWith('.gif')) mimeType = 'image/gif';
4413
4462
  else if (urlLower.endsWith('.webp')) mimeType = 'image/webp';
4414
4463
  else if (urlLower.endsWith('.jpg') || urlLower.endsWith('.jpeg')) mimeType = 'image/jpeg';
4415
- const db = getGlobalDb();
4464
+ const db = getGlobalDb(_DATA_DIR);
4416
4465
  db.prepare('INSERT OR REPLACE INTO custom_emojis (name, pack, url, mime_type) VALUES (?, ?, ?, ?)').run(safeName, targetPack, url, mimeType);
4417
4466
  res.writeHead(200, { 'Content-Type': 'application/json' });
4418
4467
  return res.end(JSON.stringify({ name: safeName, pack: targetPack, url, mime_type: mimeType }));
@@ -4424,7 +4473,7 @@ export function createApp(config = {}) {
4424
4473
  try {
4425
4474
  const { name, pack } = await parseBody(req);
4426
4475
  if (!name || !pack) { res.writeHead(400, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ error: 'Missing name or pack' })); }
4427
- const db = getGlobalDb();
4476
+ const db = getGlobalDb(_DATA_DIR);
4428
4477
  db.prepare('DELETE FROM custom_emojis WHERE name = ? AND pack = ?').run(name, pack);
4429
4478
  res.writeHead(200, { 'Content-Type': 'application/json' });
4430
4479
  return res.end(JSON.stringify({ ok: true }));
@@ -4581,7 +4630,7 @@ if (isDirectRun) {
4581
4630
  app.gatewayClient.connect();
4582
4631
 
4583
4632
  // Initialize global DB (custom emojis, etc.)
4584
- getGlobalDb();
4633
+ getGlobalDb(_DATA_DIR);
4585
4634
  });
4586
4635
 
4587
4636
  // Graceful shutdown