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
@@ -1,3 +1,5 @@
1
+ export type Scope = "global" | "project";
2
+
1
3
  export interface BotholomewConfig {
2
4
  anthropic_api_key?: string;
3
5
  model?: string;
@@ -16,6 +18,8 @@ export interface BotholomewConfig {
16
18
  schedule_claim_stale_seconds?: number;
17
19
  tui_idle_timeout_seconds?: number;
18
20
  log_level?: string;
21
+ membot_scope?: Scope;
22
+ mcpx_scope?: Scope;
19
23
  }
20
24
 
21
25
  export const DEFAULT_CONFIG: Required<BotholomewConfig> = {
@@ -36,4 +40,6 @@ export const DEFAULT_CONFIG: Required<BotholomewConfig> = {
36
40
  schedule_claim_stale_seconds: 300,
37
41
  tui_idle_timeout_seconds: 180,
38
42
  log_level: "",
43
+ membot_scope: "global",
44
+ mcpx_scope: "global",
39
45
  };
package/src/constants.ts CHANGED
@@ -6,19 +6,22 @@ import { join } from "node:path";
6
6
  *
7
7
  * <projectDir>/
8
8
  * config/config.json
9
- * prompts/*.md init seeds goals/beliefs/capabilities
9
+ * config.json membot config (separate from ours)
10
+ * index.duckdb membot-owned knowledge store
11
+ * prompts/*.md init seeds goals/beliefs/capabilities
10
12
  * skills/*.md
11
13
  * mcpx/servers.json
12
- * models/ embedding model cache
13
- * context/ user-curated knowledge tree
14
- * tasks/<id>.md tasks (status in frontmatter)
15
- * tasks/.locks/<id>.lock O_EXCL claim files
16
- * schedules/<id>.md schedules
14
+ * tasks/<id>.md tasks (status in frontmatter)
15
+ * tasks/.locks/<id>.lock O_EXCL claim files
16
+ * schedules/<id>.md
17
17
  * schedules/.locks/<id>.lock
18
- * threads/<YYYY-MM-DD>/<id>.csv conversation history
19
- * workers/<id>.json pidfile + heartbeat
20
- * logs/ worker logs
21
- * index.duckdb search index (rebuildable from disk)
18
+ * threads/<YYYY-MM-DD>/<id>.csv conversation history
19
+ * workers/<id>.json pidfile + heartbeat
20
+ * logs/ worker logs
21
+ *
22
+ * The agent's knowledge ("what used to be `context/`") now lives in
23
+ * `index.duckdb`, managed by the `membot` library. Tasks, schedules, threads,
24
+ * workers, prompts, and skills remain real files on disk.
22
25
  */
23
26
 
24
27
  export const HOME_CONFIG_DIR = join(homedir(), ".botholomew");
@@ -32,14 +35,11 @@ export const DEFAULTS = {
32
35
  UPDATE_CHECK_TIMEOUT_MS: 5_000,
33
36
  } as const;
34
37
 
35
- export const INDEX_DB_FILENAME = "index.duckdb";
36
38
  export const CONFIG_DIR = "config";
37
39
  export const CONFIG_FILENAME = "config.json";
38
40
  export const PROMPTS_DIR = "prompts";
39
41
  export const SKILLS_DIR = "skills";
40
42
  export const MCPX_DIR = "mcpx";
41
- export const MODELS_DIR = "models";
42
- export const CONTEXT_DIR = "context";
43
43
  export const TASKS_DIR = "tasks";
44
44
  export const SCHEDULES_DIR = "schedules";
45
45
  export const LOCKS_SUBDIR = ".locks";
@@ -47,24 +47,18 @@ export const LOGS_DIR = "logs";
47
47
  export const WORKERS_DIR = "workers";
48
48
  export const THREADS_DIR = "threads";
49
49
  export const MCPX_SERVERS_FILENAME = "servers.json";
50
- export const EMBEDDING_DIMENSION = 384;
51
- export const EMBEDDING_MODEL = "Xenova/bge-small-en-v1.5";
52
50
 
53
51
  /**
54
- * Top-level areas tools must never touch directly. Use as a safelist when
55
- * validating tool path arguments most file/dir tools pin to CONTEXT_DIR.
52
+ * Top-level areas tools must never touch directly. Tasks/schedule lockfile
53
+ * dirs are kept off-limits because their `O_EXCL` claim semantics break if
54
+ * something else writes into them.
56
55
  */
57
56
  export const PROTECTED_AREAS: ReadonlySet<string> = new Set([
58
- MODELS_DIR,
59
57
  LOGS_DIR,
60
58
  `${TASKS_DIR}/${LOCKS_SUBDIR}`,
61
59
  `${SCHEDULES_DIR}/${LOCKS_SUBDIR}`,
62
60
  ]);
63
61
 
64
- export function getDbPath(projectDir: string): string {
65
- return join(projectDir, INDEX_DB_FILENAME);
66
- }
67
-
68
62
  export function getWorkerLogsDir(projectDir: string): string {
69
63
  return join(projectDir, LOGS_DIR);
70
64
  }
@@ -92,12 +86,6 @@ export function getMcpxDir(projectDir: string): string {
92
86
  return join(projectDir, MCPX_DIR);
93
87
  }
94
88
 
95
- export function getModelsDir(projectDir: string): string {
96
- return (
97
- process.env.BOTHOLOMEW_MODELS_DIR_OVERRIDE ?? join(projectDir, MODELS_DIR)
98
- );
99
- }
100
-
101
89
  export function getSkillsDir(projectDir: string): string {
102
90
  return join(projectDir, SKILLS_DIR);
103
91
  }
@@ -106,10 +94,6 @@ export function getPromptsDir(projectDir: string): string {
106
94
  return join(projectDir, PROMPTS_DIR);
107
95
  }
108
96
 
109
- export function getContextDir(projectDir: string): string {
110
- return join(projectDir, CONTEXT_DIR);
111
- }
112
-
113
97
  export function getTasksDir(projectDir: string): string {
114
98
  return join(projectDir, TASKS_DIR);
115
99
  }
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
- import { createMcpxClient } from "../mcpx/client.ts";
24
+ import { createMcpxClient, resolveMcpxDir } from "../mcpx/client.ts";
25
+ import { openMembot, resolveMembotDir } 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 {
@@ -40,9 +37,17 @@ import {
40
37
  SUMMARIZE_SKILL,
41
38
  } from "./templates.ts";
42
39
 
40
+ export interface InitOptions {
41
+ force?: boolean;
42
+ /** Override the default `membot_scope` written into config/config.json. */
43
+ membotScope?: "global" | "project";
44
+ /** Override the default `mcpx_scope` written into config/config.json. */
45
+ mcpxScope?: "global" | "project";
46
+ }
47
+
43
48
  export async function initProject(
44
49
  projectDir: string,
45
- opts: { force?: boolean } = {},
50
+ opts: InitOptions = {},
46
51
  ): Promise<void> {
47
52
  // Refuse to operate inside iCloud/Dropbox/etc unless --force is passed.
48
53
  // Sync overlays break atomic rename / O_EXCL semantics that tasks and
@@ -62,7 +67,6 @@ export async function initProject(
62
67
  await mkdir(getPromptsDir(projectDir), { recursive: true });
63
68
  await mkdir(getSkillsDir(projectDir), { recursive: true });
64
69
  await mkdir(getMcpxDir(projectDir), { recursive: true });
65
- await mkdir(join(projectDir, CONTEXT_DIR), { recursive: true });
66
70
  await mkdir(getTasksDir(projectDir), { recursive: true });
67
71
  await mkdir(getTasksLockDir(projectDir), { recursive: true });
68
72
  await mkdir(getSchedulesDir(projectDir), { recursive: true });
@@ -83,48 +87,69 @@ export async function initProject(
83
87
  await Bun.write(join(skillsDir, "standup.md"), STANDUP_SKILL);
84
88
  await Bun.write(join(skillsDir, "capabilities.md"), CAPABILITIES_SKILL);
85
89
 
86
- // Config
87
- await Bun.write(configPath, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`);
90
+ // Config — apply scope overrides from caller (CLI flags / tests) on top of
91
+ // the seeded defaults so tests and `botholomew init --membot-scope=project`
92
+ // can pick a per-project layout up front.
93
+ const initialConfig = {
94
+ ...DEFAULT_CONFIG,
95
+ ...(opts.membotScope ? { membot_scope: opts.membotScope } : {}),
96
+ ...(opts.mcpxScope ? { mcpx_scope: opts.mcpxScope } : {}),
97
+ };
98
+ await Bun.write(configPath, `${JSON.stringify(initialConfig, null, 2)}\n`);
99
+ const config = await loadConfig(projectDir);
88
100
 
89
- // mcpx servers config
90
- await Bun.write(
91
- join(getMcpxDir(projectDir), MCPX_SERVERS_FILENAME),
92
- `${JSON.stringify(DEFAULT_MCPX_SERVERS, null, 2)}\n`,
93
- );
101
+ // mcpx servers config — only seed a project-local servers.json when the
102
+ // project is opting out of the shared `~/.mcpx`. The empty `mcpx/` directory
103
+ // is still created above so flipping `mcpx_scope` later is a one-line edit.
104
+ if (config.mcpx_scope === "project") {
105
+ await Bun.write(
106
+ join(getMcpxDir(projectDir), MCPX_SERVERS_FILENAME),
107
+ `${JSON.stringify(DEFAULT_MCPX_SERVERS, null, 2)}\n`,
108
+ );
109
+ }
94
110
 
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();
111
+ // Initialize the membot knowledge store. Opening + closing the client
112
+ // triggers membot's first-run migration. When `membot_scope` is "global"
113
+ // (the default) we point at `~/.membot` so the shared store is ready;
114
+ // when "project" we seed `<projectDir>/index.duckdb`.
115
+ const mem = openMembot(resolveMembotDir(projectDir, config));
116
+ await mem.connect();
117
+ await mem.close();
100
118
 
101
119
  // Populate capabilities.md with the real tool inventory.
102
120
  registerAllTools();
103
- const config = await loadConfig(projectDir);
104
- const mcpxClient = await createMcpxClient(projectDir);
121
+ const mcpxClient = await createMcpxClient(resolveMcpxDir(projectDir, config));
105
122
  try {
106
123
  await writeCapabilitiesFile(projectDir, mcpxClient, config);
107
124
  } finally {
108
125
  await mcpxClient?.close();
109
126
  }
110
127
 
128
+ const membotScopeDesc =
129
+ config.membot_scope === "project"
130
+ ? `${projectDir}/index.duckdb (project-local)`
131
+ : `~/.membot (shared across projects — set membot_scope to "project" in ${CONFIG_DIR}/${CONFIG_FILENAME} to isolate)`;
132
+ const mcpxScopeDesc =
133
+ config.mcpx_scope === "project"
134
+ ? `${projectDir}/mcpx/ (project-local)`
135
+ : `~/.mcpx (shared across projects — set mcpx_scope to "project" in ${CONFIG_DIR}/${CONFIG_FILENAME} to isolate)`;
111
136
  logger.success("Initialized Botholomew project");
112
- logger.dim(` Project root: ${projectDir}`);
113
- logger.dim(` Config: ${CONFIG_DIR}/${CONFIG_FILENAME}`);
114
- logger.dim(` Index DB: ${dbPath}`);
137
+ logger.dim(` Project root: ${projectDir}`);
138
+ logger.dim(` Config: ${CONFIG_DIR}/${CONFIG_FILENAME}`);
139
+ logger.dim(` Knowledge: ${membotScopeDesc}`);
140
+ logger.dim(` MCPX: ${mcpxScopeDesc}`);
115
141
  logger.dim("");
116
142
  logger.dim("Layout:");
117
143
  logger.dim(` ${CONFIG_DIR}/ settings`);
118
144
  logger.dim(
119
145
  ` prompts/ goals, beliefs, capabilities (and any you add)`,
120
146
  );
121
- logger.dim(` ${CONTEXT_DIR}/ agent-writable knowledge tree`);
122
147
  logger.dim(` ${TASKS_DIR}/ one markdown file per task`);
123
148
  logger.dim(` ${LOCKS_SUBDIR}/ worker claim lockfiles`);
124
149
  logger.dim(` ${SCHEDULES_DIR}/ one markdown file per schedule`);
125
150
  logger.dim(` threads/ one CSV per conversation, by UTC date`);
126
151
  logger.dim(` workers/ one JSON pidfile per worker (heartbeats)`);
127
- logger.dim(` skills/, mcpx/, models/, logs/`);
152
+ logger.dim(` skills/, mcpx/, logs/`);
128
153
  logger.dim("");
129
154
  logger.dim("Next steps:");
130
155
  logger.dim(
@@ -1,16 +1,33 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
2
3
  import { join } from "node:path";
3
4
  import { type CallToolResult, McpxClient } from "@evantahler/mcpx";
5
+ import type { BotholomewConfig } from "../config/schemas.ts";
4
6
  import { getMcpxDir, MCPX_SERVERS_FILENAME } from "../constants.ts";
5
7
 
6
8
  /**
7
- * Create an McpxClient from the project's mcpx/servers.json.
8
- * Returns null if the file is missing or has no servers configured.
9
+ * Resolve the mcpx config directory for a project, honoring `mcpx_scope`:
10
+ * - "global" → `~/.mcpx` (shared across all Botholomew projects)
11
+ * - "project" → `<projectDir>/mcpx` (isolated per project)
9
12
  */
10
- export async function createMcpxClient(
13
+ export function resolveMcpxDir(
11
14
  projectDir: string,
15
+ config: Pick<BotholomewConfig, "mcpx_scope">,
16
+ ): string {
17
+ return config.mcpx_scope === "project"
18
+ ? getMcpxDir(projectDir)
19
+ : join(homedir(), ".mcpx");
20
+ }
21
+
22
+ /**
23
+ * Create an McpxClient from `<mcpxDir>/servers.json`. Returns null if the
24
+ * file is missing or has no servers configured. The caller is responsible
25
+ * for resolving `mcpxDir` via `resolveMcpxDir`.
26
+ */
27
+ export async function createMcpxClient(
28
+ mcpxDir: string,
12
29
  ): Promise<McpxClient | null> {
13
- const serversPath = join(getMcpxDir(projectDir), MCPX_SERVERS_FILENAME);
30
+ const serversPath = join(mcpxDir, MCPX_SERVERS_FILENAME);
14
31
  if (!existsSync(serversPath)) return null;
15
32
 
16
33
  const raw = await Bun.file(serversPath).text();
@@ -20,7 +37,6 @@ export async function createMcpxClient(
20
37
  return null;
21
38
  }
22
39
 
23
- const mcpxDir = getMcpxDir(projectDir);
24
40
  const authPath = join(mcpxDir, "auth.json");
25
41
  const auth = existsSync(authPath)
26
42
  ? JSON.parse(await Bun.file(authPath).text())
@@ -0,0 +1,33 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { MembotClient } from "membot";
4
+ import type { BotholomewConfig } from "../config/schemas.ts";
5
+
6
+ /**
7
+ * Resolve the membot data directory for a project, honoring `membot_scope`:
8
+ * - "global" → `~/.membot` (shared across all Botholomew projects)
9
+ * - "project" → `<projectDir>` (isolated per project)
10
+ *
11
+ * Membot's `configFlag` doubles as its data-dir flag (see
12
+ * `node_modules/membot/src/config/loader.ts::resolveDataDir`): an explicit
13
+ * value wins over `$MEMBOT_HOME` and the `~/.membot` default. We always pass
14
+ * an explicit value so a stray `MEMBOT_HOME` cannot redirect Botholomew at a
15
+ * different store.
16
+ */
17
+ export function resolveMembotDir(
18
+ projectDir: string,
19
+ config: Pick<BotholomewConfig, "membot_scope">,
20
+ ): string {
21
+ return config.membot_scope === "project"
22
+ ? projectDir
23
+ : join(homedir(), ".membot");
24
+ }
25
+
26
+ /**
27
+ * Open a membot client rooted at `dataDir`. The caller is responsible for
28
+ * resolving the directory (via `resolveMembotDir`) and for `close()` on
29
+ * shutdown.
30
+ */
31
+ export function openMembot(dataDir: string): MembotClient {
32
+ return new MembotClient({ configFlag: dataDir });
33
+ }
@@ -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>;