botholomew 0.12.5 → 0.13.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 (103) hide show
  1. package/README.md +91 -68
  2. package/package.json +2 -2
  3. package/src/chat/agent.ts +42 -82
  4. package/src/chat/session.ts +29 -25
  5. package/src/commands/capabilities.ts +1 -1
  6. package/src/commands/context.ts +177 -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 +630 -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 +279 -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 +73 -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 +44 -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 +25 -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 +3 -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/worker/spawn.ts +28 -14
  71. package/src/tui/App.tsx +12 -19
  72. package/src/tui/components/ContextPanel.tsx +83 -316
  73. package/src/tui/components/SchedulePanel.tsx +34 -48
  74. package/src/tui/components/StatusBar.tsx +15 -15
  75. package/src/tui/components/TaskPanel.tsx +34 -38
  76. package/src/tui/components/ThreadPanel.tsx +29 -38
  77. package/src/tui/components/WorkerPanel.tsx +21 -19
  78. package/src/tui/markdown.ts +2 -8
  79. package/src/utils/title.ts +5 -7
  80. package/src/utils/v7-date.ts +47 -0
  81. package/src/worker/heartbeat.ts +46 -24
  82. package/src/worker/index.ts +13 -15
  83. package/src/worker/llm.ts +30 -37
  84. package/src/worker/prompt.ts +19 -41
  85. package/src/worker/schedules.ts +48 -69
  86. package/src/worker/spawn.ts +11 -11
  87. package/src/worker/tick.ts +39 -43
  88. package/src/workers/store.ts +247 -0
  89. package/src/commands/tools.ts +0 -367
  90. package/src/context/describer.ts +0 -140
  91. package/src/context/drives.ts +0 -110
  92. package/src/context/ingest.ts +0 -162
  93. package/src/context/refresh.ts +0 -183
  94. package/src/db/context.ts +0 -637
  95. package/src/db/daemon-state.ts +0 -6
  96. package/src/db/reembed.ts +0 -113
  97. package/src/db/schedules.ts +0 -213
  98. package/src/db/tasks.ts +0 -347
  99. package/src/db/threads.ts +0 -276
  100. package/src/db/workers.ts +0 -212
  101. package/src/tools/context/list-drives.ts +0 -36
  102. package/src/tools/context/refresh.ts +0 -165
  103. package/src/tools/context/search.ts +0 -54
@@ -1,16 +1,15 @@
1
1
  import ansis from "ansis";
2
2
  import type { Command } from "commander";
3
- import type { Task } from "../db/tasks.ts";
3
+ import type { Task } from "../tasks/schema.ts";
4
4
  import {
5
5
  createTask,
6
6
  deleteTask,
7
7
  getTask,
8
8
  listTasks,
9
- resetTask,
10
9
  updateTask,
11
- } from "../db/tasks.ts";
10
+ updateTaskStatus,
11
+ } from "../tasks/store.ts";
12
12
  import { logger } from "../utils/logger.ts";
13
- import { withDb } from "./with-db.ts";
14
13
 
15
14
  export function registerTaskCommand(program: Command) {
16
15
  const task = program.command("task").description("Manage tasks");
@@ -22,61 +21,58 @@ export function registerTaskCommand(program: Command) {
22
21
  .option("-p, --priority <priority>", "filter by priority")
23
22
  .option("-l, --limit <n>", "max number of tasks", Number.parseInt)
24
23
  .option("-o, --offset <n>", "skip first N tasks", Number.parseInt)
25
- .action((opts) =>
26
- withDb(program, async (conn) => {
27
- const tasks = await listTasks(conn, {
28
- status: opts.status,
29
- priority: opts.priority,
30
- limit: opts.limit,
31
- offset: opts.offset,
32
- });
33
-
34
- if (tasks.length === 0) {
35
- logger.dim("No tasks found.");
36
- return;
37
- }
38
-
39
- const header = `${ansis.bold("ID".padEnd(36))} ${ansis.bold("Status".padEnd(11))} ${ansis.bold("Priority".padEnd(6))} ${ansis.bold("Created".padEnd(19))} ${ansis.bold("Updated".padEnd(19))} ${ansis.bold("Name")}`;
40
- console.log(header);
41
- console.log("-".repeat(120));
42
-
43
- for (const t of tasks) {
44
- printTask(t);
45
- }
46
-
47
- console.log(`\n${ansis.dim(`${tasks.length} task(s)`)}`);
48
- }),
49
- );
24
+ .action(async (opts) => {
25
+ const dir = program.opts().dir;
26
+ const tasks = await listTasks(dir, {
27
+ status: opts.status,
28
+ priority: opts.priority,
29
+ limit: opts.limit,
30
+ offset: opts.offset,
31
+ });
32
+
33
+ if (tasks.length === 0) {
34
+ logger.dim("No tasks found.");
35
+ return;
36
+ }
37
+
38
+ const header = `${ansis.bold("ID".padEnd(36))} ${ansis.bold("Status".padEnd(11))} ${ansis.bold("Priority".padEnd(6))} ${ansis.bold("Created".padEnd(19))} ${ansis.bold("Updated".padEnd(19))} ${ansis.bold("Name")}`;
39
+ console.log(header);
40
+ console.log("-".repeat(120));
41
+
42
+ for (const t of tasks) {
43
+ printTask(t);
44
+ }
45
+
46
+ console.log(`\n${ansis.dim(`${tasks.length} task(s)`)}`);
47
+ });
50
48
 
51
49
  task
52
50
  .command("add <name>")
53
51
  .description("Create a new task")
54
52
  .option("--description <text>", "task description", "")
55
53
  .option("-p, --priority <priority>", "low, medium, or high", "medium")
56
- .action((name, opts) =>
57
- withDb(program, async (conn) => {
58
- const t = await createTask(conn, {
59
- name,
60
- description: opts.description,
61
- priority: opts.priority,
62
- });
63
- logger.success(`Created task: ${t.name} (${t.id})`);
64
- }),
65
- );
54
+ .action(async (name, opts) => {
55
+ const dir = program.opts().dir;
56
+ const t = await createTask(dir, {
57
+ name,
58
+ description: opts.description,
59
+ priority: opts.priority,
60
+ });
61
+ logger.success(`Created task: ${t.name} (${t.id})`);
62
+ });
66
63
 
67
64
  task
68
65
  .command("view <id>")
69
66
  .description("View task details")
70
- .action((id) =>
71
- withDb(program, async (conn) => {
72
- const t = await getTask(conn, id);
73
- if (!t) {
74
- logger.error(`Task not found: ${id}`);
75
- process.exit(1);
76
- }
77
- printTaskDetail(t);
78
- }),
79
- );
67
+ .action(async (id) => {
68
+ const dir = program.opts().dir;
69
+ const t = await getTask(dir, id);
70
+ if (!t) {
71
+ logger.error(`Task not found: ${id}`);
72
+ process.exit(1);
73
+ }
74
+ printTaskDetail(t);
75
+ });
80
76
 
81
77
  task
82
78
  .command("update <id>")
@@ -85,55 +81,53 @@ export function registerTaskCommand(program: Command) {
85
81
  .option("--description <text>", "new description")
86
82
  .option("-p, --priority <priority>", "low, medium, or high")
87
83
  .option("-s, --status <status>", "new status")
88
- .action((id, opts) =>
89
- withDb(program, async (conn) => {
90
- const updates: Parameters<typeof updateTask>[2] = {};
91
- if (opts.name) updates.name = opts.name;
92
- if (opts.description) updates.description = opts.description;
93
- if (opts.priority) updates.priority = opts.priority;
94
- if (opts.status) updates.status = opts.status;
95
-
96
- try {
97
- const t = await updateTask(conn, id, updates);
98
- if (!t) {
99
- logger.error(`Task not found: ${id}`);
100
- process.exit(1);
101
- }
102
- printTaskDetail(t);
103
- } catch (err) {
104
- logger.error(String(err));
84
+ .action(async (id, opts) => {
85
+ const dir = program.opts().dir;
86
+ const updates: Parameters<typeof updateTask>[2] = {};
87
+ if (opts.name) updates.name = opts.name;
88
+ if (opts.description) updates.description = opts.description;
89
+ if (opts.priority) updates.priority = opts.priority;
90
+ if (opts.status) updates.status = opts.status;
91
+
92
+ try {
93
+ const t = await updateTask(dir, id, updates);
94
+ if (!t) {
95
+ logger.error(`Task not found: ${id}`);
105
96
  process.exit(1);
106
97
  }
107
- }),
108
- );
98
+ printTaskDetail(t);
99
+ } catch (err) {
100
+ logger.error(String(err));
101
+ process.exit(1);
102
+ }
103
+ });
109
104
 
110
105
  task
111
106
  .command("delete <id>")
112
107
  .description("Delete a task")
113
- .action((id) =>
114
- withDb(program, async (conn) => {
115
- const deleted = await deleteTask(conn, id);
116
- if (!deleted) {
117
- logger.error(`Task not found: ${id}`);
118
- process.exit(1);
119
- }
120
- logger.success(`Deleted task: ${id}`);
121
- }),
122
- );
108
+ .action(async (id) => {
109
+ const dir = program.opts().dir;
110
+ const deleted = await deleteTask(dir, id);
111
+ if (!deleted) {
112
+ logger.error(`Task not found: ${id}`);
113
+ process.exit(1);
114
+ }
115
+ logger.success(`Deleted task: ${id}`);
116
+ });
123
117
 
124
118
  task
125
119
  .command("reset <id>")
126
120
  .description("Reset a stuck task back to pending")
127
- .action((id) =>
128
- withDb(program, async (conn) => {
129
- const t = await resetTask(conn, id);
130
- if (!t) {
131
- logger.error(`Task not found: ${id}`);
132
- process.exit(1);
133
- }
134
- logger.success(`Reset task: ${t.name} (${t.id})`);
135
- }),
136
- );
121
+ .action(async (id) => {
122
+ const dir = program.opts().dir;
123
+ const t = await getTask(dir, id);
124
+ if (!t) {
125
+ logger.error(`Task not found: ${id}`);
126
+ process.exit(1);
127
+ }
128
+ await updateTaskStatus(dir, id, "pending", null, null);
129
+ logger.success(`Reset task: ${t.name} (${t.id})`);
130
+ });
137
131
  }
138
132
 
139
133
  function statusColor(status: Task["status"]): string {
@@ -162,11 +156,8 @@ function priorityColor(priority: Task["priority"]): string {
162
156
  }
163
157
  }
164
158
 
165
- function formatTime(date: Date): string {
166
- return date
167
- .toISOString()
168
- .replace("T", " ")
169
- .replace(/\.\d{3}Z$/, "");
159
+ function formatTime(iso: string): string {
160
+ return iso.replace("T", " ").replace(/\.\d{3}Z$/, "");
170
161
  }
171
162
 
172
163
  function padColored(colored: string, raw: string, width: number): string {
@@ -196,6 +187,6 @@ function printTaskDetail(t: Task) {
196
187
  if (t.claimed_by) console.log(` Claimed by: ${t.claimed_by}`);
197
188
  if (t.blocked_by.length > 0)
198
189
  console.log(` Blocked by: ${t.blocked_by.join(", ")}`);
199
- console.log(` Created: ${t.created_at.toISOString()}`);
200
- console.log(` Updated: ${t.updated_at.toISOString()}`);
190
+ console.log(` Created: ${t.created_at}`);
191
+ console.log(` Updated: ${t.updated_at}`);
201
192
  }
@@ -1,16 +1,16 @@
1
1
  import ansis from "ansis";
2
2
  import type { Command } from "commander";
3
- import type { Interaction, Thread } from "../db/threads.ts";
4
3
  import {
5
4
  deleteThread,
6
5
  getActiveThread,
7
6
  getInteractionsAfter,
8
7
  getThread,
8
+ type Interaction,
9
9
  isThreadEnded,
10
10
  listThreads,
11
- } from "../db/threads.ts";
11
+ type Thread,
12
+ } from "../threads/store.ts";
12
13
  import { logger } from "../utils/logger.ts";
13
- import { withDb } from "./with-db.ts";
14
14
 
15
15
  export function registerThreadCommand(program: Command) {
16
16
  const thread = program.command("thread").description("Manage chat threads");
@@ -21,24 +21,23 @@ export function registerThreadCommand(program: Command) {
21
21
  .option("-t, --type <type>", "filter by type (worker_tick, chat_session)")
22
22
  .option("-l, --limit <n>", "max number of threads", Number.parseInt)
23
23
  .option("-o, --offset <n>", "skip first N threads", Number.parseInt)
24
- .action((opts) =>
25
- withDb(program, async (conn) => {
26
- const threads = await listThreads(conn, {
27
- type: opts.type,
28
- limit: opts.limit,
29
- offset: opts.offset,
30
- });
31
-
32
- if (threads.length === 0) {
33
- logger.dim("No threads found.");
34
- return;
35
- }
36
-
37
- for (const t of threads) {
38
- printThread(t);
39
- }
40
- }),
41
- );
24
+ .action(async (opts) => {
25
+ const dir = program.opts().dir;
26
+ const threads = await listThreads(dir, {
27
+ type: opts.type,
28
+ limit: opts.limit,
29
+ offset: opts.offset,
30
+ });
31
+
32
+ if (threads.length === 0) {
33
+ logger.dim("No threads found.");
34
+ return;
35
+ }
36
+
37
+ for (const t of threads) {
38
+ printThread(t);
39
+ }
40
+ });
42
41
 
43
42
  thread
44
43
  .command("view <id>")
@@ -47,111 +46,107 @@ export function registerThreadCommand(program: Command) {
47
46
  "--only <roles>",
48
47
  "show only these roles (comma-separated: user,assistant,tool,system)",
49
48
  )
50
- .action((id, opts) =>
51
- withDb(program, async (conn) => {
52
- const result = await getThread(conn, id);
53
- if (!result) {
54
- logger.error(`Thread not found: ${id}`);
55
- process.exit(1);
56
- }
57
- const interactions = opts.only
58
- ? result.interactions.filter((i) =>
59
- (opts.only as string).split(",").includes(i.role),
60
- )
61
- : result.interactions;
62
- printThreadDetail(result.thread, interactions);
63
- }),
64
- );
49
+ .action(async (id, opts) => {
50
+ const dir = program.opts().dir;
51
+ const result = await getThread(dir, id);
52
+ if (!result) {
53
+ logger.error(`Thread not found: ${id}`);
54
+ process.exit(1);
55
+ }
56
+ const interactions = opts.only
57
+ ? result.interactions.filter((i) =>
58
+ (opts.only as string).split(",").includes(i.role),
59
+ )
60
+ : result.interactions;
61
+ printThreadDetail(result.thread, interactions);
62
+ });
65
63
 
66
64
  thread
67
65
  .command("delete <id>")
68
66
  .description("Delete a thread and its interactions")
69
- .action((id) =>
70
- withDb(program, async (conn) => {
71
- const deleted = await deleteThread(conn, id);
72
- if (!deleted) {
73
- logger.error(`Thread not found: ${id}`);
74
- process.exit(1);
75
- }
76
- logger.success(`Deleted thread: ${id}`);
77
- }),
78
- );
67
+ .action(async (id) => {
68
+ const dir = program.opts().dir;
69
+ const deleted = await deleteThread(dir, id);
70
+ if (!deleted) {
71
+ logger.error(`Thread not found: ${id}`);
72
+ process.exit(1);
73
+ }
74
+ logger.success(`Deleted thread: ${id}`);
75
+ });
79
76
 
80
77
  thread
81
78
  .command("follow [id]")
82
79
  .description("Follow a thread live (like tail -f)")
83
80
  .option("-i, --interval <ms>", "poll interval in ms", parseInt)
84
- .action((id, opts) =>
85
- withDb(program, async (conn) => {
86
- let resolvedId: string;
87
- if (id) {
88
- resolvedId = id;
89
- } else {
90
- const active = await getActiveThread(conn);
91
- if (!active) {
92
- logger.error("No active thread found.");
93
- process.exit(1);
94
- }
95
- resolvedId = active.id;
96
- }
97
-
98
- const result = await getThread(conn, resolvedId);
99
- if (!result) {
100
- logger.error(`Thread not found: ${resolvedId}`);
81
+ .action(async (id, opts) => {
82
+ const dir = program.opts().dir;
83
+ let resolvedId: string;
84
+ if (id) {
85
+ resolvedId = id;
86
+ } else {
87
+ const active = await getActiveThread(dir);
88
+ if (!active) {
89
+ logger.error("No active thread found.");
101
90
  process.exit(1);
102
91
  }
92
+ resolvedId = active.id;
93
+ }
94
+
95
+ const result = await getThread(dir, resolvedId);
96
+ if (!result) {
97
+ logger.error(`Thread not found: ${resolvedId}`);
98
+ process.exit(1);
99
+ }
100
+
101
+ printThreadDetail(result.thread, result.interactions);
102
+
103
+ if (result.thread.ended_at) {
104
+ logger.dim("Thread already ended.");
105
+ return;
106
+ }
107
+
108
+ let lastSequence =
109
+ result.interactions.length > 0
110
+ ? (result.interactions[result.interactions.length - 1]?.sequence ?? 0)
111
+ : 0;
112
+
113
+ const pollMs = opts.interval ?? 500;
114
+ logger.info(
115
+ `Following thread ${ansis.dim(resolvedId)}... (Ctrl+C to stop)`,
116
+ );
117
+
118
+ const interval = setInterval(async () => {
119
+ try {
120
+ const newInteractions = await getInteractionsAfter(
121
+ dir,
122
+ resolvedId,
123
+ lastSequence,
124
+ );
125
+ for (const i of newInteractions) {
126
+ printInteraction(i);
127
+ lastSequence = i.sequence;
128
+ }
103
129
 
104
- printThreadDetail(result.thread, result.interactions);
105
-
106
- if (result.thread.ended_at) {
107
- logger.dim("Thread already ended.");
108
- return;
130
+ const ended = await isThreadEnded(dir, resolvedId);
131
+ if (ended) {
132
+ logger.dim("Thread ended.");
133
+ clearInterval(interval);
134
+ process.exit(0);
135
+ }
136
+ } catch {
137
+ // Transient FS errors — retry next tick
109
138
  }
139
+ }, pollMs);
110
140
 
111
- let lastSequence =
112
- result.interactions.length > 0
113
- ? (result.interactions[result.interactions.length - 1]?.sequence ??
114
- 0)
115
- : 0;
116
-
117
- const pollMs = opts.interval ?? 500;
118
- logger.info(
119
- `Following thread ${ansis.dim(resolvedId)}... (Ctrl+C to stop)`,
120
- );
121
-
122
- const interval = setInterval(async () => {
123
- try {
124
- const newInteractions = await getInteractionsAfter(
125
- conn,
126
- resolvedId,
127
- lastSequence,
128
- );
129
- for (const i of newInteractions) {
130
- printInteraction(i);
131
- lastSequence = i.sequence;
132
- }
133
-
134
- const ended = await isThreadEnded(conn, resolvedId);
135
- if (ended) {
136
- logger.dim("Thread ended.");
137
- clearInterval(interval);
138
- process.exit(0);
139
- }
140
- } catch {
141
- // Transient DB errors (e.g. SQLITE_BUSY) — retry next tick
142
- }
143
- }, pollMs);
144
-
145
- process.on("SIGINT", () => {
146
- clearInterval(interval);
147
- console.log();
148
- process.exit(0);
149
- });
150
-
151
- // Keep the process alive
152
- await new Promise(() => {});
153
- }),
154
- );
141
+ process.on("SIGINT", () => {
142
+ clearInterval(interval);
143
+ console.log();
144
+ process.exit(0);
145
+ });
146
+
147
+ // Keep the process alive
148
+ await new Promise(() => {});
149
+ });
155
150
  }
156
151
 
157
152
  function typeColor(type: Thread["type"]): string {