botholomew 0.5.0 → 0.6.1
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 +2 -2
- package/src/chat/session.ts +2 -2
- package/src/commands/context.ts +53 -42
- package/src/commands/daemon.ts +1 -1
- package/src/commands/schedule.ts +1 -1
- package/src/commands/task.ts +2 -1
- package/src/commands/thread.ts +6 -40
- package/src/commands/with-db.ts +2 -2
- package/src/constants.ts +1 -1
- package/src/context/chunker.ts +23 -46
- package/src/context/describer.ts +146 -0
- package/src/context/ingest.ts +27 -25
- package/src/daemon/index.ts +51 -5
- package/src/daemon/llm.ts +90 -13
- package/src/daemon/prompt.ts +3 -4
- package/src/daemon/schedules.ts +7 -1
- package/src/daemon/tick.ts +17 -5
- package/src/db/connection.ts +102 -40
- package/src/db/context.ts +120 -94
- package/src/db/embeddings.ts +55 -77
- package/src/db/query.ts +11 -0
- package/src/db/schedules.ts +27 -28
- package/src/db/schema.ts +9 -9
- package/src/db/sql/1-core_tables.sql +11 -11
- package/src/db/sql/2-logging_tables.sql +3 -3
- package/src/db/sql/3-daemon_state.sql +2 -2
- package/src/db/sql/6-vss_index.sql +1 -0
- package/src/db/sql/7-drop_embeddings_fk.sql +24 -0
- package/src/db/sql/8-task_output.sql +1 -0
- package/src/db/tasks.ts +89 -78
- package/src/db/threads.ts +52 -41
- package/src/init/index.ts +2 -2
- package/src/tools/file/move.ts +5 -3
- package/src/tools/file/write.ts +2 -30
- package/src/tools/search/semantic.ts +7 -4
- package/src/tools/task/list.ts +2 -0
- package/src/tools/task/view.ts +2 -0
- package/src/tui/App.tsx +20 -3
- package/src/tui/components/SchedulePanel.tsx +389 -0
- package/src/tui/components/TabBar.tsx +3 -2
- package/src/tui/components/TaskPanel.tsx +6 -0
package/src/db/threads.ts
CHANGED
|
@@ -91,10 +91,14 @@ export async function createThread(
|
|
|
91
91
|
title?: string,
|
|
92
92
|
): Promise<string> {
|
|
93
93
|
const id = uuidv7();
|
|
94
|
-
db.
|
|
94
|
+
await db.queryRun(
|
|
95
95
|
`INSERT INTO threads (id, type, task_id, title)
|
|
96
96
|
VALUES (?1, ?2, ?3, ?4)`,
|
|
97
|
-
|
|
97
|
+
id,
|
|
98
|
+
type,
|
|
99
|
+
taskId ?? null,
|
|
100
|
+
title ?? "",
|
|
101
|
+
);
|
|
98
102
|
return id;
|
|
99
103
|
}
|
|
100
104
|
|
|
@@ -112,18 +116,16 @@ export async function logInteraction(
|
|
|
112
116
|
},
|
|
113
117
|
): Promise<string> {
|
|
114
118
|
// Get next sequence number
|
|
115
|
-
const seqRow = db
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const sequence = seqRow.next_seq;
|
|
119
|
+
const seqRow = await db.queryGet<{ next_seq: number }>(
|
|
120
|
+
"SELECT COALESCE(MAX(sequence), 0) + 1 AS next_seq FROM interactions WHERE thread_id = ?1",
|
|
121
|
+
threadId,
|
|
122
|
+
);
|
|
123
|
+
const sequence = seqRow?.next_seq ?? 1;
|
|
121
124
|
|
|
122
125
|
const id = uuidv7();
|
|
123
|
-
db.
|
|
126
|
+
await db.queryRun(
|
|
124
127
|
`INSERT INTO interactions (id, thread_id, sequence, role, kind, content, tool_name, tool_input, duration_ms, token_count)
|
|
125
128
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)`,
|
|
126
|
-
).run(
|
|
127
129
|
id,
|
|
128
130
|
threadId,
|
|
129
131
|
sequence,
|
|
@@ -142,7 +144,8 @@ export async function endThread(
|
|
|
142
144
|
db: DbConnection,
|
|
143
145
|
threadId: string,
|
|
144
146
|
): Promise<void> {
|
|
145
|
-
db.
|
|
147
|
+
await db.queryRun(
|
|
148
|
+
"UPDATE threads SET ended_at = current_timestamp::VARCHAR WHERE id = ?1",
|
|
146
149
|
threadId,
|
|
147
150
|
);
|
|
148
151
|
}
|
|
@@ -151,7 +154,10 @@ export async function reopenThread(
|
|
|
151
154
|
db: DbConnection,
|
|
152
155
|
threadId: string,
|
|
153
156
|
): Promise<void> {
|
|
154
|
-
db.
|
|
157
|
+
await db.queryRun(
|
|
158
|
+
"UPDATE threads SET ended_at = NULL WHERE id = ?1",
|
|
159
|
+
threadId,
|
|
160
|
+
);
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
export async function updateThreadTitle(
|
|
@@ -159,23 +165,27 @@ export async function updateThreadTitle(
|
|
|
159
165
|
threadId: string,
|
|
160
166
|
title: string,
|
|
161
167
|
): Promise<void> {
|
|
162
|
-
db.
|
|
168
|
+
await db.queryRun(
|
|
169
|
+
"UPDATE threads SET title = ?2 WHERE id = ?1",
|
|
170
|
+
threadId,
|
|
171
|
+
title,
|
|
172
|
+
);
|
|
163
173
|
}
|
|
164
174
|
|
|
165
175
|
export async function getThread(
|
|
166
176
|
db: DbConnection,
|
|
167
177
|
threadId: string,
|
|
168
178
|
): Promise<{ thread: Thread; interactions: Interaction[] } | null> {
|
|
169
|
-
const threadRow = db
|
|
170
|
-
|
|
171
|
-
|
|
179
|
+
const threadRow = await db.queryGet<ThreadRow>(
|
|
180
|
+
"SELECT * FROM threads WHERE id = ?1",
|
|
181
|
+
threadId,
|
|
182
|
+
);
|
|
172
183
|
if (!threadRow) return null;
|
|
173
184
|
|
|
174
|
-
const interactionRows = db
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.all(threadId) as InteractionRow[];
|
|
185
|
+
const interactionRows = await db.queryAll<InteractionRow>(
|
|
186
|
+
"SELECT * FROM interactions WHERE thread_id = ?1 ORDER BY sequence ASC",
|
|
187
|
+
threadId,
|
|
188
|
+
);
|
|
179
189
|
|
|
180
190
|
return {
|
|
181
191
|
thread: rowToThread(threadRow),
|
|
@@ -187,8 +197,11 @@ export async function deleteThread(
|
|
|
187
197
|
db: DbConnection,
|
|
188
198
|
threadId: string,
|
|
189
199
|
): Promise<boolean> {
|
|
190
|
-
db.
|
|
191
|
-
const result = db.
|
|
200
|
+
await db.queryRun("DELETE FROM interactions WHERE thread_id = ?1", threadId);
|
|
201
|
+
const result = await db.queryRun(
|
|
202
|
+
"DELETE FROM threads WHERE id = ?1",
|
|
203
|
+
threadId,
|
|
204
|
+
);
|
|
192
205
|
return result.changes > 0;
|
|
193
206
|
}
|
|
194
207
|
|
|
@@ -197,22 +210,20 @@ export async function getInteractionsAfter(
|
|
|
197
210
|
threadId: string,
|
|
198
211
|
afterSequence: number,
|
|
199
212
|
): Promise<Interaction[]> {
|
|
200
|
-
const rows = db
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
213
|
+
const rows = await db.queryAll<InteractionRow>(
|
|
214
|
+
`SELECT * FROM interactions WHERE thread_id = ?1 AND sequence > ?2 ORDER BY sequence ASC`,
|
|
215
|
+
threadId,
|
|
216
|
+
afterSequence,
|
|
217
|
+
);
|
|
205
218
|
return rows.map(rowToInteraction);
|
|
206
219
|
}
|
|
207
220
|
|
|
208
221
|
export async function getActiveThread(
|
|
209
222
|
db: DbConnection,
|
|
210
223
|
): Promise<Thread | null> {
|
|
211
|
-
const row = db
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
)
|
|
215
|
-
.get() as ThreadRow | null;
|
|
224
|
+
const row = await db.queryGet<ThreadRow>(
|
|
225
|
+
`SELECT * FROM threads WHERE ended_at IS NULL ORDER BY started_at DESC LIMIT 1`,
|
|
226
|
+
);
|
|
216
227
|
return row ? rowToThread(row) : null;
|
|
217
228
|
}
|
|
218
229
|
|
|
@@ -220,9 +231,10 @@ export async function isThreadEnded(
|
|
|
220
231
|
db: DbConnection,
|
|
221
232
|
threadId: string,
|
|
222
233
|
): Promise<boolean> {
|
|
223
|
-
const row = db
|
|
224
|
-
|
|
225
|
-
|
|
234
|
+
const row = await db.queryGet<{ ended_at: string | null }>(
|
|
235
|
+
`SELECT ended_at FROM threads WHERE id = ?1`,
|
|
236
|
+
threadId,
|
|
237
|
+
);
|
|
226
238
|
if (!row) return true;
|
|
227
239
|
return row.ended_at !== null;
|
|
228
240
|
}
|
|
@@ -241,12 +253,11 @@ export async function listThreads(
|
|
|
241
253
|
]);
|
|
242
254
|
const limit = filters?.limit ? `LIMIT ${filters.limit}` : "";
|
|
243
255
|
|
|
244
|
-
const rows = db
|
|
245
|
-
|
|
246
|
-
`SELECT * FROM threads ${where}
|
|
256
|
+
const rows = await db.queryAll<ThreadRow>(
|
|
257
|
+
`SELECT * FROM threads ${where}
|
|
247
258
|
ORDER BY started_at DESC
|
|
248
259
|
${limit}`,
|
|
249
|
-
|
|
250
|
-
|
|
260
|
+
...params,
|
|
261
|
+
);
|
|
251
262
|
return rows.map(rowToThread);
|
|
252
263
|
}
|
package/src/init/index.ts
CHANGED
|
@@ -50,8 +50,8 @@ export async function initProject(
|
|
|
50
50
|
|
|
51
51
|
// Initialize database
|
|
52
52
|
const dbPath = getDbPath(projectDir);
|
|
53
|
-
const conn = getConnection(dbPath);
|
|
54
|
-
migrate(conn);
|
|
53
|
+
const conn = await getConnection(dbPath);
|
|
54
|
+
await migrate(conn);
|
|
55
55
|
conn.close();
|
|
56
56
|
|
|
57
57
|
// Update .gitignore
|
package/src/tools/file/move.ts
CHANGED
|
@@ -35,9 +35,11 @@ export const fileMoveTool = {
|
|
|
35
35
|
await moveContextItem(ctx.conn, input.src, input.dst);
|
|
36
36
|
|
|
37
37
|
// Update embedding source_paths to match new location
|
|
38
|
-
ctx.conn
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
await ctx.conn.queryRun(
|
|
39
|
+
"UPDATE embeddings SET source_path = ?1 WHERE source_path = ?2",
|
|
40
|
+
input.dst,
|
|
41
|
+
input.src,
|
|
42
|
+
);
|
|
41
43
|
|
|
42
44
|
return { path: input.dst, is_error: false };
|
|
43
45
|
},
|
package/src/tools/file/write.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { isText } from "istextorbinary";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { ingestByPath } from "../../context/ingest.ts";
|
|
4
|
-
import {
|
|
5
|
-
createContextItem,
|
|
6
|
-
getContextItemByPath,
|
|
7
|
-
updateContextItem,
|
|
8
|
-
updateContextItemContent,
|
|
9
|
-
} from "../../db/context.ts";
|
|
4
|
+
import { upsertContextItem } from "../../db/context.ts";
|
|
10
5
|
import type { ToolDefinition } from "../tool.ts";
|
|
11
6
|
|
|
12
7
|
function mimeFromPath(path: string): string {
|
|
@@ -53,33 +48,10 @@ export const fileWriteTool = {
|
|
|
53
48
|
execute: async (input, ctx) => {
|
|
54
49
|
const mimeType = mimeFromPath(input.path);
|
|
55
50
|
const isTextual = isTextualPath(input.path);
|
|
56
|
-
const existing = await getContextItemByPath(ctx.conn, input.path);
|
|
57
|
-
|
|
58
|
-
if (existing) {
|
|
59
|
-
if (input.content_base64) {
|
|
60
|
-
// Binary update — store as content for now (DB blob support can be added later)
|
|
61
|
-
await updateContextItemContent(
|
|
62
|
-
ctx.conn,
|
|
63
|
-
input.path,
|
|
64
|
-
input.content_base64,
|
|
65
|
-
);
|
|
66
|
-
} else {
|
|
67
|
-
await updateContextItemContent(ctx.conn, input.path, input.content);
|
|
68
|
-
}
|
|
69
|
-
if (input.title || input.description) {
|
|
70
|
-
await updateContextItem(ctx.conn, existing.id, {
|
|
71
|
-
title: input.title,
|
|
72
|
-
description: input.description,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
await ingestByPath(ctx.conn, input.path, ctx.config);
|
|
76
|
-
return { id: existing.id, path: input.path, is_error: false };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
51
|
const title =
|
|
80
52
|
input.title ?? input.path.split("/").filter(Boolean).pop() ?? input.path;
|
|
81
53
|
|
|
82
|
-
const item = await
|
|
54
|
+
const item = await upsertContextItem(ctx.conn, {
|
|
83
55
|
title,
|
|
84
56
|
description: input.description,
|
|
85
57
|
content: input.content_base64 ?? input.content,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { embedSingle } from "../../context/embedder.ts";
|
|
3
|
-
import { hybridSearch
|
|
3
|
+
import { hybridSearch } from "../../db/embeddings.ts";
|
|
4
4
|
import type { ToolDefinition } from "../tool.ts";
|
|
5
5
|
|
|
6
6
|
const inputSchema = z.object({
|
|
@@ -36,10 +36,13 @@ export const searchSemanticTool = {
|
|
|
36
36
|
inputSchema,
|
|
37
37
|
outputSchema,
|
|
38
38
|
execute: async (input, ctx) => {
|
|
39
|
-
initVectorSearch(ctx.conn);
|
|
40
|
-
|
|
41
39
|
const queryVec = await embedSingle(input.query, ctx.config);
|
|
42
|
-
const results = hybridSearch(
|
|
40
|
+
const results = await hybridSearch(
|
|
41
|
+
ctx.conn,
|
|
42
|
+
input.query,
|
|
43
|
+
queryVec,
|
|
44
|
+
input.top_k,
|
|
45
|
+
);
|
|
43
46
|
|
|
44
47
|
const threshold = input.threshold;
|
|
45
48
|
const filtered =
|
package/src/tools/task/list.ts
CHANGED
|
@@ -16,6 +16,7 @@ const outputSchema = z.object({
|
|
|
16
16
|
status: z.string(),
|
|
17
17
|
priority: z.string(),
|
|
18
18
|
description: z.string(),
|
|
19
|
+
output: z.string().nullable(),
|
|
19
20
|
created_at: z.string(),
|
|
20
21
|
}),
|
|
21
22
|
),
|
|
@@ -42,6 +43,7 @@ export const listTasksTool = {
|
|
|
42
43
|
status: t.status,
|
|
43
44
|
priority: t.priority,
|
|
44
45
|
description: t.description,
|
|
46
|
+
output: t.output,
|
|
45
47
|
created_at: t.created_at.toISOString(),
|
|
46
48
|
})),
|
|
47
49
|
count: tasks.length,
|
package/src/tools/task/view.ts
CHANGED
|
@@ -15,6 +15,7 @@ const outputSchema = z.object({
|
|
|
15
15
|
status: z.string(),
|
|
16
16
|
priority: z.string(),
|
|
17
17
|
waiting_reason: z.string().nullable(),
|
|
18
|
+
output: z.string().nullable(),
|
|
18
19
|
claimed_by: z.string().nullable(),
|
|
19
20
|
blocked_by: z.array(z.string()),
|
|
20
21
|
context_ids: z.array(z.string()),
|
|
@@ -42,6 +43,7 @@ export const viewTaskTool = {
|
|
|
42
43
|
status: task.status,
|
|
43
44
|
priority: task.priority,
|
|
44
45
|
waiting_reason: task.waiting_reason,
|
|
46
|
+
output: task.output,
|
|
45
47
|
claimed_by: task.claimed_by,
|
|
46
48
|
blocked_by: task.blocked_by,
|
|
47
49
|
context_ids: task.context_ids,
|
package/src/tui/App.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
MessageList,
|
|
20
20
|
} from "./components/MessageList.tsx";
|
|
21
21
|
import { QueuePanel } from "./components/QueuePanel.tsx";
|
|
22
|
+
import { SchedulePanel } from "./components/SchedulePanel.tsx";
|
|
22
23
|
import { StatusBar } from "./components/StatusBar.tsx";
|
|
23
24
|
import { TabBar, type TabId } from "./components/TabBar.tsx";
|
|
24
25
|
import { TaskPanel } from "./components/TaskPanel.tsx";
|
|
@@ -219,7 +220,7 @@ export function App({
|
|
|
219
220
|
|
|
220
221
|
// Tab key cycles tabs — always active (InputBar ignores tab)
|
|
221
222
|
if (key.tab && !key.shift) {
|
|
222
|
-
setActiveTab((t) => ((t %
|
|
223
|
+
setActiveTab((t) => ((t % 7) + 1) as TabId);
|
|
223
224
|
return;
|
|
224
225
|
}
|
|
225
226
|
|
|
@@ -256,7 +257,7 @@ export function App({
|
|
|
256
257
|
if (tab !== 1) {
|
|
257
258
|
// Number keys jump to tab on non-chat tabs
|
|
258
259
|
const num = Number.parseInt(input, 10);
|
|
259
|
-
if (num >= 1 && num <=
|
|
260
|
+
if (num >= 1 && num <= 7) {
|
|
260
261
|
setActiveTab(num as TabId);
|
|
261
262
|
return;
|
|
262
263
|
}
|
|
@@ -419,7 +420,7 @@ export function App({
|
|
|
419
420
|
content: [
|
|
420
421
|
"Navigation:",
|
|
421
422
|
" Tab Cycle between panels",
|
|
422
|
-
" 1-
|
|
423
|
+
" 1-7 Jump to panel (when not in Chat)",
|
|
423
424
|
" Escape Return to Chat",
|
|
424
425
|
"",
|
|
425
426
|
"Chat (Tab 1):",
|
|
@@ -455,6 +456,15 @@ export function App({
|
|
|
455
456
|
" d Delete thread (with confirmation)",
|
|
456
457
|
" r Refresh threads",
|
|
457
458
|
"",
|
|
459
|
+
"Schedules (Tab 6):",
|
|
460
|
+
" ↑/↓ Navigate schedule list",
|
|
461
|
+
" Shift+↑/↓ Scroll detail pane",
|
|
462
|
+
" j/k Scroll detail pane",
|
|
463
|
+
" f Cycle enabled/disabled filter",
|
|
464
|
+
" e Toggle enable/disable",
|
|
465
|
+
" d Delete schedule (with confirmation)",
|
|
466
|
+
" r Refresh schedules",
|
|
467
|
+
"",
|
|
458
468
|
"Commands:",
|
|
459
469
|
" /help Show this help",
|
|
460
470
|
" /quit, /exit End the chat session",
|
|
@@ -583,6 +593,13 @@ export function App({
|
|
|
583
593
|
display={activeTab === 6 ? "flex" : "none"}
|
|
584
594
|
flexDirection="column"
|
|
585
595
|
flexGrow={1}
|
|
596
|
+
>
|
|
597
|
+
<SchedulePanel conn={conn} isActive={activeTab === 6} />
|
|
598
|
+
</Box>
|
|
599
|
+
<Box
|
|
600
|
+
display={activeTab === 7 ? "flex" : "none"}
|
|
601
|
+
flexDirection="column"
|
|
602
|
+
flexGrow={1}
|
|
586
603
|
>
|
|
587
604
|
<HelpPanel
|
|
588
605
|
projectDir={projectDir}
|