otherwise-cli 0.1.0

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 (81) hide show
  1. package/README.md +193 -0
  2. package/bin/otherwise.js +5 -0
  3. package/frontend/404.html +84 -0
  4. package/frontend/assets/OpenDyslexic3-Bold-CDyRs55Y.ttf +0 -0
  5. package/frontend/assets/OpenDyslexic3-Regular-CIBXa4WE.ttf +0 -0
  6. package/frontend/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  7. package/frontend/assets/conversational-worker-CeKiciGk.js +2929 -0
  8. package/frontend/assets/dictation-worker-D0aYfq8b.js +29 -0
  9. package/frontend/assets/gemini-color-CgSQmmva.png +0 -0
  10. package/frontend/assets/index-BLux5ps4.js +21 -0
  11. package/frontend/assets/index-Blh8_TEM.js +5272 -0
  12. package/frontend/assets/index-BpQ1PuKu.js +18 -0
  13. package/frontend/assets/index-Df737c8w.css +1 -0
  14. package/frontend/assets/index-xaYHL6wb.js +113 -0
  15. package/frontend/assets/ort-wasm-simd-threaded.asyncify-BynIiDiv.wasm +0 -0
  16. package/frontend/assets/ort-wasm-simd-threaded.jsep-B0T3yYHD.wasm +0 -0
  17. package/frontend/assets/transformers-tULNc5V3.js +31 -0
  18. package/frontend/assets/tts-worker-DPJWqT7N.js +2899 -0
  19. package/frontend/assets/voice-mode-worker-GzvIE_uh.js +2927 -0
  20. package/frontend/assets/worker-2d5ABSLU.js +31 -0
  21. package/frontend/banner.png +0 -0
  22. package/frontend/favicon.svg +3 -0
  23. package/frontend/google55e5ec47ee14a5f8.html +1 -0
  24. package/frontend/index.html +234 -0
  25. package/frontend/manifest.json +17 -0
  26. package/frontend/pdf.worker.min.mjs +21 -0
  27. package/frontend/robots.txt +5 -0
  28. package/frontend/sitemap.xml +27 -0
  29. package/package.json +81 -0
  30. package/src/agent/index.js +1066 -0
  31. package/src/agent/location.js +51 -0
  32. package/src/agent/prompt.js +548 -0
  33. package/src/agent/tools.js +4372 -0
  34. package/src/browser/detect.js +68 -0
  35. package/src/browser/session.js +1109 -0
  36. package/src/config.js +137 -0
  37. package/src/email/client.js +503 -0
  38. package/src/index.js +557 -0
  39. package/src/inference/anthropic.js +113 -0
  40. package/src/inference/google.js +373 -0
  41. package/src/inference/index.js +81 -0
  42. package/src/inference/ollama.js +383 -0
  43. package/src/inference/openai.js +140 -0
  44. package/src/inference/openrouter.js +378 -0
  45. package/src/inference/xai.js +200 -0
  46. package/src/logBridge.js +9 -0
  47. package/src/models.js +146 -0
  48. package/src/remote/client.js +225 -0
  49. package/src/scheduler/cron.js +243 -0
  50. package/src/server.js +3876 -0
  51. package/src/storage/db.js +1135 -0
  52. package/src/storage/supabase.js +364 -0
  53. package/src/tunnel/cloudflare.js +241 -0
  54. package/src/ui/components/App.jsx +687 -0
  55. package/src/ui/components/BrowserSelect.jsx +111 -0
  56. package/src/ui/components/FilePicker.jsx +472 -0
  57. package/src/ui/components/Header.jsx +444 -0
  58. package/src/ui/components/HelpPanel.jsx +173 -0
  59. package/src/ui/components/HistoryPanel.jsx +158 -0
  60. package/src/ui/components/MessageList.jsx +235 -0
  61. package/src/ui/components/ModelSelector.jsx +304 -0
  62. package/src/ui/components/PromptInput.jsx +515 -0
  63. package/src/ui/components/StreamingResponse.jsx +134 -0
  64. package/src/ui/components/ThinkingIndicator.jsx +365 -0
  65. package/src/ui/components/ToolExecution.jsx +714 -0
  66. package/src/ui/components/index.js +82 -0
  67. package/src/ui/context/TerminalContext.jsx +150 -0
  68. package/src/ui/context/index.js +13 -0
  69. package/src/ui/hooks/index.js +16 -0
  70. package/src/ui/hooks/useChatState.js +675 -0
  71. package/src/ui/hooks/useCommands.js +280 -0
  72. package/src/ui/hooks/useFileAttachments.js +216 -0
  73. package/src/ui/hooks/useKeyboardShortcuts.js +173 -0
  74. package/src/ui/hooks/useNotifications.js +185 -0
  75. package/src/ui/hooks/useTerminalSize.js +151 -0
  76. package/src/ui/hooks/useWebSocket.js +273 -0
  77. package/src/ui/index.js +94 -0
  78. package/src/ui/ink-runner.js +22 -0
  79. package/src/ui/utils/formatters.js +424 -0
  80. package/src/ui/utils/index.js +6 -0
  81. package/src/ui/utils/markdown.js +166 -0
@@ -0,0 +1,1135 @@
1
+ import Database from "better-sqlite3";
2
+ import { randomUUID } from "crypto";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ import { mkdirSync, existsSync } from "fs";
6
+
7
+ let db = null;
8
+
9
+ /** UUID regex for validation */
10
+ const UUID_REGEX =
11
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
12
+ export function isValidChatId(id) {
13
+ if (id == null || typeof id !== "string") return false;
14
+ return UUID_REGEX.test(id);
15
+ }
16
+
17
+ /**
18
+ * Get the database directory path
19
+ */
20
+ function getDbPath() {
21
+ const dataDir = join(homedir(), ".otherwise");
22
+ if (!existsSync(dataDir)) {
23
+ mkdirSync(dataDir, { recursive: true });
24
+ }
25
+ return join(dataDir, "otherwise.db");
26
+ }
27
+
28
+ /**
29
+ * Initialize the database and create tables
30
+ */
31
+ export async function initDb() {
32
+ const dbPath = getDbPath();
33
+ db = new Database(dbPath);
34
+
35
+ // Enable WAL mode for better concurrent access
36
+ db.pragma("journal_mode = WAL");
37
+
38
+ // Create tables
39
+ db.exec(`
40
+ -- Chats table
41
+ CREATE TABLE IF NOT EXISTS chats (
42
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
43
+ title TEXT,
44
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
45
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
46
+ );
47
+
48
+ -- Messages table
49
+ CREATE TABLE IF NOT EXISTS messages (
50
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
51
+ chat_id INTEGER NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
52
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
53
+ content TEXT NOT NULL,
54
+ metadata JSON,
55
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
56
+ );
57
+
58
+ -- Index for faster message lookup by chat
59
+ CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id);
60
+
61
+ -- Embeddings table for memory visualization
62
+ CREATE TABLE IF NOT EXISTS embeddings (
63
+ chat_id INTEGER PRIMARY KEY REFERENCES chats(id) ON DELETE CASCADE,
64
+ embedding BLOB NOT NULL,
65
+ provider TEXT DEFAULT 'openai',
66
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
67
+ );
68
+
69
+ -- Scheduled tasks table
70
+ CREATE TABLE IF NOT EXISTS scheduled_tasks (
71
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
72
+ cron_expression TEXT NOT NULL,
73
+ description TEXT NOT NULL,
74
+ next_run DATETIME,
75
+ last_run DATETIME,
76
+ enabled INTEGER DEFAULT 1,
77
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
78
+ );
79
+
80
+ -- Index for finding due tasks
81
+ CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_next_run ON scheduled_tasks(next_run);
82
+
83
+ -- File snapshots table for undo on regeneration
84
+ -- Stores file content BEFORE write_file/edit_file tool modifies it
85
+ CREATE TABLE IF NOT EXISTS file_snapshots (
86
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
87
+ chat_id INTEGER NOT NULL,
88
+ message_index INTEGER NOT NULL,
89
+ tool_call_id TEXT,
90
+ file_path TEXT NOT NULL,
91
+ original_content TEXT,
92
+ file_existed INTEGER NOT NULL DEFAULT 1,
93
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
94
+ UNIQUE(chat_id, message_index, file_path)
95
+ );
96
+
97
+ -- Index for fast lookup during revert
98
+ CREATE INDEX IF NOT EXISTS idx_file_snapshots_chat_message ON file_snapshots(chat_id, message_index);
99
+
100
+ -- RAG Documents table for document storage
101
+ CREATE TABLE IF NOT EXISTS rag_documents (
102
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
103
+ name TEXT NOT NULL,
104
+ chunk_count INTEGER DEFAULT 0,
105
+ file_count INTEGER DEFAULT 1,
106
+ files TEXT,
107
+ summary TEXT,
108
+ upload_date DATETIME DEFAULT CURRENT_TIMESTAMP,
109
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
110
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
111
+ );
112
+
113
+ -- RAG Chunks table for document chunks with embeddings
114
+ CREATE TABLE IF NOT EXISTS rag_chunks (
115
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
116
+ doc_id INTEGER NOT NULL REFERENCES rag_documents(id) ON DELETE CASCADE,
117
+ text TEXT NOT NULL,
118
+ page_number INTEGER,
119
+ chunk_index INTEGER NOT NULL,
120
+ embedding BLOB NOT NULL,
121
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
122
+ );
123
+
124
+ -- Index for fast chunk lookup by document
125
+ CREATE INDEX IF NOT EXISTS idx_rag_chunks_doc_id ON rag_chunks(doc_id);
126
+
127
+ -- Shell undo log for revert on regeneration (Strategy 1: undo shell commands like mkdir)
128
+ CREATE TABLE IF NOT EXISTS shell_undo_log (
129
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
130
+ chat_id INTEGER NOT NULL,
131
+ message_index INTEGER NOT NULL,
132
+ tool_call_id TEXT,
133
+ op TEXT NOT NULL,
134
+ path TEXT,
135
+ path_src TEXT,
136
+ path_dest TEXT,
137
+ cwd TEXT,
138
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
139
+ );
140
+ CREATE INDEX IF NOT EXISTS idx_shell_undo_log_chat_message ON shell_undo_log(chat_id, message_index);
141
+ `);
142
+
143
+ // Migration: add created_dir to file_snapshots if missing (Strategy 2: undo dirs created by write_file)
144
+ const tableInfo = db.prepare("PRAGMA table_info(file_snapshots)").all();
145
+ if (!tableInfo.some((col) => col.name === "created_dir")) {
146
+ db.exec("ALTER TABLE file_snapshots ADD COLUMN created_dir TEXT");
147
+ }
148
+
149
+ // Migration: add cloud_chat_id to chats for CLI→Supabase sync (frontend reads from Supabase only)
150
+ const chatsInfo = db.prepare("PRAGMA table_info(chats)").all();
151
+ if (!chatsInfo.some((col) => col.name === "cloud_chat_id")) {
152
+ db.exec("ALTER TABLE chats ADD COLUMN cloud_chat_id TEXT");
153
+ }
154
+
155
+ // Migration: integer chat id -> UUID (unified local/cloud id)
156
+ const idCol = chatsInfo.find((c) => c.name === "id");
157
+ if (idCol && idCol.type.toUpperCase() === "INTEGER") {
158
+ if (process.env.SILENT_MODE !== "true") {
159
+ console.log("Database: migrating chat ids to UUID...");
160
+ }
161
+ const idMap = new Map(); // old integer id -> new uuid
162
+
163
+ db.exec(`
164
+ CREATE TABLE chats_new (
165
+ id TEXT PRIMARY KEY,
166
+ title TEXT,
167
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
168
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
169
+ cloud_chat_id TEXT
170
+ );
171
+ `);
172
+ const oldChats = db.prepare("SELECT * FROM chats").all();
173
+ const insertChat = db.prepare(
174
+ "INSERT INTO chats_new (id, title, created_at, updated_at, cloud_chat_id) VALUES (?, ?, ?, ?, ?)",
175
+ );
176
+ for (const c of oldChats) {
177
+ const uuid =
178
+ c.cloud_chat_id && UUID_REGEX.test(String(c.cloud_chat_id))
179
+ ? c.cloud_chat_id
180
+ : randomUUID();
181
+ idMap.set(c.id, uuid);
182
+ insertChat.run(
183
+ uuid,
184
+ c.title,
185
+ c.created_at,
186
+ c.updated_at,
187
+ c.cloud_chat_id || null,
188
+ );
189
+ }
190
+
191
+ db.exec(`
192
+ CREATE TABLE messages_new (
193
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
194
+ chat_id TEXT NOT NULL,
195
+ role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
196
+ content TEXT NOT NULL,
197
+ metadata JSON,
198
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
199
+ );
200
+ CREATE INDEX IF NOT EXISTS idx_messages_new_chat_id ON messages_new(chat_id);
201
+ `);
202
+ const oldMessages = db.prepare("SELECT * FROM messages").all();
203
+ const insertMsg = db.prepare(
204
+ "INSERT INTO messages_new (chat_id, role, content, metadata, created_at) VALUES (?, ?, ?, ?, ?)",
205
+ );
206
+ for (const m of oldMessages) {
207
+ const newChatId = idMap.get(m.chat_id);
208
+ if (newChatId)
209
+ insertMsg.run(newChatId, m.role, m.content, m.metadata, m.created_at);
210
+ }
211
+
212
+ db.exec(`
213
+ CREATE TABLE embeddings_new (
214
+ chat_id TEXT PRIMARY KEY,
215
+ embedding BLOB NOT NULL,
216
+ provider TEXT DEFAULT 'openai',
217
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
218
+ );
219
+ `);
220
+ const oldEmbeddings = db.prepare("SELECT * FROM embeddings").all();
221
+ const insertEmb = db.prepare(
222
+ "INSERT INTO embeddings_new (chat_id, embedding, provider, updated_at) VALUES (?, ?, ?, ?)",
223
+ );
224
+ for (const e of oldEmbeddings) {
225
+ const newChatId = idMap.get(e.chat_id);
226
+ if (newChatId)
227
+ insertEmb.run(newChatId, e.embedding, e.provider, e.updated_at);
228
+ }
229
+
230
+ db.exec(`
231
+ CREATE TABLE file_snapshots_new (
232
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
233
+ chat_id TEXT NOT NULL,
234
+ message_index INTEGER NOT NULL,
235
+ tool_call_id TEXT,
236
+ file_path TEXT NOT NULL,
237
+ original_content TEXT,
238
+ file_existed INTEGER NOT NULL DEFAULT 1,
239
+ created_dir TEXT,
240
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
241
+ UNIQUE(chat_id, message_index, file_path)
242
+ );
243
+ CREATE INDEX IF NOT EXISTS idx_file_snapshots_new_chat_message ON file_snapshots_new(chat_id, message_index);
244
+ `);
245
+ const oldSnapshots = db.prepare("SELECT * FROM file_snapshots").all();
246
+ const insertSnap = db.prepare(
247
+ "INSERT INTO file_snapshots_new (chat_id, message_index, tool_call_id, file_path, original_content, file_existed, created_dir, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
248
+ );
249
+ for (const s of oldSnapshots) {
250
+ const newChatId = idMap.get(s.chat_id);
251
+ if (newChatId)
252
+ insertSnap.run(
253
+ newChatId,
254
+ s.message_index,
255
+ s.tool_call_id,
256
+ s.file_path,
257
+ s.original_content,
258
+ s.file_existed,
259
+ s.created_dir,
260
+ s.created_at,
261
+ );
262
+ }
263
+
264
+ db.exec(`
265
+ CREATE TABLE shell_undo_log_new (
266
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
267
+ chat_id TEXT NOT NULL,
268
+ message_index INTEGER NOT NULL,
269
+ tool_call_id TEXT,
270
+ op TEXT NOT NULL,
271
+ path TEXT,
272
+ path_src TEXT,
273
+ path_dest TEXT,
274
+ cwd TEXT,
275
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
276
+ );
277
+ CREATE INDEX IF NOT EXISTS idx_shell_undo_log_new_chat_message ON shell_undo_log_new(chat_id, message_index);
278
+ `);
279
+ const oldUndos = db.prepare("SELECT * FROM shell_undo_log").all();
280
+ const insertUndo = db.prepare(
281
+ "INSERT INTO shell_undo_log_new (chat_id, message_index, tool_call_id, op, path, path_src, path_dest, cwd, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
282
+ );
283
+ for (const u of oldUndos) {
284
+ const newChatId = idMap.get(u.chat_id);
285
+ if (newChatId)
286
+ insertUndo.run(
287
+ newChatId,
288
+ u.message_index,
289
+ u.tool_call_id,
290
+ u.op,
291
+ u.path,
292
+ u.path_src,
293
+ u.path_dest,
294
+ u.cwd,
295
+ u.created_at,
296
+ );
297
+ }
298
+
299
+ db.exec(`
300
+ DROP TABLE shell_undo_log;
301
+ DROP TABLE file_snapshots;
302
+ DROP TABLE embeddings;
303
+ DROP TABLE messages;
304
+ DROP TABLE chats;
305
+ ALTER TABLE chats_new RENAME TO chats;
306
+ ALTER TABLE messages_new RENAME TO messages;
307
+ ALTER TABLE embeddings_new RENAME TO embeddings;
308
+ ALTER TABLE file_snapshots_new RENAME TO file_snapshots;
309
+ ALTER TABLE shell_undo_log_new RENAME TO shell_undo_log;
310
+ `);
311
+ if (process.env.SILENT_MODE !== "true") {
312
+ console.log("Database: migration to UUID chat ids complete.");
313
+ }
314
+ }
315
+
316
+ if (process.env.SILENT_MODE !== "true") {
317
+ console.log("Database initialized at:", dbPath);
318
+ }
319
+ return db;
320
+ }
321
+
322
+ /**
323
+ * Get the database instance
324
+ */
325
+ export function getDb() {
326
+ if (!db) {
327
+ throw new Error("Database not initialized. Call initDb() first.");
328
+ }
329
+ return db;
330
+ }
331
+
332
+ /**
333
+ * Close the database connection
334
+ */
335
+ export function closeDb() {
336
+ if (db) {
337
+ db.close();
338
+ db = null;
339
+ }
340
+ }
341
+
342
+ // ============================================
343
+ // Chat Operations
344
+ // ============================================
345
+
346
+ /**
347
+ * Create a new chat with a UUID id (same id can be used for cloud/Supabase).
348
+ * @param {string|null} title - Chat title
349
+ * @param {string|null} id - Optional UUID to use (e.g. from Supabase); if not provided, one is generated
350
+ * @returns {string} The chat id (UUID)
351
+ */
352
+ export function createChat(title = null, id = null) {
353
+ const chatId = id && UUID_REGEX.test(String(id)) ? id : randomUUID();
354
+ getDb()
355
+ .prepare("INSERT INTO chats (id, title, cloud_chat_id) VALUES (?, ?, ?)")
356
+ .run(chatId, title, id && UUID_REGEX.test(String(id)) ? id : null);
357
+ return chatId;
358
+ }
359
+
360
+ export function getChat(id) {
361
+ return getDb().prepare("SELECT * FROM chats WHERE id = ?").get(id);
362
+ }
363
+
364
+ /** Get local chat id by cloud chat UUID. With unified ids, local id equals cloud id. */
365
+ export function getLocalChatIdByCloudId(cloudChatId) {
366
+ if (!cloudChatId || typeof cloudChatId !== "string") return null;
367
+ const row = getDb()
368
+ .prepare("SELECT id FROM chats WHERE id = ? OR cloud_chat_id = ?")
369
+ .get(cloudChatId, cloudChatId);
370
+ return row ? row.id : null;
371
+ }
372
+
373
+ /** Set cloud_chat_id for an existing local chat (after creating in Supabase) */
374
+ export function setCloudChatId(localChatId, cloudChatId) {
375
+ getDb()
376
+ .prepare("UPDATE chats SET cloud_chat_id = ? WHERE id = ?")
377
+ .run(cloudChatId, localChatId);
378
+ }
379
+
380
+ export function getAllChats() {
381
+ return getDb().prepare("SELECT * FROM chats ORDER BY updated_at DESC").all();
382
+ }
383
+
384
+ export function updateChatTitle(id, title) {
385
+ getDb()
386
+ .prepare(
387
+ "UPDATE chats SET title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
388
+ )
389
+ .run(title, id);
390
+ }
391
+
392
+ export function deleteChat(id) {
393
+ const db = getDb();
394
+ db.prepare("DELETE FROM messages WHERE chat_id = ?").run(id);
395
+ db.prepare("DELETE FROM embeddings WHERE chat_id = ?").run(id);
396
+ db.prepare("DELETE FROM chats WHERE id = ?").run(id);
397
+ }
398
+
399
+ // ============================================
400
+ // Message Operations
401
+ // ============================================
402
+
403
+ export function addMessage(chatId, role, content, metadata = null) {
404
+ const result = getDb()
405
+ .prepare(
406
+ `
407
+ INSERT INTO messages (chat_id, role, content, metadata) VALUES (?, ?, ?, ?)
408
+ `,
409
+ )
410
+ .run(chatId, role, content, metadata ? JSON.stringify(metadata) : null);
411
+
412
+ // Update chat timestamp
413
+ getDb()
414
+ .prepare("UPDATE chats SET updated_at = CURRENT_TIMESTAMP WHERE id = ?")
415
+ .run(chatId);
416
+
417
+ return result.lastInsertRowid;
418
+ }
419
+
420
+ export function getMessages(chatId) {
421
+ const messages = getDb()
422
+ .prepare(
423
+ `
424
+ SELECT * FROM messages WHERE chat_id = ? ORDER BY created_at ASC
425
+ `,
426
+ )
427
+ .all(chatId);
428
+
429
+ return messages.map((m) => ({
430
+ ...m,
431
+ metadata: m.metadata ? JSON.parse(m.metadata) : null,
432
+ }));
433
+ }
434
+
435
+ export function deleteMessage(id) {
436
+ getDb().prepare("DELETE FROM messages WHERE id = ?").run(id);
437
+ }
438
+
439
+ // ============================================
440
+ // Memory Search Operations
441
+ // ============================================
442
+
443
+ /**
444
+ * Extract a snippet showing where the match was found with context
445
+ * @param {string} content - Full message content
446
+ * @param {string} query - Search query
447
+ * @param {number} contextChars - Characters of context on each side
448
+ * @returns {string} Snippet with match highlighted
449
+ */
450
+ function extractMatchSnippet(content, query, contextChars = 80) {
451
+ if (!content || !query) return "";
452
+
453
+ const lowerContent = content.toLowerCase();
454
+ const lowerQuery = query.toLowerCase();
455
+ const matchIndex = lowerContent.indexOf(lowerQuery);
456
+
457
+ if (matchIndex === -1) return content.substring(0, 150) + "...";
458
+
459
+ const start = Math.max(0, matchIndex - contextChars);
460
+ const end = Math.min(
461
+ content.length,
462
+ matchIndex + query.length + contextChars,
463
+ );
464
+
465
+ let snippet = "";
466
+ if (start > 0) snippet += "...";
467
+ snippet += content.substring(start, end);
468
+ if (end < content.length) snippet += "...";
469
+
470
+ return snippet;
471
+ }
472
+
473
+ /**
474
+ * Text search across chats and messages with better snippets
475
+ * @param {string} query - Search query string
476
+ * @param {number} limit - Maximum results to return
477
+ * @returns {Array} Matching chats with relevant snippets
478
+ */
479
+ export function searchChatsText(query, limit = 10) {
480
+ const searchPattern = `%${query}%`;
481
+ const db = getDb();
482
+
483
+ // First, find all matching messages with their chat info
484
+ const matchingMessages = db
485
+ .prepare(
486
+ `
487
+ SELECT
488
+ m.chat_id,
489
+ m.role,
490
+ m.content,
491
+ m.created_at as message_date,
492
+ c.title,
493
+ c.updated_at
494
+ FROM messages m
495
+ JOIN chats c ON c.id = m.chat_id
496
+ WHERE m.content LIKE ?
497
+ ORDER BY c.updated_at DESC, m.created_at DESC
498
+ `,
499
+ )
500
+ .all(searchPattern);
501
+
502
+ // Also find chats matching by title
503
+ const titleMatches = db
504
+ .prepare(
505
+ `
506
+ SELECT id, title, updated_at
507
+ FROM chats
508
+ WHERE title LIKE ?
509
+ ORDER BY updated_at DESC
510
+ `,
511
+ )
512
+ .all(searchPattern);
513
+
514
+ // Group by chat and pick best snippet
515
+ const chatMap = new Map();
516
+
517
+ // Add title matches first
518
+ for (const chat of titleMatches) {
519
+ if (!chatMap.has(chat.id)) {
520
+ chatMap.set(chat.id, {
521
+ id: chat.id,
522
+ title: chat.title,
523
+ updated_at: chat.updated_at,
524
+ match_type: "title",
525
+ matching_snippet: `Title matches: "${chat.title}"`,
526
+ match_count: 0,
527
+ });
528
+ }
529
+ }
530
+
531
+ // Add message matches with snippets
532
+ for (const msg of matchingMessages) {
533
+ const existing = chatMap.get(msg.chat_id);
534
+ const snippet = extractMatchSnippet(msg.content, query);
535
+ const roleLabel = msg.role === "user" ? "You said" : "I said";
536
+
537
+ if (!existing) {
538
+ chatMap.set(msg.chat_id, {
539
+ id: msg.chat_id,
540
+ title: msg.title,
541
+ updated_at: msg.updated_at,
542
+ match_type: "content",
543
+ matching_snippet: `${roleLabel}: "${snippet}"`,
544
+ match_count: 1,
545
+ });
546
+ } else {
547
+ existing.match_count++;
548
+ // Keep the first (most recent) snippet but update count
549
+ if (existing.match_type === "title") {
550
+ // Title matched, but also has content match - upgrade snippet
551
+ existing.matching_snippet = `${roleLabel}: "${snippet}"`;
552
+ existing.match_type = "both";
553
+ }
554
+ }
555
+ }
556
+
557
+ // Convert to array and limit
558
+ const results = Array.from(chatMap.values())
559
+ .sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at))
560
+ .slice(0, limit);
561
+
562
+ return results;
563
+ }
564
+
565
+ /**
566
+ * Get chat with all its messages for full context
567
+ * @param {number} chatId - The chat ID
568
+ * @returns {Object|null} Chat with messages array, or null if not found
569
+ */
570
+ export function getChatWithMessages(chatId) {
571
+ const db = getDb();
572
+
573
+ const chat = db.prepare("SELECT * FROM chats WHERE id = ?").get(chatId);
574
+ if (!chat) return null;
575
+
576
+ const messages = db
577
+ .prepare(
578
+ `
579
+ SELECT role, content, created_at
580
+ FROM messages
581
+ WHERE chat_id = ?
582
+ ORDER BY created_at ASC
583
+ `,
584
+ )
585
+ .all(chatId);
586
+
587
+ return { ...chat, messages };
588
+ }
589
+
590
+ // ============================================
591
+ // Embedding Operations
592
+ // ============================================
593
+
594
+ export function saveEmbedding(chatId, embedding, provider = "openai") {
595
+ // Convert array to Buffer
596
+ const buffer = Buffer.from(new Float32Array(embedding).buffer);
597
+
598
+ getDb()
599
+ .prepare(
600
+ `
601
+ INSERT OR REPLACE INTO embeddings (chat_id, embedding, provider, updated_at)
602
+ VALUES (?, ?, ?, CURRENT_TIMESTAMP)
603
+ `,
604
+ )
605
+ .run(chatId, buffer, provider);
606
+ }
607
+
608
+ export function getEmbedding(chatId) {
609
+ const row = getDb()
610
+ .prepare("SELECT * FROM embeddings WHERE chat_id = ?")
611
+ .get(chatId);
612
+ if (!row) return null;
613
+
614
+ // Convert Buffer back to array
615
+ const embedding = Array.from(
616
+ new Float32Array(
617
+ row.embedding.buffer,
618
+ row.embedding.byteOffset,
619
+ row.embedding.length / 4,
620
+ ),
621
+ );
622
+ return { ...row, embedding };
623
+ }
624
+
625
+ export function getAllEmbeddings() {
626
+ const rows = getDb().prepare("SELECT * FROM embeddings").all();
627
+ return rows.map((row) => ({
628
+ ...row,
629
+ embedding: Array.from(
630
+ new Float32Array(
631
+ row.embedding.buffer,
632
+ row.embedding.byteOffset,
633
+ row.embedding.length / 4,
634
+ ),
635
+ ),
636
+ }));
637
+ }
638
+
639
+ // ============================================
640
+ // Scheduled Task Operations
641
+ // ============================================
642
+
643
+ export function createScheduledTask(cronExpression, description) {
644
+ const result = getDb()
645
+ .prepare(
646
+ `
647
+ INSERT INTO scheduled_tasks (cron_expression, description) VALUES (?, ?)
648
+ `,
649
+ )
650
+ .run(cronExpression, description);
651
+ return result.lastInsertRowid;
652
+ }
653
+
654
+ export function getScheduledTasks(enabledOnly = true) {
655
+ if (enabledOnly) {
656
+ return getDb()
657
+ .prepare(
658
+ "SELECT * FROM scheduled_tasks WHERE enabled = 1 ORDER BY next_run ASC",
659
+ )
660
+ .all();
661
+ }
662
+ return getDb()
663
+ .prepare("SELECT * FROM scheduled_tasks ORDER BY created_at DESC")
664
+ .all();
665
+ }
666
+
667
+ export function getScheduledTask(id) {
668
+ return getDb().prepare("SELECT * FROM scheduled_tasks WHERE id = ?").get(id);
669
+ }
670
+
671
+ export function updateScheduledTaskNextRun(id, nextRun) {
672
+ getDb()
673
+ .prepare("UPDATE scheduled_tasks SET next_run = ? WHERE id = ?")
674
+ .run(nextRun, id);
675
+ }
676
+
677
+ export function markScheduledTaskRun(id) {
678
+ getDb()
679
+ .prepare(
680
+ "UPDATE scheduled_tasks SET last_run = CURRENT_TIMESTAMP WHERE id = ?",
681
+ )
682
+ .run(id);
683
+ }
684
+
685
+ export function disableScheduledTask(id) {
686
+ getDb()
687
+ .prepare("UPDATE scheduled_tasks SET enabled = 0 WHERE id = ?")
688
+ .run(id);
689
+ }
690
+
691
+ export function deleteScheduledTask(id) {
692
+ getDb().prepare("DELETE FROM scheduled_tasks WHERE id = ?").run(id);
693
+ }
694
+
695
+ // ============================================
696
+ // File Snapshot Operations (for undo on regeneration)
697
+ // ============================================
698
+
699
+ /** Max size (bytes) for original_content in file_snapshots; larger files are not snapshotted (revert will skip them) */
700
+ const MAX_SNAPSHOT_CONTENT_BYTES = 2 * 1024 * 1024; // 2MB
701
+
702
+ /**
703
+ * Save a file snapshot before a tool modifies the file
704
+ * Uses INSERT OR IGNORE to keep only the first (original) snapshot per file per message.
705
+ * Skips saving if originalContent exceeds MAX_SNAPSHOT_CONTENT_BYTES to avoid DB bloat and OOM.
706
+ * @param {number} chatId - The chat ID
707
+ * @param {number} messageIndex - Index of the assistant message containing the tool call
708
+ * @param {string} toolCallId - Unique ID of the tool call
709
+ * @param {string} filePath - Absolute path to the file
710
+ * @param {string|null} originalContent - Content before modification (null if file didn't exist)
711
+ * @param {boolean} fileExisted - Whether the file existed before the tool call
712
+ * @param {string|null} createdDir - If write_file created the parent dir, its absolute path (for revert)
713
+ */
714
+ export function saveFileSnapshot(
715
+ chatId,
716
+ messageIndex,
717
+ toolCallId,
718
+ filePath,
719
+ originalContent,
720
+ fileExisted,
721
+ createdDir = null,
722
+ ) {
723
+ const contentLength =
724
+ originalContent != null
725
+ ? typeof originalContent === "string"
726
+ ? Buffer.byteLength(originalContent, "utf8")
727
+ : originalContent.length
728
+ : 0;
729
+ if (contentLength > MAX_SNAPSHOT_CONTENT_BYTES) {
730
+ if (process.env.SILENT_MODE !== "true") {
731
+ console.warn(
732
+ "[db] Skipping file snapshot (too large):",
733
+ filePath,
734
+ `${(contentLength / 1024 / 1024).toFixed(2)}MB > ${MAX_SNAPSHOT_CONTENT_BYTES / 1024 / 1024}MB`,
735
+ );
736
+ }
737
+ return;
738
+ }
739
+ // Use INSERT OR IGNORE to keep only the first snapshot per file per message
740
+ // This ensures we capture the ORIGINAL state before any modifications
741
+ getDb()
742
+ .prepare(
743
+ `
744
+ INSERT OR IGNORE INTO file_snapshots (chat_id, message_index, tool_call_id, file_path, original_content, file_existed, created_dir)
745
+ VALUES (?, ?, ?, ?, ?, ?, ?)
746
+ `,
747
+ )
748
+ .run(
749
+ chatId,
750
+ messageIndex,
751
+ toolCallId,
752
+ filePath,
753
+ originalContent,
754
+ fileExisted ? 1 : 0,
755
+ createdDir,
756
+ );
757
+ }
758
+
759
+ /**
760
+ * Get all snapshots that need to be reverted for a regeneration
761
+ * Returns snapshots where messageIndex >= fromMessageIndex
762
+ * @param {number} chatId - The chat ID
763
+ * @param {number} fromMessageIndex - The message index being regenerated (inclusive)
764
+ * @returns {Array} Snapshots to revert, ordered by message_index DESC (revert newest first)
765
+ */
766
+ export function getSnapshotsToRevert(chatId, fromMessageIndex) {
767
+ return getDb()
768
+ .prepare(
769
+ `
770
+ SELECT * FROM file_snapshots
771
+ WHERE chat_id = ? AND message_index >= ?
772
+ ORDER BY message_index DESC, id DESC
773
+ `,
774
+ )
775
+ .all(chatId, fromMessageIndex);
776
+ }
777
+
778
+ /**
779
+ * Delete snapshots after they've been reverted
780
+ * @param {number} chatId - The chat ID
781
+ * @param {number} fromMessageIndex - Delete snapshots at or after this index
782
+ */
783
+ export function deleteSnapshots(chatId, fromMessageIndex) {
784
+ getDb()
785
+ .prepare(
786
+ `
787
+ DELETE FROM file_snapshots WHERE chat_id = ? AND message_index >= ?
788
+ `,
789
+ )
790
+ .run(chatId, fromMessageIndex);
791
+ }
792
+
793
+ /**
794
+ * Delete all snapshots for a chat (used when chat is deleted)
795
+ * @param {number} chatId - The chat ID
796
+ */
797
+ export function deleteAllChatSnapshots(chatId) {
798
+ getDb().prepare("DELETE FROM file_snapshots WHERE chat_id = ?").run(chatId);
799
+ }
800
+
801
+ /**
802
+ * Get all snapshots for a chat (for debugging/inspection)
803
+ * @param {number} chatId - The chat ID
804
+ * @returns {Array} All snapshots for the chat
805
+ */
806
+ export function getChatSnapshots(chatId) {
807
+ return getDb()
808
+ .prepare(
809
+ `
810
+ SELECT * FROM file_snapshots WHERE chat_id = ? ORDER BY message_index ASC, id ASC
811
+ `,
812
+ )
813
+ .all(chatId);
814
+ }
815
+
816
+ // ============================================
817
+ // Shell Undo Log (Strategy 1: undo shell commands on regeneration)
818
+ // ============================================
819
+
820
+ /**
821
+ * Save a reversible shell operation for undo on regeneration
822
+ * @param {number} chatId - The chat ID
823
+ * @param {number} messageIndex - Index of the assistant message
824
+ * @param {string|null} toolCallId - Unique ID of the tool call
825
+ * @param {object} entry - { op, path?, path_src?, path_dest?, cwd }
826
+ */
827
+ export function saveShellUndo(chatId, messageIndex, toolCallId, entry) {
828
+ getDb()
829
+ .prepare(
830
+ `
831
+ INSERT INTO shell_undo_log (chat_id, message_index, tool_call_id, op, path, path_src, path_dest, cwd)
832
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
833
+ `,
834
+ )
835
+ .run(
836
+ chatId,
837
+ messageIndex,
838
+ toolCallId,
839
+ entry.op,
840
+ entry.path ?? null,
841
+ entry.path_src ?? null,
842
+ entry.path_dest ?? null,
843
+ entry.cwd ?? null,
844
+ );
845
+ }
846
+
847
+ /**
848
+ * Get shell undo entries to revert for a regeneration (newest first for reverse-order revert)
849
+ * @param {number} chatId - The chat ID
850
+ * @param {number} fromMessageIndex - The message index being regenerated (inclusive)
851
+ * @returns {Array} Entries to revert, ordered by message_index DESC, id DESC
852
+ */
853
+ export function getShellUndosToRevert(chatId, fromMessageIndex) {
854
+ return getDb()
855
+ .prepare(
856
+ `
857
+ SELECT * FROM shell_undo_log
858
+ WHERE chat_id = ? AND message_index >= ?
859
+ ORDER BY message_index DESC, id DESC
860
+ `,
861
+ )
862
+ .all(chatId, fromMessageIndex);
863
+ }
864
+
865
+ /**
866
+ * Delete shell undo entries after they've been reverted
867
+ * @param {number} chatId - The chat ID
868
+ * @param {number} fromMessageIndex - Delete entries at or after this index
869
+ */
870
+ export function deleteShellUndos(chatId, fromMessageIndex) {
871
+ getDb()
872
+ .prepare(
873
+ `
874
+ DELETE FROM shell_undo_log WHERE chat_id = ? AND message_index >= ?
875
+ `,
876
+ )
877
+ .run(chatId, fromMessageIndex);
878
+ }
879
+
880
+ /**
881
+ * Delete all shell undo entries for a chat (e.g. when chat is deleted)
882
+ * @param {number} chatId - The chat ID
883
+ */
884
+ export function deleteAllChatShellUndos(chatId) {
885
+ getDb().prepare("DELETE FROM shell_undo_log WHERE chat_id = ?").run(chatId);
886
+ }
887
+
888
+ // ============================================
889
+ // RAG Document Operations
890
+ // ============================================
891
+
892
+ /**
893
+ * Create a new RAG document
894
+ * @param {Object} doc - Document data { name, chunkCount, fileCount, files, summary }
895
+ * @returns {number} Document ID
896
+ */
897
+ export function createRagDocument(doc) {
898
+ const result = getDb()
899
+ .prepare(
900
+ `
901
+ INSERT INTO rag_documents (name, chunk_count, file_count, files, summary, upload_date)
902
+ VALUES (?, ?, ?, ?, ?, ?)
903
+ `,
904
+ )
905
+ .run(
906
+ doc.name,
907
+ doc.chunkCount || 0,
908
+ doc.fileCount || 1,
909
+ doc.files || null,
910
+ doc.summary || null,
911
+ doc.uploadDate || new Date().toISOString(),
912
+ );
913
+ return result.lastInsertRowid;
914
+ }
915
+
916
+ /**
917
+ * Get all RAG documents
918
+ * @returns {Array} All documents
919
+ */
920
+ export function getAllRagDocuments() {
921
+ return getDb()
922
+ .prepare(
923
+ `
924
+ SELECT id, name, chunk_count as chunkCount, file_count as fileCount,
925
+ files, summary, upload_date as uploadDate, created_at, updated_at
926
+ FROM rag_documents
927
+ ORDER BY created_at DESC
928
+ `,
929
+ )
930
+ .all();
931
+ }
932
+
933
+ /**
934
+ * Get a single RAG document by ID
935
+ * @param {number} id - Document ID
936
+ * @returns {Object|null} Document or null
937
+ */
938
+ export function getRagDocument(id) {
939
+ return getDb()
940
+ .prepare(
941
+ `
942
+ SELECT id, name, chunk_count as chunkCount, file_count as fileCount,
943
+ files, summary, upload_date as uploadDate, created_at, updated_at
944
+ FROM rag_documents WHERE id = ?
945
+ `,
946
+ )
947
+ .get(id);
948
+ }
949
+
950
+ /**
951
+ * Update a RAG document
952
+ * @param {number} id - Document ID
953
+ * @param {Object} updates - Fields to update
954
+ */
955
+ export function updateRagDocument(id, updates) {
956
+ const fields = [];
957
+ const values = [];
958
+
959
+ if (updates.name !== undefined) {
960
+ fields.push("name = ?");
961
+ values.push(updates.name);
962
+ }
963
+ if (updates.chunkCount !== undefined) {
964
+ fields.push("chunk_count = ?");
965
+ values.push(updates.chunkCount);
966
+ }
967
+ if (updates.summary !== undefined) {
968
+ fields.push("summary = ?");
969
+ values.push(updates.summary);
970
+ }
971
+
972
+ if (fields.length === 0) return;
973
+
974
+ fields.push("updated_at = CURRENT_TIMESTAMP");
975
+ values.push(id);
976
+
977
+ getDb()
978
+ .prepare(
979
+ `
980
+ UPDATE rag_documents SET ${fields.join(", ")} WHERE id = ?
981
+ `,
982
+ )
983
+ .run(...values);
984
+ }
985
+
986
+ /**
987
+ * Delete a RAG document and its chunks
988
+ * @param {number} id - Document ID
989
+ */
990
+ export function deleteRagDocument(id) {
991
+ const db = getDb();
992
+ db.prepare("DELETE FROM rag_chunks WHERE doc_id = ?").run(id);
993
+ db.prepare("DELETE FROM rag_documents WHERE id = ?").run(id);
994
+ }
995
+
996
+ /**
997
+ * Add chunks for a RAG document
998
+ * @param {number} docId - Document ID
999
+ * @param {Array} chunks - Array of { text, pageNumber, chunkIndex, embedding }
1000
+ */
1001
+ export function addRagChunks(docId, chunks) {
1002
+ const db = getDb();
1003
+ const insert = db.prepare(`
1004
+ INSERT INTO rag_chunks (doc_id, text, page_number, chunk_index, embedding)
1005
+ VALUES (?, ?, ?, ?, ?)
1006
+ `);
1007
+
1008
+ const insertMany = db.transaction((items) => {
1009
+ for (const chunk of items) {
1010
+ // Convert embedding array to Buffer
1011
+ const embeddingBuffer = Buffer.from(
1012
+ new Float32Array(chunk.embedding).buffer,
1013
+ );
1014
+ insert.run(
1015
+ docId,
1016
+ chunk.text,
1017
+ chunk.pageNumber || null,
1018
+ chunk.chunkIndex,
1019
+ embeddingBuffer,
1020
+ );
1021
+ }
1022
+ });
1023
+
1024
+ insertMany(chunks);
1025
+
1026
+ // Update document chunk count
1027
+ db.prepare("UPDATE rag_documents SET chunk_count = ? WHERE id = ?").run(
1028
+ chunks.length,
1029
+ docId,
1030
+ );
1031
+ }
1032
+
1033
+ /**
1034
+ * Get all chunks for a RAG document
1035
+ * @param {number} docId - Document ID
1036
+ * @returns {Array} Chunks with embeddings converted to arrays
1037
+ */
1038
+ export function getRagChunksByDocument(docId) {
1039
+ const chunks = getDb()
1040
+ .prepare(
1041
+ `
1042
+ SELECT id, doc_id as docId, text, page_number as pageNumber, chunk_index as chunkIndex, embedding
1043
+ FROM rag_chunks WHERE doc_id = ?
1044
+ ORDER BY chunk_index ASC
1045
+ `,
1046
+ )
1047
+ .all(docId);
1048
+
1049
+ // Convert embedding Buffers back to arrays
1050
+ return chunks.map((chunk) => ({
1051
+ ...chunk,
1052
+ embedding: Array.from(
1053
+ new Float32Array(
1054
+ chunk.embedding.buffer,
1055
+ chunk.embedding.byteOffset,
1056
+ chunk.embedding.length / 4,
1057
+ ),
1058
+ ),
1059
+ }));
1060
+ }
1061
+
1062
+ /**
1063
+ * Get chunks from multiple documents for RAG search
1064
+ * @param {Array<number>} docIds - Array of document IDs
1065
+ * @returns {Array} Chunks with embeddings
1066
+ */
1067
+ export function getRagChunksByDocuments(docIds) {
1068
+ if (!docIds || docIds.length === 0) return [];
1069
+
1070
+ const placeholders = docIds.map(() => "?").join(",");
1071
+ const chunks = getDb()
1072
+ .prepare(
1073
+ `
1074
+ SELECT id, doc_id as docId, text, page_number as pageNumber, chunk_index as chunkIndex, embedding
1075
+ FROM rag_chunks WHERE doc_id IN (${placeholders})
1076
+ ORDER BY doc_id, chunk_index ASC
1077
+ `,
1078
+ )
1079
+ .all(...docIds);
1080
+
1081
+ // Convert embedding Buffers back to arrays
1082
+ return chunks.map((chunk) => ({
1083
+ ...chunk,
1084
+ embedding: Array.from(
1085
+ new Float32Array(
1086
+ chunk.embedding.buffer,
1087
+ chunk.embedding.byteOffset,
1088
+ chunk.embedding.length / 4,
1089
+ ),
1090
+ ),
1091
+ }));
1092
+ }
1093
+
1094
+ export default {
1095
+ initDb,
1096
+ getDb,
1097
+ closeDb,
1098
+ createChat,
1099
+ getChat,
1100
+ getLocalChatIdByCloudId,
1101
+ setCloudChatId,
1102
+ getAllChats,
1103
+ updateChatTitle,
1104
+ deleteChat,
1105
+ addMessage,
1106
+ getMessages,
1107
+ deleteMessage,
1108
+ searchChatsText,
1109
+ getChatWithMessages,
1110
+ saveEmbedding,
1111
+ getEmbedding,
1112
+ getAllEmbeddings,
1113
+ createScheduledTask,
1114
+ getScheduledTasks,
1115
+ getScheduledTask,
1116
+ updateScheduledTaskNextRun,
1117
+ markScheduledTaskRun,
1118
+ disableScheduledTask,
1119
+ deleteScheduledTask,
1120
+ // File snapshot operations for undo on regeneration
1121
+ saveFileSnapshot,
1122
+ getSnapshotsToRevert,
1123
+ deleteSnapshots,
1124
+ deleteAllChatSnapshots,
1125
+ getChatSnapshots,
1126
+ // RAG document operations
1127
+ createRagDocument,
1128
+ getAllRagDocuments,
1129
+ getRagDocument,
1130
+ updateRagDocument,
1131
+ deleteRagDocument,
1132
+ addRagChunks,
1133
+ getRagChunksByDocument,
1134
+ getRagChunksByDocuments,
1135
+ };