botholomew 0.12.5 → 0.14.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 (107) hide show
  1. package/README.md +91 -68
  2. package/package.json +2 -2
  3. package/src/chat/agent.ts +59 -86
  4. package/src/chat/session.ts +29 -25
  5. package/src/commands/capabilities.ts +1 -1
  6. package/src/commands/context.ts +178 -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 +803 -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 +293 -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 +74 -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 +53 -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 +27 -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 +8 -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/tool.ts +5 -0
  71. package/src/tools/util/sleep.ts +77 -0
  72. package/src/tools/worker/spawn.ts +28 -14
  73. package/src/tui/App.tsx +12 -19
  74. package/src/tui/components/ContextPanel.tsx +83 -316
  75. package/src/tui/components/SchedulePanel.tsx +34 -48
  76. package/src/tui/components/SleepProgress.tsx +70 -0
  77. package/src/tui/components/StatusBar.tsx +15 -15
  78. package/src/tui/components/TaskPanel.tsx +34 -38
  79. package/src/tui/components/ThreadPanel.tsx +29 -38
  80. package/src/tui/components/ToolCall.tsx +10 -0
  81. package/src/tui/components/WorkerPanel.tsx +21 -19
  82. package/src/tui/markdown.ts +2 -8
  83. package/src/utils/title.ts +5 -7
  84. package/src/utils/v7-date.ts +47 -0
  85. package/src/worker/heartbeat.ts +46 -24
  86. package/src/worker/index.ts +13 -15
  87. package/src/worker/llm.ts +30 -37
  88. package/src/worker/prompt.ts +19 -41
  89. package/src/worker/schedules.ts +48 -69
  90. package/src/worker/spawn.ts +11 -11
  91. package/src/worker/tick.ts +39 -43
  92. package/src/workers/store.ts +247 -0
  93. package/src/commands/tools.ts +0 -367
  94. package/src/context/describer.ts +0 -140
  95. package/src/context/drives.ts +0 -110
  96. package/src/context/ingest.ts +0 -162
  97. package/src/context/refresh.ts +0 -183
  98. package/src/db/context.ts +0 -637
  99. package/src/db/daemon-state.ts +0 -6
  100. package/src/db/reembed.ts +0 -113
  101. package/src/db/schedules.ts +0 -213
  102. package/src/db/tasks.ts +0 -347
  103. package/src/db/threads.ts +0 -276
  104. package/src/db/workers.ts +0 -212
  105. package/src/tools/context/list-drives.ts +0 -36
  106. package/src/tools/context/refresh.ts +0 -165
  107. package/src/tools/context/search.ts +0 -54
@@ -1,28 +1,11 @@
1
- import { isText } from "istextorbinary";
2
1
  import { z } from "zod";
3
- import { formatDriveRef } from "../../context/drives.ts";
4
- import { ingestByPath } from "../../context/ingest.ts";
5
- import {
6
- createContextItemStrict,
7
- PathConflictError,
8
- upsertContextItem,
9
- } from "../../db/context.ts";
2
+ import { PathConflictError, writeContextFile } from "../../context/store.ts";
10
3
  import { getTool, type ToolDefinition } from "../tool.ts";
11
4
 
12
5
  const PREVIEW_CHARS = 200;
13
6
  const ERROR_MESSAGE_CAP = 2000;
14
7
  const TOOL_NAME = "pipe_to_context";
15
8
 
16
- function mimeFromPath(path: string): string {
17
- const type = Bun.file(path).type.split(";")[0];
18
- return type ?? "application/octet-stream";
19
- }
20
-
21
- function isTextualPath(path: string): boolean {
22
- const filename = path.split("/").pop() ?? path;
23
- return isText(filename) !== false;
24
- }
25
-
26
9
  function truncate(s: string, cap: number): string {
27
10
  if (s.length <= cap) return s;
28
11
  return `${s.slice(0, cap)}…[truncated, ${s.length - cap} more chars]`;
@@ -32,39 +15,29 @@ const inputSchema = z.object({
32
15
  tool_name: z
33
16
  .string()
34
17
  .describe(
35
- "Name of the tool to dispatch. Its full output is piped into a context item; you (the LLM) will only see the storage acknowledgment, never the raw bytes.",
18
+ "Name of the tool to dispatch. Its full output is piped into a file under context/; you (the LLM) will only see the storage acknowledgment, never the raw bytes.",
36
19
  ),
37
20
  tool_input: z
38
21
  .record(z.string(), z.unknown())
39
22
  .describe(
40
23
  "Arguments to pass to the inner tool (same shape as a normal call).",
41
24
  ),
42
- drive: z
25
+ path: z
43
26
  .string()
44
- .default("agent")
45
27
  .describe(
46
- "Drive to write to (defaults to 'agent', the agent's scratch drive).",
28
+ "Project-relative path under context/ to write the captured output to (e.g. 'gdoc/quarterly-plan.md').",
47
29
  ),
48
- path: z.string().describe("Path within the drive (starts with /)"),
49
- title: z
50
- .string()
51
- .optional()
52
- .describe("Title for the file (defaults to filename)"),
53
- description: z.string().optional().describe("Description of the file"),
54
30
  on_conflict: z
55
31
  .enum(["error", "overwrite"])
56
32
  .optional()
57
33
  .describe(
58
- "What to do if a file already exists at this (drive, path). Defaults to 'error'. Pass 'overwrite' to replace.",
34
+ "What to do if a file already exists at this path. Defaults to 'error'. Pass 'overwrite' to replace.",
59
35
  ),
60
36
  });
61
37
 
62
38
  const outputSchema = z.object({
63
39
  is_error: z.boolean(),
64
- id: z.string().optional(),
65
- drive: z.string().optional(),
66
40
  path: z.string().optional(),
67
- ref: z.string().optional(),
68
41
  bytes_written: z.number().optional(),
69
42
  preview: z
70
43
  .string()
@@ -89,7 +62,7 @@ const outputSchema = z.object({
89
62
  export const pipeToContextTool = {
90
63
  name: TOOL_NAME,
91
64
  description:
92
- "[[ bash equivalent command: cmd > file ]] Run another tool and pipe its full output directly into a context item, without the result flowing through the conversation. Use this when you need a large tool output (web pages, search dumps, big mcp_exec results) to be searchable/embedded for later but you do NOT need to read the bytes yourself. You'll only see the storage ack (drive, path, id, size, short preview).",
65
+ "[[ bash equivalent command: cmd > file ]] Run another tool and pipe its full output directly into a file under context/, without the result flowing through the conversation. Use this when you need a large tool output (Google Docs via mcp_exec, web fetches, search dumps) saved for later inspection but you do NOT need to read the bytes yourself. You'll only see the storage ack (path, size, short preview).",
93
66
  group: "context",
94
67
  inputSchema,
95
68
  outputSchema,
@@ -111,7 +84,7 @@ export const pipeToContextTool = {
111
84
  error_type: "forbidden_tool",
112
85
  message: `Tool "${inner.name}" cannot be piped (terminal tools and pipe_to_context itself are not allowed).`,
113
86
  next_action_hint:
114
- "Pipe a non-terminal tool (search_grep, mcp_exec, context_refresh, etc.) instead.",
87
+ "Pipe a non-terminal tool (search, mcp_exec, etc.) instead.",
115
88
  };
116
89
  }
117
90
 
@@ -169,43 +142,16 @@ export const pipeToContextTool = {
169
142
  };
170
143
  }
171
144
 
172
- const mimeType = mimeFromPath(input.path);
173
- const isTextual = isTextualPath(input.path);
174
- const title =
175
- input.title ?? input.path.split("/").filter(Boolean).pop() ?? input.path;
176
- const onConflict = input.on_conflict ?? "error";
177
- const target = { drive: input.drive, path: input.path };
178
-
179
145
  try {
180
- const item =
181
- onConflict === "overwrite"
182
- ? await upsertContextItem(ctx.conn, {
183
- title,
184
- description: input.description,
185
- content: innerOutput,
186
- drive: target.drive,
187
- path: target.path,
188
- mimeType,
189
- isTextual,
190
- })
191
- : await createContextItemStrict(ctx.conn, {
192
- title,
193
- description: input.description,
194
- content: innerOutput,
195
- drive: target.drive,
196
- path: target.path,
197
- mimeType,
198
- isTextual,
199
- });
200
-
201
- await ingestByPath(ctx.conn, target, ctx.config);
202
-
146
+ const entry = await writeContextFile(
147
+ ctx.projectDir,
148
+ input.path,
149
+ innerOutput,
150
+ { onConflict: input.on_conflict ?? "error" },
151
+ );
203
152
  return {
204
153
  is_error: false,
205
- id: item.id,
206
- drive: item.drive,
207
- path: item.path,
208
- ref: formatDriveRef(item),
154
+ path: entry.path,
209
155
  bytes_written: innerOutput.length,
210
156
  preview: innerOutput.slice(0, PREVIEW_CHARS),
211
157
  };
@@ -214,10 +160,8 @@ export const pipeToContextTool = {
214
160
  return {
215
161
  is_error: true,
216
162
  error_type: "path_conflict",
217
- drive: err.drive,
218
163
  path: err.path,
219
- ref: formatDriveRef({ drive: err.drive, path: err.path }),
220
- message: `A file already exists at ${formatDriveRef({ drive: err.drive, path: err.path })} (id: ${err.existingId}). The inner tool ran but its output was discarded.`,
164
+ message: `A file already exists at context/${err.path}. The inner tool ran but its output was discarded.`,
221
165
  next_action_hint:
222
166
  "Retry with on_conflict='overwrite' to replace, or pick a different path.",
223
167
  };
@@ -1,6 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { z } from "zod";
3
- import { getBotholomewDir } from "../../constants.ts";
3
+ import { getPromptsDir } from "../../constants.ts";
4
4
  import {
5
5
  type ContextFileMeta,
6
6
  parseContextFile,
@@ -25,12 +25,12 @@ const outputSchema = z.object({
25
25
  export const updateBeliefsTool = {
26
26
  name: "update_beliefs",
27
27
  description:
28
- "Update the agent's beliefs file (.botholomew/beliefs.md). Preserves frontmatter, replaces content body.",
28
+ "Update the agent's beliefs file (prompts/beliefs.md). Preserves frontmatter, replaces content body.",
29
29
  group: "context",
30
30
  inputSchema,
31
31
  outputSchema,
32
32
  execute: async (input, ctx) => {
33
- const filePath = join(getBotholomewDir(ctx.projectDir), "beliefs.md");
33
+ const filePath = join(getPromptsDir(ctx.projectDir), "beliefs.md");
34
34
  const file = Bun.file(filePath);
35
35
 
36
36
  let meta: ContextFileMeta = {
@@ -1,6 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { z } from "zod";
3
- import { getBotholomewDir } from "../../constants.ts";
3
+ import { getPromptsDir } from "../../constants.ts";
4
4
  import {
5
5
  type ContextFileMeta,
6
6
  parseContextFile,
@@ -25,12 +25,12 @@ const outputSchema = z.object({
25
25
  export const updateGoalsTool = {
26
26
  name: "update_goals",
27
27
  description:
28
- "Update the agent's goals file (.botholomew/goals.md). Preserves frontmatter, replaces content body.",
28
+ "Update the agent's goals file (prompts/goals.md). Preserves frontmatter, replaces content body.",
29
29
  group: "context",
30
30
  inputSchema,
31
31
  outputSchema,
32
32
  execute: async (input, ctx) => {
33
- const filePath = join(getBotholomewDir(ctx.projectDir), "goals.md");
33
+ const filePath = join(getPromptsDir(ctx.projectDir), "goals.md");
34
34
  const file = Bun.file(filePath);
35
35
 
36
36
  let meta: ContextFileMeta = {
@@ -1,44 +1,47 @@
1
1
  import { z } from "zod";
2
- import { formatDriveRef } from "../../context/drives.ts";
3
- import { contextPathExists, createContextItem } from "../../db/context.ts";
2
+ import { createContextDir, fileExists } from "../../context/store.ts";
4
3
  import type { ToolDefinition } from "../tool.ts";
5
4
 
6
5
  const inputSchema = z.object({
7
- drive: z
8
- .string()
9
- .default("agent")
10
- .describe("Drive to create the directory in (defaults to 'agent')"),
11
- path: z.string().describe("Directory path to create (starts with /)"),
6
+ path: z.string().describe("Directory path to create under context/"),
12
7
  });
13
8
 
14
9
  const outputSchema = z.object({
10
+ path: z.string(),
15
11
  created: z.boolean(),
16
- ref: z.string(),
17
12
  is_error: z.boolean(),
13
+ error_type: z.string().optional(),
14
+ message: z.string().optional(),
15
+ next_action_hint: z.string().optional(),
18
16
  });
19
17
 
20
18
  export const contextCreateDirTool = {
21
19
  name: "context_create_dir",
22
20
  description:
23
- "[[ bash equivalent command: mkdir -p ]] Create a directory placeholder in context.",
21
+ "[[ bash equivalent command: mkdir -p ]] Create a directory (recursively) under context/. Paths that traverse a user symlink fail with PathEscapeError.",
24
22
  group: "context",
25
23
  inputSchema,
26
24
  outputSchema,
27
25
  execute: async (input, ctx) => {
28
- const target = { drive: input.drive, path: input.path };
29
- const exists = await contextPathExists(ctx.conn, target);
30
- if (exists) {
31
- return { created: false, ref: formatDriveRef(target), is_error: false };
26
+ try {
27
+ const existed = await fileExists(ctx.projectDir, input.path);
28
+ await createContextDir(ctx.projectDir, input.path);
29
+ return { path: input.path, created: !existed, is_error: false };
30
+ } catch (err) {
31
+ // mkdir surfaces ENOTDIR when a path component is a file, EACCES on
32
+ // permission issues, etc. Convert to a structured error so the agent
33
+ // can pick a different parent or read what's actually there first.
34
+ const code = (err as NodeJS.ErrnoException).code;
35
+ const message = err instanceof Error ? err.message : String(err);
36
+ return {
37
+ path: input.path,
38
+ created: false,
39
+ is_error: true,
40
+ error_type: code === "ENOTDIR" ? "not_a_directory" : "create_failed",
41
+ message,
42
+ next_action_hint:
43
+ "Run context_tree on the parent path to see what's there before retrying.",
44
+ };
32
45
  }
33
-
34
- await createContextItem(ctx.conn, {
35
- title: input.path.split("/").filter(Boolean).pop() ?? input.path,
36
- drive: target.drive,
37
- path: target.path,
38
- mimeType: "inode/directory",
39
- isTextual: false,
40
- });
41
-
42
- return { created: true, ref: formatDriveRef(target), is_error: false };
43
46
  },
44
47
  } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -1,5 +1,9 @@
1
1
  import { z } from "zod";
2
- import { listContextItemsByPrefix } from "../../db/context.ts";
2
+ import {
3
+ dirSizeBytes,
4
+ NotDirectoryError,
5
+ NotFoundError,
6
+ } from "../../context/store.ts";
3
7
  import type { ToolDefinition } from "../tool.ts";
4
8
 
5
9
  function formatBytes(bytes: number): string {
@@ -11,38 +15,63 @@ function formatBytes(bytes: number): string {
11
15
  }
12
16
 
13
17
  const inputSchema = z.object({
14
- drive: z.string().describe("Drive name"),
15
- path: z.string().optional().describe("Directory path (defaults to /)"),
16
- recursive: z
17
- .boolean()
18
+ path: z
19
+ .string()
18
20
  .optional()
19
- .describe("Include subdirectories (defaults to true)"),
21
+ .default("")
22
+ .describe("Directory path under context/ (defaults to context root)"),
20
23
  });
21
24
 
22
25
  const outputSchema = z.object({
26
+ files: z.number(),
23
27
  bytes: z.number(),
24
28
  formatted: z.string(),
25
29
  is_error: z.boolean(),
30
+ error_type: z.string().optional(),
31
+ message: z.string().optional(),
26
32
  });
27
33
 
28
34
  export const contextDirSizeTool = {
29
35
  name: "context_dir_size",
30
36
  description:
31
- "[[ bash equivalent command: du -s ]] Get the total size of context items under a drive/directory.",
37
+ "[[ bash equivalent command: du -s ]] Get the total size of files under a directory in context/.",
32
38
  group: "context",
33
39
  inputSchema,
34
40
  outputSchema,
35
41
  execute: async (input, ctx) => {
36
- const path = input.path ?? "/";
37
- const items = await listContextItemsByPrefix(ctx.conn, input.drive, path, {
38
- recursive: input.recursive !== false,
39
- });
40
-
41
- let bytes = 0;
42
- for (const item of items) {
43
- if (item.content != null) bytes += item.content.length;
42
+ try {
43
+ const { files, bytes } = await dirSizeBytes(
44
+ ctx.projectDir,
45
+ input.path ?? "",
46
+ );
47
+ return {
48
+ files,
49
+ bytes,
50
+ formatted: formatBytes(bytes),
51
+ is_error: false,
52
+ };
53
+ } catch (err) {
54
+ if (err instanceof NotFoundError) {
55
+ return {
56
+ files: 0,
57
+ bytes: 0,
58
+ formatted: formatBytes(0),
59
+ is_error: true,
60
+ error_type: "not_found",
61
+ message: `No directory at context/${err.path}`,
62
+ };
63
+ }
64
+ if (err instanceof NotDirectoryError) {
65
+ return {
66
+ files: 0,
67
+ bytes: 0,
68
+ formatted: formatBytes(0),
69
+ is_error: true,
70
+ error_type: "not_a_directory",
71
+ message: `context/${err.path} is not a directory`,
72
+ };
73
+ }
74
+ throw err;
44
75
  }
45
-
46
- return { bytes, formatted: formatBytes(bytes), is_error: false };
47
76
  },
48
77
  } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;