@clawchatsai/connector 0.0.40 → 0.0.42

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 +155 -73
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawchatsai/connector",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
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
@@ -370,6 +370,36 @@ function closeAllDbs() {
370
370
  if (_globalDb) { _globalDb.close(); _globalDb = null; }
371
371
  }
372
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
+ `);
401
+ }
402
+
373
403
  function migrate(db) {
374
404
  db.exec(`
375
405
  CREATE TABLE IF NOT EXISTS threads (
@@ -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 ---
@@ -1556,6 +1595,35 @@ async function handleWorkspaceFileWrite(req, res, query) {
1556
1595
  send(res, 200, { ok: true });
1557
1596
  }
1558
1597
 
1598
+ function handleWorkspaceFileDelete(req, res, query) {
1599
+ const filePath = query.path;
1600
+ if (!filePath) return sendError(res, 400, 'Missing path parameter');
1601
+
1602
+ const resolved = path.resolve(filePath.replace(/^~/, HOME));
1603
+
1604
+ // Security: only allow paths under home directory
1605
+ if (!resolved.startsWith(HOME)) {
1606
+ return sendError(res, 403, 'Access denied');
1607
+ }
1608
+
1609
+ if (!fs.existsSync(resolved)) {
1610
+ return sendError(res, 404, 'Path not found');
1611
+ }
1612
+
1613
+ try {
1614
+ const stat = fs.statSync(resolved);
1615
+ if (stat.isDirectory()) {
1616
+ fs.rmSync(resolved, { recursive: true, force: true });
1617
+ send(res, 200, { ok: true, type: 'dir' });
1618
+ } else {
1619
+ fs.unlinkSync(resolved);
1620
+ send(res, 200, { ok: true, type: 'file' });
1621
+ }
1622
+ } catch (err) {
1623
+ sendError(res, 500, 'Delete failed: ' + err.message);
1624
+ }
1625
+ }
1626
+
1559
1627
  async function handleWorkspaceUpload(req, res, query) {
1560
1628
  const targetDir = query.path;
1561
1629
  if (!targetDir) return sendError(res, 400, 'Missing path parameter');
@@ -2288,6 +2356,9 @@ async function handleRequest(req, res) {
2288
2356
  if (method === 'PUT' && urlPath === '/api/workspace/file') {
2289
2357
  return await handleWorkspaceFileWrite(req, res, query);
2290
2358
  }
2359
+ if (method === 'DELETE' && urlPath === '/api/workspace/file') {
2360
+ return handleWorkspaceFileDelete(req, res, query);
2361
+ }
2291
2362
  if (method === 'POST' && urlPath === '/api/workspace/upload') {
2292
2363
  return await handleWorkspaceUpload(req, res, query);
2293
2364
  }
@@ -3453,12 +3524,17 @@ export function createApp(config = {}) {
3453
3524
  const search = query.search || '';
3454
3525
  let threads, total;
3455
3526
  if (search) {
3456
- const ftsQuery = `SELECT DISTINCT m.thread_id FROM messages m JOIN messages_fts ON messages_fts.rowid = m.rowid WHERE messages_fts MATCH ?`;
3457
- const matchingIds = db.prepare(ftsQuery).all(search).map(r => r.thread_id);
3458
- if (matchingIds.length === 0) return send(res, 200, { threads: [], total: 0, page });
3459
- const placeholders = matchingIds.map(() => '?').join(',');
3460
- total = db.prepare(`SELECT COUNT(*) as c FROM threads WHERE id IN (${placeholders})`).get(...matchingIds).c;
3461
- 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
+ }
3462
3538
  } else {
3463
3539
  total = db.prepare('SELECT COUNT(*) as c FROM threads').get().c;
3464
3540
  threads = db.prepare('SELECT * FROM threads ORDER BY pinned DESC, sort_order DESC, updated_at DESC LIMIT ? OFFSET ?').all(limit, offset);
@@ -3665,14 +3741,19 @@ export function createApp(config = {}) {
3665
3741
  const page = parseInt(query.page || '1', 10);
3666
3742
  const limit = Math.min(parseInt(query.limit || '20', 10), 100);
3667
3743
  const offset = (page - 1) * limit;
3668
- const results = db.prepare(`
3669
- SELECT m.id as messageId, m.thread_id as threadId, t.title as threadTitle, m.role,
3670
- snippet(messages_fts, 0, '<mark>', '</mark>', '...', 40) as content, m.timestamp
3671
- FROM messages_fts JOIN messages m ON messages_fts.rowid = m.rowid JOIN threads t ON m.thread_id = t.id
3672
- WHERE messages_fts MATCH ? ORDER BY rank LIMIT ? OFFSET ?
3673
- `).all(q, limit, offset);
3674
- const totalRow = db.prepare(`SELECT COUNT(*) as c FROM messages_fts WHERE messages_fts MATCH ?`).get(q);
3675
- 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
+ }
3676
3757
  }
3677
3758
 
3678
3759
  function _handleExport(req, res) {
@@ -4404,6 +4485,7 @@ export function createApp(config = {}) {
4404
4485
  if (method === 'GET' && urlPath === '/api/workspace') return handleWorkspaceList(req, res, query);
4405
4486
  if (method === 'GET' && urlPath === '/api/workspace/file') return handleWorkspaceFileRead(req, res, query);
4406
4487
  if (method === 'PUT' && urlPath === '/api/workspace/file') return await handleWorkspaceFileWrite(req, res, query);
4488
+ if (method === 'DELETE' && urlPath === '/api/workspace/file') return handleWorkspaceFileDelete(req, res, query);
4407
4489
  if (method === 'POST' && urlPath === '/api/workspace/upload') return await handleWorkspaceUpload(req, res, query);
4408
4490
  if (method === 'GET' && urlPath === '/api/memory/status') return await _handleMemoryStatus(req, res);
4409
4491
  if (method === 'GET' && urlPath === '/api/memory/list') return await _handleMemoryList(req, res, query);