botholomew 0.12.5 → 0.14.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 (107) hide show
  1. package/README.md +91 -68
  2. package/package.json +2 -2
  3. package/src/chat/agent.ts +59 -86
  4. package/src/chat/session.ts +29 -25
  5. package/src/commands/capabilities.ts +1 -1
  6. package/src/commands/context.ts +178 -926
  7. package/src/commands/db.ts +9 -13
  8. package/src/commands/init.ts +4 -1
  9. package/src/commands/nuke.ts +57 -90
  10. package/src/commands/schedule.ts +103 -124
  11. package/src/commands/skill.ts +2 -2
  12. package/src/commands/task.ts +86 -95
  13. package/src/commands/thread.ts +107 -112
  14. package/src/commands/worker.ts +88 -88
  15. package/src/constants.ts +93 -16
  16. package/src/context/capabilities.ts +10 -10
  17. package/src/context/fetcher.ts +9 -10
  18. package/src/context/reindex.ts +189 -0
  19. package/src/context/store.ts +803 -0
  20. package/src/db/doctor.ts +1 -8
  21. package/src/db/embeddings.ts +227 -175
  22. package/src/db/sql/19-disk_backed_index.sql +36 -0
  23. package/src/db/sql/20-drop_db_tables_for_files.sql +19 -0
  24. package/src/fs/atomic.ts +217 -0
  25. package/src/fs/compat.ts +86 -0
  26. package/src/fs/sandbox.ts +293 -0
  27. package/src/init/index.ts +69 -52
  28. package/src/init/templates.ts +1 -1
  29. package/src/mcpx/client.ts +1 -1
  30. package/src/schedules/schema.ts +19 -0
  31. package/src/schedules/store.ts +296 -0
  32. package/src/skills/commands.ts +1 -3
  33. package/src/tasks/schema.ts +47 -0
  34. package/src/tasks/store.ts +486 -0
  35. package/src/threads/store.ts +559 -0
  36. package/src/tools/capabilities/refresh.ts +42 -21
  37. package/src/tools/context/pipe.ts +15 -71
  38. package/src/tools/context/update-beliefs.ts +3 -3
  39. package/src/tools/context/update-goals.ts +3 -3
  40. package/src/tools/dir/create.ts +26 -23
  41. package/src/tools/dir/size.ts +46 -17
  42. package/src/tools/dir/tree.ts +74 -279
  43. package/src/tools/file/copy.ts +50 -24
  44. package/src/tools/file/count-lines.ts +34 -10
  45. package/src/tools/file/delete.ts +53 -23
  46. package/src/tools/file/edit.ts +39 -14
  47. package/src/tools/file/exists.ts +12 -26
  48. package/src/tools/file/info.ts +27 -85
  49. package/src/tools/file/move.ts +39 -24
  50. package/src/tools/file/read.ts +32 -80
  51. package/src/tools/file/write.ts +14 -91
  52. package/src/tools/registry.ts +8 -7
  53. package/src/tools/schedule/create.ts +2 -2
  54. package/src/tools/schedule/list.ts +7 -3
  55. package/src/tools/search/fuse.ts +12 -33
  56. package/src/tools/search/index.ts +36 -43
  57. package/src/tools/search/regexp.ts +29 -17
  58. package/src/tools/search/semantic.ts +137 -51
  59. package/src/tools/skill/delete.ts +1 -1
  60. package/src/tools/skill/list.ts +1 -1
  61. package/src/tools/skill/write.ts +1 -1
  62. package/src/tools/task/create.ts +41 -16
  63. package/src/tools/task/delete.ts +3 -3
  64. package/src/tools/task/list.ts +6 -3
  65. package/src/tools/task/update.ts +31 -9
  66. package/src/tools/task/view.ts +6 -6
  67. package/src/tools/thread/list.ts +2 -2
  68. package/src/tools/thread/search.ts +208 -0
  69. package/src/tools/thread/view.ts +50 -5
  70. package/src/tools/tool.ts +5 -0
  71. package/src/tools/util/sleep.ts +77 -0
  72. package/src/tools/worker/spawn.ts +28 -14
  73. package/src/tui/App.tsx +12 -19
  74. package/src/tui/components/ContextPanel.tsx +83 -316
  75. package/src/tui/components/SchedulePanel.tsx +34 -48
  76. package/src/tui/components/SleepProgress.tsx +70 -0
  77. package/src/tui/components/StatusBar.tsx +15 -15
  78. package/src/tui/components/TaskPanel.tsx +34 -38
  79. package/src/tui/components/ThreadPanel.tsx +29 -38
  80. package/src/tui/components/ToolCall.tsx +10 -0
  81. package/src/tui/components/WorkerPanel.tsx +21 -19
  82. package/src/tui/markdown.ts +2 -8
  83. package/src/utils/title.ts +5 -7
  84. package/src/utils/v7-date.ts +47 -0
  85. package/src/worker/heartbeat.ts +46 -24
  86. package/src/worker/index.ts +13 -15
  87. package/src/worker/llm.ts +30 -37
  88. package/src/worker/prompt.ts +19 -41
  89. package/src/worker/schedules.ts +48 -69
  90. package/src/worker/spawn.ts +11 -11
  91. package/src/worker/tick.ts +39 -43
  92. package/src/workers/store.ts +247 -0
  93. package/src/commands/tools.ts +0 -367
  94. package/src/context/describer.ts +0 -140
  95. package/src/context/drives.ts +0 -110
  96. package/src/context/ingest.ts +0 -162
  97. package/src/context/refresh.ts +0 -183
  98. package/src/db/context.ts +0 -637
  99. package/src/db/daemon-state.ts +0 -6
  100. package/src/db/reembed.ts +0 -113
  101. package/src/db/schedules.ts +0 -213
  102. package/src/db/tasks.ts +0 -347
  103. package/src/db/threads.ts +0 -276
  104. package/src/db/workers.ts +0 -212
  105. package/src/tools/context/list-drives.ts +0 -36
  106. package/src/tools/context/refresh.ts +0 -165
  107. package/src/tools/context/search.ts +0 -54
package/src/db/context.ts DELETED
@@ -1,637 +0,0 @@
1
- import { resolve as resolvePath } from "node:path";
2
- import {
3
- type DriveTarget,
4
- formatDriveRef,
5
- parseDriveRef,
6
- } from "../context/drives.ts";
7
- import type { DbConnection } from "./connection.ts";
8
- import { buildSetClauses, buildWhereClause, sanitizeInt } from "./query.ts";
9
- import { isUuid, uuidv7 } from "./uuid.ts";
10
-
11
- export interface ContextItem {
12
- id: string;
13
- title: string;
14
- description: string;
15
- content: string | null;
16
- mime_type: string;
17
- is_textual: boolean;
18
- drive: string;
19
- path: string;
20
- source_url: string | null;
21
- indexed_at: Date | null;
22
- created_at: Date;
23
- updated_at: Date;
24
- }
25
-
26
- export interface Patch {
27
- start_line: number;
28
- end_line: number;
29
- content: string;
30
- }
31
-
32
- interface ContextItemRow {
33
- id: string;
34
- title: string;
35
- description: string;
36
- content: string | null;
37
- content_blob: unknown;
38
- mime_type: string;
39
- is_textual: boolean;
40
- drive: string;
41
- path: string;
42
- source_url: string | null;
43
- indexed_at: string | null;
44
- created_at: string;
45
- updated_at: string;
46
- }
47
-
48
- function rowToContextItem(row: ContextItemRow): ContextItem {
49
- return {
50
- id: row.id,
51
- title: row.title,
52
- description: row.description,
53
- content: row.content,
54
- mime_type: row.mime_type,
55
- is_textual: !!row.is_textual,
56
- drive: row.drive,
57
- path: row.path,
58
- source_url: row.source_url,
59
- indexed_at: row.indexed_at ? new Date(row.indexed_at) : null,
60
- created_at: new Date(row.created_at),
61
- updated_at: new Date(row.updated_at),
62
- };
63
- }
64
-
65
- export class PathConflictError extends Error {
66
- existingId: string;
67
- drive: string;
68
- path: string;
69
- constructor(existingId: string, target: DriveTarget) {
70
- super(`Path already exists: ${formatDriveRef(target)}`);
71
- this.name = "PathConflictError";
72
- this.existingId = existingId;
73
- this.drive = target.drive;
74
- this.path = target.path;
75
- }
76
- }
77
-
78
- // --- Basic CRUD ---
79
-
80
- export async function createContextItem(
81
- db: DbConnection,
82
- params: {
83
- title: string;
84
- content?: string;
85
- mimeType?: string;
86
- drive: string;
87
- path: string;
88
- description?: string;
89
- isTextual?: boolean;
90
- sourceUrl?: string | null;
91
- },
92
- ): Promise<ContextItem> {
93
- const id = uuidv7();
94
- const row = await db.queryGet<ContextItemRow>(
95
- `INSERT INTO context_items (id, title, description, content, mime_type, is_textual, drive, path, source_url)
96
- VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
97
- RETURNING *`,
98
- id,
99
- params.title,
100
- params.description ?? "",
101
- params.content ?? null,
102
- params.mimeType ?? "text/plain",
103
- params.isTextual !== false,
104
- params.drive,
105
- params.path,
106
- params.sourceUrl ?? null,
107
- );
108
- if (!row) throw new Error("INSERT did not return a row");
109
- return rowToContextItem(row);
110
- }
111
-
112
- /**
113
- * Atomic upsert by (drive, path): updates if the pair exists, inserts otherwise.
114
- *
115
- * DuckDB implements UPDATE as delete+insert on tables with unique indexes,
116
- * which violates foreign keys from the embeddings table. We must delete
117
- * embeddings before updating; callers (context add, context_write) re-create
118
- * them in their ingestion phase.
119
- */
120
- export async function upsertContextItem(
121
- db: DbConnection,
122
- params: {
123
- title: string;
124
- content?: string;
125
- mimeType?: string;
126
- drive: string;
127
- path: string;
128
- description?: string;
129
- isTextual?: boolean;
130
- sourceUrl?: string | null;
131
- },
132
- ): Promise<ContextItem> {
133
- const existing = await getContextItem(db, {
134
- drive: params.drive,
135
- path: params.path,
136
- });
137
- if (existing) {
138
- const updated = await updateContextItem(db, existing.id, {
139
- title: params.title,
140
- content: params.content,
141
- mime_type: params.mimeType,
142
- source_url: params.sourceUrl,
143
- });
144
- if (!updated)
145
- throw new Error(
146
- `Failed to update: ${formatDriveRef({ drive: params.drive, path: params.path })}`,
147
- );
148
- return updated;
149
- }
150
- return createContextItem(db, params);
151
- }
152
-
153
- /**
154
- * Strict creator: throws PathConflictError if (drive, path) already exists.
155
- * Use when callers want to surface collisions instead of silently overwriting.
156
- */
157
- export async function createContextItemStrict(
158
- db: DbConnection,
159
- params: {
160
- title: string;
161
- content?: string;
162
- mimeType?: string;
163
- drive: string;
164
- path: string;
165
- description?: string;
166
- isTextual?: boolean;
167
- sourceUrl?: string | null;
168
- },
169
- ): Promise<ContextItem> {
170
- const existing = await getContextItem(db, {
171
- drive: params.drive,
172
- path: params.path,
173
- });
174
- if (existing)
175
- throw new PathConflictError(existing.id, {
176
- drive: params.drive,
177
- path: params.path,
178
- });
179
- return createContextItem(db, params);
180
- }
181
-
182
- export async function getContextItemById(
183
- db: DbConnection,
184
- id: string,
185
- ): Promise<ContextItem | null> {
186
- const row = await db.queryGet<ContextItemRow>(
187
- "SELECT * FROM context_items WHERE id = ?1",
188
- id,
189
- );
190
- return row ? rowToContextItem(row) : null;
191
- }
192
-
193
- export async function getContextItem(
194
- db: DbConnection,
195
- target: DriveTarget,
196
- ): Promise<ContextItem | null> {
197
- const row = await db.queryGet<ContextItemRow>(
198
- "SELECT * FROM context_items WHERE drive = ?1 AND path = ?2",
199
- target.drive,
200
- target.path,
201
- );
202
- return row ? rowToContextItem(row) : null;
203
- }
204
-
205
- /**
206
- * Look up a context item by UUID, `drive:/path`, or bare filesystem path
207
- * (resolved against cwd and treated as `disk:/...`).
208
- *
209
- * The bare-path fallback lets users pass the same argument they used for
210
- * `context add` (e.g. a relative `README.md`) to management commands like
211
- * `context refresh` / `context chunks`.
212
- */
213
- export async function resolveContextItem(
214
- db: DbConnection,
215
- ref: string,
216
- ): Promise<ContextItem | null> {
217
- if (isUuid(ref)) return getContextItemById(db, ref);
218
-
219
- const parsed = parseDriveRef(ref);
220
- if (parsed) return getContextItem(db, parsed);
221
-
222
- // Bare filesystem path — try the `disk` drive with an absolute path.
223
- const absolute = resolvePath(ref);
224
- return getContextItem(db, { drive: "disk", path: absolute });
225
- }
226
-
227
- /**
228
- * Like resolveContextItem but throws if not found.
229
- */
230
- export async function resolveContextItemOrThrow(
231
- db: DbConnection,
232
- ref: string,
233
- ): Promise<ContextItem> {
234
- const item = await resolveContextItem(db, ref);
235
- if (!item) throw new Error(`Not found: ${ref}`);
236
- return item;
237
- }
238
-
239
- export interface NearbyContextPaths {
240
- /** Directory we found neighbours under (may be an ancestor if the direct parent was empty). */
241
- parent: string;
242
- /** Exact `drive:/path` values of the parent's immediate children. */
243
- siblings: string[];
244
- /** True if we walked up from the requested path's direct parent to find a populated ancestor. */
245
- walkedUp: boolean;
246
- }
247
-
248
- /**
249
- * Find context items near a requested path to power "did you mean?" suggestions
250
- * when a lookup misses. Returns up to `limit` immediate neighbours within the
251
- * same drive; if the parent has no rows, walks up until it finds a populated
252
- * ancestor (or hits root).
253
- */
254
- export async function findNearbyContextPaths(
255
- db: DbConnection,
256
- drive: string,
257
- requestedPath: string,
258
- limit = 5,
259
- ): Promise<NearbyContextPaths> {
260
- let parent = parentDir(requestedPath);
261
- let walkedUp = false;
262
- while (true) {
263
- const items = await listContextItemsByPrefix(db, drive, parent, {
264
- recursive: false,
265
- limit,
266
- });
267
- if (items.length > 0 || parent === "/") {
268
- return {
269
- parent: `${drive}:${parent}`,
270
- siblings: items.map((i) => formatDriveRef(i)),
271
- walkedUp,
272
- };
273
- }
274
- parent = parentDir(parent);
275
- walkedUp = true;
276
- }
277
- }
278
-
279
- function parentDir(p: string): string {
280
- if (!p || p === "/") return "/";
281
- const trimmed = p.endsWith("/") && p.length > 1 ? p.slice(0, -1) : p;
282
- const idx = trimmed.lastIndexOf("/");
283
- if (idx <= 0) return "/";
284
- return trimmed.slice(0, idx);
285
- }
286
-
287
- export async function listContextItems(
288
- db: DbConnection,
289
- filters?: {
290
- drive?: string;
291
- mimeType?: string;
292
- limit?: number;
293
- offset?: number;
294
- },
295
- ): Promise<ContextItem[]> {
296
- const { where, params } = buildWhereClause([
297
- ["drive", filters?.drive],
298
- ["mime_type", filters?.mimeType],
299
- ]);
300
- const limit = filters?.limit ? `LIMIT ${sanitizeInt(filters.limit)}` : "";
301
- const offset = filters?.offset ? `OFFSET ${sanitizeInt(filters.offset)}` : "";
302
-
303
- const rows = await db.queryAll<ContextItemRow>(
304
- `SELECT * FROM context_items ${where} ORDER BY drive ASC, path ASC, id ASC ${limit} ${offset}`,
305
- ...params,
306
- );
307
- return rows.map(rowToContextItem);
308
- }
309
-
310
- export async function listContextItemsByPrefix(
311
- db: DbConnection,
312
- drive: string,
313
- prefix: string,
314
- opts?: { recursive?: boolean; limit?: number; offset?: number },
315
- ): Promise<ContextItem[]> {
316
- const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
317
-
318
- const limit = opts?.limit ? `LIMIT ${sanitizeInt(opts.limit)}` : "";
319
- const offset = opts?.offset ? `OFFSET ${sanitizeInt(opts.offset)}` : "";
320
-
321
- let rows: ContextItemRow[];
322
- if (opts?.recursive) {
323
- rows = await db.queryAll<ContextItemRow>(
324
- `SELECT * FROM context_items
325
- WHERE drive = ?1 AND path LIKE ?2
326
- ORDER BY path ASC, id ASC ${limit} ${offset}`,
327
- drive,
328
- `${normalizedPrefix}%`,
329
- );
330
- } else {
331
- rows = await db.queryAll<ContextItemRow>(
332
- `SELECT * FROM context_items
333
- WHERE drive = ?1 AND path LIKE ?2
334
- AND path NOT LIKE ?3
335
- ORDER BY path ASC, id ASC ${limit} ${offset}`,
336
- drive,
337
- `${normalizedPrefix}%`,
338
- `${normalizedPrefix}%/%`,
339
- );
340
- }
341
-
342
- return rows.map(rowToContextItem);
343
- }
344
-
345
- export async function contextPathExists(
346
- db: DbConnection,
347
- target: DriveTarget,
348
- ): Promise<boolean> {
349
- const row = await db.queryGet(
350
- "SELECT 1 AS found FROM context_items WHERE drive = ?1 AND path = ?2 LIMIT 1",
351
- target.drive,
352
- target.path,
353
- );
354
- return row != null;
355
- }
356
-
357
- export async function countContextItemsByPrefix(
358
- db: DbConnection,
359
- drive: string,
360
- prefix: string,
361
- opts?: { recursive?: boolean },
362
- ): Promise<number> {
363
- const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
364
- let row: { cnt: number } | null;
365
- if (opts?.recursive !== false) {
366
- row = await db.queryGet<{ cnt: number }>(
367
- `SELECT COUNT(*) AS cnt FROM context_items WHERE drive = ?1 AND path LIKE ?2`,
368
- drive,
369
- `${normalizedPrefix}%`,
370
- );
371
- } else {
372
- row = await db.queryGet<{ cnt: number }>(
373
- `SELECT COUNT(*) AS cnt FROM context_items
374
- WHERE drive = ?1 AND path LIKE ?2 AND path NOT LIKE ?3`,
375
- drive,
376
- `${normalizedPrefix}%`,
377
- `${normalizedPrefix}%/%`,
378
- );
379
- }
380
- return row ? Number(row.cnt) : 0;
381
- }
382
-
383
- export async function getDistinctDirectories(
384
- db: DbConnection,
385
- drive: string,
386
- prefix?: string,
387
- ): Promise<string[]> {
388
- const normalizedPrefix = prefix
389
- ? prefix.endsWith("/")
390
- ? prefix
391
- : `${prefix}/`
392
- : "/";
393
-
394
- // Extract the first path segment after the prefix
395
- const rows = await db.queryAll<{ dir: string }>(
396
- `SELECT DISTINCT
397
- ?1 || CASE
398
- WHEN strpos(substr(path, length(?1) + 1), '/') > 0
399
- THEN substr(substr(path, length(?1) + 1), 1, strpos(substr(path, length(?1) + 1), '/') - 1)
400
- ELSE substr(path, length(?1) + 1)
401
- END AS dir
402
- FROM context_items
403
- WHERE drive = ?2 AND path LIKE ?3
404
- ORDER BY dir ASC`,
405
- normalizedPrefix,
406
- drive,
407
- `${normalizedPrefix}%/%`,
408
- );
409
-
410
- return rows.map((row) => row.dir);
411
- }
412
-
413
- export interface DriveSummary {
414
- drive: string;
415
- count: number;
416
- }
417
-
418
- export async function listDriveSummaries(
419
- db: DbConnection,
420
- ): Promise<DriveSummary[]> {
421
- const rows = await db.queryAll<{ drive: string; cnt: number }>(
422
- "SELECT drive, COUNT(*) AS cnt FROM context_items GROUP BY drive ORDER BY drive ASC",
423
- );
424
- return rows.map((r) => ({ drive: r.drive, count: Number(r.cnt) }));
425
- }
426
-
427
- // --- Mutations ---
428
-
429
- // `UPDATE context_items ... RETURNING *` can crash @duckdb/node-api via a C++
430
- // exception when the table's unique index is in a violated state. Update +
431
- // separate SELECT is equivalent here (single-connection, no concurrent writers)
432
- // and avoids the crash entirely.
433
- export async function updateContextItem(
434
- db: DbConnection,
435
- id: string,
436
- updates: Partial<
437
- Pick<
438
- ContextItem,
439
- "title" | "description" | "content" | "mime_type" | "source_url"
440
- >
441
- >,
442
- ): Promise<ContextItem | null> {
443
- const { setClauses, params } = buildSetClauses([
444
- ["title", updates.title],
445
- ["description", updates.description],
446
- ["content", updates.content],
447
- ["mime_type", updates.mime_type],
448
- ["source_url", updates.source_url],
449
- ]);
450
-
451
- setClauses.push("updated_at = current_timestamp::VARCHAR");
452
- params.push(id);
453
-
454
- await db.queryRun(
455
- `UPDATE context_items
456
- SET ${setClauses.join(", ")}
457
- WHERE id = ?${params.length}`,
458
- ...params,
459
- );
460
- return getContextItemById(db, id);
461
- }
462
-
463
- export async function updateContextItemContent(
464
- db: DbConnection,
465
- target: DriveTarget,
466
- content: string,
467
- ): Promise<ContextItem | null> {
468
- await db.queryRun(
469
- `UPDATE context_items
470
- SET content = ?1, updated_at = current_timestamp::VARCHAR
471
- WHERE drive = ?2 AND path = ?3`,
472
- content,
473
- target.drive,
474
- target.path,
475
- );
476
- return getContextItem(db, target);
477
- }
478
-
479
- export async function applyPatchesToContextItem(
480
- db: DbConnection,
481
- target: DriveTarget,
482
- patches: Patch[],
483
- ): Promise<{ item: ContextItem; applied: number }> {
484
- const item = await getContextItem(db, target);
485
- if (!item) throw new Error(`Not found: ${formatDriveRef(target)}`);
486
- if (item.content == null)
487
- throw new Error(`No text content: ${formatDriveRef(target)}`);
488
-
489
- const lines = item.content.split("\n");
490
-
491
- // Sort patches by start_line descending so we apply bottom-up
492
- const sorted = [...patches].sort((a, b) => b.start_line - a.start_line);
493
-
494
- for (const patch of sorted) {
495
- if (patch.end_line === 0) {
496
- // Insert at start_line without replacing
497
- const insertLines = patch.content === "" ? [] : patch.content.split("\n");
498
- lines.splice(patch.start_line - 1, 0, ...insertLines);
499
- } else {
500
- // Replace lines [start_line, end_line] inclusive (1-based)
501
- const deleteCount = patch.end_line - patch.start_line + 1;
502
- const insertLines = patch.content === "" ? [] : patch.content.split("\n");
503
- lines.splice(patch.start_line - 1, deleteCount, ...insertLines);
504
- }
505
- }
506
-
507
- const newContent = lines.join("\n");
508
- const updated = await updateContextItemContent(db, target, newContent);
509
- if (!updated) throw new Error(`Failed to update: ${formatDriveRef(target)}`);
510
- return { item: updated, applied: patches.length };
511
- }
512
-
513
- export async function copyContextItem(
514
- db: DbConnection,
515
- src: DriveTarget,
516
- dst: DriveTarget,
517
- ): Promise<ContextItem> {
518
- const source = await getContextItem(db, src);
519
- if (!source) throw new Error(`Not found: ${formatDriveRef(src)}`);
520
-
521
- return createContextItem(db, {
522
- title: source.title,
523
- description: source.description,
524
- content: source.content ?? undefined,
525
- mimeType: source.mime_type,
526
- drive: dst.drive,
527
- path: dst.path,
528
- isTextual: source.is_textual,
529
- sourceUrl: source.source_url,
530
- });
531
- }
532
-
533
- export async function moveContextItem(
534
- db: DbConnection,
535
- src: DriveTarget,
536
- dst: DriveTarget,
537
- ): Promise<void> {
538
- const row = await db.queryGet(
539
- `UPDATE context_items
540
- SET drive = ?1, path = ?2, updated_at = current_timestamp::VARCHAR
541
- WHERE drive = ?3 AND path = ?4
542
- RETURNING id`,
543
- dst.drive,
544
- dst.path,
545
- src.drive,
546
- src.path,
547
- );
548
- if (!row) {
549
- throw new Error(`Not found: ${formatDriveRef(src)}`);
550
- }
551
- }
552
-
553
- // --- Deletion ---
554
-
555
- export async function deleteContextItem(
556
- db: DbConnection,
557
- id: string,
558
- ): Promise<boolean> {
559
- // Delete embeddings first (foreign key)
560
- await db.queryRun("DELETE FROM embeddings WHERE context_item_id = ?1", id);
561
- const row = await db.queryGet(
562
- "DELETE FROM context_items WHERE id = ?1 RETURNING id",
563
- id,
564
- );
565
- return row != null;
566
- }
567
-
568
- export async function deleteContextItemByPath(
569
- db: DbConnection,
570
- target: DriveTarget,
571
- ): Promise<boolean> {
572
- const item = await getContextItem(db, target);
573
- if (!item) return false;
574
- return deleteContextItem(db, item.id);
575
- }
576
-
577
- export async function deleteAllContextItems(
578
- db: DbConnection,
579
- ): Promise<{ contextItems: number; embeddings: number }> {
580
- const embeddings = await db.queryRun("DELETE FROM embeddings");
581
- const contextItems = await db.queryRun("DELETE FROM context_items");
582
- return {
583
- contextItems: contextItems.changes,
584
- embeddings: embeddings.changes,
585
- };
586
- }
587
-
588
- export async function deleteContextItemsByPrefix(
589
- db: DbConnection,
590
- drive: string,
591
- prefix: string,
592
- ): Promise<number> {
593
- const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
594
-
595
- // Delete embeddings for all matching items
596
- await db.queryRun(
597
- `DELETE FROM embeddings
598
- WHERE context_item_id IN (
599
- SELECT id FROM context_items
600
- WHERE drive = ?1 AND path LIKE ?2
601
- )`,
602
- drive,
603
- `${normalizedPrefix}%`,
604
- );
605
-
606
- const rows = await db.queryAll(
607
- `DELETE FROM context_items
608
- WHERE drive = ?1 AND path LIKE ?2
609
- RETURNING id`,
610
- drive,
611
- `${normalizedPrefix}%`,
612
- );
613
- return rows.length;
614
- }
615
-
616
- // --- Search ---
617
-
618
- export async function searchContextByKeyword(
619
- db: DbConnection,
620
- query: string,
621
- limit = 20,
622
- ): Promise<ContextItem[]> {
623
- const pattern = `%${query}%`;
624
- const rows = await db.queryAll<ContextItemRow>(
625
- `SELECT * FROM context_items
626
- WHERE content IS NOT NULL
627
- AND (
628
- content ILIKE ?1
629
- OR title ILIKE ?1
630
- )
631
- ORDER BY updated_at DESC
632
- LIMIT ?2`,
633
- pattern,
634
- limit,
635
- );
636
- return rows.map(rowToContextItem);
637
- }
@@ -1,6 +0,0 @@
1
- import type { DbConnection } from "./connection.ts";
2
-
3
- export async function deleteAllDaemonState(db: DbConnection): Promise<number> {
4
- const result = await db.queryRun("DELETE FROM daemon_state");
5
- return result.changes;
6
- }