botholomew 0.7.8 → 0.7.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/README.md +2 -0
- package/package.json +1 -1
- package/src/chat/agent.ts +68 -35
- package/src/chat/session.ts +27 -31
- package/src/commands/context.ts +168 -12
- package/src/commands/daemon.ts +15 -2
- package/src/commands/schedule.ts +10 -1
- package/src/commands/skill.ts +18 -3
- package/src/commands/task.ts +28 -4
- package/src/commands/thread.ts +3 -1
- package/src/commands/tools.ts +2 -0
- package/src/commands/with-db.ts +8 -9
- package/src/context/fetcher.ts +1 -0
- package/src/context/ingest.ts +3 -1
- package/src/daemon/index.ts +6 -5
- package/src/daemon/llm.ts +68 -42
- package/src/daemon/prompt.ts +6 -4
- package/src/daemon/schedules.ts +15 -10
- package/src/daemon/tick.ts +54 -38
- package/src/db/connection.ts +143 -14
- package/src/db/context.ts +13 -0
- package/src/db/schedules.ts +7 -3
- package/src/db/tasks.ts +4 -4
- package/src/db/threads.ts +6 -4
- package/src/tools/tool.ts +8 -0
- package/src/tui/App.tsx +16 -11
- package/src/tui/components/ContextPanel.tsx +19 -15
- package/src/tui/components/SchedulePanel.tsx +15 -9
- package/src/tui/components/StatusBar.tsx +8 -6
- package/src/tui/components/TaskPanel.tsx +6 -6
- package/src/tui/components/ThreadPanel.tsx +29 -19
- package/src/utils/title.ts +5 -3
package/src/commands/skill.ts
CHANGED
|
@@ -27,7 +27,9 @@ export function registerSkillCommand(program: Command) {
|
|
|
27
27
|
skill
|
|
28
28
|
.command("list")
|
|
29
29
|
.description("List all skills loaded from .botholomew/skills/")
|
|
30
|
-
.
|
|
30
|
+
.option("-l, --limit <n>", "max number of skills", Number.parseInt)
|
|
31
|
+
.option("-o, --offset <n>", "skip first N skills", Number.parseInt)
|
|
32
|
+
.action(async (opts: { limit?: number; offset?: number }) => {
|
|
31
33
|
const dir = program.opts().dir;
|
|
32
34
|
const skills = await loadSkills(dir);
|
|
33
35
|
|
|
@@ -39,12 +41,21 @@ export function registerSkillCommand(program: Command) {
|
|
|
39
41
|
const sorted = [...skills.values()].sort((a, b) =>
|
|
40
42
|
a.name.localeCompare(b.name),
|
|
41
43
|
);
|
|
44
|
+
const total = sorted.length;
|
|
45
|
+
const start = opts.offset ?? 0;
|
|
46
|
+
const end = opts.limit ? start + opts.limit : undefined;
|
|
47
|
+
const page = sorted.slice(start, end);
|
|
48
|
+
|
|
49
|
+
if (page.length === 0) {
|
|
50
|
+
logger.dim(`No skills on this page (total: ${total}).`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
42
53
|
|
|
43
54
|
const header = `${ansis.bold("Name".padEnd(20))} ${ansis.bold("Description".padEnd(40))} ${ansis.bold("Args".padEnd(20))} ${ansis.bold("Path")}`;
|
|
44
55
|
console.log(header);
|
|
45
56
|
console.log("-".repeat(header.length));
|
|
46
57
|
|
|
47
|
-
for (const s of
|
|
58
|
+
for (const s of page) {
|
|
48
59
|
const name = s.name.padEnd(20);
|
|
49
60
|
const desc = s.description
|
|
50
61
|
? s.description.slice(0, 39).padEnd(40)
|
|
@@ -61,7 +72,11 @@ export function registerSkillCommand(program: Command) {
|
|
|
61
72
|
console.log(`${name} ${desc} ${args} ${path}`);
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
const footer =
|
|
76
|
+
page.length === total
|
|
77
|
+
? `${total} skill(s)`
|
|
78
|
+
: `showing ${page.length} of ${total} skill(s)`;
|
|
79
|
+
console.log(`\n${ansis.dim(footer)}`);
|
|
65
80
|
});
|
|
66
81
|
|
|
67
82
|
skill
|
package/src/commands/task.ts
CHANGED
|
@@ -17,16 +17,18 @@ export function registerTaskCommand(program: Command) {
|
|
|
17
17
|
|
|
18
18
|
task
|
|
19
19
|
.command("list")
|
|
20
|
-
.description("List all tasks")
|
|
20
|
+
.description("List all tasks (newest first)")
|
|
21
21
|
.option("-s, --status <status>", "filter by status")
|
|
22
22
|
.option("-p, --priority <priority>", "filter by priority")
|
|
23
|
-
.option("-l, --limit <n>", "max number of tasks", parseInt)
|
|
23
|
+
.option("-l, --limit <n>", "max number of tasks", Number.parseInt)
|
|
24
|
+
.option("-o, --offset <n>", "skip first N tasks", Number.parseInt)
|
|
24
25
|
.action((opts) =>
|
|
25
26
|
withDb(program, async (conn) => {
|
|
26
27
|
const tasks = await listTasks(conn, {
|
|
27
28
|
status: opts.status,
|
|
28
29
|
priority: opts.priority,
|
|
29
30
|
limit: opts.limit,
|
|
31
|
+
offset: opts.offset,
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
if (tasks.length === 0) {
|
|
@@ -34,9 +36,15 @@ export function registerTaskCommand(program: Command) {
|
|
|
34
36
|
return;
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
const header = `${ansis.bold("ID".padEnd(36))} ${ansis.bold("Status".padEnd(11))} ${ansis.bold("Priority".padEnd(6))} ${ansis.bold("Created".padEnd(19))} ${ansis.bold("Updated".padEnd(19))} ${ansis.bold("Name")}`;
|
|
40
|
+
console.log(header);
|
|
41
|
+
console.log("-".repeat(120));
|
|
42
|
+
|
|
37
43
|
for (const t of tasks) {
|
|
38
44
|
printTask(t);
|
|
39
45
|
}
|
|
46
|
+
|
|
47
|
+
console.log(`\n${ansis.dim(`${tasks.length} task(s)`)}`);
|
|
40
48
|
}),
|
|
41
49
|
);
|
|
42
50
|
|
|
@@ -154,10 +162,26 @@ function priorityColor(priority: Task["priority"]): string {
|
|
|
154
162
|
}
|
|
155
163
|
}
|
|
156
164
|
|
|
165
|
+
function formatTime(date: Date): string {
|
|
166
|
+
return date
|
|
167
|
+
.toISOString()
|
|
168
|
+
.replace("T", " ")
|
|
169
|
+
.replace(/\.\d{3}Z$/, "");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function padColored(colored: string, raw: string, width: number): string {
|
|
173
|
+
const padding = Math.max(0, width - raw.length);
|
|
174
|
+
return colored + " ".repeat(padding);
|
|
175
|
+
}
|
|
176
|
+
|
|
157
177
|
function printTask(t: Task) {
|
|
158
|
-
const id = ansis.dim(t.id);
|
|
178
|
+
const id = ansis.dim(t.id.padEnd(36));
|
|
179
|
+
const status = padColored(statusColor(t.status), t.status, 11);
|
|
180
|
+
const priority = padColored(priorityColor(t.priority), t.priority, 6);
|
|
181
|
+
const created = ansis.dim(formatTime(t.created_at).padEnd(19));
|
|
182
|
+
const updated = ansis.dim(formatTime(t.updated_at).padEnd(19));
|
|
159
183
|
console.log(
|
|
160
|
-
|
|
184
|
+
`${id} ${status} ${priority} ${created} ${updated} ${t.name}`,
|
|
161
185
|
);
|
|
162
186
|
}
|
|
163
187
|
|
package/src/commands/thread.ts
CHANGED
|
@@ -19,12 +19,14 @@ export function registerThreadCommand(program: Command) {
|
|
|
19
19
|
.command("list")
|
|
20
20
|
.description("List threads")
|
|
21
21
|
.option("-t, --type <type>", "filter by type (daemon_tick, chat_session)")
|
|
22
|
-
.option("-l, --limit <n>", "max number of threads", parseInt)
|
|
22
|
+
.option("-l, --limit <n>", "max number of threads", Number.parseInt)
|
|
23
|
+
.option("-o, --offset <n>", "skip first N threads", Number.parseInt)
|
|
23
24
|
.action((opts) =>
|
|
24
25
|
withDb(program, async (conn) => {
|
|
25
26
|
const threads = await listThreads(conn, {
|
|
26
27
|
type: opts.type,
|
|
27
28
|
limit: opts.limit,
|
|
29
|
+
offset: opts.offset,
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
if (threads.length === 0) {
|
package/src/commands/tools.ts
CHANGED
|
@@ -2,6 +2,7 @@ import ansis from "ansis";
|
|
|
2
2
|
import type { Command } from "commander";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { loadConfig } from "../config/loader.ts";
|
|
5
|
+
import { getDbPath } from "../constants.ts";
|
|
5
6
|
import { registerAllTools } from "../tools/registry.ts";
|
|
6
7
|
import {
|
|
7
8
|
type AnyToolDefinition,
|
|
@@ -112,6 +113,7 @@ function registerToolAsCLI(parent: Command, tool: AnyToolDefinition) {
|
|
|
112
113
|
|
|
113
114
|
const ctx: ToolContext = {
|
|
114
115
|
conn,
|
|
116
|
+
dbPath: getDbPath(dir),
|
|
115
117
|
projectDir: dir,
|
|
116
118
|
config: await loadConfig(dir),
|
|
117
119
|
mcpxClient: null,
|
package/src/commands/with-db.ts
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { getDbPath } from "../constants.ts";
|
|
3
3
|
import type { DbConnection } from "../db/connection.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { withDb as coreWithDb } from "../db/connection.ts";
|
|
5
5
|
import { migrate } from "../db/schema.ts";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Open a migrated DB connection from the CLI --dir flag, run the callback,
|
|
9
|
-
* and guarantee the connection is closed afterward.
|
|
9
|
+
* and guarantee the connection is closed afterward. Retries on lock
|
|
10
|
+
* conflicts so CLI invocations cooperate with a running daemon/chat.
|
|
10
11
|
*/
|
|
11
12
|
export async function withDb<T>(
|
|
12
13
|
program: Command,
|
|
13
14
|
fn: (conn: DbConnection, dir: string) => Promise<T>,
|
|
14
15
|
): Promise<T> {
|
|
15
16
|
const dir = program.opts().dir;
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return
|
|
20
|
-
}
|
|
21
|
-
conn.close();
|
|
22
|
-
}
|
|
17
|
+
const dbPath = getDbPath(dir);
|
|
18
|
+
return coreWithDb(dbPath, async (conn) => {
|
|
19
|
+
await migrate(conn);
|
|
20
|
+
return fn(conn, dir);
|
|
21
|
+
});
|
|
23
22
|
}
|
package/src/context/fetcher.ts
CHANGED
package/src/context/ingest.ts
CHANGED
|
@@ -79,7 +79,9 @@ export interface IngestionResult {
|
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* Store a prepared ingestion into the database.
|
|
82
|
-
*
|
|
82
|
+
* All statements in BEGIN/COMMIT/ROLLBACK must share one connection, so the
|
|
83
|
+
* caller must pass a connection that lives long enough for the transaction
|
|
84
|
+
* (the tool executor wraps each tool call in `withDb`, which satisfies this).
|
|
83
85
|
*/
|
|
84
86
|
export async function storeIngestion(
|
|
85
87
|
conn: DbConnection,
|
package/src/daemon/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ansis from "ansis";
|
|
2
2
|
import { loadConfig } from "../config/loader.ts";
|
|
3
3
|
import { getDbPath } from "../constants.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { withDb } from "../db/connection.ts";
|
|
5
5
|
import { migrate } from "../db/schema.ts";
|
|
6
6
|
import { createMcpxClient } from "../mcpx/client.ts";
|
|
7
7
|
import { logger } from "../utils/logger.ts";
|
|
@@ -49,8 +49,10 @@ export async function startDaemon(
|
|
|
49
49
|
): Promise<void> {
|
|
50
50
|
const config = await loadConfig(projectDir);
|
|
51
51
|
const dbPath = getDbPath(projectDir);
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
|
|
53
|
+
// One short-lived connection to apply migrations. After this returns,
|
|
54
|
+
// the file lock is released so CLI invocations can run freely.
|
|
55
|
+
await withDb(dbPath, (conn) => migrate(conn));
|
|
54
56
|
|
|
55
57
|
// Initialize MCPX client for external tool access
|
|
56
58
|
const mcpxClient = await createMcpxClient(projectDir);
|
|
@@ -64,7 +66,6 @@ export async function startDaemon(
|
|
|
64
66
|
logger.info("Daemon shutting down...");
|
|
65
67
|
await mcpxClient?.close();
|
|
66
68
|
await removePidFile(projectDir);
|
|
67
|
-
conn.close();
|
|
68
69
|
process.exit(0);
|
|
69
70
|
};
|
|
70
71
|
|
|
@@ -87,7 +88,7 @@ export async function startDaemon(
|
|
|
87
88
|
try {
|
|
88
89
|
didWork = await tick(
|
|
89
90
|
projectDir,
|
|
90
|
-
|
|
91
|
+
dbPath,
|
|
91
92
|
config,
|
|
92
93
|
mcpxClient,
|
|
93
94
|
callbacks,
|
package/src/daemon/llm.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
} from "@anthropic-ai/sdk/resources/messages";
|
|
8
8
|
import type { McpxClient } from "@evantahler/mcpx";
|
|
9
9
|
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
10
|
-
import
|
|
10
|
+
import { withDb } from "../db/connection.ts";
|
|
11
11
|
import { getTask, type Task } from "../db/tasks.ts";
|
|
12
12
|
import { logInteraction } from "../db/threads.ts";
|
|
13
13
|
import { registerAllTools } from "../tools/registry.ts";
|
|
@@ -44,32 +44,32 @@ export async function runAgentLoop(input: {
|
|
|
44
44
|
systemPrompt: string;
|
|
45
45
|
task: Task;
|
|
46
46
|
config: Required<BotholomewConfig>;
|
|
47
|
-
|
|
47
|
+
dbPath: string;
|
|
48
48
|
threadId: string;
|
|
49
49
|
projectDir: string;
|
|
50
50
|
mcpxClient?: McpxClient | null;
|
|
51
51
|
callbacks?: DaemonStreamCallbacks;
|
|
52
52
|
}): Promise<AgentLoopResult> {
|
|
53
|
-
const {
|
|
54
|
-
|
|
53
|
+
const {
|
|
54
|
+
systemPrompt,
|
|
55
|
+
task,
|
|
56
|
+
config,
|
|
57
|
+
dbPath,
|
|
58
|
+
threadId,
|
|
59
|
+
projectDir,
|
|
60
|
+
callbacks,
|
|
61
|
+
} = input;
|
|
55
62
|
|
|
56
63
|
const client = new Anthropic({
|
|
57
64
|
apiKey: config.anthropic_api_key || undefined,
|
|
58
65
|
});
|
|
59
66
|
|
|
60
|
-
const toolCtx: ToolContext = {
|
|
61
|
-
conn,
|
|
62
|
-
projectDir,
|
|
63
|
-
config,
|
|
64
|
-
mcpxClient: input.mcpxClient ?? null,
|
|
65
|
-
};
|
|
66
|
-
|
|
67
67
|
// Build predecessor context from completed blocking tasks
|
|
68
68
|
let predecessorContext = "";
|
|
69
69
|
if (task.blocked_by.length > 0) {
|
|
70
70
|
const predecessorOutputs: string[] = [];
|
|
71
71
|
for (const blockerId of task.blocked_by) {
|
|
72
|
-
const blocker = await getTask(conn, blockerId);
|
|
72
|
+
const blocker = await withDb(dbPath, (conn) => getTask(conn, blockerId));
|
|
73
73
|
if (blocker?.output) {
|
|
74
74
|
predecessorOutputs.push(
|
|
75
75
|
`### ${blocker.name} (${blocker.id})\n${blocker.output}`,
|
|
@@ -86,11 +86,13 @@ export async function runAgentLoop(input: {
|
|
|
86
86
|
const messages: MessageParam[] = [{ role: "user", content: userMessage }];
|
|
87
87
|
|
|
88
88
|
// Log the initial user message
|
|
89
|
-
await
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
await withDb(dbPath, (conn) =>
|
|
90
|
+
logInteraction(conn, threadId, {
|
|
91
|
+
role: "user",
|
|
92
|
+
kind: "message",
|
|
93
|
+
content: userMessage,
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
94
96
|
|
|
95
97
|
clearLargeResults();
|
|
96
98
|
const daemonTools = toAnthropicTools();
|
|
@@ -144,13 +146,15 @@ export async function runAgentLoop(input: {
|
|
|
144
146
|
// Log assistant text blocks
|
|
145
147
|
for (const block of response.content) {
|
|
146
148
|
if (block.type === "text" && block.text) {
|
|
147
|
-
await
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
149
|
+
await withDb(dbPath, (conn) =>
|
|
150
|
+
logInteraction(conn, threadId, {
|
|
151
|
+
role: "assistant",
|
|
152
|
+
kind: "message",
|
|
153
|
+
content: block.text,
|
|
154
|
+
durationMs,
|
|
155
|
+
tokenCount,
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
154
158
|
}
|
|
155
159
|
}
|
|
156
160
|
|
|
@@ -173,20 +177,30 @@ export async function runAgentLoop(input: {
|
|
|
173
177
|
for (const toolUse of toolUseBlocks) {
|
|
174
178
|
const toolInput = JSON.stringify(toolUse.input);
|
|
175
179
|
callbacks?.onToolStart(toolUse.name, toolInput);
|
|
176
|
-
await
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
await withDb(dbPath, (conn) =>
|
|
181
|
+
logInteraction(conn, threadId, {
|
|
182
|
+
role: "assistant",
|
|
183
|
+
kind: "tool_use",
|
|
184
|
+
content: `Calling ${toolUse.name}`,
|
|
185
|
+
toolName: toolUse.name,
|
|
186
|
+
toolInput,
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
183
189
|
}
|
|
184
190
|
|
|
185
|
-
// Execute all tools in parallel
|
|
191
|
+
// Execute all tools in parallel. Each tool call opens its own short-lived
|
|
192
|
+
// connection (or none, if the tool uses dbPath internally) via
|
|
193
|
+
// executeToolCall — so parallel tool calls share the process-local
|
|
194
|
+
// DuckDB instance and release the file lock as soon as they finish.
|
|
186
195
|
const execResults = await Promise.all(
|
|
187
196
|
toolUseBlocks.map(async (toolUse) => {
|
|
188
197
|
const start = Date.now();
|
|
189
|
-
const result = await executeToolCall(toolUse,
|
|
198
|
+
const result = await executeToolCall(toolUse, {
|
|
199
|
+
dbPath,
|
|
200
|
+
projectDir,
|
|
201
|
+
config,
|
|
202
|
+
mcpxClient: input.mcpxClient ?? null,
|
|
203
|
+
});
|
|
190
204
|
const elapsed = Date.now() - start;
|
|
191
205
|
callbacks?.onToolEnd(
|
|
192
206
|
toolUse.name,
|
|
@@ -201,13 +215,15 @@ export async function runAgentLoop(input: {
|
|
|
201
215
|
// Log results and collect tool_result messages
|
|
202
216
|
const toolResults: ToolResultBlockParam[] = [];
|
|
203
217
|
for (const { toolUse, result, durationMs } of execResults) {
|
|
204
|
-
await
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
await withDb(dbPath, (conn) =>
|
|
219
|
+
logInteraction(conn, threadId, {
|
|
220
|
+
role: "tool",
|
|
221
|
+
kind: "tool_result",
|
|
222
|
+
content: result.output,
|
|
223
|
+
toolName: toolUse.name,
|
|
224
|
+
durationMs,
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
211
227
|
|
|
212
228
|
if (result.terminal && result.agentResult) {
|
|
213
229
|
return result.agentResult;
|
|
@@ -234,9 +250,16 @@ interface ToolCallResult {
|
|
|
234
250
|
agentResult?: AgentLoopResult;
|
|
235
251
|
}
|
|
236
252
|
|
|
253
|
+
interface ToolCallCtx {
|
|
254
|
+
dbPath: string;
|
|
255
|
+
projectDir: string;
|
|
256
|
+
config: Required<BotholomewConfig>;
|
|
257
|
+
mcpxClient: McpxClient | null;
|
|
258
|
+
}
|
|
259
|
+
|
|
237
260
|
async function executeToolCall(
|
|
238
261
|
toolUse: ToolUseBlock,
|
|
239
|
-
|
|
262
|
+
baseCtx: ToolCallCtx,
|
|
240
263
|
): Promise<ToolCallResult> {
|
|
241
264
|
const tool = getTool(toolUse.name);
|
|
242
265
|
if (!tool) {
|
|
@@ -261,7 +284,10 @@ async function executeToolCall(
|
|
|
261
284
|
|
|
262
285
|
let result: unknown;
|
|
263
286
|
try {
|
|
264
|
-
result = await
|
|
287
|
+
result = await withDb(baseCtx.dbPath, (conn) => {
|
|
288
|
+
const ctx: ToolContext = { ...baseCtx, conn };
|
|
289
|
+
return tool.execute(parsed.data, ctx);
|
|
290
|
+
});
|
|
265
291
|
} catch (err) {
|
|
266
292
|
return {
|
|
267
293
|
output: `Tool ${toolUse.name} threw an error: ${err}. You may retry with different parameters or try an alternative approach.`,
|
package/src/daemon/prompt.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
4
4
|
import { getBotholomewDir } from "../constants.ts";
|
|
5
5
|
import { embedSingle } from "../context/embedder.ts";
|
|
6
|
-
import
|
|
6
|
+
import { withDb } from "../db/connection.ts";
|
|
7
7
|
import { hybridSearch } from "../db/embeddings.ts";
|
|
8
8
|
import type { Task } from "../db/tasks.ts";
|
|
9
9
|
import { parseContextFile } from "../utils/frontmatter.ts";
|
|
@@ -89,7 +89,7 @@ export function buildMetaHeader(projectDir: string): string[] {
|
|
|
89
89
|
export async function buildSystemPrompt(
|
|
90
90
|
projectDir: string,
|
|
91
91
|
task?: Task,
|
|
92
|
-
|
|
92
|
+
dbPath?: string,
|
|
93
93
|
_config?: Required<BotholomewConfig>,
|
|
94
94
|
options?: { hasMcpTools?: boolean },
|
|
95
95
|
): Promise<string> {
|
|
@@ -107,11 +107,13 @@ export async function buildSystemPrompt(
|
|
|
107
107
|
parts.push(...(await loadPersistentContext(projectDir, taskKeywords)));
|
|
108
108
|
|
|
109
109
|
// Relevant context from embeddings search
|
|
110
|
-
if (task &&
|
|
110
|
+
if (task && dbPath && _config?.openai_api_key) {
|
|
111
111
|
try {
|
|
112
112
|
const query = `${task.name} ${task.description}`;
|
|
113
113
|
const queryVec = await embedSingle(query, _config);
|
|
114
|
-
const results = await
|
|
114
|
+
const results = await withDb(dbPath, (conn) =>
|
|
115
|
+
hybridSearch(conn, query, queryVec, 5),
|
|
116
|
+
);
|
|
115
117
|
|
|
116
118
|
if (results.length > 0) {
|
|
117
119
|
parts.push("## Relevant Context");
|
package/src/daemon/schedules.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Anthropic from "@anthropic-ai/sdk";
|
|
2
2
|
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
3
|
-
import
|
|
3
|
+
import { withDb } from "../db/connection.ts";
|
|
4
4
|
import {
|
|
5
5
|
listSchedules,
|
|
6
6
|
markScheduleRun,
|
|
@@ -105,14 +105,17 @@ Is this schedule due to run? If yes, what tasks should be created?`;
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
export async function processSchedules(
|
|
108
|
-
|
|
108
|
+
dbPath: string,
|
|
109
109
|
config: Required<BotholomewConfig>,
|
|
110
110
|
): Promise<void> {
|
|
111
|
-
const schedules = await
|
|
111
|
+
const schedules = await withDb(dbPath, (conn) =>
|
|
112
|
+
listSchedules(conn, { enabled: true }),
|
|
113
|
+
);
|
|
112
114
|
if (schedules.length === 0) return;
|
|
113
115
|
|
|
114
116
|
for (const schedule of schedules) {
|
|
115
117
|
try {
|
|
118
|
+
// LLM evaluation does no DB work — no connection held here.
|
|
116
119
|
const evaluation = await evaluateSchedule(config, schedule);
|
|
117
120
|
|
|
118
121
|
if (!evaluation.isDue) {
|
|
@@ -128,16 +131,18 @@ export async function processSchedules(
|
|
|
128
131
|
.map((i: number) => createdIds[i])
|
|
129
132
|
.filter(Boolean) as string[];
|
|
130
133
|
|
|
131
|
-
const task = await
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
const task = await withDb(dbPath, (conn) =>
|
|
135
|
+
createTask(conn, {
|
|
136
|
+
name: taskDef.name,
|
|
137
|
+
description: taskDef.description,
|
|
138
|
+
priority: taskDef.priority,
|
|
139
|
+
blocked_by: blockedBy,
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
137
142
|
createdIds.push(task.id);
|
|
138
143
|
}
|
|
139
144
|
|
|
140
|
-
await markScheduleRun(conn, schedule.id);
|
|
145
|
+
await withDb(dbPath, (conn) => markScheduleRun(conn, schedule.id));
|
|
141
146
|
logger.info(
|
|
142
147
|
`Schedule "${schedule.name}" fired, created ${createdIds.length} task(s)`,
|
|
143
148
|
);
|