@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.
- package/package.json +1 -1
- package/server.js +155 -73
package/package.json
CHANGED
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
|
|
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
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
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
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
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);
|