botholomew 0.12.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +91 -68
  2. package/package.json +3 -3
  3. package/src/chat/agent.ts +42 -82
  4. package/src/chat/session.ts +29 -25
  5. package/src/commands/capabilities.ts +1 -1
  6. package/src/commands/context.ts +177 -926
  7. package/src/commands/db.ts +9 -13
  8. package/src/commands/init.ts +4 -1
  9. package/src/commands/nuke.ts +57 -90
  10. package/src/commands/schedule.ts +103 -124
  11. package/src/commands/skill.ts +2 -2
  12. package/src/commands/task.ts +86 -95
  13. package/src/commands/thread.ts +107 -112
  14. package/src/commands/worker.ts +88 -88
  15. package/src/constants.ts +93 -16
  16. package/src/context/capabilities.ts +10 -10
  17. package/src/context/fetcher.ts +9 -10
  18. package/src/context/reindex.ts +189 -0
  19. package/src/context/store.ts +630 -0
  20. package/src/db/doctor.ts +1 -8
  21. package/src/db/embeddings.ts +227 -175
  22. package/src/db/sql/19-disk_backed_index.sql +36 -0
  23. package/src/db/sql/20-drop_db_tables_for_files.sql +19 -0
  24. package/src/fs/atomic.ts +217 -0
  25. package/src/fs/compat.ts +86 -0
  26. package/src/fs/sandbox.ts +279 -0
  27. package/src/init/index.ts +69 -52
  28. package/src/init/templates.ts +1 -1
  29. package/src/mcpx/client.ts +1 -1
  30. package/src/schedules/schema.ts +19 -0
  31. package/src/schedules/store.ts +296 -0
  32. package/src/skills/commands.ts +1 -3
  33. package/src/tasks/schema.ts +47 -0
  34. package/src/tasks/store.ts +486 -0
  35. package/src/threads/store.ts +559 -0
  36. package/src/tools/capabilities/refresh.ts +42 -21
  37. package/src/tools/context/pipe.ts +15 -71
  38. package/src/tools/context/update-beliefs.ts +3 -3
  39. package/src/tools/context/update-goals.ts +3 -3
  40. package/src/tools/dir/create.ts +26 -23
  41. package/src/tools/dir/size.ts +46 -17
  42. package/src/tools/dir/tree.ts +73 -279
  43. package/src/tools/file/copy.ts +50 -24
  44. package/src/tools/file/count-lines.ts +34 -10
  45. package/src/tools/file/delete.ts +44 -23
  46. package/src/tools/file/edit.ts +39 -14
  47. package/src/tools/file/exists.ts +12 -26
  48. package/src/tools/file/info.ts +25 -85
  49. package/src/tools/file/move.ts +39 -24
  50. package/src/tools/file/read.ts +32 -80
  51. package/src/tools/file/write.ts +14 -91
  52. package/src/tools/registry.ts +3 -7
  53. package/src/tools/schedule/create.ts +2 -2
  54. package/src/tools/schedule/list.ts +7 -3
  55. package/src/tools/search/fuse.ts +12 -33
  56. package/src/tools/search/index.ts +36 -43
  57. package/src/tools/search/regexp.ts +29 -17
  58. package/src/tools/search/semantic.ts +137 -51
  59. package/src/tools/skill/delete.ts +1 -1
  60. package/src/tools/skill/list.ts +1 -1
  61. package/src/tools/skill/write.ts +1 -1
  62. package/src/tools/task/create.ts +41 -16
  63. package/src/tools/task/delete.ts +3 -3
  64. package/src/tools/task/list.ts +6 -3
  65. package/src/tools/task/update.ts +31 -9
  66. package/src/tools/task/view.ts +6 -6
  67. package/src/tools/thread/list.ts +2 -2
  68. package/src/tools/thread/search.ts +208 -0
  69. package/src/tools/thread/view.ts +50 -5
  70. package/src/tools/worker/spawn.ts +28 -14
  71. package/src/tui/App.tsx +12 -19
  72. package/src/tui/components/ContextPanel.tsx +83 -316
  73. package/src/tui/components/SchedulePanel.tsx +34 -48
  74. package/src/tui/components/StatusBar.tsx +15 -15
  75. package/src/tui/components/TaskPanel.tsx +34 -38
  76. package/src/tui/components/ThreadPanel.tsx +29 -38
  77. package/src/tui/components/WorkerPanel.tsx +21 -19
  78. package/src/tui/markdown.ts +2 -8
  79. package/src/types/file-imports.d.ts +9 -0
  80. package/src/utils/title.ts +5 -7
  81. package/src/utils/v7-date.ts +47 -0
  82. package/src/worker/heartbeat.ts +46 -24
  83. package/src/worker/index.ts +13 -15
  84. package/src/worker/llm.ts +30 -37
  85. package/src/worker/prompt.ts +19 -41
  86. package/src/worker/schedules.ts +48 -69
  87. package/src/worker/spawn.ts +11 -11
  88. package/src/worker/tick.ts +39 -43
  89. package/src/workers/store.ts +247 -0
  90. package/src/commands/tools.ts +0 -367
  91. package/src/context/describer.ts +0 -140
  92. package/src/context/drives.ts +0 -110
  93. package/src/context/ingest.ts +0 -162
  94. package/src/context/refresh.ts +0 -183
  95. package/src/db/context.ts +0 -637
  96. package/src/db/daemon-state.ts +0 -6
  97. package/src/db/reembed.ts +0 -113
  98. package/src/db/schedules.ts +0 -213
  99. package/src/db/tasks.ts +0 -347
  100. package/src/db/threads.ts +0 -276
  101. package/src/db/workers.ts +0 -212
  102. package/src/tools/context/list-drives.ts +0 -36
  103. package/src/tools/context/refresh.ts +0 -165
  104. package/src/tools/context/search.ts +0 -54
@@ -1,6 +1,7 @@
1
1
  import ansis from "ansis";
2
2
  import type { Command } from "commander";
3
3
  import { loadConfig } from "../config/loader.ts";
4
+ import { logger } from "../utils/logger.ts";
4
5
  import {
5
6
  getWorker,
6
7
  listWorkers,
@@ -10,11 +11,10 @@ import {
10
11
  reapDeadWorkers,
11
12
  WORKER_STATUSES,
12
13
  type Worker,
13
- } from "../db/workers.ts";
14
- import { logger } from "../utils/logger.ts";
15
- import { withDb } from "./with-db.ts";
14
+ } from "../workers/store.ts";
16
15
 
17
- function formatAge(from: Date, to = new Date()): string {
16
+ function formatAge(fromIso: string, to = new Date()): string {
17
+ const from = new Date(fromIso);
18
18
  const secs = Math.max(0, Math.floor((to.getTime() - from.getTime()) / 1000));
19
19
  if (secs < 60) return `${secs}s ago`;
20
20
  const mins = Math.floor(secs / 60);
@@ -40,11 +40,11 @@ function printWorker(w: Worker) {
40
40
  const short = w.id.slice(0, 8);
41
41
  const lines = [
42
42
  `${ansis.bold(short)} pid=${w.pid} mode=${w.mode} ${statusColor(w.status)} host=${w.hostname}`,
43
- ` started: ${w.started_at.toISOString()} (${formatAge(w.started_at)})`,
44
- ` heartbeat: ${w.last_heartbeat_at.toISOString()} (${formatAge(w.last_heartbeat_at)})`,
43
+ ` started: ${w.started_at} (${formatAge(w.started_at)})`,
44
+ ` heartbeat: ${w.last_heartbeat_at} (${formatAge(w.last_heartbeat_at)})`,
45
45
  ];
46
46
  if (w.task_id) lines.push(` task: ${w.task_id}`);
47
- if (w.stopped_at) lines.push(` stopped: ${w.stopped_at.toISOString()}`);
47
+ if (w.stopped_at) lines.push(` stopped: ${w.stopped_at}`);
48
48
  console.log(lines.join("\n"));
49
49
  }
50
50
 
@@ -105,7 +105,7 @@ export function registerWorkerCommand(program: Command) {
105
105
 
106
106
  worker
107
107
  .command("list")
108
- .description("List workers registered in this project's database")
108
+ .description("List workers (one JSON record per file under workers/)")
109
109
  .option(
110
110
  "-s, --status <status>",
111
111
  `filter by status (${WORKER_STATUSES.join("|")})`,
@@ -113,106 +113,106 @@ export function registerWorkerCommand(program: Command) {
113
113
  .option("-l, --limit <n>", "max number of workers", Number.parseInt)
114
114
  .option("-o, --offset <n>", "skip first N workers", Number.parseInt)
115
115
  .action(
116
- (opts: { status?: Worker["status"]; limit?: number; offset?: number }) =>
117
- withDb(program, async (conn) => {
118
- if (opts.status && !WORKER_STATUSES.includes(opts.status)) {
119
- logger.error(
120
- `Unknown status: ${opts.status}. Use one of: ${WORKER_STATUSES.join(", ")}`,
121
- );
122
- process.exit(1);
123
- }
124
- const workers = await listWorkers(conn, {
125
- status: opts.status,
126
- limit: opts.limit,
127
- offset: opts.offset,
128
- });
129
- if (workers.length === 0) {
130
- logger.dim("No workers found.");
131
- return;
132
- }
133
- for (const w of workers) {
134
- printWorker(w);
135
- console.log("");
136
- }
137
- }),
116
+ async (opts: {
117
+ status?: Worker["status"];
118
+ limit?: number;
119
+ offset?: number;
120
+ }) => {
121
+ if (opts.status && !WORKER_STATUSES.includes(opts.status)) {
122
+ logger.error(
123
+ `Unknown status: ${opts.status}. Use one of: ${WORKER_STATUSES.join(", ")}`,
124
+ );
125
+ process.exit(1);
126
+ }
127
+ const dir = program.opts().dir;
128
+ const workers = await listWorkers(dir, {
129
+ status: opts.status,
130
+ limit: opts.limit,
131
+ offset: opts.offset,
132
+ });
133
+ if (workers.length === 0) {
134
+ logger.dim("No workers found.");
135
+ return;
136
+ }
137
+ for (const w of workers) {
138
+ printWorker(w);
139
+ console.log("");
140
+ }
141
+ },
138
142
  );
139
143
 
140
144
  worker
141
145
  .command("status <id>")
142
146
  .description("Show details for a single worker")
143
- .action((id: string) =>
144
- withDb(program, async (conn) => {
145
- const w = await getWorker(conn, id);
146
- if (!w) {
147
- logger.error(`No worker found with id ${id}.`);
148
- process.exit(1);
149
- }
150
- printWorker(w);
151
- }),
152
- );
147
+ .action(async (id: string) => {
148
+ const dir = program.opts().dir;
149
+ const w = await getWorker(dir, id);
150
+ if (!w) {
151
+ logger.error(`No worker found with id ${id}.`);
152
+ process.exit(1);
153
+ }
154
+ printWorker(w);
155
+ });
153
156
 
154
157
  worker
155
158
  .command("stop <id>")
156
159
  .description("SIGTERM the worker's process (graceful) and mark stopped")
157
- .action((id: string) =>
158
- withDb(program, async (conn) => {
159
- const w = await getWorker(conn, id);
160
- if (!w) {
161
- logger.error(`No worker found with id ${id}.`);
162
- process.exit(1);
163
- }
164
- signalWorker(w, "SIGTERM");
165
- await markWorkerStopped(conn, id);
166
- logger.success(`Worker ${id} signaled (SIGTERM) and marked stopped.`);
167
- }),
168
- );
160
+ .action(async (id: string) => {
161
+ const dir = program.opts().dir;
162
+ const w = await getWorker(dir, id);
163
+ if (!w) {
164
+ logger.error(`No worker found with id ${id}.`);
165
+ process.exit(1);
166
+ }
167
+ signalWorker(w, "SIGTERM");
168
+ await markWorkerStopped(dir, id);
169
+ logger.success(`Worker ${id} signaled (SIGTERM) and marked stopped.`);
170
+ });
169
171
 
170
172
  worker
171
173
  .command("kill <id>")
172
174
  .description("SIGKILL the worker's process and mark dead")
173
- .action((id: string) =>
174
- withDb(program, async (conn) => {
175
- const w = await getWorker(conn, id);
176
- if (!w) {
177
- logger.error(`No worker found with id ${id}.`);
178
- process.exit(1);
179
- }
180
- signalWorker(w, "SIGKILL");
181
- await markWorkerDead(conn, id);
182
- logger.success(`Worker ${id} killed (SIGKILL) and marked dead.`);
183
- }),
184
- );
175
+ .action(async (id: string) => {
176
+ const dir = program.opts().dir;
177
+ const w = await getWorker(dir, id);
178
+ if (!w) {
179
+ logger.error(`No worker found with id ${id}.`);
180
+ process.exit(1);
181
+ }
182
+ signalWorker(w, "SIGKILL");
183
+ await markWorkerDead(dir, id);
184
+ logger.success(`Worker ${id} killed (SIGKILL) and marked dead.`);
185
+ });
185
186
 
186
187
  worker
187
188
  .command("reap")
188
189
  .description(
189
190
  "Mark stale workers dead (releasing their tasks/schedule claims) and prune cleanly-stopped workers older than the retention window",
190
191
  )
191
- .action(() =>
192
- withDb(program, async (conn, dir) => {
193
- const config = await loadConfig(dir);
194
- const reaped = await reapDeadWorkers(
195
- conn,
196
- config.worker_dead_after_seconds,
192
+ .action(async () => {
193
+ const dir = program.opts().dir;
194
+ const config = await loadConfig(dir);
195
+ const reaped = await reapDeadWorkers(
196
+ dir,
197
+ config.worker_dead_after_seconds,
198
+ );
199
+ if (reaped.length === 0) {
200
+ logger.dim("No stale workers to reap.");
201
+ } else {
202
+ logger.success(
203
+ `Reaped ${reaped.length} worker(s): ${reaped.join(", ")}`,
197
204
  );
198
- if (reaped.length === 0) {
199
- logger.dim("No stale workers to reap.");
200
- } else {
201
- logger.success(
202
- `Reaped ${reaped.length} worker(s): ${reaped.join(", ")}`,
203
- );
204
- }
205
- const pruned = await pruneStoppedWorkers(
206
- conn,
207
- config.worker_stopped_retention_seconds,
205
+ }
206
+ const pruned = await pruneStoppedWorkers(
207
+ dir,
208
+ config.worker_stopped_retention_seconds,
209
+ );
210
+ if (pruned.length > 0) {
211
+ logger.success(
212
+ `Pruned ${pruned.length} stopped worker(s) older than retention window.`,
208
213
  );
209
- if (pruned.length > 0) {
210
- logger.success(
211
- `Pruned ${pruned.length} stopped worker(s) older than retention window.`,
212
- );
213
- }
214
- }),
215
- );
214
+ }
215
+ });
216
216
  }
217
217
 
218
218
  function signalWorker(w: Worker, signal: "SIGTERM" | "SIGKILL"): void {
@@ -225,7 +225,7 @@ function signalWorker(w: Worker, signal: "SIGTERM" | "SIGKILL"): void {
225
225
  process.kill(w.pid, signal);
226
226
  } catch (err) {
227
227
  logger.warn(
228
- `Could not send ${signal} to PID ${w.pid}: ${err instanceof Error ? err.message : err}. Marking DB state only.`,
228
+ `Could not send ${signal} to PID ${w.pid}: ${err instanceof Error ? err.message : err}. Marking state only.`,
229
229
  );
230
230
  }
231
231
  }
package/src/constants.ts CHANGED
@@ -1,7 +1,26 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
3
 
4
- export const BOTHOLOMEW_DIR = ".botholomew";
4
+ /**
5
+ * Project layout (rooted at `projectDir`, typically the user's cwd):
6
+ *
7
+ * <projectDir>/
8
+ * config/config.json
9
+ * prompts/{soul,beliefs,goals,capabilities}.md
10
+ * skills/*.md
11
+ * 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
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)
22
+ */
23
+
5
24
  export const HOME_CONFIG_DIR = join(homedir(), ".botholomew");
6
25
 
7
26
  export const ENV = {
@@ -12,47 +31,105 @@ export const DEFAULTS = {
12
31
  UPDATE_CHECK_INTERVAL_MS: 24 * 60 * 60 * 1000, // 24 hours
13
32
  UPDATE_CHECK_TIMEOUT_MS: 5_000,
14
33
  } as const;
15
- export const DB_FILENAME = "data.duckdb";
16
- export const LOGS_DIR = "logs";
34
+
35
+ export const INDEX_DB_FILENAME = "index.duckdb";
36
+ export const CONFIG_DIR = "config";
17
37
  export const CONFIG_FILENAME = "config.json";
38
+ export const PROMPTS_DIR = "prompts";
39
+ export const SKILLS_DIR = "skills";
18
40
  export const MCPX_DIR = "mcpx";
19
41
  export const MODELS_DIR = "models";
20
- export const SKILLS_DIR = "skills";
42
+ export const CONTEXT_DIR = "context";
43
+ export const TASKS_DIR = "tasks";
44
+ export const SCHEDULES_DIR = "schedules";
45
+ export const LOCKS_SUBDIR = ".locks";
46
+ export const LOGS_DIR = "logs";
47
+ export const WORKERS_DIR = "workers";
48
+ export const THREADS_DIR = "threads";
21
49
  export const MCPX_SERVERS_FILENAME = "servers.json";
22
50
  export const EMBEDDING_DIMENSION = 384;
23
51
  export const EMBEDDING_MODEL = "Xenova/bge-small-en-v1.5";
24
52
 
25
- export function getBotholomewDir(projectDir: string): string {
26
- return join(projectDir, BOTHOLOMEW_DIR);
27
- }
53
+ /**
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.
56
+ */
57
+ export const PROTECTED_AREAS: ReadonlySet<string> = new Set([
58
+ MODELS_DIR,
59
+ LOGS_DIR,
60
+ `${TASKS_DIR}/${LOCKS_SUBDIR}`,
61
+ `${SCHEDULES_DIR}/${LOCKS_SUBDIR}`,
62
+ ]);
28
63
 
29
64
  export function getDbPath(projectDir: string): string {
30
- return join(projectDir, BOTHOLOMEW_DIR, DB_FILENAME);
65
+ return join(projectDir, INDEX_DB_FILENAME);
31
66
  }
32
67
 
33
68
  export function getWorkerLogsDir(projectDir: string): string {
34
- return join(projectDir, BOTHOLOMEW_DIR, LOGS_DIR);
69
+ return join(projectDir, LOGS_DIR);
35
70
  }
36
71
 
37
- export function getWorkerLogPath(projectDir: string, workerId: string): string {
38
- return join(projectDir, BOTHOLOMEW_DIR, LOGS_DIR, `${workerId}.log`);
72
+ /**
73
+ * Per-worker log file at `<logs>/<YYYY-MM-DD>/<workerId>.log`. The date
74
+ * subdir keeps the logs directory browsable as workers accumulate.
75
+ * Callers derive `date` from the worker's uuidv7 timestamp via
76
+ * `src/utils/v7-date.ts::dateForId` so the path is a pure function of
77
+ * the id and survives a process restart.
78
+ */
79
+ export function getWorkerLogPath(
80
+ projectDir: string,
81
+ workerId: string,
82
+ date: string,
83
+ ): string {
84
+ return join(projectDir, LOGS_DIR, date, `${workerId}.log`);
39
85
  }
40
86
 
41
87
  export function getConfigPath(projectDir: string): string {
42
- return join(projectDir, BOTHOLOMEW_DIR, CONFIG_FILENAME);
88
+ return join(projectDir, CONFIG_DIR, CONFIG_FILENAME);
43
89
  }
44
90
 
45
91
  export function getMcpxDir(projectDir: string): string {
46
- return join(projectDir, BOTHOLOMEW_DIR, MCPX_DIR);
92
+ return join(projectDir, MCPX_DIR);
47
93
  }
48
94
 
49
95
  export function getModelsDir(projectDir: string): string {
50
96
  return (
51
- process.env.BOTHOLOMEW_MODELS_DIR_OVERRIDE ??
52
- join(projectDir, BOTHOLOMEW_DIR, MODELS_DIR)
97
+ process.env.BOTHOLOMEW_MODELS_DIR_OVERRIDE ?? join(projectDir, MODELS_DIR)
53
98
  );
54
99
  }
55
100
 
56
101
  export function getSkillsDir(projectDir: string): string {
57
- return join(projectDir, BOTHOLOMEW_DIR, SKILLS_DIR);
102
+ return join(projectDir, SKILLS_DIR);
103
+ }
104
+
105
+ export function getPromptsDir(projectDir: string): string {
106
+ return join(projectDir, PROMPTS_DIR);
107
+ }
108
+
109
+ export function getContextDir(projectDir: string): string {
110
+ return join(projectDir, CONTEXT_DIR);
111
+ }
112
+
113
+ export function getTasksDir(projectDir: string): string {
114
+ return join(projectDir, TASKS_DIR);
115
+ }
116
+
117
+ export function getTasksLockDir(projectDir: string): string {
118
+ return join(projectDir, TASKS_DIR, LOCKS_SUBDIR);
119
+ }
120
+
121
+ export function getSchedulesDir(projectDir: string): string {
122
+ return join(projectDir, SCHEDULES_DIR);
123
+ }
124
+
125
+ export function getSchedulesLockDir(projectDir: string): string {
126
+ return join(projectDir, SCHEDULES_DIR, LOCKS_SUBDIR);
127
+ }
128
+
129
+ export function getWorkersDir(projectDir: string): string {
130
+ return join(projectDir, WORKERS_DIR);
131
+ }
132
+
133
+ export function getThreadsDir(projectDir: string): string {
134
+ return join(projectDir, THREADS_DIR);
58
135
  }
@@ -2,7 +2,7 @@ import { join } from "node:path";
2
2
  import Anthropic from "@anthropic-ai/sdk";
3
3
  import type { McpxClient } from "@evantahler/mcpx";
4
4
  import type { BotholomewConfig } from "../config/schemas.ts";
5
- import { getBotholomewDir } from "../constants.ts";
5
+ import { getPromptsDir } from "../constants.ts";
6
6
  import { getAllTools, type ToolDefinition } from "../tools/tool.ts";
7
7
  import {
8
8
  type ContextFileMeta,
@@ -151,7 +151,7 @@ const SUMMARIZE_TOOL = {
151
151
  internal_themes: {
152
152
  type: "array",
153
153
  description:
154
- "Themes covering the agent's built-in tools (task queue, virtual filesystem, search, threads, MCPX meta-tools, workers, self-reflection, etc.).",
154
+ "Themes covering the agent's built-in tools (task queue, files & sandbox, search, threads, MCPX meta-tools, workers, self-reflection, etc.).",
155
155
  items: {
156
156
  type: "object",
157
157
  properties: {
@@ -241,12 +241,12 @@ Rules:
241
241
  - Do NOT list specific tool names. The agent discovers exact names via the MCPX meta-tools (mcp_search, mcp_list_tools, mcp_info) when it actually needs to invoke one.
242
242
  - Group tools into natural themes.
243
243
  - For MCPX tools, one theme usually = one external service (Gmail, Google Calendar, GitHub, Linear, Slack, Google Docs, Google Drive, Google Sheets, Apple Notes, etc.). Split a single server into multiple themes when it clearly exposes distinct services.
244
- - For internal tools, use coarse buckets aligned with the provided groups (task management, virtual filesystem, search, threads, MCPX meta-tools, workers, self-reflection, capabilities). Merge overlapping groups if natural.
244
+ - For internal tools, use coarse buckets aligned with the provided groups (task management, files & sandbox, search, threads, MCPX meta-tools, workers, self-reflection, capabilities). Merge overlapping groups if natural.
245
245
  - Each summary is ONE sentence with concrete action verbs. Present-tense imperative, no preamble.
246
246
 
247
247
  GOOD examples:
248
248
  "Gmail — read, send, draft, search, and reply to emails; manage labels and threads"
249
- "Virtual filesystem — read, write, edit, move, copy, delete, and navigate items in the agent's persistent memory store"
249
+ "Files & sandbox — read, write, edit, move, copy, delete, and navigate files under the agent's context/ tree"
250
250
  "GitHub — read and write repositories, branches, files, issues, pull requests, reviews, and labels"
251
251
 
252
252
  BAD examples (do not produce):
@@ -368,8 +368,8 @@ function renderFallback(inv: RawInventory, now: Date): string {
368
368
  schedule:
369
369
  "create and list recurring schedules that automatically generate tasks",
370
370
  context:
371
- "read, write, edit, move, copy, delete, and navigate items in the agent's persistent memory store; update beliefs and goals; read large tool results",
372
- search: "keyword and semantic search over the virtual filesystem",
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/",
373
373
  thread: "list and view past conversation threads and tool interactions",
374
374
  mcp: "search, list, inspect, and execute tools exposed by configured MCPX servers",
375
375
  worker: "spawn background workers to run tasks asynchronously",
@@ -461,9 +461,9 @@ export interface WriteResult {
461
461
  }
462
462
 
463
463
  /**
464
- * Regenerate and write `.botholomew/capabilities.md`. Preserves any existing
465
- * frontmatter (so a human-edited `loading:` flag survives). On first write
466
- * the default frontmatter is `loading: always`, `agent-modification: true`.
464
+ * Regenerate and write `prompts/capabilities.md`. Preserves any
465
+ * existing frontmatter (so a human-edited `loading:` flag survives). On first
466
+ * write the default frontmatter is `loading: always`, `agent-modification: true`.
467
467
  */
468
468
  export async function writeCapabilitiesFile(
469
469
  projectDir: string,
@@ -471,7 +471,7 @@ export async function writeCapabilitiesFile(
471
471
  config: Required<BotholomewConfig>,
472
472
  onPhase?: ProgressCallback,
473
473
  ): Promise<WriteResult> {
474
- const filePath = join(getBotholomewDir(projectDir), CAPABILITIES_FILENAME);
474
+ const filePath = join(getPromptsDir(projectDir), CAPABILITIES_FILENAME);
475
475
  const file = Bun.file(filePath);
476
476
 
477
477
  let meta: ContextFileMeta = {
@@ -15,7 +15,6 @@ import { mcpSearchTool } from "../tools/mcp/search.ts";
15
15
  import type { ToolContext } from "../tools/tool.ts";
16
16
  import { type AnyToolDefinition, toAnthropicTool } from "../tools/tool.ts";
17
17
  import { logger } from "../utils/logger.ts";
18
- import { detectDriveFromUrl } from "./drives.ts";
19
18
  import { stripHtmlTags } from "./url-utils.ts";
20
19
 
21
20
  const MAX_CONTENT_BYTES = 500_000;
@@ -29,8 +28,12 @@ export interface FetchedContent {
29
28
  content: string;
30
29
  mimeType: string;
31
30
  sourceUrl: string;
32
- drive: string;
33
- path: string;
31
+ /**
32
+ * MCP server that produced the content (e.g. "google-docs", "github",
33
+ * "firecrawl"), or null when we fell back to a plain HTTP fetch. Useful
34
+ * for `bothy context import` to pick a default destination subdirectory.
35
+ */
36
+ source: string | null;
34
37
  }
35
38
 
36
39
  export class FetchFailureError extends Error {
@@ -138,7 +141,7 @@ export async function fetchUrl(
138
141
  ): Promise<FetchedContent> {
139
142
  if (!config.anthropic_api_key) {
140
143
  throw new Error(
141
- "Anthropic API key is required for URL fetching. Set ANTHROPIC_API_KEY or configure it in .botholomew/config.json",
144
+ "Anthropic API key is required for URL fetching. Set ANTHROPIC_API_KEY or configure it in config/config.json",
142
145
  );
143
146
  }
144
147
 
@@ -293,14 +296,12 @@ async function runFetcherLoop(
293
296
  logger.dim(
294
297
  ` turn ${turn + 1}: accept_content: "${input.title}" (${cached.content.length} chars, ${mimeType}, from ${cached.server}/${cached.tool})`,
295
298
  );
296
- const { drive, path } = detectDriveFromUrl(url, cached.server);
297
299
  return {
298
300
  title: input.title,
299
301
  content: cached.content.slice(0, MAX_CONTENT_BYTES),
300
302
  mimeType,
301
303
  sourceUrl: url,
302
- drive,
303
- path,
304
+ source: cached.server,
304
305
  };
305
306
  }
306
307
 
@@ -435,13 +436,11 @@ export async function httpFallback(url: string): Promise<FetchedContent> {
435
436
  ? "text/markdown"
436
437
  : contentType.split(";")[0] || "text/plain";
437
438
 
438
- const { drive, path } = detectDriveFromUrl(url);
439
439
  return {
440
440
  title,
441
441
  content: text,
442
442
  mimeType,
443
443
  sourceUrl: url,
444
- drive,
445
- path,
444
+ source: null,
446
445
  };
447
446
  }