botholomew 0.12.5 → 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.
- package/README.md +91 -68
- package/package.json +2 -2
- package/src/chat/agent.ts +42 -82
- package/src/chat/session.ts +29 -25
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +177 -926
- package/src/commands/db.ts +9 -13
- package/src/commands/init.ts +4 -1
- package/src/commands/nuke.ts +57 -90
- package/src/commands/schedule.ts +103 -124
- package/src/commands/skill.ts +2 -2
- package/src/commands/task.ts +86 -95
- package/src/commands/thread.ts +107 -112
- package/src/commands/worker.ts +88 -88
- package/src/constants.ts +93 -16
- package/src/context/capabilities.ts +10 -10
- package/src/context/fetcher.ts +9 -10
- package/src/context/reindex.ts +189 -0
- package/src/context/store.ts +630 -0
- package/src/db/doctor.ts +1 -8
- package/src/db/embeddings.ts +227 -175
- package/src/db/sql/19-disk_backed_index.sql +36 -0
- package/src/db/sql/20-drop_db_tables_for_files.sql +19 -0
- package/src/fs/atomic.ts +217 -0
- package/src/fs/compat.ts +86 -0
- package/src/fs/sandbox.ts +279 -0
- package/src/init/index.ts +69 -52
- package/src/init/templates.ts +1 -1
- package/src/mcpx/client.ts +1 -1
- package/src/schedules/schema.ts +19 -0
- package/src/schedules/store.ts +296 -0
- package/src/skills/commands.ts +1 -3
- package/src/tasks/schema.ts +47 -0
- package/src/tasks/store.ts +486 -0
- package/src/threads/store.ts +559 -0
- package/src/tools/capabilities/refresh.ts +42 -21
- package/src/tools/context/pipe.ts +15 -71
- package/src/tools/context/update-beliefs.ts +3 -3
- package/src/tools/context/update-goals.ts +3 -3
- package/src/tools/dir/create.ts +26 -23
- package/src/tools/dir/size.ts +46 -17
- package/src/tools/dir/tree.ts +73 -279
- package/src/tools/file/copy.ts +50 -24
- package/src/tools/file/count-lines.ts +34 -10
- package/src/tools/file/delete.ts +44 -23
- package/src/tools/file/edit.ts +39 -14
- package/src/tools/file/exists.ts +12 -26
- package/src/tools/file/info.ts +25 -85
- package/src/tools/file/move.ts +39 -24
- package/src/tools/file/read.ts +32 -80
- package/src/tools/file/write.ts +14 -91
- package/src/tools/registry.ts +3 -7
- package/src/tools/schedule/create.ts +2 -2
- package/src/tools/schedule/list.ts +7 -3
- package/src/tools/search/fuse.ts +12 -33
- package/src/tools/search/index.ts +36 -43
- package/src/tools/search/regexp.ts +29 -17
- package/src/tools/search/semantic.ts +137 -51
- package/src/tools/skill/delete.ts +1 -1
- package/src/tools/skill/list.ts +1 -1
- package/src/tools/skill/write.ts +1 -1
- package/src/tools/task/create.ts +41 -16
- package/src/tools/task/delete.ts +3 -3
- package/src/tools/task/list.ts +6 -3
- package/src/tools/task/update.ts +31 -9
- package/src/tools/task/view.ts +6 -6
- package/src/tools/thread/list.ts +2 -2
- package/src/tools/thread/search.ts +208 -0
- package/src/tools/thread/view.ts +50 -5
- package/src/tools/worker/spawn.ts +28 -14
- package/src/tui/App.tsx +12 -19
- package/src/tui/components/ContextPanel.tsx +83 -316
- package/src/tui/components/SchedulePanel.tsx +34 -48
- package/src/tui/components/StatusBar.tsx +15 -15
- package/src/tui/components/TaskPanel.tsx +34 -38
- package/src/tui/components/ThreadPanel.tsx +29 -38
- package/src/tui/components/WorkerPanel.tsx +21 -19
- package/src/tui/markdown.ts +2 -8
- package/src/utils/title.ts +5 -7
- package/src/utils/v7-date.ts +47 -0
- package/src/worker/heartbeat.ts +46 -24
- package/src/worker/index.ts +13 -15
- package/src/worker/llm.ts +30 -37
- package/src/worker/prompt.ts +19 -41
- package/src/worker/schedules.ts +48 -69
- package/src/worker/spawn.ts +11 -11
- package/src/worker/tick.ts +39 -43
- package/src/workers/store.ts +247 -0
- package/src/commands/tools.ts +0 -367
- package/src/context/describer.ts +0 -140
- package/src/context/drives.ts +0 -110
- package/src/context/ingest.ts +0 -162
- package/src/context/refresh.ts +0 -183
- package/src/db/context.ts +0 -637
- package/src/db/daemon-state.ts +0 -6
- package/src/db/reembed.ts +0 -113
- package/src/db/schedules.ts +0 -213
- package/src/db/tasks.ts +0 -347
- package/src/db/threads.ts +0 -276
- package/src/db/workers.ts +0 -212
- package/src/tools/context/list-drives.ts +0 -36
- package/src/tools/context/refresh.ts +0 -165
- package/src/tools/context/search.ts +0 -54
package/src/commands/worker.ts
CHANGED
|
@@ -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 "../
|
|
14
|
-
import { logger } from "../utils/logger.ts";
|
|
15
|
-
import { withDb } from "./with-db.ts";
|
|
14
|
+
} from "../workers/store.ts";
|
|
16
15
|
|
|
17
|
-
function formatAge(
|
|
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
|
|
44
|
-
` heartbeat: ${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
|
|
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
|
|
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: {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
export const
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
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,
|
|
65
|
+
return join(projectDir, INDEX_DB_FILENAME);
|
|
31
66
|
}
|
|
32
67
|
|
|
33
68
|
export function getWorkerLogsDir(projectDir: string): string {
|
|
34
|
-
return join(projectDir,
|
|
69
|
+
return join(projectDir, LOGS_DIR);
|
|
35
70
|
}
|
|
36
71
|
|
|
37
|
-
|
|
38
|
-
|
|
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,
|
|
88
|
+
return join(projectDir, CONFIG_DIR, CONFIG_FILENAME);
|
|
43
89
|
}
|
|
44
90
|
|
|
45
91
|
export function getMcpxDir(projectDir: string): string {
|
|
46
|
-
return join(projectDir,
|
|
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,
|
|
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 {
|
|
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,
|
|
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,
|
|
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
|
-
"
|
|
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
|
|
372
|
-
search: "keyword and
|
|
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
|
|
465
|
-
* frontmatter (so a human-edited `loading:` flag survives). On first
|
|
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(
|
|
474
|
+
const filePath = join(getPromptsDir(projectDir), CAPABILITIES_FILENAME);
|
|
475
475
|
const file = Bun.file(filePath);
|
|
476
476
|
|
|
477
477
|
let meta: ContextFileMeta = {
|
package/src/context/fetcher.ts
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
445
|
-
path,
|
|
444
|
+
source: null,
|
|
446
445
|
};
|
|
447
446
|
}
|