botholomew 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.
- package/package.json +42 -0
- package/src/cli.ts +45 -0
- package/src/commands/chat.ts +11 -0
- package/src/commands/check-update.ts +62 -0
- package/src/commands/context.ts +27 -0
- package/src/commands/daemon.ts +61 -0
- package/src/commands/init.ts +19 -0
- package/src/commands/mcpx.ts +29 -0
- package/src/commands/task.ts +126 -0
- package/src/commands/tools.ts +257 -0
- package/src/commands/upgrade.ts +185 -0
- package/src/config/loader.ts +31 -0
- package/src/config/schemas.ts +15 -0
- package/src/constants.ts +44 -0
- package/src/daemon/index.ts +39 -0
- package/src/daemon/llm.ts +186 -0
- package/src/daemon/prompt.ts +55 -0
- package/src/daemon/run.ts +14 -0
- package/src/daemon/spawn.ts +38 -0
- package/src/daemon/tick.ts +70 -0
- package/src/db/connection.ts +32 -0
- package/src/db/context.ts +415 -0
- package/src/db/embeddings.ts +22 -0
- package/src/db/schedules.ts +17 -0
- package/src/db/schema.ts +66 -0
- package/src/db/sql/1-core_tables.sql +53 -0
- package/src/db/sql/2-logging_tables.sql +24 -0
- package/src/db/sql/3-daemon_state.sql +5 -0
- package/src/db/tasks.ts +194 -0
- package/src/db/threads.ts +202 -0
- package/src/db/uuid.ts +1 -0
- package/src/init/index.ts +84 -0
- package/src/init/templates.ts +48 -0
- package/src/tools/dir/create.ts +39 -0
- package/src/tools/dir/list.ts +87 -0
- package/src/tools/dir/size.ts +45 -0
- package/src/tools/dir/tree.ts +91 -0
- package/src/tools/file/copy.ts +30 -0
- package/src/tools/file/count-lines.ts +26 -0
- package/src/tools/file/delete.ts +43 -0
- package/src/tools/file/edit.ts +40 -0
- package/src/tools/file/exists.ts +23 -0
- package/src/tools/file/info.ts +50 -0
- package/src/tools/file/move.ts +29 -0
- package/src/tools/file/read.ts +40 -0
- package/src/tools/file/write.ts +90 -0
- package/src/tools/registry.ts +53 -0
- package/src/tools/search/grep.ts +94 -0
- package/src/tools/search/semantic.ts +40 -0
- package/src/tools/task/complete.ts +23 -0
- package/src/tools/task/create.ts +42 -0
- package/src/tools/task/fail.ts +22 -0
- package/src/tools/task/wait.ts +23 -0
- package/src/tools/tool.ts +73 -0
- package/src/tui/App.tsx +14 -0
- package/src/types/istextorbinary.d.ts +10 -0
- package/src/update/background.ts +89 -0
- package/src/update/cache.ts +40 -0
- package/src/update/checker.ts +133 -0
- package/src/utils/frontmatter.ts +24 -0
- package/src/utils/logger.ts +29 -0
- package/src/utils/pid.ts +55 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
export type DbConnection = Database;
|
|
4
|
+
|
|
5
|
+
export function getConnection(dbPath: string): Database {
|
|
6
|
+
const db = new Database(dbPath, { create: true });
|
|
7
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
8
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
9
|
+
return db;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function withRetry<T>(
|
|
13
|
+
fn: () => Promise<T>,
|
|
14
|
+
maxRetries = 5,
|
|
15
|
+
): Promise<T> {
|
|
16
|
+
let lastError: unknown;
|
|
17
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
18
|
+
try {
|
|
19
|
+
return await fn();
|
|
20
|
+
} catch (err) {
|
|
21
|
+
lastError = err;
|
|
22
|
+
const isBusy =
|
|
23
|
+
err instanceof Error &&
|
|
24
|
+
(err.message.includes("SQLITE_BUSY") ||
|
|
25
|
+
err.message.includes("database is locked"));
|
|
26
|
+
if (!isBusy || attempt === maxRetries - 1) throw err;
|
|
27
|
+
// exponential backoff: 100ms, 200ms, 400ms, 800ms, 1600ms
|
|
28
|
+
await Bun.sleep(100 * 2 ** attempt);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw lastError;
|
|
32
|
+
}
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import type { DbConnection } from "./connection.ts";
|
|
2
|
+
import { uuidv7 } from "./uuid.ts";
|
|
3
|
+
|
|
4
|
+
export interface ContextItem {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
description: string;
|
|
8
|
+
content: string | null;
|
|
9
|
+
mime_type: string;
|
|
10
|
+
is_textual: boolean;
|
|
11
|
+
source_path: string | null;
|
|
12
|
+
context_path: string;
|
|
13
|
+
indexed_at: Date | null;
|
|
14
|
+
created_at: Date;
|
|
15
|
+
updated_at: Date;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Patch {
|
|
19
|
+
start_line: number;
|
|
20
|
+
end_line: number;
|
|
21
|
+
content: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ContextItemRow {
|
|
25
|
+
id: string;
|
|
26
|
+
title: string;
|
|
27
|
+
description: string;
|
|
28
|
+
content: string | null;
|
|
29
|
+
content_blob: unknown;
|
|
30
|
+
mime_type: string;
|
|
31
|
+
is_textual: number;
|
|
32
|
+
source_path: string | null;
|
|
33
|
+
context_path: string;
|
|
34
|
+
indexed_at: string | null;
|
|
35
|
+
created_at: string;
|
|
36
|
+
updated_at: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function rowToContextItem(row: ContextItemRow): ContextItem {
|
|
40
|
+
return {
|
|
41
|
+
id: row.id,
|
|
42
|
+
title: row.title,
|
|
43
|
+
description: row.description,
|
|
44
|
+
content: row.content,
|
|
45
|
+
mime_type: row.mime_type,
|
|
46
|
+
is_textual: row.is_textual === 1,
|
|
47
|
+
source_path: row.source_path,
|
|
48
|
+
context_path: row.context_path,
|
|
49
|
+
indexed_at: row.indexed_at ? new Date(row.indexed_at) : null,
|
|
50
|
+
created_at: new Date(row.created_at),
|
|
51
|
+
updated_at: new Date(row.updated_at),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Basic CRUD ---
|
|
56
|
+
|
|
57
|
+
export async function createContextItem(
|
|
58
|
+
db: DbConnection,
|
|
59
|
+
params: {
|
|
60
|
+
title: string;
|
|
61
|
+
content?: string;
|
|
62
|
+
mimeType?: string;
|
|
63
|
+
sourcePath?: string;
|
|
64
|
+
contextPath: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
isTextual?: boolean;
|
|
67
|
+
},
|
|
68
|
+
): Promise<ContextItem> {
|
|
69
|
+
const id = uuidv7();
|
|
70
|
+
const row = db
|
|
71
|
+
.query(
|
|
72
|
+
`INSERT INTO context_items (id, title, description, content, mime_type, is_textual, source_path, context_path)
|
|
73
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
|
|
74
|
+
RETURNING *`,
|
|
75
|
+
)
|
|
76
|
+
.get(
|
|
77
|
+
id,
|
|
78
|
+
params.title,
|
|
79
|
+
params.description ?? "",
|
|
80
|
+
params.content ?? null,
|
|
81
|
+
params.mimeType ?? "text/plain",
|
|
82
|
+
params.isTextual !== false ? 1 : 0,
|
|
83
|
+
params.sourcePath ?? null,
|
|
84
|
+
params.contextPath,
|
|
85
|
+
) as ContextItemRow | null;
|
|
86
|
+
if (!row) throw new Error("INSERT did not return a row");
|
|
87
|
+
return rowToContextItem(row);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function getContextItem(
|
|
91
|
+
db: DbConnection,
|
|
92
|
+
id: string,
|
|
93
|
+
): Promise<ContextItem | null> {
|
|
94
|
+
const row = db
|
|
95
|
+
.query("SELECT * FROM context_items WHERE id = ?1")
|
|
96
|
+
.get(id) as ContextItemRow | null;
|
|
97
|
+
return row ? rowToContextItem(row) : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function getContextItemByPath(
|
|
101
|
+
db: DbConnection,
|
|
102
|
+
contextPath: string,
|
|
103
|
+
): Promise<ContextItem | null> {
|
|
104
|
+
const row = db
|
|
105
|
+
.query("SELECT * FROM context_items WHERE context_path = ?1")
|
|
106
|
+
.get(contextPath) as ContextItemRow | null;
|
|
107
|
+
return row ? rowToContextItem(row) : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function listContextItems(
|
|
111
|
+
db: DbConnection,
|
|
112
|
+
filters?: {
|
|
113
|
+
contextPath?: string;
|
|
114
|
+
mimeType?: string;
|
|
115
|
+
limit?: number;
|
|
116
|
+
},
|
|
117
|
+
): Promise<ContextItem[]> {
|
|
118
|
+
const conditions: string[] = [];
|
|
119
|
+
const params: (string | null)[] = [];
|
|
120
|
+
|
|
121
|
+
if (filters?.contextPath) {
|
|
122
|
+
params.push(filters.contextPath);
|
|
123
|
+
conditions.push(`context_path = ?${params.length}`);
|
|
124
|
+
}
|
|
125
|
+
if (filters?.mimeType) {
|
|
126
|
+
params.push(filters.mimeType);
|
|
127
|
+
conditions.push(`mime_type = ?${params.length}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const where =
|
|
131
|
+
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
132
|
+
const limit = filters?.limit ? `LIMIT ${filters.limit}` : "";
|
|
133
|
+
|
|
134
|
+
const rows = db
|
|
135
|
+
.query(
|
|
136
|
+
`SELECT * FROM context_items ${where} ORDER BY context_path ASC ${limit}`,
|
|
137
|
+
)
|
|
138
|
+
.all(...params) as ContextItemRow[];
|
|
139
|
+
return rows.map(rowToContextItem);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function listContextItemsByPrefix(
|
|
143
|
+
db: DbConnection,
|
|
144
|
+
prefix: string,
|
|
145
|
+
opts?: { recursive?: boolean; limit?: number },
|
|
146
|
+
): Promise<ContextItem[]> {
|
|
147
|
+
const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
148
|
+
|
|
149
|
+
const limit = opts?.limit ? `LIMIT ${opts.limit}` : "";
|
|
150
|
+
|
|
151
|
+
let rows: ContextItemRow[];
|
|
152
|
+
if (opts?.recursive) {
|
|
153
|
+
rows = db
|
|
154
|
+
.query(
|
|
155
|
+
`SELECT * FROM context_items
|
|
156
|
+
WHERE context_path LIKE ?1
|
|
157
|
+
ORDER BY context_path ASC ${limit}`,
|
|
158
|
+
)
|
|
159
|
+
.all(`${normalizedPrefix}%`) as ContextItemRow[];
|
|
160
|
+
} else {
|
|
161
|
+
// Only immediate children: match prefix but no further slashes
|
|
162
|
+
rows = db
|
|
163
|
+
.query(
|
|
164
|
+
`SELECT * FROM context_items
|
|
165
|
+
WHERE context_path LIKE ?1
|
|
166
|
+
AND context_path NOT LIKE ?2
|
|
167
|
+
ORDER BY context_path ASC ${limit}`,
|
|
168
|
+
)
|
|
169
|
+
.all(
|
|
170
|
+
`${normalizedPrefix}%`,
|
|
171
|
+
`${normalizedPrefix}%/%`,
|
|
172
|
+
) as ContextItemRow[];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return rows.map(rowToContextItem);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function contextPathExists(
|
|
179
|
+
db: DbConnection,
|
|
180
|
+
contextPath: string,
|
|
181
|
+
): Promise<boolean> {
|
|
182
|
+
const row = db
|
|
183
|
+
.query(
|
|
184
|
+
"SELECT 1 AS found FROM context_items WHERE context_path = ?1 LIMIT 1",
|
|
185
|
+
)
|
|
186
|
+
.get(contextPath);
|
|
187
|
+
return row != null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function getDistinctDirectories(
|
|
191
|
+
db: DbConnection,
|
|
192
|
+
prefix?: string,
|
|
193
|
+
): Promise<string[]> {
|
|
194
|
+
const normalizedPrefix = prefix
|
|
195
|
+
? prefix.endsWith("/")
|
|
196
|
+
? prefix
|
|
197
|
+
: `${prefix}/`
|
|
198
|
+
: "/";
|
|
199
|
+
|
|
200
|
+
// Extract the first path segment after the prefix
|
|
201
|
+
const rows = db
|
|
202
|
+
.query(
|
|
203
|
+
`SELECT DISTINCT
|
|
204
|
+
?1 || CASE
|
|
205
|
+
WHEN instr(substr(context_path, length(?1) + 1), '/') > 0
|
|
206
|
+
THEN substr(substr(context_path, length(?1) + 1), 1, instr(substr(context_path, length(?1) + 1), '/') - 1)
|
|
207
|
+
ELSE substr(context_path, length(?1) + 1)
|
|
208
|
+
END AS dir
|
|
209
|
+
FROM context_items
|
|
210
|
+
WHERE context_path LIKE ?2
|
|
211
|
+
ORDER BY dir ASC`,
|
|
212
|
+
)
|
|
213
|
+
.all(normalizedPrefix, `${normalizedPrefix}%/%`) as { dir: string }[];
|
|
214
|
+
|
|
215
|
+
return rows.map((row) => row.dir);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --- Mutations ---
|
|
219
|
+
|
|
220
|
+
export async function updateContextItem(
|
|
221
|
+
db: DbConnection,
|
|
222
|
+
id: string,
|
|
223
|
+
updates: Partial<
|
|
224
|
+
Pick<ContextItem, "title" | "description" | "content" | "mime_type">
|
|
225
|
+
>,
|
|
226
|
+
): Promise<ContextItem | null> {
|
|
227
|
+
const setClauses: string[] = ["updated_at = datetime('now')"];
|
|
228
|
+
const params: (string | null)[] = [];
|
|
229
|
+
|
|
230
|
+
if (updates.title !== undefined) {
|
|
231
|
+
params.push(updates.title);
|
|
232
|
+
setClauses.push(`title = ?${params.length}`);
|
|
233
|
+
}
|
|
234
|
+
if (updates.description !== undefined) {
|
|
235
|
+
params.push(updates.description);
|
|
236
|
+
setClauses.push(`description = ?${params.length}`);
|
|
237
|
+
}
|
|
238
|
+
if (updates.content !== undefined) {
|
|
239
|
+
params.push(updates.content);
|
|
240
|
+
setClauses.push(`content = ?${params.length}`);
|
|
241
|
+
}
|
|
242
|
+
if (updates.mime_type !== undefined) {
|
|
243
|
+
params.push(updates.mime_type);
|
|
244
|
+
setClauses.push(`mime_type = ?${params.length}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
params.push(id);
|
|
248
|
+
const row = db
|
|
249
|
+
.query(
|
|
250
|
+
`UPDATE context_items
|
|
251
|
+
SET ${setClauses.join(", ")}
|
|
252
|
+
WHERE id = ?${params.length}
|
|
253
|
+
RETURNING *`,
|
|
254
|
+
)
|
|
255
|
+
.get(...params) as ContextItemRow | null;
|
|
256
|
+
return row ? rowToContextItem(row) : null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function updateContextItemContent(
|
|
260
|
+
db: DbConnection,
|
|
261
|
+
contextPath: string,
|
|
262
|
+
content: string,
|
|
263
|
+
): Promise<ContextItem | null> {
|
|
264
|
+
const row = db
|
|
265
|
+
.query(
|
|
266
|
+
`UPDATE context_items
|
|
267
|
+
SET content = ?1, updated_at = datetime('now')
|
|
268
|
+
WHERE context_path = ?2
|
|
269
|
+
RETURNING *`,
|
|
270
|
+
)
|
|
271
|
+
.get(content, contextPath) as ContextItemRow | null;
|
|
272
|
+
return row ? rowToContextItem(row) : null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function applyPatchesToContextItem(
|
|
276
|
+
db: DbConnection,
|
|
277
|
+
contextPath: string,
|
|
278
|
+
patches: Patch[],
|
|
279
|
+
): Promise<{ item: ContextItem; applied: number }> {
|
|
280
|
+
const item = await getContextItemByPath(db, contextPath);
|
|
281
|
+
if (!item) throw new Error(`Not found: ${contextPath}`);
|
|
282
|
+
if (item.content == null) throw new Error(`No text content: ${contextPath}`);
|
|
283
|
+
|
|
284
|
+
const lines = item.content.split("\n");
|
|
285
|
+
|
|
286
|
+
// Sort patches by start_line descending so we apply bottom-up
|
|
287
|
+
const sorted = [...patches].sort((a, b) => b.start_line - a.start_line);
|
|
288
|
+
|
|
289
|
+
for (const patch of sorted) {
|
|
290
|
+
if (patch.end_line === 0) {
|
|
291
|
+
// Insert at start_line without replacing
|
|
292
|
+
const insertLines = patch.content === "" ? [] : patch.content.split("\n");
|
|
293
|
+
lines.splice(patch.start_line - 1, 0, ...insertLines);
|
|
294
|
+
} else {
|
|
295
|
+
// Replace lines [start_line, end_line] inclusive (1-based)
|
|
296
|
+
const deleteCount = patch.end_line - patch.start_line + 1;
|
|
297
|
+
const insertLines = patch.content === "" ? [] : patch.content.split("\n");
|
|
298
|
+
lines.splice(patch.start_line - 1, deleteCount, ...insertLines);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const newContent = lines.join("\n");
|
|
303
|
+
const updated = await updateContextItemContent(db, contextPath, newContent);
|
|
304
|
+
if (!updated) throw new Error(`Failed to update: ${contextPath}`);
|
|
305
|
+
return { item: updated, applied: patches.length };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function copyContextItem(
|
|
309
|
+
db: DbConnection,
|
|
310
|
+
srcPath: string,
|
|
311
|
+
dstPath: string,
|
|
312
|
+
): Promise<ContextItem> {
|
|
313
|
+
const src = await getContextItemByPath(db, srcPath);
|
|
314
|
+
if (!src) throw new Error(`Not found: ${srcPath}`);
|
|
315
|
+
|
|
316
|
+
return createContextItem(db, {
|
|
317
|
+
title: src.title,
|
|
318
|
+
description: src.description,
|
|
319
|
+
content: src.content ?? undefined,
|
|
320
|
+
mimeType: src.mime_type,
|
|
321
|
+
sourcePath: src.source_path ?? undefined,
|
|
322
|
+
contextPath: dstPath,
|
|
323
|
+
isTextual: src.is_textual,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function moveContextItem(
|
|
328
|
+
db: DbConnection,
|
|
329
|
+
oldPath: string,
|
|
330
|
+
newPath: string,
|
|
331
|
+
): Promise<void> {
|
|
332
|
+
const row = db
|
|
333
|
+
.query(
|
|
334
|
+
`UPDATE context_items
|
|
335
|
+
SET context_path = ?1, updated_at = datetime('now')
|
|
336
|
+
WHERE context_path = ?2
|
|
337
|
+
RETURNING id`,
|
|
338
|
+
)
|
|
339
|
+
.get(newPath, oldPath);
|
|
340
|
+
if (!row) {
|
|
341
|
+
throw new Error(`Not found: ${oldPath}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// --- Deletion ---
|
|
346
|
+
|
|
347
|
+
export async function deleteContextItem(
|
|
348
|
+
db: DbConnection,
|
|
349
|
+
id: string,
|
|
350
|
+
): Promise<boolean> {
|
|
351
|
+
// Delete embeddings first (foreign key)
|
|
352
|
+
db.query("DELETE FROM embeddings WHERE context_item_id = ?1").run(id);
|
|
353
|
+
const row = db
|
|
354
|
+
.query("DELETE FROM context_items WHERE id = ?1 RETURNING id")
|
|
355
|
+
.get(id);
|
|
356
|
+
return row != null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export async function deleteContextItemByPath(
|
|
360
|
+
db: DbConnection,
|
|
361
|
+
contextPath: string,
|
|
362
|
+
): Promise<boolean> {
|
|
363
|
+
// Get ID first so we can cascade embeddings
|
|
364
|
+
const item = await getContextItemByPath(db, contextPath);
|
|
365
|
+
if (!item) return false;
|
|
366
|
+
return deleteContextItem(db, item.id);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export async function deleteContextItemsByPrefix(
|
|
370
|
+
db: DbConnection,
|
|
371
|
+
prefix: string,
|
|
372
|
+
): Promise<number> {
|
|
373
|
+
const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
374
|
+
|
|
375
|
+
// Delete embeddings for all matching items
|
|
376
|
+
db.query(
|
|
377
|
+
`DELETE FROM embeddings
|
|
378
|
+
WHERE context_item_id IN (
|
|
379
|
+
SELECT id FROM context_items
|
|
380
|
+
WHERE context_path LIKE ?1
|
|
381
|
+
)`,
|
|
382
|
+
).run(`${normalizedPrefix}%`);
|
|
383
|
+
|
|
384
|
+
const rows = db
|
|
385
|
+
.query(
|
|
386
|
+
`DELETE FROM context_items
|
|
387
|
+
WHERE context_path LIKE ?1
|
|
388
|
+
RETURNING id`,
|
|
389
|
+
)
|
|
390
|
+
.all(`${normalizedPrefix}%`);
|
|
391
|
+
return rows.length;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// --- Search ---
|
|
395
|
+
|
|
396
|
+
export async function searchContextByKeyword(
|
|
397
|
+
db: DbConnection,
|
|
398
|
+
query: string,
|
|
399
|
+
limit = 20,
|
|
400
|
+
): Promise<ContextItem[]> {
|
|
401
|
+
const pattern = `%${query}%`;
|
|
402
|
+
const rows = db
|
|
403
|
+
.query(
|
|
404
|
+
`SELECT * FROM context_items
|
|
405
|
+
WHERE content IS NOT NULL
|
|
406
|
+
AND (
|
|
407
|
+
content LIKE ?1 COLLATE NOCASE
|
|
408
|
+
OR title LIKE ?1 COLLATE NOCASE
|
|
409
|
+
)
|
|
410
|
+
ORDER BY updated_at DESC
|
|
411
|
+
LIMIT ?2`,
|
|
412
|
+
)
|
|
413
|
+
.all(pattern, limit) as ContextItemRow[];
|
|
414
|
+
return rows.map(rowToContextItem);
|
|
415
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DbConnection } from "./connection.ts";
|
|
2
|
+
|
|
3
|
+
export interface Embedding {
|
|
4
|
+
id: string;
|
|
5
|
+
context_item_id: string;
|
|
6
|
+
chunk_index: number;
|
|
7
|
+
chunk_content: string | null;
|
|
8
|
+
title: string;
|
|
9
|
+
description: string;
|
|
10
|
+
source_path: string | null;
|
|
11
|
+
embedding: number[];
|
|
12
|
+
created_at: Date;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Stub — full implementation in a later milestone
|
|
16
|
+
export async function searchEmbeddings(
|
|
17
|
+
_db: DbConnection,
|
|
18
|
+
_query: number[],
|
|
19
|
+
_limit?: number,
|
|
20
|
+
): Promise<Embedding[]> {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DbConnection } from "./connection.ts";
|
|
2
|
+
|
|
3
|
+
export interface Schedule {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
frequency: string;
|
|
8
|
+
last_run_at: Date | null;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
created_at: Date;
|
|
11
|
+
updated_at: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Stub — full implementation in a later milestone
|
|
15
|
+
export async function listSchedules(_db: DbConnection): Promise<Schedule[]> {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
package/src/db/schema.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { DbConnection } from "./connection.ts";
|
|
4
|
+
|
|
5
|
+
interface Migration {
|
|
6
|
+
id: number;
|
|
7
|
+
name: string;
|
|
8
|
+
sql: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const sqlDir = join(import.meta.dir, "sql");
|
|
12
|
+
|
|
13
|
+
function loadMigrations(): Migration[] {
|
|
14
|
+
const files = readdirSync(sqlDir)
|
|
15
|
+
.filter((f) => f.endsWith(".sql"))
|
|
16
|
+
.sort();
|
|
17
|
+
|
|
18
|
+
return files.map((file) => {
|
|
19
|
+
const match = file.match(/^(\d+)-(.+)\.sql$/);
|
|
20
|
+
if (!match) throw new Error(`Invalid migration filename: ${file}`);
|
|
21
|
+
const id = match[1];
|
|
22
|
+
const name = match[2];
|
|
23
|
+
if (!id || !name) throw new Error(`Invalid migration filename: ${file}`);
|
|
24
|
+
return {
|
|
25
|
+
id: parseInt(id, 10),
|
|
26
|
+
name,
|
|
27
|
+
sql: readFileSync(join(sqlDir, file), "utf-8"),
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function migrate(db: DbConnection): void {
|
|
33
|
+
// Create migrations tracking table
|
|
34
|
+
db.exec(`
|
|
35
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
36
|
+
id INTEGER PRIMARY KEY,
|
|
37
|
+
name TEXT NOT NULL,
|
|
38
|
+
applied_at TEXT DEFAULT (datetime('now'))
|
|
39
|
+
)
|
|
40
|
+
`);
|
|
41
|
+
|
|
42
|
+
// Get already-applied migrations
|
|
43
|
+
const rows = db.query("SELECT id FROM _migrations").all() as {
|
|
44
|
+
id: number;
|
|
45
|
+
}[];
|
|
46
|
+
const applied = new Set(rows.map((row) => row.id));
|
|
47
|
+
|
|
48
|
+
// Run pending migrations in order
|
|
49
|
+
for (const migration of loadMigrations()) {
|
|
50
|
+
if (applied.has(migration.id)) continue;
|
|
51
|
+
|
|
52
|
+
// Split on semicolons and run each statement individually
|
|
53
|
+
const statements = migration.sql
|
|
54
|
+
.split(";")
|
|
55
|
+
.map((s) => s.trim())
|
|
56
|
+
.filter((s) => s.length > 0);
|
|
57
|
+
|
|
58
|
+
for (const statement of statements) {
|
|
59
|
+
db.exec(statement);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
db.exec(
|
|
63
|
+
`INSERT INTO _migrations (id, name) VALUES (${migration.id}, '${migration.name}')`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
CREATE TABLE tasks (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
name TEXT NOT NULL,
|
|
4
|
+
description TEXT NOT NULL DEFAULT '',
|
|
5
|
+
priority TEXT NOT NULL DEFAULT 'medium' CHECK(priority IN ('low', 'medium', 'high')),
|
|
6
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'failed', 'complete', 'waiting')),
|
|
7
|
+
waiting_reason TEXT,
|
|
8
|
+
claimed_by TEXT,
|
|
9
|
+
claimed_at TEXT,
|
|
10
|
+
blocked_by TEXT NOT NULL DEFAULT '[]',
|
|
11
|
+
context_ids TEXT NOT NULL DEFAULT '[]',
|
|
12
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
13
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE TABLE schedules (
|
|
17
|
+
id TEXT PRIMARY KEY,
|
|
18
|
+
name TEXT NOT NULL,
|
|
19
|
+
description TEXT NOT NULL DEFAULT '',
|
|
20
|
+
frequency TEXT NOT NULL,
|
|
21
|
+
last_run_at TEXT,
|
|
22
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
23
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
24
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE TABLE context_items (
|
|
28
|
+
id TEXT PRIMARY KEY,
|
|
29
|
+
title TEXT NOT NULL,
|
|
30
|
+
description TEXT NOT NULL DEFAULT '',
|
|
31
|
+
content TEXT,
|
|
32
|
+
content_blob BLOB,
|
|
33
|
+
mime_type TEXT NOT NULL DEFAULT 'text/plain',
|
|
34
|
+
is_textual INTEGER NOT NULL DEFAULT 1,
|
|
35
|
+
source_path TEXT,
|
|
36
|
+
context_path TEXT NOT NULL,
|
|
37
|
+
indexed_at TEXT,
|
|
38
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
39
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE embeddings (
|
|
43
|
+
id TEXT PRIMARY KEY,
|
|
44
|
+
context_item_id TEXT NOT NULL REFERENCES context_items(id),
|
|
45
|
+
chunk_index INTEGER NOT NULL,
|
|
46
|
+
chunk_content TEXT,
|
|
47
|
+
title TEXT NOT NULL,
|
|
48
|
+
description TEXT NOT NULL DEFAULT '',
|
|
49
|
+
source_path TEXT,
|
|
50
|
+
embedding TEXT,
|
|
51
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
52
|
+
UNIQUE(context_item_id, chunk_index)
|
|
53
|
+
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
CREATE TABLE threads (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
type TEXT NOT NULL CHECK(type IN ('daemon_tick', 'chat_session')),
|
|
4
|
+
task_id TEXT,
|
|
5
|
+
title TEXT NOT NULL DEFAULT '',
|
|
6
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
7
|
+
ended_at TEXT,
|
|
8
|
+
metadata TEXT
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
CREATE TABLE interactions (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
thread_id TEXT NOT NULL REFERENCES threads(id),
|
|
14
|
+
sequence INTEGER NOT NULL,
|
|
15
|
+
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'tool')),
|
|
16
|
+
kind TEXT NOT NULL CHECK(kind IN ('message', 'thinking', 'tool_use', 'tool_result', 'context_update', 'status_change')),
|
|
17
|
+
content TEXT NOT NULL,
|
|
18
|
+
tool_name TEXT,
|
|
19
|
+
tool_input TEXT,
|
|
20
|
+
duration_ms INTEGER,
|
|
21
|
+
token_count INTEGER,
|
|
22
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
23
|
+
UNIQUE(thread_id, sequence)
|
|
24
|
+
);
|