botholomew 0.8.9 → 0.9.4
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/src/chat/agent.ts +34 -31
- package/src/commands/context.ts +223 -373
- package/src/commands/tools.ts +100 -11
- package/src/context/describer.ts +3 -118
- package/src/context/drives.ts +110 -0
- package/src/context/fetcher.ts +11 -1
- package/src/context/ingest.ts +13 -10
- package/src/context/refresh.ts +39 -24
- package/src/context/url-utils.ts +0 -23
- package/src/db/context.ts +195 -119
- package/src/db/embeddings.ts +35 -16
- package/src/db/sql/13-drive-paths.sql +49 -0
- package/src/tools/context/list-drives.ts +36 -0
- package/src/tools/context/refresh.ts +41 -23
- package/src/tools/context/search.ts +8 -3
- package/src/tools/dir/create.ts +14 -11
- package/src/tools/dir/size.ts +3 -2
- package/src/tools/dir/tree.ts +57 -17
- package/src/tools/file/copy.ts +14 -8
- package/src/tools/file/count-lines.ts +6 -3
- package/src/tools/file/delete.ts +12 -5
- package/src/tools/file/edit.ts +5 -3
- package/src/tools/file/exists.ts +25 -3
- package/src/tools/file/info.ts +90 -18
- package/src/tools/file/move.ts +15 -16
- package/src/tools/file/read.ts +79 -5
- package/src/tools/file/write.ts +29 -12
- package/src/tools/registry.ts +2 -2
- package/src/tools/search/grep.ts +44 -11
- package/src/tools/search/semantic.ts +7 -3
- package/src/tui/components/ContextPanel.tsx +73 -35
- package/src/tui/markdown.ts +2 -3
- package/src/worker/prompt.ts +42 -46
- package/src/tools/dir/list.ts +0 -89
package/src/db/context.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { resolve as resolvePath } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
type DriveTarget,
|
|
4
|
+
formatDriveRef,
|
|
5
|
+
parseDriveRef,
|
|
6
|
+
} from "../context/drives.ts";
|
|
2
7
|
import type { DbConnection } from "./connection.ts";
|
|
3
8
|
import { buildSetClauses, buildWhereClause, sanitizeInt } from "./query.ts";
|
|
4
9
|
import { isUuid, uuidv7 } from "./uuid.ts";
|
|
@@ -10,9 +15,8 @@ export interface ContextItem {
|
|
|
10
15
|
content: string | null;
|
|
11
16
|
mime_type: string;
|
|
12
17
|
is_textual: boolean;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
context_path: string;
|
|
18
|
+
drive: string;
|
|
19
|
+
path: string;
|
|
16
20
|
indexed_at: Date | null;
|
|
17
21
|
created_at: Date;
|
|
18
22
|
updated_at: Date;
|
|
@@ -32,9 +36,8 @@ interface ContextItemRow {
|
|
|
32
36
|
content_blob: unknown;
|
|
33
37
|
mime_type: string;
|
|
34
38
|
is_textual: boolean;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
context_path: string;
|
|
39
|
+
drive: string;
|
|
40
|
+
path: string;
|
|
38
41
|
indexed_at: string | null;
|
|
39
42
|
created_at: string;
|
|
40
43
|
updated_at: string;
|
|
@@ -48,9 +51,8 @@ function rowToContextItem(row: ContextItemRow): ContextItem {
|
|
|
48
51
|
content: row.content,
|
|
49
52
|
mime_type: row.mime_type,
|
|
50
53
|
is_textual: !!row.is_textual,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
context_path: row.context_path,
|
|
54
|
+
drive: row.drive,
|
|
55
|
+
path: row.path,
|
|
54
56
|
indexed_at: row.indexed_at ? new Date(row.indexed_at) : null,
|
|
55
57
|
created_at: new Date(row.created_at),
|
|
56
58
|
updated_at: new Date(row.updated_at),
|
|
@@ -59,12 +61,14 @@ function rowToContextItem(row: ContextItemRow): ContextItem {
|
|
|
59
61
|
|
|
60
62
|
export class PathConflictError extends Error {
|
|
61
63
|
existingId: string;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
drive: string;
|
|
65
|
+
path: string;
|
|
66
|
+
constructor(existingId: string, target: DriveTarget) {
|
|
67
|
+
super(`Path already exists: ${formatDriveRef(target)}`);
|
|
65
68
|
this.name = "PathConflictError";
|
|
66
69
|
this.existingId = existingId;
|
|
67
|
-
this.
|
|
70
|
+
this.drive = target.drive;
|
|
71
|
+
this.path = target.path;
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
74
|
|
|
@@ -76,17 +80,16 @@ export async function createContextItem(
|
|
|
76
80
|
title: string;
|
|
77
81
|
content?: string;
|
|
78
82
|
mimeType?: string;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
contextPath: string;
|
|
83
|
+
drive: string;
|
|
84
|
+
path: string;
|
|
82
85
|
description?: string;
|
|
83
86
|
isTextual?: boolean;
|
|
84
87
|
},
|
|
85
88
|
): Promise<ContextItem> {
|
|
86
89
|
const id = uuidv7();
|
|
87
90
|
const row = await db.queryGet<ContextItemRow>(
|
|
88
|
-
`INSERT INTO context_items (id, title, description, content, mime_type, is_textual,
|
|
89
|
-
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8
|
|
91
|
+
`INSERT INTO context_items (id, title, description, content, mime_type, is_textual, drive, path)
|
|
92
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
|
90
93
|
RETURNING *`,
|
|
91
94
|
id,
|
|
92
95
|
params.title,
|
|
@@ -94,16 +97,15 @@ export async function createContextItem(
|
|
|
94
97
|
params.content ?? null,
|
|
95
98
|
params.mimeType ?? "text/plain",
|
|
96
99
|
params.isTextual !== false,
|
|
97
|
-
params.
|
|
98
|
-
params.
|
|
99
|
-
params.contextPath,
|
|
100
|
+
params.drive,
|
|
101
|
+
params.path,
|
|
100
102
|
);
|
|
101
103
|
if (!row) throw new Error("INSERT did not return a row");
|
|
102
104
|
return rowToContextItem(row);
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
/**
|
|
106
|
-
* Atomic upsert by
|
|
108
|
+
* Atomic upsert by (drive, path): updates if the pair exists, inserts otherwise.
|
|
107
109
|
*
|
|
108
110
|
* DuckDB implements UPDATE as delete+insert on tables with unique indexes,
|
|
109
111
|
* which violates foreign keys from the embeddings table. We must delete
|
|
@@ -116,28 +118,33 @@ export async function upsertContextItem(
|
|
|
116
118
|
title: string;
|
|
117
119
|
content?: string;
|
|
118
120
|
mimeType?: string;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
contextPath: string;
|
|
121
|
+
drive: string;
|
|
122
|
+
path: string;
|
|
122
123
|
description?: string;
|
|
123
124
|
isTextual?: boolean;
|
|
124
125
|
},
|
|
125
126
|
): Promise<ContextItem> {
|
|
126
|
-
const existing = await
|
|
127
|
+
const existing = await getContextItem(db, {
|
|
128
|
+
drive: params.drive,
|
|
129
|
+
path: params.path,
|
|
130
|
+
});
|
|
127
131
|
if (existing) {
|
|
128
132
|
const updated = await updateContextItem(db, existing.id, {
|
|
129
133
|
title: params.title,
|
|
130
134
|
content: params.content,
|
|
131
135
|
mime_type: params.mimeType,
|
|
132
136
|
});
|
|
133
|
-
if (!updated)
|
|
137
|
+
if (!updated)
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Failed to update: ${formatDriveRef({ drive: params.drive, path: params.path })}`,
|
|
140
|
+
);
|
|
134
141
|
return updated;
|
|
135
142
|
}
|
|
136
143
|
return createContextItem(db, params);
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
/**
|
|
140
|
-
* Strict creator: throws PathConflictError if
|
|
147
|
+
* Strict creator: throws PathConflictError if (drive, path) already exists.
|
|
141
148
|
* Use when callers want to surface collisions instead of silently overwriting.
|
|
142
149
|
*/
|
|
143
150
|
export async function createContextItemStrict(
|
|
@@ -146,19 +153,25 @@ export async function createContextItemStrict(
|
|
|
146
153
|
title: string;
|
|
147
154
|
content?: string;
|
|
148
155
|
mimeType?: string;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
contextPath: string;
|
|
156
|
+
drive: string;
|
|
157
|
+
path: string;
|
|
152
158
|
description?: string;
|
|
153
159
|
isTextual?: boolean;
|
|
154
160
|
},
|
|
155
161
|
): Promise<ContextItem> {
|
|
156
|
-
const existing = await
|
|
157
|
-
|
|
162
|
+
const existing = await getContextItem(db, {
|
|
163
|
+
drive: params.drive,
|
|
164
|
+
path: params.path,
|
|
165
|
+
});
|
|
166
|
+
if (existing)
|
|
167
|
+
throw new PathConflictError(existing.id, {
|
|
168
|
+
drive: params.drive,
|
|
169
|
+
path: params.path,
|
|
170
|
+
});
|
|
158
171
|
return createContextItem(db, params);
|
|
159
172
|
}
|
|
160
173
|
|
|
161
|
-
export async function
|
|
174
|
+
export async function getContextItemById(
|
|
162
175
|
db: DbConnection,
|
|
163
176
|
id: string,
|
|
164
177
|
): Promise<ContextItem | null> {
|
|
@@ -169,51 +182,38 @@ export async function getContextItem(
|
|
|
169
182
|
return row ? rowToContextItem(row) : null;
|
|
170
183
|
}
|
|
171
184
|
|
|
172
|
-
export async function
|
|
173
|
-
db: DbConnection,
|
|
174
|
-
contextPath: string,
|
|
175
|
-
): Promise<ContextItem | null> {
|
|
176
|
-
const row = await db.queryGet<ContextItemRow>(
|
|
177
|
-
"SELECT * FROM context_items WHERE context_path = ?1",
|
|
178
|
-
contextPath,
|
|
179
|
-
);
|
|
180
|
-
return row ? rowToContextItem(row) : null;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export async function getContextItemBySourcePath(
|
|
185
|
+
export async function getContextItem(
|
|
184
186
|
db: DbConnection,
|
|
185
|
-
|
|
186
|
-
sourceType: "file" | "url",
|
|
187
|
+
target: DriveTarget,
|
|
187
188
|
): Promise<ContextItem | null> {
|
|
188
189
|
const row = await db.queryGet<ContextItemRow>(
|
|
189
|
-
"SELECT * FROM context_items WHERE
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
"SELECT * FROM context_items WHERE drive = ?1 AND path = ?2",
|
|
191
|
+
target.drive,
|
|
192
|
+
target.path,
|
|
192
193
|
);
|
|
193
194
|
return row ? rowToContextItem(row) : null;
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
/**
|
|
197
|
-
* Look up a context item by UUID, `
|
|
198
|
+
* Look up a context item by UUID, `drive:/path`, or bare filesystem path
|
|
199
|
+
* (resolved against cwd and treated as `disk:/...`).
|
|
198
200
|
*
|
|
199
|
-
*
|
|
200
|
-
* `context add` (e.g. a
|
|
201
|
-
* `context refresh` / `context chunks`.
|
|
202
|
-
* against `process.cwd()` to match the absolute `source_path` stored on add.
|
|
201
|
+
* The bare-path fallback lets users pass the same argument they used for
|
|
202
|
+
* `context add` (e.g. a relative `README.md`) to management commands like
|
|
203
|
+
* `context refresh` / `context chunks`.
|
|
203
204
|
*/
|
|
204
205
|
export async function resolveContextItem(
|
|
205
206
|
db: DbConnection,
|
|
206
|
-
|
|
207
|
+
ref: string,
|
|
207
208
|
): Promise<ContextItem | null> {
|
|
208
|
-
if (isUuid(
|
|
209
|
+
if (isUuid(ref)) return getContextItemById(db, ref);
|
|
209
210
|
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
211
|
+
const parsed = parseDriveRef(ref);
|
|
212
|
+
if (parsed) return getContextItem(db, parsed);
|
|
212
213
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return getContextItemBySourcePath(db, resolvePath(pathOrId), "file");
|
|
214
|
+
// Bare filesystem path — try the `disk` drive with an absolute path.
|
|
215
|
+
const absolute = resolvePath(ref);
|
|
216
|
+
return getContextItem(db, { drive: "disk", path: absolute });
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
/**
|
|
@@ -221,31 +221,79 @@ export async function resolveContextItem(
|
|
|
221
221
|
*/
|
|
222
222
|
export async function resolveContextItemOrThrow(
|
|
223
223
|
db: DbConnection,
|
|
224
|
-
|
|
224
|
+
ref: string,
|
|
225
225
|
): Promise<ContextItem> {
|
|
226
|
-
const item = await resolveContextItem(db,
|
|
227
|
-
if (!item) throw new Error(`Not found: ${
|
|
226
|
+
const item = await resolveContextItem(db, ref);
|
|
227
|
+
if (!item) throw new Error(`Not found: ${ref}`);
|
|
228
228
|
return item;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
export interface NearbyContextPaths {
|
|
232
|
+
/** Directory we found neighbours under (may be an ancestor if the direct parent was empty). */
|
|
233
|
+
parent: string;
|
|
234
|
+
/** Exact `drive:/path` values of the parent's immediate children. */
|
|
235
|
+
siblings: string[];
|
|
236
|
+
/** True if we walked up from the requested path's direct parent to find a populated ancestor. */
|
|
237
|
+
walkedUp: boolean;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Find context items near a requested path to power "did you mean?" suggestions
|
|
242
|
+
* when a lookup misses. Returns up to `limit` immediate neighbours within the
|
|
243
|
+
* same drive; if the parent has no rows, walks up until it finds a populated
|
|
244
|
+
* ancestor (or hits root).
|
|
245
|
+
*/
|
|
246
|
+
export async function findNearbyContextPaths(
|
|
247
|
+
db: DbConnection,
|
|
248
|
+
drive: string,
|
|
249
|
+
requestedPath: string,
|
|
250
|
+
limit = 5,
|
|
251
|
+
): Promise<NearbyContextPaths> {
|
|
252
|
+
let parent = parentDir(requestedPath);
|
|
253
|
+
let walkedUp = false;
|
|
254
|
+
while (true) {
|
|
255
|
+
const items = await listContextItemsByPrefix(db, drive, parent, {
|
|
256
|
+
recursive: false,
|
|
257
|
+
limit,
|
|
258
|
+
});
|
|
259
|
+
if (items.length > 0 || parent === "/") {
|
|
260
|
+
return {
|
|
261
|
+
parent: `${drive}:${parent}`,
|
|
262
|
+
siblings: items.map((i) => formatDriveRef(i)),
|
|
263
|
+
walkedUp,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
parent = parentDir(parent);
|
|
267
|
+
walkedUp = true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function parentDir(p: string): string {
|
|
272
|
+
if (!p || p === "/") return "/";
|
|
273
|
+
const trimmed = p.endsWith("/") && p.length > 1 ? p.slice(0, -1) : p;
|
|
274
|
+
const idx = trimmed.lastIndexOf("/");
|
|
275
|
+
if (idx <= 0) return "/";
|
|
276
|
+
return trimmed.slice(0, idx);
|
|
277
|
+
}
|
|
278
|
+
|
|
231
279
|
export async function listContextItems(
|
|
232
280
|
db: DbConnection,
|
|
233
281
|
filters?: {
|
|
234
|
-
|
|
282
|
+
drive?: string;
|
|
235
283
|
mimeType?: string;
|
|
236
284
|
limit?: number;
|
|
237
285
|
offset?: number;
|
|
238
286
|
},
|
|
239
287
|
): Promise<ContextItem[]> {
|
|
240
288
|
const { where, params } = buildWhereClause([
|
|
241
|
-
["
|
|
289
|
+
["drive", filters?.drive],
|
|
242
290
|
["mime_type", filters?.mimeType],
|
|
243
291
|
]);
|
|
244
292
|
const limit = filters?.limit ? `LIMIT ${sanitizeInt(filters.limit)}` : "";
|
|
245
293
|
const offset = filters?.offset ? `OFFSET ${sanitizeInt(filters.offset)}` : "";
|
|
246
294
|
|
|
247
295
|
const rows = await db.queryAll<ContextItemRow>(
|
|
248
|
-
`SELECT * FROM context_items ${where} ORDER BY
|
|
296
|
+
`SELECT * FROM context_items ${where} ORDER BY drive ASC, path ASC, id ASC ${limit} ${offset}`,
|
|
249
297
|
...params,
|
|
250
298
|
);
|
|
251
299
|
return rows.map(rowToContextItem);
|
|
@@ -253,6 +301,7 @@ export async function listContextItems(
|
|
|
253
301
|
|
|
254
302
|
export async function listContextItemsByPrefix(
|
|
255
303
|
db: DbConnection,
|
|
304
|
+
drive: string,
|
|
256
305
|
prefix: string,
|
|
257
306
|
opts?: { recursive?: boolean; limit?: number; offset?: number },
|
|
258
307
|
): Promise<ContextItem[]> {
|
|
@@ -265,17 +314,18 @@ export async function listContextItemsByPrefix(
|
|
|
265
314
|
if (opts?.recursive) {
|
|
266
315
|
rows = await db.queryAll<ContextItemRow>(
|
|
267
316
|
`SELECT * FROM context_items
|
|
268
|
-
WHERE
|
|
269
|
-
ORDER BY
|
|
317
|
+
WHERE drive = ?1 AND path LIKE ?2
|
|
318
|
+
ORDER BY path ASC, id ASC ${limit} ${offset}`,
|
|
319
|
+
drive,
|
|
270
320
|
`${normalizedPrefix}%`,
|
|
271
321
|
);
|
|
272
322
|
} else {
|
|
273
|
-
// Only immediate children: match prefix but no further slashes
|
|
274
323
|
rows = await db.queryAll<ContextItemRow>(
|
|
275
324
|
`SELECT * FROM context_items
|
|
276
|
-
WHERE
|
|
277
|
-
AND
|
|
278
|
-
ORDER BY
|
|
325
|
+
WHERE drive = ?1 AND path LIKE ?2
|
|
326
|
+
AND path NOT LIKE ?3
|
|
327
|
+
ORDER BY path ASC, id ASC ${limit} ${offset}`,
|
|
328
|
+
drive,
|
|
279
329
|
`${normalizedPrefix}%`,
|
|
280
330
|
`${normalizedPrefix}%/%`,
|
|
281
331
|
);
|
|
@@ -286,17 +336,19 @@ export async function listContextItemsByPrefix(
|
|
|
286
336
|
|
|
287
337
|
export async function contextPathExists(
|
|
288
338
|
db: DbConnection,
|
|
289
|
-
|
|
339
|
+
target: DriveTarget,
|
|
290
340
|
): Promise<boolean> {
|
|
291
341
|
const row = await db.queryGet(
|
|
292
|
-
"SELECT 1 AS found FROM context_items WHERE
|
|
293
|
-
|
|
342
|
+
"SELECT 1 AS found FROM context_items WHERE drive = ?1 AND path = ?2 LIMIT 1",
|
|
343
|
+
target.drive,
|
|
344
|
+
target.path,
|
|
294
345
|
);
|
|
295
346
|
return row != null;
|
|
296
347
|
}
|
|
297
348
|
|
|
298
349
|
export async function countContextItemsByPrefix(
|
|
299
350
|
db: DbConnection,
|
|
351
|
+
drive: string,
|
|
300
352
|
prefix: string,
|
|
301
353
|
opts?: { recursive?: boolean },
|
|
302
354
|
): Promise<number> {
|
|
@@ -304,13 +356,15 @@ export async function countContextItemsByPrefix(
|
|
|
304
356
|
let row: { cnt: number } | null;
|
|
305
357
|
if (opts?.recursive !== false) {
|
|
306
358
|
row = await db.queryGet<{ cnt: number }>(
|
|
307
|
-
`SELECT COUNT(*) AS cnt FROM context_items WHERE
|
|
359
|
+
`SELECT COUNT(*) AS cnt FROM context_items WHERE drive = ?1 AND path LIKE ?2`,
|
|
360
|
+
drive,
|
|
308
361
|
`${normalizedPrefix}%`,
|
|
309
362
|
);
|
|
310
363
|
} else {
|
|
311
364
|
row = await db.queryGet<{ cnt: number }>(
|
|
312
365
|
`SELECT COUNT(*) AS cnt FROM context_items
|
|
313
|
-
WHERE
|
|
366
|
+
WHERE drive = ?1 AND path LIKE ?2 AND path NOT LIKE ?3`,
|
|
367
|
+
drive,
|
|
314
368
|
`${normalizedPrefix}%`,
|
|
315
369
|
`${normalizedPrefix}%/%`,
|
|
316
370
|
);
|
|
@@ -320,6 +374,7 @@ export async function countContextItemsByPrefix(
|
|
|
320
374
|
|
|
321
375
|
export async function getDistinctDirectories(
|
|
322
376
|
db: DbConnection,
|
|
377
|
+
drive: string,
|
|
323
378
|
prefix?: string,
|
|
324
379
|
): Promise<string[]> {
|
|
325
380
|
const normalizedPrefix = prefix
|
|
@@ -332,20 +387,35 @@ export async function getDistinctDirectories(
|
|
|
332
387
|
const rows = await db.queryAll<{ dir: string }>(
|
|
333
388
|
`SELECT DISTINCT
|
|
334
389
|
?1 || CASE
|
|
335
|
-
WHEN strpos(substr(
|
|
336
|
-
THEN substr(substr(
|
|
337
|
-
ELSE substr(
|
|
390
|
+
WHEN strpos(substr(path, length(?1) + 1), '/') > 0
|
|
391
|
+
THEN substr(substr(path, length(?1) + 1), 1, strpos(substr(path, length(?1) + 1), '/') - 1)
|
|
392
|
+
ELSE substr(path, length(?1) + 1)
|
|
338
393
|
END AS dir
|
|
339
394
|
FROM context_items
|
|
340
|
-
WHERE
|
|
395
|
+
WHERE drive = ?2 AND path LIKE ?3
|
|
341
396
|
ORDER BY dir ASC`,
|
|
342
397
|
normalizedPrefix,
|
|
398
|
+
drive,
|
|
343
399
|
`${normalizedPrefix}%/%`,
|
|
344
400
|
);
|
|
345
401
|
|
|
346
402
|
return rows.map((row) => row.dir);
|
|
347
403
|
}
|
|
348
404
|
|
|
405
|
+
export interface DriveSummary {
|
|
406
|
+
drive: string;
|
|
407
|
+
count: number;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function listDriveSummaries(
|
|
411
|
+
db: DbConnection,
|
|
412
|
+
): Promise<DriveSummary[]> {
|
|
413
|
+
const rows = await db.queryAll<{ drive: string; cnt: number }>(
|
|
414
|
+
"SELECT drive, COUNT(*) AS cnt FROM context_items GROUP BY drive ORDER BY drive ASC",
|
|
415
|
+
);
|
|
416
|
+
return rows.map((r) => ({ drive: r.drive, count: Number(r.cnt) }));
|
|
417
|
+
}
|
|
418
|
+
|
|
349
419
|
// --- Mutations ---
|
|
350
420
|
|
|
351
421
|
// `UPDATE context_items ... RETURNING *` can crash @duckdb/node-api via a C++
|
|
@@ -375,32 +445,34 @@ export async function updateContextItem(
|
|
|
375
445
|
WHERE id = ?${params.length}`,
|
|
376
446
|
...params,
|
|
377
447
|
);
|
|
378
|
-
return
|
|
448
|
+
return getContextItemById(db, id);
|
|
379
449
|
}
|
|
380
450
|
|
|
381
451
|
export async function updateContextItemContent(
|
|
382
452
|
db: DbConnection,
|
|
383
|
-
|
|
453
|
+
target: DriveTarget,
|
|
384
454
|
content: string,
|
|
385
455
|
): Promise<ContextItem | null> {
|
|
386
456
|
await db.queryRun(
|
|
387
457
|
`UPDATE context_items
|
|
388
458
|
SET content = ?1, updated_at = current_timestamp::VARCHAR
|
|
389
|
-
WHERE
|
|
459
|
+
WHERE drive = ?2 AND path = ?3`,
|
|
390
460
|
content,
|
|
391
|
-
|
|
461
|
+
target.drive,
|
|
462
|
+
target.path,
|
|
392
463
|
);
|
|
393
|
-
return
|
|
464
|
+
return getContextItem(db, target);
|
|
394
465
|
}
|
|
395
466
|
|
|
396
467
|
export async function applyPatchesToContextItem(
|
|
397
468
|
db: DbConnection,
|
|
398
|
-
|
|
469
|
+
target: DriveTarget,
|
|
399
470
|
patches: Patch[],
|
|
400
471
|
): Promise<{ item: ContextItem; applied: number }> {
|
|
401
|
-
const item = await
|
|
402
|
-
if (!item) throw new Error(`Not found: ${
|
|
403
|
-
if (item.content == null)
|
|
472
|
+
const item = await getContextItem(db, target);
|
|
473
|
+
if (!item) throw new Error(`Not found: ${formatDriveRef(target)}`);
|
|
474
|
+
if (item.content == null)
|
|
475
|
+
throw new Error(`No text content: ${formatDriveRef(target)}`);
|
|
404
476
|
|
|
405
477
|
const lines = item.content.split("\n");
|
|
406
478
|
|
|
@@ -421,45 +493,47 @@ export async function applyPatchesToContextItem(
|
|
|
421
493
|
}
|
|
422
494
|
|
|
423
495
|
const newContent = lines.join("\n");
|
|
424
|
-
const updated = await updateContextItemContent(db,
|
|
425
|
-
if (!updated) throw new Error(`Failed to update: ${
|
|
496
|
+
const updated = await updateContextItemContent(db, target, newContent);
|
|
497
|
+
if (!updated) throw new Error(`Failed to update: ${formatDriveRef(target)}`);
|
|
426
498
|
return { item: updated, applied: patches.length };
|
|
427
499
|
}
|
|
428
500
|
|
|
429
501
|
export async function copyContextItem(
|
|
430
502
|
db: DbConnection,
|
|
431
|
-
|
|
432
|
-
|
|
503
|
+
src: DriveTarget,
|
|
504
|
+
dst: DriveTarget,
|
|
433
505
|
): Promise<ContextItem> {
|
|
434
|
-
const
|
|
435
|
-
if (!
|
|
506
|
+
const source = await getContextItem(db, src);
|
|
507
|
+
if (!source) throw new Error(`Not found: ${formatDriveRef(src)}`);
|
|
436
508
|
|
|
437
509
|
return createContextItem(db, {
|
|
438
|
-
title:
|
|
439
|
-
description:
|
|
440
|
-
content:
|
|
441
|
-
mimeType:
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
isTextual:
|
|
510
|
+
title: source.title,
|
|
511
|
+
description: source.description,
|
|
512
|
+
content: source.content ?? undefined,
|
|
513
|
+
mimeType: source.mime_type,
|
|
514
|
+
drive: dst.drive,
|
|
515
|
+
path: dst.path,
|
|
516
|
+
isTextual: source.is_textual,
|
|
445
517
|
});
|
|
446
518
|
}
|
|
447
519
|
|
|
448
520
|
export async function moveContextItem(
|
|
449
521
|
db: DbConnection,
|
|
450
|
-
|
|
451
|
-
|
|
522
|
+
src: DriveTarget,
|
|
523
|
+
dst: DriveTarget,
|
|
452
524
|
): Promise<void> {
|
|
453
525
|
const row = await db.queryGet(
|
|
454
526
|
`UPDATE context_items
|
|
455
|
-
SET
|
|
456
|
-
WHERE
|
|
527
|
+
SET drive = ?1, path = ?2, updated_at = current_timestamp::VARCHAR
|
|
528
|
+
WHERE drive = ?3 AND path = ?4
|
|
457
529
|
RETURNING id`,
|
|
458
|
-
|
|
459
|
-
|
|
530
|
+
dst.drive,
|
|
531
|
+
dst.path,
|
|
532
|
+
src.drive,
|
|
533
|
+
src.path,
|
|
460
534
|
);
|
|
461
535
|
if (!row) {
|
|
462
|
-
throw new Error(`Not found: ${
|
|
536
|
+
throw new Error(`Not found: ${formatDriveRef(src)}`);
|
|
463
537
|
}
|
|
464
538
|
}
|
|
465
539
|
|
|
@@ -480,10 +554,9 @@ export async function deleteContextItem(
|
|
|
480
554
|
|
|
481
555
|
export async function deleteContextItemByPath(
|
|
482
556
|
db: DbConnection,
|
|
483
|
-
|
|
557
|
+
target: DriveTarget,
|
|
484
558
|
): Promise<boolean> {
|
|
485
|
-
|
|
486
|
-
const item = await getContextItemByPath(db, contextPath);
|
|
559
|
+
const item = await getContextItem(db, target);
|
|
487
560
|
if (!item) return false;
|
|
488
561
|
return deleteContextItem(db, item.id);
|
|
489
562
|
}
|
|
@@ -501,6 +574,7 @@ export async function deleteAllContextItems(
|
|
|
501
574
|
|
|
502
575
|
export async function deleteContextItemsByPrefix(
|
|
503
576
|
db: DbConnection,
|
|
577
|
+
drive: string,
|
|
504
578
|
prefix: string,
|
|
505
579
|
): Promise<number> {
|
|
506
580
|
const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
@@ -510,15 +584,17 @@ export async function deleteContextItemsByPrefix(
|
|
|
510
584
|
`DELETE FROM embeddings
|
|
511
585
|
WHERE context_item_id IN (
|
|
512
586
|
SELECT id FROM context_items
|
|
513
|
-
WHERE
|
|
587
|
+
WHERE drive = ?1 AND path LIKE ?2
|
|
514
588
|
)`,
|
|
589
|
+
drive,
|
|
515
590
|
`${normalizedPrefix}%`,
|
|
516
591
|
);
|
|
517
592
|
|
|
518
593
|
const rows = await db.queryAll(
|
|
519
594
|
`DELETE FROM context_items
|
|
520
|
-
WHERE
|
|
595
|
+
WHERE drive = ?1 AND path LIKE ?2
|
|
521
596
|
RETURNING id`,
|
|
597
|
+
drive,
|
|
522
598
|
`${normalizedPrefix}%`,
|
|
523
599
|
);
|
|
524
600
|
return rows.length;
|