botholomew 0.16.4 → 0.18.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 (98) hide show
  1. package/README.md +46 -41
  2. package/package.json +4 -9
  3. package/src/chat/agent.ts +37 -40
  4. package/src/chat/session.ts +10 -10
  5. package/src/cli.ts +0 -2
  6. package/src/commands/capabilities.ts +35 -33
  7. package/src/commands/context.ts +133 -221
  8. package/src/commands/init.ts +22 -1
  9. package/src/commands/mcpx.ts +21 -8
  10. package/src/commands/nuke.ts +52 -15
  11. package/src/commands/prepare.ts +16 -13
  12. package/src/config/loader.ts +1 -8
  13. package/src/config/schemas.ts +6 -0
  14. package/src/constants.ts +16 -32
  15. package/src/init/index.ts +52 -27
  16. package/src/mcpx/client.ts +21 -5
  17. package/src/mem/client.ts +33 -0
  18. package/src/{context → prompts}/capabilities.ts +11 -7
  19. package/src/schedules/store.ts +1 -1
  20. package/src/tasks/store.ts +1 -1
  21. package/src/threads/store.ts +1 -1
  22. package/src/tools/capabilities/refresh.ts +1 -1
  23. package/src/tools/membot/adapter.ts +111 -0
  24. package/src/tools/membot/copy.ts +59 -0
  25. package/src/tools/membot/count_lines.ts +53 -0
  26. package/src/tools/membot/edit.ts +72 -0
  27. package/src/tools/membot/exists.ts +54 -0
  28. package/src/tools/membot/index.ts +26 -0
  29. package/src/tools/{context → membot}/pipe.ts +34 -32
  30. package/src/tools/registry.ts +6 -37
  31. package/src/tools/tool.ts +6 -8
  32. package/src/tui/App.tsx +3 -4
  33. package/src/tui/components/ContextPanel.tsx +109 -226
  34. package/src/tui/components/HelpPanel.tsx +2 -2
  35. package/src/tui/components/StatusBar.tsx +0 -6
  36. package/src/tui/components/ThreadPanel.tsx +8 -7
  37. package/src/tui/wrapDetail.ts +11 -0
  38. package/src/worker/heartbeat.ts +0 -20
  39. package/src/worker/index.ts +13 -13
  40. package/src/worker/llm.ts +7 -9
  41. package/src/worker/prompt.ts +25 -13
  42. package/src/worker/spawn.ts +1 -1
  43. package/src/worker/tick.ts +10 -9
  44. package/src/commands/db.ts +0 -119
  45. package/src/commands/with-db.ts +0 -22
  46. package/src/context/chunker.ts +0 -275
  47. package/src/context/embedder-impl.ts +0 -100
  48. package/src/context/embedder.ts +0 -9
  49. package/src/context/fetcher-errors.ts +0 -8
  50. package/src/context/fetcher.ts +0 -515
  51. package/src/context/locks.ts +0 -146
  52. package/src/context/markdown-converter.ts +0 -186
  53. package/src/context/reindex.ts +0 -198
  54. package/src/context/store.ts +0 -841
  55. package/src/context/url-utils.ts +0 -25
  56. package/src/db/connection.ts +0 -255
  57. package/src/db/doctor.ts +0 -235
  58. package/src/db/embeddings.ts +0 -317
  59. package/src/db/query.ts +0 -56
  60. package/src/db/schema.ts +0 -93
  61. package/src/db/sql/1-core_tables.sql +0 -53
  62. package/src/db/sql/10-dedupe_context_items.sql +0 -26
  63. package/src/db/sql/11-rebuild_hnsw.sql +0 -8
  64. package/src/db/sql/12-workers.sql +0 -66
  65. package/src/db/sql/13-drive-paths.sql +0 -47
  66. package/src/db/sql/14-drop_hnsw_index.sql +0 -8
  67. package/src/db/sql/15-fts_index.sql +0 -8
  68. package/src/db/sql/16-source_url.sql +0 -7
  69. package/src/db/sql/17-worker_log_path.sql +0 -3
  70. package/src/db/sql/18-reset_embeddings_for_local.sql +0 -39
  71. package/src/db/sql/19-disk_backed_index.sql +0 -36
  72. package/src/db/sql/2-logging_tables.sql +0 -24
  73. package/src/db/sql/20-drop_db_tables_for_files.sql +0 -19
  74. package/src/db/sql/3-daemon_state.sql +0 -5
  75. package/src/db/sql/4-unique_context_path.sql +0 -1
  76. package/src/db/sql/5-reset_embeddings_for_openai.sql +0 -1
  77. package/src/db/sql/6-vss_index.sql +0 -7
  78. package/src/db/sql/7-drop_embeddings_fk.sql +0 -23
  79. package/src/db/sql/8-task_output.sql +0 -1
  80. package/src/db/sql/9-source-type.sql +0 -1
  81. package/src/tools/context/read-large-result.ts +0 -33
  82. package/src/tools/dir/create.ts +0 -47
  83. package/src/tools/dir/size.ts +0 -77
  84. package/src/tools/dir/tree.ts +0 -124
  85. package/src/tools/file/copy.ts +0 -73
  86. package/src/tools/file/count-lines.ts +0 -54
  87. package/src/tools/file/delete.ts +0 -83
  88. package/src/tools/file/edit.ts +0 -76
  89. package/src/tools/file/exists.ts +0 -33
  90. package/src/tools/file/info.ts +0 -66
  91. package/src/tools/file/move.ts +0 -66
  92. package/src/tools/file/read.ts +0 -67
  93. package/src/tools/file/write.ts +0 -58
  94. package/src/tools/search/fuse.ts +0 -96
  95. package/src/tools/search/index.ts +0 -127
  96. package/src/tools/search/regexp.ts +0 -82
  97. package/src/tools/search/semantic.ts +0 -167
  98. /package/src/{db → utils}/uuid.ts +0 -0
@@ -0,0 +1,72 @@
1
+ import { isHelpfulError } from "membot";
2
+ import { z } from "zod";
3
+ import { applyLinePatches, LinePatchSchema } from "../../fs/patches.ts";
4
+ import type { ToolDefinition } from "../tool.ts";
5
+
6
+ const inputSchema = z.object({
7
+ logical_path: z
8
+ .string()
9
+ .describe("Logical path of the file to edit (e.g. 'notes/foo.md')."),
10
+ patches: z
11
+ .array(LinePatchSchema)
12
+ .min(1)
13
+ .describe(
14
+ "Git-hunk-style edits applied bottom-up. `end_line: 0` inserts; empty `content` deletes.",
15
+ ),
16
+ change_note: z
17
+ .string()
18
+ .optional()
19
+ .describe("Free-text note attached to the new version."),
20
+ });
21
+
22
+ const outputSchema = z.object({
23
+ is_error: z.boolean(),
24
+ logical_path: z.string().optional(),
25
+ version_id: z.string().optional(),
26
+ size_bytes: z.number().optional(),
27
+ error_type: z.string().optional(),
28
+ message: z.string().optional(),
29
+ next_action_hint: z.string().optional(),
30
+ });
31
+
32
+ export const membotEditTool = {
33
+ name: "membot_edit",
34
+ description:
35
+ "[[ bash equivalent command: patch ]] Apply line-range edits to a stored file: reads the current version, applies bottom-up patches, and writes the result back as a new version. Prefer this over membot_write when you only need to change part of a file — the diff is small and the change_note travels with the new version. To replace the whole body, use membot_write. To delete the file, use membot_delete.",
36
+ group: "membot",
37
+ inputSchema,
38
+ outputSchema,
39
+ execute: async (input, ctx) => {
40
+ try {
41
+ const current = await ctx.mem.read({ logical_path: input.logical_path });
42
+ const next = applyLinePatches(current.content ?? "", input.patches);
43
+ const result = await ctx.mem.write({
44
+ logical_path: input.logical_path,
45
+ content: next,
46
+ change_note: input.change_note,
47
+ });
48
+ return {
49
+ is_error: false,
50
+ logical_path: result.logical_path,
51
+ version_id: result.version_id,
52
+ size_bytes: result.size_bytes,
53
+ };
54
+ } catch (err) {
55
+ if (isHelpfulError(err)) {
56
+ return {
57
+ is_error: true,
58
+ error_type: err.kind,
59
+ message: err.message,
60
+ next_action_hint: err.hint,
61
+ };
62
+ }
63
+ return {
64
+ is_error: true,
65
+ error_type: "internal_error",
66
+ message: err instanceof Error ? err.message : String(err),
67
+ next_action_hint:
68
+ "Re-read the file with membot_read to confirm current line numbers, then retry.",
69
+ };
70
+ }
71
+ },
72
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -0,0 +1,54 @@
1
+ import { isHelpfulError } from "membot";
2
+ import { z } from "zod";
3
+ import type { ToolDefinition } from "../tool.ts";
4
+
5
+ const inputSchema = z.object({
6
+ logical_path: z.string().describe("Logical path to check."),
7
+ });
8
+
9
+ const outputSchema = z.object({
10
+ is_error: z.boolean(),
11
+ exists: z.boolean().optional(),
12
+ logical_path: z.string().optional(),
13
+ error_type: z.string().optional(),
14
+ message: z.string().optional(),
15
+ });
16
+
17
+ export const membotExistsTool = {
18
+ name: "membot_exists",
19
+ description:
20
+ "[[ bash equivalent command: test -e ]] Check whether a logical_path has a current (non-tombstoned) version in the store. Returns `{ exists: true|false }` — never throws on absence. Use before membot_write when you want to avoid clobbering, or to disambiguate a not_found from a real error.",
21
+ group: "membot",
22
+ inputSchema,
23
+ outputSchema,
24
+ execute: async (input, ctx) => {
25
+ try {
26
+ await ctx.mem.info({ logical_path: input.logical_path });
27
+ return {
28
+ is_error: false,
29
+ exists: true,
30
+ logical_path: input.logical_path,
31
+ };
32
+ } catch (err) {
33
+ if (isHelpfulError(err) && err.kind === "not_found") {
34
+ return {
35
+ is_error: false,
36
+ exists: false,
37
+ logical_path: input.logical_path,
38
+ };
39
+ }
40
+ if (isHelpfulError(err)) {
41
+ return {
42
+ is_error: true,
43
+ error_type: err.kind,
44
+ message: err.message,
45
+ };
46
+ }
47
+ return {
48
+ is_error: true,
49
+ error_type: "internal_error",
50
+ message: err instanceof Error ? err.message : String(err),
51
+ };
52
+ }
53
+ },
54
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -0,0 +1,26 @@
1
+ import { OPERATIONS } from "membot";
2
+ import { type AnyToolDefinition, registerTool } from "../tool.ts";
3
+ import { adaptOperation } from "./adapter.ts";
4
+ import { membotCopyTool } from "./copy.ts";
5
+ import { membotCountLinesTool } from "./count_lines.ts";
6
+ import { membotEditTool } from "./edit.ts";
7
+ import { membotExistsTool } from "./exists.ts";
8
+ import { membotPipeTool } from "./pipe.ts";
9
+
10
+ /**
11
+ * Register every membot operation as a Botholomew tool. The 14 verbs that
12
+ * have a direct membot Operation (add, list, tree, read, search, info,
13
+ * stats, versions, diff, write, move, delete, refresh, prune) are wired via
14
+ * `adaptOperation`; the five Botholomew-side wrappers (edit, copy, exists,
15
+ * count_lines, pipe) bolt on the file-shaped UX our agents already know.
16
+ */
17
+ export function registerMembotTools(): void {
18
+ for (const op of OPERATIONS) {
19
+ registerTool(adaptOperation(op) as unknown as AnyToolDefinition);
20
+ }
21
+ registerTool(membotEditTool);
22
+ registerTool(membotCopyTool);
23
+ registerTool(membotExistsTool);
24
+ registerTool(membotCountLinesTool);
25
+ registerTool(membotPipeTool);
26
+ }
@@ -1,10 +1,10 @@
1
+ import { isHelpfulError } from "membot";
1
2
  import { z } from "zod";
2
- import { PathConflictError, writeContextFile } from "../../context/store.ts";
3
3
  import { getTool, type ToolDefinition } from "../tool.ts";
4
4
 
5
5
  const PREVIEW_CHARS = 200;
6
6
  const ERROR_MESSAGE_CAP = 2000;
7
- const TOOL_NAME = "pipe_to_context";
7
+ const TOOL_NAME = "membot_pipe";
8
8
 
9
9
  function truncate(s: string, cap: number): string {
10
10
  if (s.length <= cap) return s;
@@ -15,29 +15,28 @@ const inputSchema = z.object({
15
15
  tool_name: z
16
16
  .string()
17
17
  .describe(
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.",
18
+ "Name of the tool to dispatch. Its full output is captured and written to membot under `logical_path`; you (the LLM) only see the storage acknowledgment, never the raw bytes.",
19
19
  ),
20
20
  tool_input: z
21
21
  .record(z.string(), z.unknown())
22
22
  .describe(
23
23
  "Arguments to pass to the inner tool (same shape as a normal call).",
24
24
  ),
25
- path: z
25
+ logical_path: z
26
26
  .string()
27
27
  .describe(
28
- "Project-relative path under context/ to write the captured output to (e.g. 'gdoc/quarterly-plan.md').",
28
+ "Destination logical_path under which to store the captured output (e.g. 'gdoc/quarterly-plan.md'). Creates a new version on every call.",
29
29
  ),
30
- on_conflict: z
31
- .enum(["error", "overwrite"])
30
+ change_note: z
31
+ .string()
32
32
  .optional()
33
- .describe(
34
- "What to do if a file already exists at this path. Defaults to 'error'. Pass 'overwrite' to replace.",
35
- ),
33
+ .describe("Free-text note attached to the new version."),
36
34
  });
37
35
 
38
36
  const outputSchema = z.object({
39
37
  is_error: z.boolean(),
40
- path: z.string().optional(),
38
+ logical_path: z.string().optional(),
39
+ version_id: z.string().optional(),
41
40
  bytes_written: z.number().optional(),
42
41
  preview: z
43
42
  .string()
@@ -52,18 +51,19 @@ const outputSchema = z.object({
52
51
  "forbidden_tool",
53
52
  "invalid_input",
54
53
  "inner_tool_error",
55
- "path_conflict",
54
+ "write_failed",
55
+ "internal_error",
56
56
  ])
57
57
  .optional(),
58
58
  message: z.string().optional(),
59
59
  next_action_hint: z.string().optional(),
60
60
  });
61
61
 
62
- export const pipeToContextTool = {
62
+ export const membotPipeTool = {
63
63
  name: TOOL_NAME,
64
64
  description:
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).",
66
- group: "context",
65
+ "[[ bash equivalent command: cmd > file ]] Run another tool and pipe its full output directly into a membot logical_path, 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) captured for later inspection but you do NOT need to read the bytes yourself. You'll only see the storage ack (logical_path, version_id, short preview).",
66
+ group: "membot",
67
67
  inputSchema,
68
68
  outputSchema,
69
69
  execute: async (input, ctx) => {
@@ -82,9 +82,9 @@ export const pipeToContextTool = {
82
82
  return {
83
83
  is_error: true,
84
84
  error_type: "forbidden_tool",
85
- message: `Tool "${inner.name}" cannot be piped (terminal tools and pipe_to_context itself are not allowed).`,
85
+ message: `Tool "${inner.name}" cannot be piped (terminal tools and ${TOOL_NAME} itself are not allowed).`,
86
86
  next_action_hint:
87
- "Pipe a non-terminal tool (search, mcp_exec, etc.) instead.",
87
+ "Pipe a non-terminal tool (mcp_exec, membot_read, etc.) instead.",
88
88
  };
89
89
  }
90
90
 
@@ -143,30 +143,32 @@ export const pipeToContextTool = {
143
143
  }
144
144
 
145
145
  try {
146
- const entry = await writeContextFile(
147
- ctx.projectDir,
148
- input.path,
149
- innerOutput,
150
- { onConflict: input.on_conflict ?? "error" },
151
- );
146
+ const written = await ctx.mem.write({
147
+ logical_path: input.logical_path,
148
+ content: innerOutput,
149
+ change_note: input.change_note,
150
+ });
152
151
  return {
153
152
  is_error: false,
154
- path: entry.path,
155
- bytes_written: innerOutput.length,
153
+ logical_path: written.logical_path,
154
+ version_id: written.version_id,
155
+ bytes_written: written.size_bytes,
156
156
  preview: innerOutput.slice(0, PREVIEW_CHARS),
157
157
  };
158
158
  } catch (err) {
159
- if (err instanceof PathConflictError) {
159
+ if (isHelpfulError(err)) {
160
160
  return {
161
161
  is_error: true,
162
- error_type: "path_conflict",
163
- path: err.path,
164
- message: `A file already exists at context/${err.path}. The inner tool ran but its output was discarded.`,
165
- next_action_hint:
166
- "Retry with on_conflict='overwrite' to replace, or pick a different path.",
162
+ error_type: "write_failed",
163
+ message: `Inner tool ran, but write to ${input.logical_path} failed: ${err.message}`,
164
+ next_action_hint: err.hint,
167
165
  };
168
166
  }
169
- throw err;
167
+ return {
168
+ is_error: true,
169
+ error_type: "internal_error",
170
+ message: err instanceof Error ? err.message : String(err),
171
+ };
170
172
  }
171
173
  },
172
174
  } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -1,27 +1,12 @@
1
1
  // Capabilities tools
2
2
  import { capabilitiesRefreshTool } from "./capabilities/refresh.ts";
3
- // Context tools
4
- import { pipeToContextTool } from "./context/pipe.ts";
5
- import { readLargeResultTool } from "./context/read-large-result.ts";
6
- // Context — directory operations
7
- import { contextCreateDirTool } from "./dir/create.ts";
8
- import { contextDirSizeTool } from "./dir/size.ts";
9
- import { contextTreeTool } from "./dir/tree.ts";
10
- // Context — file operations
11
- import { contextCopyTool } from "./file/copy.ts";
12
- import { contextCountLinesTool } from "./file/count-lines.ts";
13
- import { contextDeleteTool } from "./file/delete.ts";
14
- import { contextEditTool } from "./file/edit.ts";
15
- import { contextExistsTool } from "./file/exists.ts";
16
- import { contextInfoTool } from "./file/info.ts";
17
- import { contextMoveTool } from "./file/move.ts";
18
- import { contextReadTool } from "./file/read.ts";
19
- import { contextWriteTool } from "./file/write.ts";
20
3
  // MCP tools
21
4
  import { mcpExecTool } from "./mcp/exec.ts";
22
5
  import { mcpInfoTool } from "./mcp/info.ts";
23
6
  import { mcpListToolsTool } from "./mcp/list-tools.ts";
24
7
  import { mcpSearchTool } from "./mcp/search.ts";
8
+ // Membot tools (knowledge store)
9
+ import { registerMembotTools } from "./membot/index.ts";
25
10
  // Prompt tools
26
11
  import { promptCreateTool } from "./prompt/create.ts";
27
12
  import { promptDeleteTool } from "./prompt/delete.ts";
@@ -32,8 +17,6 @@ import { promptReadTool } from "./prompt/read.ts";
32
17
  import { createScheduleTool } from "./schedule/create.ts";
33
18
  import { scheduleEditTool } from "./schedule/edit.ts";
34
19
  import { listSchedulesTool } from "./schedule/list.ts";
35
- // Search tools
36
- import { searchTool } from "./search/index.ts";
37
20
  // Skill tools
38
21
  import { skillDeleteTool } from "./skill/delete.ts";
39
22
  import { skillEditTool } from "./skill/edit.ts";
@@ -73,26 +56,15 @@ export function registerAllTools(): void {
73
56
  registerTool(listTasksTool);
74
57
  registerTool(viewTaskTool);
75
58
 
76
- // Context (file/dir + self-reflection)
77
- registerTool(contextCreateDirTool);
78
- registerTool(contextTreeTool);
79
- registerTool(contextDirSizeTool);
80
- registerTool(contextReadTool);
81
- registerTool(contextWriteTool);
82
- registerTool(contextEditTool);
83
- registerTool(contextDeleteTool);
84
- registerTool(contextCopyTool);
85
- registerTool(contextMoveTool);
86
- registerTool(contextInfoTool);
87
- registerTool(contextExistsTool);
88
- registerTool(contextCountLinesTool);
59
+ // Knowledge store (membot) — add/read/write/edit/search/versions/refresh etc.
60
+ registerMembotTools();
61
+
62
+ // Prompts
89
63
  registerTool(promptListTool);
90
64
  registerTool(promptReadTool);
91
65
  registerTool(promptCreateTool);
92
66
  registerTool(promptEditTool);
93
67
  registerTool(promptDeleteTool);
94
- registerTool(readLargeResultTool);
95
- registerTool(pipeToContextTool);
96
68
 
97
69
  // Capabilities
98
70
  registerTool(capabilitiesRefreshTool);
@@ -102,9 +74,6 @@ export function registerAllTools(): void {
102
74
  registerTool(scheduleEditTool);
103
75
  registerTool(listSchedulesTool);
104
76
 
105
- // Search
106
- registerTool(searchTool);
107
-
108
77
  // Skill
109
78
  registerTool(skillListTool);
110
79
  registerTool(skillReadTool);
package/src/tools/tool.ts CHANGED
@@ -1,19 +1,17 @@
1
1
  import type { Tool as AnthropicTool } from "@anthropic-ai/sdk/resources/messages";
2
2
  import type { McpxClient } from "@evantahler/mcpx";
3
+ import type { MembotClient } from "membot";
3
4
  import { z } from "zod";
4
5
  import type { BotholomewConfig } from "../config/schemas.ts";
5
- import type { DbConnection } from "../db/connection.ts";
6
6
 
7
7
  export interface ToolContext {
8
8
  /**
9
- * Short-lived DB connection scoped to this tool call. Safe for single-query
10
- * tools. Do NOT hold it across slow work (network, embedding, long loops) —
11
- * the instance-level file lock stays held until every connection closes.
12
- * For long-running tools, use `dbPath` with `withDb` per logical operation.
9
+ * Per-process membot client. Backs every `membot_*` tool. Membot manages
10
+ * its own DuckDB connection lifecycle internally (lazy claim, release
11
+ * between operations), so tools just call `ctx.mem.<op>(...)` directly
12
+ * no per-call open/close needed.
13
13
  */
14
- conn: DbConnection;
15
- /** Path to the DuckDB file. Use with `withDb` for long-running tools. */
16
- dbPath: string;
14
+ mem: MembotClient;
17
15
  projectDir: string;
18
16
  config: Required<BotholomewConfig>;
19
17
  mcpxClient: McpxClient | null;
package/src/tui/App.tsx CHANGED
@@ -186,18 +186,17 @@ function AppInner({
186
186
  setUsage,
187
187
  });
188
188
 
189
- const sessionDbPath = sessionRef.current?.dbPath;
189
+ const sessionReady = sessionRef.current != null;
190
190
  const inputBarHeader = useMemo(
191
191
  () =>
192
- sessionDbPath ? (
192
+ sessionReady ? (
193
193
  <StatusBar
194
194
  projectDir={projectDir}
195
- dbPath={sessionDbPath}
196
195
  chatTitle={chatTitle}
197
196
  onWorkerStatusChange={setWorkerRunning}
198
197
  />
199
198
  ) : null,
200
- [projectDir, sessionDbPath, chatTitle],
199
+ [projectDir, sessionReady, chatTitle],
201
200
  );
202
201
 
203
202
  const allToolCalls = useMemo(