botholomew 0.8.8 → 0.8.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botholomew",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "description": "An autonomous AI agent for knowledge work — works your task queue while you sleep.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/chat/agent.ts CHANGED
@@ -65,18 +65,16 @@ export async function buildChatSystemPrompt(
65
65
  keywordSource?: string;
66
66
  dbPath?: string;
67
67
  config?: Required<BotholomewConfig>;
68
+ hasMcpTools?: boolean;
68
69
  },
69
70
  ): Promise<string> {
70
- const parts: string[] = [];
71
-
72
- parts.push(...buildMetaHeader(projectDir));
71
+ let prompt = buildMetaHeader(projectDir);
73
72
 
74
73
  const keywordSource = options?.keywordSource?.trim();
75
74
  const taskKeywords = keywordSource ? extractKeywords(keywordSource) : null;
76
75
 
77
- parts.push(...(await loadPersistentContext(projectDir, taskKeywords)));
76
+ prompt += await loadPersistentContext(projectDir, taskKeywords);
78
77
 
79
- // Relevant context from embeddings search
80
78
  const dbPath = options?.dbPath;
81
79
  const config = options?.config;
82
80
  if (dbPath && config?.openai_api_key && keywordSource) {
@@ -87,14 +85,14 @@ export async function buildChatSystemPrompt(
87
85
  );
88
86
 
89
87
  if (results.length > 0) {
90
- parts.push("## Relevant Context");
88
+ prompt += "## Relevant Context\n";
91
89
  for (const r of results) {
92
90
  const path = r.source_path || r.context_item_id;
93
- parts.push(`### ${r.title} (${path})`);
91
+ prompt += `### ${r.title} (${path})\n`;
94
92
  if (r.chunk_content) {
95
- parts.push(r.chunk_content.slice(0, 1000));
93
+ prompt += `${r.chunk_content.slice(0, 1000)}\n`;
96
94
  }
97
- parts.push("");
95
+ prompt += "\n";
98
96
  }
99
97
  }
100
98
  } catch (err) {
@@ -102,28 +100,30 @@ export async function buildChatSystemPrompt(
102
100
  }
103
101
  }
104
102
 
105
- parts.push("## Instructions");
106
- parts.push(
107
- "You are Botholomew, an AI agent personified by a wise owl. This is your interactive chat interface. Help the user manage tasks, review results from background worker activity, search context, and answer questions.",
108
- );
109
- parts.push(
110
- "You do NOT execute long-running work directly — enqueue tasks for a background worker instead using create_task, and spawn a worker via spawn_worker when the user wants the task run now.",
111
- );
112
- parts.push(
113
- "Use the available tools to look up tasks, threads, schedules, and context when the user asks about them. Context items can be looked up by virtual path or by UUID via `context_info` and refreshed via `context_refresh`.",
114
- );
115
- parts.push(
116
- "When multiple tool calls are independent of each other (i.e., one does not depend on the result of another), call them all in a single response. They will be executed in parallel, which is faster than calling them one at a time.",
117
- );
118
- parts.push(
119
- "You can update the agent's beliefs and goals files when the user asks you to.",
120
- );
121
- parts.push(
122
- "Format your responses using Markdown. Use headings, bold, italic, lists, and code blocks to make your responses clear and well-structured.",
123
- );
124
- parts.push("");
103
+ prompt += `## Instructions
104
+ You are Botholomew, an AI agent personified by a wise owl. This is your interactive chat interface. Help the user manage tasks, review results from background worker activity, search context, and answer questions.
105
+ You do NOT execute long-running work directly enqueue tasks for a background worker instead using create_task, and spawn a worker via spawn_worker when the user wants the task run now.
106
+ Use the available tools to look up tasks, threads, schedules, and context when the user asks about them. Context items can be looked up by virtual path or by UUID via \`context_info\` and refreshed via \`context_refresh\`.
107
+ When multiple tool calls are independent of each other (i.e., one does not depend on the result of another), call them all in a single response. They will be executed in parallel, which is faster than calling them one at a time.
108
+ You can update the agent's beliefs and goals files when the user asks you to.
109
+ Format your responses using Markdown. Use headings, bold, italic, lists, and code blocks to make your responses clear and well-structured.
110
+ `;
111
+
112
+ if (options?.hasMcpTools) {
113
+ prompt += `
114
+ ## External Tools (MCP)
115
+
116
+ You have access to external tools via MCP servers. Before calling any MCP tool you haven't used yet this session, you MUST fetch its schema first:
117
+
118
+ 1. Discover tools with \`mcp_search\` (preferred — semantic) or \`mcp_list_tools\`.
119
+ 2. Call \`mcp_info\` with the exact \`server\` and \`tool\` to read the tool's input schema, required fields, and types.
120
+ 3. Only then call \`mcp_exec\` with arguments that conform to that schema.
121
+
122
+ Skip step 2 only if you already called \`mcp_info\` for that exact server+tool earlier in this conversation. Do not guess arguments from the tool's description alone — descriptions omit types and required/optional markers.
123
+ `;
124
+ }
125
125
 
126
- return parts.join("\n");
126
+ return prompt;
127
127
  }
128
128
 
129
129
  export interface ToolEndMeta {
@@ -202,6 +202,7 @@ export async function runChatTurn(input: {
202
202
  keywordSource,
203
203
  dbPath,
204
204
  config,
205
+ hasMcpTools: mcpxClient != null,
205
206
  });
206
207
 
207
208
  fitToContextWindow(messages, systemPrompt, maxInputTokens);
@@ -36,10 +36,11 @@ export function registerCapabilitiesCommand(program: Command) {
36
36
  });
37
37
  } catch (err) {
38
38
  spinner.error({ text: `Failed: ${(err as Error).message}` });
39
- process.exit(1);
40
- } finally {
41
39
  await mcpxClient?.close();
40
+ process.exit(1);
42
41
  }
42
+ await mcpxClient?.close();
43
+ process.exit(0);
43
44
  }),
44
45
  );
45
46
  }
@@ -268,22 +268,17 @@ async function summarizeViaLLM(
268
268
  const userPrompt = `Summarize this tool inventory. Return via the \`${SUMMARIZE_TOOL_NAME}\` tool.\n\n${renderInventoryForPrompt(inv)}`;
269
269
 
270
270
  try {
271
- const response = await Promise.race([
272
- client.messages.create({
271
+ const response = await client.messages.create(
272
+ {
273
273
  model: config.chunker_model,
274
274
  max_tokens: SUMMARIZE_MAX_TOKENS,
275
275
  system: SUMMARIZE_SYSTEM,
276
276
  tools: [SUMMARIZE_TOOL],
277
277
  tool_choice: { type: "tool", name: SUMMARIZE_TOOL_NAME },
278
278
  messages: [{ role: "user", content: userPrompt }],
279
- }),
280
- new Promise<never>((_, reject) =>
281
- setTimeout(
282
- () => reject(new Error("Capability summarization timeout")),
283
- SUMMARIZE_TIMEOUT_MS,
284
- ),
285
- ),
286
- ]);
279
+ },
280
+ { timeout: SUMMARIZE_TIMEOUT_MS },
281
+ );
287
282
 
288
283
  const toolBlock = response.content.find((b) => b.type === "tool_use");
289
284
  if (!toolBlock || toolBlock.type !== "tool_use") return null;
@@ -29,16 +29,16 @@ export function extractKeywords(text: string): Set<string> {
29
29
  }
30
30
 
31
31
  /**
32
- * Load persistent context files from .botholomew/ directory.
33
- * Returns an array of formatted string sections for "always" loaded files.
34
- * If taskKeywords are provided, also includes "contextual" files that match.
32
+ * Load persistent context files from .botholomew/ directory as a single
33
+ * formatted string. Includes "always" files unconditionally and "contextual"
34
+ * files whose content overlaps the provided taskKeywords.
35
35
  */
36
36
  export async function loadPersistentContext(
37
37
  projectDir: string,
38
38
  taskKeywords?: Set<string> | null,
39
- ): Promise<string[]> {
39
+ ): Promise<string> {
40
40
  const dotDir = getBotholomewDir(projectDir);
41
- const parts: string[] = [];
41
+ let out = "";
42
42
 
43
43
  try {
44
44
  const files = await readdir(dotDir);
@@ -50,18 +50,14 @@ export async function loadPersistentContext(
50
50
  const { meta, content } = parseContextFile(raw);
51
51
 
52
52
  if (meta.loading === "always") {
53
- parts.push(`## ${filename}`);
54
- parts.push(content);
55
- parts.push("");
53
+ out += `## ${filename}\n${content}\n\n`;
56
54
  } else if (meta.loading === "contextual" && taskKeywords) {
57
55
  const contentLower = content.toLowerCase();
58
56
  const hasOverlap = [...taskKeywords].some((kw) =>
59
57
  contentLower.includes(kw),
60
58
  );
61
59
  if (hasOverlap) {
62
- parts.push(`## ${filename} (contextual)`);
63
- parts.push(content);
64
- parts.push("");
60
+ out += `## ${filename} (contextual)\n${content}\n\n`;
65
61
  }
66
62
  }
67
63
  }
@@ -69,21 +65,20 @@ export async function loadPersistentContext(
69
65
  // .botholomew dir might not have md files yet
70
66
  }
71
67
 
72
- return parts;
68
+ return out;
73
69
  }
74
70
 
75
71
  /**
76
72
  * Build common meta header (version, time, OS, user).
77
73
  */
78
- export function buildMetaHeader(projectDir: string): string[] {
79
- return [
80
- `# Botholomew v${pkg.version}`,
81
- `Current time: ${new Date().toISOString()}`,
82
- `Project directory: ${projectDir}`,
83
- `OS: ${process.platform} ${process.arch}`,
84
- `User: ${process.env.USER || process.env.USERNAME || "unknown"}`,
85
- "",
86
- ];
74
+ export function buildMetaHeader(projectDir: string): string {
75
+ return `# Botholomew v${pkg.version}
76
+ Current time: ${new Date().toISOString()}
77
+ Project directory: ${projectDir}
78
+ OS: ${process.platform} ${process.arch}
79
+ User: ${process.env.USER || process.env.USERNAME || "unknown"}
80
+
81
+ `;
87
82
  }
88
83
 
89
84
  export async function buildSystemPrompt(
@@ -93,20 +88,14 @@ export async function buildSystemPrompt(
93
88
  _config?: Required<BotholomewConfig>,
94
89
  options?: { hasMcpTools?: boolean },
95
90
  ): Promise<string> {
96
- const parts: string[] = [];
97
-
98
- // Meta information
99
- parts.push(...buildMetaHeader(projectDir));
91
+ let prompt = buildMetaHeader(projectDir);
100
92
 
101
- // Build keyword set from task for contextual loading
102
93
  const taskKeywords = task
103
94
  ? extractKeywords(`${task.name} ${task.description}`)
104
95
  : null;
105
96
 
106
- // Load context files from .botholomew/
107
- parts.push(...(await loadPersistentContext(projectDir, taskKeywords)));
97
+ prompt += await loadPersistentContext(projectDir, taskKeywords);
108
98
 
109
- // Relevant context from embeddings search
110
99
  if (task && dbPath && _config?.openai_api_key) {
111
100
  try {
112
101
  const query = `${task.name} ${task.description}`;
@@ -116,14 +105,14 @@ export async function buildSystemPrompt(
116
105
  );
117
106
 
118
107
  if (results.length > 0) {
119
- parts.push("## Relevant Context");
108
+ prompt += "## Relevant Context\n";
120
109
  for (const r of results) {
121
110
  const path = r.source_path || r.context_item_id;
122
- parts.push(`### ${r.title} (${path})`);
111
+ prompt += `### ${r.title} (${path})\n`;
123
112
  if (r.chunk_content) {
124
- parts.push(r.chunk_content.slice(0, 1000));
113
+ prompt += `${r.chunk_content.slice(0, 1000)}\n`;
125
114
  }
126
- parts.push("");
115
+ prompt += "\n";
127
116
  }
128
117
  }
129
118
  } catch (err) {
@@ -131,19 +120,25 @@ export async function buildSystemPrompt(
131
120
  }
132
121
  }
133
122
 
134
- // Instructions
135
- parts.push("## Instructions");
136
- parts.push(
137
- "You are Botholomew, a wise-owl worker that works through tasks. Use available tools to complete your assigned task, then call complete_task, fail_task, or wait_task. Use create_task for subtasks and update_task to refine pending tasks. Batch independent tool calls in a single response for parallel execution.\n\nWhen calling complete_task, write a summary that captures your key findings, decisions, and outputs. This summary becomes the task's output and is provided to any downstream tasks that depend on this one. Include specific results (data, names, paths, conclusions) rather than vague descriptions of what you did — downstream tasks will rely on this information to do their work.",
138
- );
123
+ prompt += `## Instructions
124
+ You are Botholomew, a wise-owl worker that works through tasks. Use available tools to complete your assigned task, then call complete_task, fail_task, or wait_task. Use create_task for subtasks and update_task to refine pending tasks. Batch independent tool calls in a single response for parallel execution.
125
+
126
+ When calling complete_task, write a summary that captures your key findings, decisions, and outputs. This summary becomes the task's output and is provided to any downstream tasks that depend on this one. Include specific results (data, names, paths, conclusions) rather than vague descriptions of what you did — downstream tasks will rely on this information to do their work.
127
+ `;
128
+
139
129
  if (options?.hasMcpTools) {
140
- parts.push("");
141
- parts.push("## External Tools (MCP)");
142
- parts.push(
143
- "You have access to external tools via MCP servers. Use `mcp_list_tools` or `mcp_search` to discover available tools, `mcp_info` to get a tool's input schema, then `mcp_exec` to call them.",
144
- );
130
+ prompt += `
131
+ ## External Tools (MCP)
132
+
133
+ You have access to external tools via MCP servers. Before calling any MCP tool you haven't used yet this session, you MUST fetch its schema first:
134
+
135
+ 1. Discover tools with \`mcp_search\` (preferred — semantic) or \`mcp_list_tools\`.
136
+ 2. Call \`mcp_info\` with the exact \`server\` and \`tool\` to read the tool's input schema, required fields, and types.
137
+ 3. Only then call \`mcp_exec\` with arguments that conform to that schema.
138
+
139
+ Skip step 2 only if you already called \`mcp_info\` for that exact server+tool earlier in this conversation. Do not guess arguments from the tool's description alone — descriptions omit types and required/optional markers.
140
+ `;
145
141
  }
146
- parts.push("");
147
142
 
148
- return parts.join("\n");
143
+ return prompt;
149
144
  }