botholomew 0.16.4 → 0.17.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 (95) hide show
  1. package/README.md +46 -41
  2. package/package.json +3 -8
  3. package/src/chat/agent.ts +37 -40
  4. package/src/chat/session.ts +8 -8
  5. package/src/cli.ts +0 -2
  6. package/src/commands/capabilities.ts +32 -32
  7. package/src/commands/context.ts +124 -223
  8. package/src/commands/mcpx.ts +1 -1
  9. package/src/commands/nuke.ts +44 -15
  10. package/src/commands/prepare.ts +17 -13
  11. package/src/config/loader.ts +1 -8
  12. package/src/constants.ts +16 -32
  13. package/src/init/index.ts +11 -14
  14. package/src/mem/client.ts +17 -0
  15. package/src/{context → prompts}/capabilities.ts +11 -7
  16. package/src/schedules/store.ts +1 -1
  17. package/src/tasks/store.ts +1 -1
  18. package/src/threads/store.ts +1 -1
  19. package/src/tools/capabilities/refresh.ts +1 -1
  20. package/src/tools/membot/adapter.ts +111 -0
  21. package/src/tools/membot/copy.ts +59 -0
  22. package/src/tools/membot/count_lines.ts +53 -0
  23. package/src/tools/membot/edit.ts +72 -0
  24. package/src/tools/membot/exists.ts +54 -0
  25. package/src/tools/membot/index.ts +26 -0
  26. package/src/tools/{context → membot}/pipe.ts +34 -32
  27. package/src/tools/registry.ts +6 -37
  28. package/src/tools/tool.ts +6 -8
  29. package/src/tui/App.tsx +3 -4
  30. package/src/tui/components/ContextPanel.tsx +109 -226
  31. package/src/tui/components/HelpPanel.tsx +2 -2
  32. package/src/tui/components/StatusBar.tsx +0 -6
  33. package/src/tui/components/ThreadPanel.tsx +8 -7
  34. package/src/tui/wrapDetail.ts +11 -0
  35. package/src/worker/heartbeat.ts +0 -20
  36. package/src/worker/index.ts +11 -11
  37. package/src/worker/llm.ts +7 -9
  38. package/src/worker/prompt.ts +25 -13
  39. package/src/worker/spawn.ts +1 -1
  40. package/src/worker/tick.ts +10 -9
  41. package/src/commands/db.ts +0 -119
  42. package/src/commands/with-db.ts +0 -22
  43. package/src/context/chunker.ts +0 -275
  44. package/src/context/embedder-impl.ts +0 -100
  45. package/src/context/embedder.ts +0 -9
  46. package/src/context/fetcher-errors.ts +0 -8
  47. package/src/context/fetcher.ts +0 -515
  48. package/src/context/locks.ts +0 -146
  49. package/src/context/markdown-converter.ts +0 -186
  50. package/src/context/reindex.ts +0 -198
  51. package/src/context/store.ts +0 -841
  52. package/src/context/url-utils.ts +0 -25
  53. package/src/db/connection.ts +0 -255
  54. package/src/db/doctor.ts +0 -235
  55. package/src/db/embeddings.ts +0 -317
  56. package/src/db/query.ts +0 -56
  57. package/src/db/schema.ts +0 -93
  58. package/src/db/sql/1-core_tables.sql +0 -53
  59. package/src/db/sql/10-dedupe_context_items.sql +0 -26
  60. package/src/db/sql/11-rebuild_hnsw.sql +0 -8
  61. package/src/db/sql/12-workers.sql +0 -66
  62. package/src/db/sql/13-drive-paths.sql +0 -47
  63. package/src/db/sql/14-drop_hnsw_index.sql +0 -8
  64. package/src/db/sql/15-fts_index.sql +0 -8
  65. package/src/db/sql/16-source_url.sql +0 -7
  66. package/src/db/sql/17-worker_log_path.sql +0 -3
  67. package/src/db/sql/18-reset_embeddings_for_local.sql +0 -39
  68. package/src/db/sql/19-disk_backed_index.sql +0 -36
  69. package/src/db/sql/2-logging_tables.sql +0 -24
  70. package/src/db/sql/20-drop_db_tables_for_files.sql +0 -19
  71. package/src/db/sql/3-daemon_state.sql +0 -5
  72. package/src/db/sql/4-unique_context_path.sql +0 -1
  73. package/src/db/sql/5-reset_embeddings_for_openai.sql +0 -1
  74. package/src/db/sql/6-vss_index.sql +0 -7
  75. package/src/db/sql/7-drop_embeddings_fk.sql +0 -23
  76. package/src/db/sql/8-task_output.sql +0 -1
  77. package/src/db/sql/9-source-type.sql +0 -1
  78. package/src/tools/context/read-large-result.ts +0 -33
  79. package/src/tools/dir/create.ts +0 -47
  80. package/src/tools/dir/size.ts +0 -77
  81. package/src/tools/dir/tree.ts +0 -124
  82. package/src/tools/file/copy.ts +0 -73
  83. package/src/tools/file/count-lines.ts +0 -54
  84. package/src/tools/file/delete.ts +0 -83
  85. package/src/tools/file/edit.ts +0 -76
  86. package/src/tools/file/exists.ts +0 -33
  87. package/src/tools/file/info.ts +0 -66
  88. package/src/tools/file/move.ts +0 -66
  89. package/src/tools/file/read.ts +0 -67
  90. package/src/tools/file/write.ts +0 -58
  91. package/src/tools/search/fuse.ts +0 -96
  92. package/src/tools/search/index.ts +0 -127
  93. package/src/tools/search/regexp.ts +0 -82
  94. package/src/tools/search/semantic.ts +0 -167
  95. /package/src/{db → utils}/uuid.ts +0 -0
package/src/init/index.ts CHANGED
@@ -4,9 +4,7 @@ import { loadConfig } from "../config/loader.ts";
4
4
  import {
5
5
  CONFIG_DIR,
6
6
  CONFIG_FILENAME,
7
- CONTEXT_DIR,
8
7
  getConfigPath,
9
- getDbPath,
10
8
  getMcpxDir,
11
9
  getPromptsDir,
12
10
  getSchedulesDir,
@@ -22,11 +20,10 @@ import {
22
20
  SCHEDULES_DIR,
23
21
  TASKS_DIR,
24
22
  } from "../constants.ts";
25
- import { writeCapabilitiesFile } from "../context/capabilities.ts";
26
- import { getConnection } from "../db/connection.ts";
27
- import { migrate } from "../db/schema.ts";
28
23
  import { assertCompatibleFilesystem } from "../fs/compat.ts";
29
24
  import { createMcpxClient } from "../mcpx/client.ts";
25
+ import { openMembot } from "../mem/client.ts";
26
+ import { writeCapabilitiesFile } from "../prompts/capabilities.ts";
30
27
  import { registerAllTools } from "../tools/registry.ts";
31
28
  import { logger } from "../utils/logger.ts";
32
29
  import {
@@ -62,7 +59,6 @@ export async function initProject(
62
59
  await mkdir(getPromptsDir(projectDir), { recursive: true });
63
60
  await mkdir(getSkillsDir(projectDir), { recursive: true });
64
61
  await mkdir(getMcpxDir(projectDir), { recursive: true });
65
- await mkdir(join(projectDir, CONTEXT_DIR), { recursive: true });
66
62
  await mkdir(getTasksDir(projectDir), { recursive: true });
67
63
  await mkdir(getTasksLockDir(projectDir), { recursive: true });
68
64
  await mkdir(getSchedulesDir(projectDir), { recursive: true });
@@ -92,11 +88,12 @@ export async function initProject(
92
88
  `${JSON.stringify(DEFAULT_MCPX_SERVERS, null, 2)}\n`,
93
89
  );
94
90
 
95
- // Initialize the index database (search index sidecar; rebuildable).
96
- const dbPath = getDbPath(projectDir);
97
- const conn = await getConnection(dbPath);
98
- await migrate(conn);
99
- conn.close();
91
+ // Initialize the membot knowledge store. Opening + closing the client
92
+ // triggers membot's first-run migration so the project ships with a
93
+ // ready-to-use index.duckdb.
94
+ const mem = openMembot(projectDir);
95
+ await mem.connect();
96
+ await mem.close();
100
97
 
101
98
  // Populate capabilities.md with the real tool inventory.
102
99
  registerAllTools();
@@ -111,20 +108,20 @@ export async function initProject(
111
108
  logger.success("Initialized Botholomew project");
112
109
  logger.dim(` Project root: ${projectDir}`);
113
110
  logger.dim(` Config: ${CONFIG_DIR}/${CONFIG_FILENAME}`);
114
- logger.dim(` Index DB: ${dbPath}`);
111
+ logger.dim(` Knowledge: index.duckdb (managed by membot)`);
115
112
  logger.dim("");
116
113
  logger.dim("Layout:");
117
114
  logger.dim(` ${CONFIG_DIR}/ settings`);
118
115
  logger.dim(
119
116
  ` prompts/ goals, beliefs, capabilities (and any you add)`,
120
117
  );
121
- logger.dim(` ${CONTEXT_DIR}/ agent-writable knowledge tree`);
118
+ logger.dim(` index.duckdb agent's knowledge store (membot)`);
122
119
  logger.dim(` ${TASKS_DIR}/ one markdown file per task`);
123
120
  logger.dim(` ${LOCKS_SUBDIR}/ worker claim lockfiles`);
124
121
  logger.dim(` ${SCHEDULES_DIR}/ one markdown file per schedule`);
125
122
  logger.dim(` threads/ one CSV per conversation, by UTC date`);
126
123
  logger.dim(` workers/ one JSON pidfile per worker (heartbeats)`);
127
- logger.dim(` skills/, mcpx/, models/, logs/`);
124
+ logger.dim(` skills/, mcpx/, logs/`);
128
125
  logger.dim("");
129
126
  logger.dim("Next steps:");
130
127
  logger.dim(
@@ -0,0 +1,17 @@
1
+ import { MembotClient } from "membot";
2
+
3
+ /**
4
+ * Open a per-project membot client. Each Botholomew project gets its own
5
+ * membot data dir (`<projectDir>/config.json` + `<projectDir>/index.duckdb`)
6
+ * so projects don't share knowledge. The caller is responsible for `close()`
7
+ * on shutdown.
8
+ *
9
+ * Membot's `configFlag` doubles as its data-dir flag (see
10
+ * `membot/src/config/loader.ts::resolveDataDir`): an explicit value wins over
11
+ * `$MEMBOT_HOME` and the `~/.membot` default. We pass the project directory
12
+ * unconditionally so a stray `MEMBOT_HOME` in the user's environment cannot
13
+ * redirect Botholomew at a different store.
14
+ */
15
+ export function openMembot(projectDir: string): MembotClient {
16
+ return new MembotClient({ configFlag: projectDir });
17
+ }
@@ -27,8 +27,9 @@ type AnyTool = ToolDefinition<any, any>;
27
27
  const GROUP_ORDER = [
28
28
  "task",
29
29
  "schedule",
30
- "context",
31
- "search",
30
+ "membot",
31
+ "prompt",
32
+ "skill",
32
33
  "thread",
33
34
  "mcp",
34
35
  "worker",
@@ -38,8 +39,9 @@ const GROUP_ORDER = [
38
39
  const GROUP_HEADINGS: Record<string, string> = {
39
40
  task: "Task management",
40
41
  schedule: "Schedules",
41
- context: "Virtual filesystem & self-reflection",
42
- search: "Search",
42
+ membot: "Knowledge store (membot)",
43
+ prompt: "Prompts",
44
+ skill: "Skills",
43
45
  thread: "Threads",
44
46
  mcp: "MCPX meta-tools",
45
47
  worker: "Workers",
@@ -367,9 +369,11 @@ function renderFallback(inv: RawInventory, now: Date): string {
367
369
  task: "create, list, view, update, complete, fail, and wait on tasks in the agent's work queue",
368
370
  schedule:
369
371
  "create and list recurring schedules that automatically generate tasks",
370
- context:
371
- "read, write, edit, move, copy, delete, and navigate files in the agent's context/ tree; update beliefs and goals; read large tool results",
372
- search: "keyword, semantic, and regexp search over files in context/",
372
+ membot:
373
+ "add, read, write, edit, move, copy, delete, and search content in the agent's knowledge store; track every version and refresh from URL sources",
374
+ prompt:
375
+ "list, read, create, edit, and delete the project's prompt files (goals, beliefs, capabilities, plus any agent-authored ones)",
376
+ skill: "list, read, write, edit, delete, and search slash-command skills",
373
377
  thread: "list and view past conversation threads and tool interactions",
374
378
  mcp: "search, list, inspect, and execute tools exposed by configured MCPX servers",
375
379
  worker: "spawn background workers to run tasks asynchronously",
@@ -2,7 +2,6 @@ import { readdir, unlink } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import matter from "gray-matter";
4
4
  import { getSchedulesDir, getSchedulesLockDir } from "../constants.ts";
5
- import { uuidv7 } from "../db/uuid.ts";
6
5
  import {
7
6
  acquireLock,
8
7
  atomicWrite,
@@ -13,6 +12,7 @@ import {
13
12
  releaseLock,
14
13
  } from "../fs/atomic.ts";
15
14
  import { logger } from "../utils/logger.ts";
15
+ import { uuidv7 } from "../utils/uuid.ts";
16
16
  import {
17
17
  type Schedule,
18
18
  type ScheduleFrontmatter,
@@ -2,7 +2,6 @@ import { readdir, stat, unlink } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import matter from "gray-matter";
4
4
  import { getTasksDir, getTasksLockDir } from "../constants.ts";
5
- import { uuidv7 } from "../db/uuid.ts";
6
5
  import {
7
6
  acquireLock,
8
7
  atomicWrite,
@@ -13,6 +12,7 @@ import {
13
12
  releaseLock,
14
13
  } from "../fs/atomic.ts";
15
14
  import { logger } from "../utils/logger.ts";
15
+ import { uuidv7 } from "../utils/uuid.ts";
16
16
  import {
17
17
  type Task,
18
18
  type TaskFrontmatter,
@@ -1,8 +1,8 @@
1
1
  import { appendFile, readdir, readFile, rm, stat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { getThreadsDir } from "../constants.ts";
4
- import { uuidv7 } from "../db/uuid.ts";
5
4
  import { atomicWrite } from "../fs/atomic.ts";
5
+ import { uuidv7 } from "../utils/uuid.ts";
6
6
  import { DATE_DIR_RE, dateForId } from "../utils/v7-date.ts";
7
7
 
8
8
  /**
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { writeCapabilitiesFile } from "../../context/capabilities.ts";
2
+ import { writeCapabilitiesFile } from "../../prompts/capabilities.ts";
3
3
  import type { ToolDefinition } from "../tool.ts";
4
4
 
5
5
  const inputSchema = z.object({
@@ -0,0 +1,111 @@
1
+ import type { Operation } from "membot";
2
+ import { composeDescription, isHelpfulError } from "membot";
3
+ import { z } from "zod";
4
+ import type { ToolContext, ToolDefinition } from "../tool.ts";
5
+
6
+ /**
7
+ * Common output envelope for every membot-backed tool. The membot operation's
8
+ * own result is parked under `data` so the LLM gets a single, predictable
9
+ * shape across all 14 verbs. Errors flatten the HelpfulError's `kind`/`hint`
10
+ * into `error_type` / `next_action_hint`, which is the recovery cue Botholomew
11
+ * agents already know from the rest of the tool surface.
12
+ */
13
+ export const membotOutputSchema = z.object({
14
+ is_error: z.boolean(),
15
+ data: z.unknown().optional(),
16
+ error_type: z.string().optional(),
17
+ message: z.string().optional(),
18
+ next_action_hint: z.string().optional(),
19
+ });
20
+
21
+ export type MembotOutput = z.infer<typeof membotOutputSchema>;
22
+
23
+ type MembotMethodName =
24
+ | "add"
25
+ | "list"
26
+ | "tree"
27
+ | "read"
28
+ | "search"
29
+ | "info"
30
+ | "stats"
31
+ | "versions"
32
+ | "diff"
33
+ | "write"
34
+ | "move"
35
+ | "remove"
36
+ | "refresh"
37
+ | "prune";
38
+
39
+ /**
40
+ * Map an Operation's exposed name (`membot_add`, `membot_delete`, …) to the
41
+ * `MembotClient` method that actually runs it. The two diverge in a couple
42
+ * of spots — `membot_delete` calls `client.remove`, `membot_move` calls
43
+ * `client.move` — so we keep the routing explicit rather than guessing.
44
+ */
45
+ const METHOD_BY_OP_NAME: Record<string, MembotMethodName> = {
46
+ membot_add: "add",
47
+ membot_list: "list",
48
+ membot_tree: "tree",
49
+ membot_read: "read",
50
+ membot_search: "search",
51
+ membot_info: "info",
52
+ membot_stats: "stats",
53
+ membot_versions: "versions",
54
+ membot_diff: "diff",
55
+ membot_write: "write",
56
+ membot_move: "move",
57
+ membot_delete: "remove",
58
+ membot_refresh: "refresh",
59
+ membot_prune: "prune",
60
+ };
61
+
62
+ /**
63
+ * Adapt one membot {@link Operation} into a Botholomew {@link ToolDefinition}.
64
+ * The input schema passes through unchanged so the LLM sees membot's
65
+ * upstream prose/aliases verbatim. Success calls return `{ is_error: false,
66
+ * data: <op output> }`; HelpfulErrors return the flattened error envelope.
67
+ * Unknown errors are wrapped as `internal_error` so a thrown handler never
68
+ * crashes the agent loop.
69
+ */
70
+ export function adaptOperation(
71
+ // biome-ignore lint/suspicious/noExplicitAny: Operation generic is heterogeneous across 14 verbs
72
+ op: Operation<any, any>,
73
+ ): ToolDefinition<z.ZodObject<z.ZodRawShape>, typeof membotOutputSchema> {
74
+ const methodName = METHOD_BY_OP_NAME[op.name];
75
+ if (!methodName) {
76
+ throw new Error(
77
+ `adaptOperation: no MembotClient method registered for op '${op.name}'. ` +
78
+ `Add it to METHOD_BY_OP_NAME in src/tools/membot/adapter.ts.`,
79
+ );
80
+ }
81
+ return {
82
+ name: op.name,
83
+ description: composeDescription(op),
84
+ group: "membot",
85
+ inputSchema: op.inputSchema as z.ZodObject<z.ZodRawShape>,
86
+ outputSchema: membotOutputSchema,
87
+ execute: async (input, ctx: ToolContext) => {
88
+ try {
89
+ const method = ctx.mem[methodName] as (i: unknown) => Promise<unknown>;
90
+ const data = await method.call(ctx.mem, input);
91
+ return { is_error: false, data };
92
+ } catch (err) {
93
+ if (isHelpfulError(err)) {
94
+ return {
95
+ is_error: true,
96
+ error_type: err.kind,
97
+ message: err.message,
98
+ next_action_hint: err.hint,
99
+ };
100
+ }
101
+ return {
102
+ is_error: true,
103
+ error_type: "internal_error",
104
+ message: err instanceof Error ? err.message : String(err),
105
+ next_action_hint:
106
+ "Check the project's membot store (run `botholomew context stats`) and try again. If this persists, file a bug.",
107
+ };
108
+ }
109
+ },
110
+ };
111
+ }
@@ -0,0 +1,59 @@
1
+ import { isHelpfulError } from "membot";
2
+ import { z } from "zod";
3
+ import type { ToolDefinition } from "../tool.ts";
4
+
5
+ const inputSchema = z.object({
6
+ from_logical_path: z.string().describe("Source path"),
7
+ to_logical_path: z.string().describe("Destination path"),
8
+ change_note: z.string().optional(),
9
+ });
10
+
11
+ const outputSchema = z.object({
12
+ is_error: z.boolean(),
13
+ from_logical_path: z.string().optional(),
14
+ to_logical_path: z.string().optional(),
15
+ new_version_id: z.string().optional(),
16
+ error_type: z.string().optional(),
17
+ message: z.string().optional(),
18
+ next_action_hint: z.string().optional(),
19
+ });
20
+
21
+ export const membotCopyTool = {
22
+ name: "membot_copy",
23
+ description:
24
+ "[[ bash equivalent command: cp ]] Duplicate a file's current content under a new logical_path. The source is left untouched; the destination becomes a new inline-source version. Use membot_move to rename instead (the source is tombstoned in that case).",
25
+ group: "membot",
26
+ inputSchema,
27
+ outputSchema,
28
+ execute: async (input, ctx) => {
29
+ try {
30
+ const src = await ctx.mem.read({ logical_path: input.from_logical_path });
31
+ const written = await ctx.mem.write({
32
+ logical_path: input.to_logical_path,
33
+ content: src.content ?? "",
34
+ change_note:
35
+ input.change_note ?? `copied from ${input.from_logical_path}`,
36
+ });
37
+ return {
38
+ is_error: false,
39
+ from_logical_path: input.from_logical_path,
40
+ to_logical_path: written.logical_path,
41
+ new_version_id: written.version_id,
42
+ };
43
+ } catch (err) {
44
+ if (isHelpfulError(err)) {
45
+ return {
46
+ is_error: true,
47
+ error_type: err.kind,
48
+ message: err.message,
49
+ next_action_hint: err.hint,
50
+ };
51
+ }
52
+ return {
53
+ is_error: true,
54
+ error_type: "internal_error",
55
+ message: err instanceof Error ? err.message : String(err),
56
+ };
57
+ }
58
+ },
59
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -0,0 +1,53 @@
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 of the file to count."),
7
+ });
8
+
9
+ const outputSchema = z.object({
10
+ is_error: z.boolean(),
11
+ logical_path: z.string().optional(),
12
+ line_count: z.number().optional(),
13
+ size_bytes: z.number().nullable().optional(),
14
+ error_type: z.string().optional(),
15
+ message: z.string().optional(),
16
+ next_action_hint: z.string().optional(),
17
+ });
18
+
19
+ export const membotCountLinesTool = {
20
+ name: "membot_count_lines",
21
+ description:
22
+ "[[ bash equivalent command: wc -l ]] Count lines in a stored file's markdown surrogate. Useful before a large membot_read or membot_edit to decide whether to fetch the whole body or page through it with `offset`/`limit`.",
23
+ group: "membot",
24
+ inputSchema,
25
+ outputSchema,
26
+ execute: async (input, ctx) => {
27
+ try {
28
+ const result = await ctx.mem.read({ logical_path: input.logical_path });
29
+ const content = result.content ?? "";
30
+ const lineCount = content === "" ? 0 : content.split("\n").length;
31
+ return {
32
+ is_error: false,
33
+ logical_path: result.logical_path,
34
+ line_count: lineCount,
35
+ size_bytes: result.size_bytes,
36
+ };
37
+ } catch (err) {
38
+ if (isHelpfulError(err)) {
39
+ return {
40
+ is_error: true,
41
+ error_type: err.kind,
42
+ message: err.message,
43
+ next_action_hint: err.hint,
44
+ };
45
+ }
46
+ return {
47
+ is_error: true,
48
+ error_type: "internal_error",
49
+ message: err instanceof Error ? err.message : String(err),
50
+ };
51
+ }
52
+ },
53
+ } satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
@@ -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
+ }