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.
Files changed (41) hide show
  1. package/package.json +2 -2
  2. package/src/chat/session.ts +2 -2
  3. package/src/commands/context.ts +53 -42
  4. package/src/commands/daemon.ts +1 -1
  5. package/src/commands/schedule.ts +1 -1
  6. package/src/commands/task.ts +2 -1
  7. package/src/commands/thread.ts +6 -40
  8. package/src/commands/with-db.ts +2 -2
  9. package/src/constants.ts +1 -1
  10. package/src/context/chunker.ts +23 -46
  11. package/src/context/describer.ts +146 -0
  12. package/src/context/ingest.ts +27 -25
  13. package/src/daemon/index.ts +51 -5
  14. package/src/daemon/llm.ts +90 -13
  15. package/src/daemon/prompt.ts +3 -4
  16. package/src/daemon/schedules.ts +7 -1
  17. package/src/daemon/tick.ts +17 -5
  18. package/src/db/connection.ts +102 -40
  19. package/src/db/context.ts +120 -94
  20. package/src/db/embeddings.ts +55 -77
  21. package/src/db/query.ts +11 -0
  22. package/src/db/schedules.ts +27 -28
  23. package/src/db/schema.ts +9 -9
  24. package/src/db/sql/1-core_tables.sql +11 -11
  25. package/src/db/sql/2-logging_tables.sql +3 -3
  26. package/src/db/sql/3-daemon_state.sql +2 -2
  27. package/src/db/sql/6-vss_index.sql +1 -0
  28. package/src/db/sql/7-drop_embeddings_fk.sql +24 -0
  29. package/src/db/sql/8-task_output.sql +1 -0
  30. package/src/db/tasks.ts +89 -78
  31. package/src/db/threads.ts +52 -41
  32. package/src/init/index.ts +2 -2
  33. package/src/tools/file/move.ts +5 -3
  34. package/src/tools/file/write.ts +2 -30
  35. package/src/tools/search/semantic.ts +7 -4
  36. package/src/tools/task/list.ts +2 -0
  37. package/src/tools/task/view.ts +2 -0
  38. package/src/tui/App.tsx +20 -3
  39. package/src/tui/components/SchedulePanel.tsx +389 -0
  40. package/src/tui/components/TabBar.tsx +3 -2
  41. 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.query(
94
+ await db.queryRun(
95
95
  `INSERT INTO threads (id, type, task_id, title)
96
96
  VALUES (?1, ?2, ?3, ?4)`,
97
- ).run(id, type, taskId ?? null, title ?? "");
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
- .query(
117
- "SELECT COALESCE(MAX(sequence), 0) + 1 AS next_seq FROM interactions WHERE thread_id = ?1",
118
- )
119
- .get(threadId) as { next_seq: number };
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.query(
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.query("UPDATE threads SET ended_at = datetime('now') WHERE id = ?1").run(
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.query("UPDATE threads SET ended_at = NULL WHERE id = ?1").run(threadId);
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.query("UPDATE threads SET title = ?2 WHERE id = ?1").run(threadId, title);
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
- .query("SELECT * FROM threads WHERE id = ?1")
171
- .get(threadId) as ThreadRow | null;
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
- .query(
176
- "SELECT * FROM interactions WHERE thread_id = ?1 ORDER BY sequence ASC",
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.query("DELETE FROM interactions WHERE thread_id = ?1").run(threadId);
191
- const result = db.query("DELETE FROM threads WHERE id = ?1").run(threadId);
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
- .query(
202
- `SELECT * FROM interactions WHERE thread_id = ?1 AND sequence > ?2 ORDER BY sequence ASC`,
203
- )
204
- .all(threadId, afterSequence) as InteractionRow[];
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
- .query(
213
- `SELECT * FROM threads WHERE ended_at IS NULL ORDER BY started_at DESC LIMIT 1`,
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
- .query(`SELECT ended_at FROM threads WHERE id = ?1`)
225
- .get(threadId) as { ended_at: string | null } | null;
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
- .query(
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
- .all(...params) as ThreadRow[];
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
@@ -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
- .query("UPDATE embeddings SET source_path = ?1 WHERE source_path = ?2")
40
- .run(input.dst, input.src);
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
  },
@@ -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 createContextItem(ctx.conn, {
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, initVectorSearch } from "../../db/embeddings.ts";
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(ctx.conn, input.query, queryVec, input.top_k);
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 =
@@ -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,
@@ -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 % 6) + 1) as TabId);
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 <= 6) {
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-6 Jump to panel (when not in Chat)",
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}