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.
- package/README.md +46 -41
- package/package.json +4 -9
- package/src/chat/agent.ts +37 -40
- package/src/chat/session.ts +10 -10
- package/src/cli.ts +0 -2
- package/src/commands/capabilities.ts +35 -33
- package/src/commands/context.ts +133 -221
- package/src/commands/init.ts +22 -1
- package/src/commands/mcpx.ts +21 -8
- package/src/commands/nuke.ts +52 -15
- package/src/commands/prepare.ts +16 -13
- package/src/config/loader.ts +1 -8
- package/src/config/schemas.ts +6 -0
- package/src/constants.ts +16 -32
- package/src/init/index.ts +52 -27
- package/src/mcpx/client.ts +21 -5
- package/src/mem/client.ts +33 -0
- package/src/{context → prompts}/capabilities.ts +11 -7
- package/src/schedules/store.ts +1 -1
- package/src/tasks/store.ts +1 -1
- package/src/threads/store.ts +1 -1
- package/src/tools/capabilities/refresh.ts +1 -1
- package/src/tools/membot/adapter.ts +111 -0
- package/src/tools/membot/copy.ts +59 -0
- package/src/tools/membot/count_lines.ts +53 -0
- package/src/tools/membot/edit.ts +72 -0
- package/src/tools/membot/exists.ts +54 -0
- package/src/tools/membot/index.ts +26 -0
- package/src/tools/{context → membot}/pipe.ts +34 -32
- package/src/tools/registry.ts +6 -37
- package/src/tools/tool.ts +6 -8
- package/src/tui/App.tsx +3 -4
- package/src/tui/components/ContextPanel.tsx +109 -226
- package/src/tui/components/HelpPanel.tsx +2 -2
- package/src/tui/components/StatusBar.tsx +0 -6
- package/src/tui/components/ThreadPanel.tsx +8 -7
- package/src/tui/wrapDetail.ts +11 -0
- package/src/worker/heartbeat.ts +0 -20
- package/src/worker/index.ts +13 -13
- package/src/worker/llm.ts +7 -9
- package/src/worker/prompt.ts +25 -13
- package/src/worker/spawn.ts +1 -1
- package/src/worker/tick.ts +10 -9
- package/src/commands/db.ts +0 -119
- package/src/commands/with-db.ts +0 -22
- package/src/context/chunker.ts +0 -275
- package/src/context/embedder-impl.ts +0 -100
- package/src/context/embedder.ts +0 -9
- package/src/context/fetcher-errors.ts +0 -8
- package/src/context/fetcher.ts +0 -515
- package/src/context/locks.ts +0 -146
- package/src/context/markdown-converter.ts +0 -186
- package/src/context/reindex.ts +0 -198
- package/src/context/store.ts +0 -841
- package/src/context/url-utils.ts +0 -25
- package/src/db/connection.ts +0 -255
- package/src/db/doctor.ts +0 -235
- package/src/db/embeddings.ts +0 -317
- package/src/db/query.ts +0 -56
- package/src/db/schema.ts +0 -93
- package/src/db/sql/1-core_tables.sql +0 -53
- package/src/db/sql/10-dedupe_context_items.sql +0 -26
- package/src/db/sql/11-rebuild_hnsw.sql +0 -8
- package/src/db/sql/12-workers.sql +0 -66
- package/src/db/sql/13-drive-paths.sql +0 -47
- package/src/db/sql/14-drop_hnsw_index.sql +0 -8
- package/src/db/sql/15-fts_index.sql +0 -8
- package/src/db/sql/16-source_url.sql +0 -7
- package/src/db/sql/17-worker_log_path.sql +0 -3
- package/src/db/sql/18-reset_embeddings_for_local.sql +0 -39
- package/src/db/sql/19-disk_backed_index.sql +0 -36
- package/src/db/sql/2-logging_tables.sql +0 -24
- package/src/db/sql/20-drop_db_tables_for_files.sql +0 -19
- package/src/db/sql/3-daemon_state.sql +0 -5
- package/src/db/sql/4-unique_context_path.sql +0 -1
- package/src/db/sql/5-reset_embeddings_for_openai.sql +0 -1
- package/src/db/sql/6-vss_index.sql +0 -7
- package/src/db/sql/7-drop_embeddings_fk.sql +0 -23
- package/src/db/sql/8-task_output.sql +0 -1
- package/src/db/sql/9-source-type.sql +0 -1
- package/src/tools/context/read-large-result.ts +0 -33
- package/src/tools/dir/create.ts +0 -47
- package/src/tools/dir/size.ts +0 -77
- package/src/tools/dir/tree.ts +0 -124
- package/src/tools/file/copy.ts +0 -73
- package/src/tools/file/count-lines.ts +0 -54
- package/src/tools/file/delete.ts +0 -83
- package/src/tools/file/edit.ts +0 -76
- package/src/tools/file/exists.ts +0 -33
- package/src/tools/file/info.ts +0 -66
- package/src/tools/file/move.ts +0 -66
- package/src/tools/file/read.ts +0 -67
- package/src/tools/file/write.ts +0 -58
- package/src/tools/search/fuse.ts +0 -96
- package/src/tools/search/index.ts +0 -127
- package/src/tools/search/regexp.ts +0 -82
- package/src/tools/search/semantic.ts +0 -167
- /package/src/{db → utils}/uuid.ts +0 -0
package/src/worker/index.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { hostname } from "node:os";
|
|
2
2
|
import ansis from "ansis";
|
|
3
3
|
import { loadConfig } from "../config/loader.ts";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { migrate } from "../db/schema.ts";
|
|
7
|
-
import { uuidv7 } from "../db/uuid.ts";
|
|
8
|
-
import { createMcpxClient } from "../mcpx/client.ts";
|
|
4
|
+
import { createMcpxClient, resolveMcpxDir } from "../mcpx/client.ts";
|
|
5
|
+
import { openMembot, resolveMembotDir } from "../mem/client.ts";
|
|
9
6
|
import { logger } from "../utils/logger.ts";
|
|
7
|
+
import { uuidv7 } from "../utils/uuid.ts";
|
|
10
8
|
import { markWorkerStopped, registerWorker } from "../workers/store.ts";
|
|
11
9
|
import { startHeartbeat, startReaper } from "./heartbeat.ts";
|
|
12
10
|
import type { WorkerStreamCallbacks } from "./llm.ts";
|
|
@@ -89,12 +87,12 @@ export async function startWorker(
|
|
|
89
87
|
const evalSchedules = options.evalSchedules ?? !taskId;
|
|
90
88
|
|
|
91
89
|
const config = await loadConfig(projectDir);
|
|
92
|
-
const
|
|
90
|
+
const mem = openMembot(resolveMembotDir(projectDir, config));
|
|
91
|
+
// Surface init-time failures (bad config, locked DB) up front rather than
|
|
92
|
+
// letting the first tool call do it.
|
|
93
|
+
await mem.connect();
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
await withDb(dbPath, (conn) => migrate(conn));
|
|
96
|
-
|
|
97
|
-
const mcpxClient = await createMcpxClient(projectDir);
|
|
95
|
+
const mcpxClient = await createMcpxClient(resolveMcpxDir(projectDir, config));
|
|
98
96
|
if (mcpxClient) {
|
|
99
97
|
logger.info("MCPX client initialized with external tools");
|
|
100
98
|
}
|
|
@@ -129,6 +127,7 @@ export async function startWorker(
|
|
|
129
127
|
stopHeartbeat();
|
|
130
128
|
stopReaper();
|
|
131
129
|
await mcpxClient?.close();
|
|
130
|
+
await mem.close();
|
|
132
131
|
try {
|
|
133
132
|
await markWorkerStopped(projectDir, workerId);
|
|
134
133
|
} catch (err) {
|
|
@@ -151,7 +150,7 @@ export async function startWorker(
|
|
|
151
150
|
if (taskId) {
|
|
152
151
|
await runSpecificTask({
|
|
153
152
|
projectDir,
|
|
154
|
-
|
|
153
|
+
mem,
|
|
155
154
|
config,
|
|
156
155
|
workerId,
|
|
157
156
|
taskId,
|
|
@@ -161,7 +160,7 @@ export async function startWorker(
|
|
|
161
160
|
} else {
|
|
162
161
|
await tick({
|
|
163
162
|
projectDir,
|
|
164
|
-
|
|
163
|
+
mem,
|
|
165
164
|
config,
|
|
166
165
|
workerId,
|
|
167
166
|
mcpxClient,
|
|
@@ -182,7 +181,7 @@ export async function startWorker(
|
|
|
182
181
|
try {
|
|
183
182
|
didWork = await tick({
|
|
184
183
|
projectDir,
|
|
185
|
-
|
|
184
|
+
mem,
|
|
186
185
|
config,
|
|
187
186
|
workerId,
|
|
188
187
|
mcpxClient,
|
|
@@ -208,5 +207,6 @@ export async function startWorker(
|
|
|
208
207
|
logger.warn(`failed to mark worker stopped: ${err}`);
|
|
209
208
|
}
|
|
210
209
|
await mcpxClient?.close();
|
|
210
|
+
await mem.close();
|
|
211
211
|
}
|
|
212
212
|
}
|
package/src/worker/llm.ts
CHANGED
|
@@ -5,8 +5,8 @@ import type {
|
|
|
5
5
|
ToolUseBlock,
|
|
6
6
|
} from "@anthropic-ai/sdk/resources/messages";
|
|
7
7
|
import type { McpxClient } from "@evantahler/mcpx";
|
|
8
|
+
import type { MembotClient } from "membot";
|
|
8
9
|
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
9
|
-
import { withDb } from "../db/connection.ts";
|
|
10
10
|
import type { Task } from "../tasks/schema.ts";
|
|
11
11
|
import { getTask } from "../tasks/store.ts";
|
|
12
12
|
import { logInteraction } from "../threads/store.ts";
|
|
@@ -50,7 +50,7 @@ export async function runAgentLoop(input: {
|
|
|
50
50
|
systemPrompt: string;
|
|
51
51
|
task: Task;
|
|
52
52
|
config: Required<BotholomewConfig>;
|
|
53
|
-
|
|
53
|
+
mem: MembotClient;
|
|
54
54
|
threadId: string;
|
|
55
55
|
projectDir: string;
|
|
56
56
|
workerId?: string;
|
|
@@ -61,7 +61,7 @@ export async function runAgentLoop(input: {
|
|
|
61
61
|
systemPrompt,
|
|
62
62
|
task,
|
|
63
63
|
config,
|
|
64
|
-
|
|
64
|
+
mem,
|
|
65
65
|
threadId,
|
|
66
66
|
projectDir,
|
|
67
67
|
workerId,
|
|
@@ -205,7 +205,7 @@ export async function runAgentLoop(input: {
|
|
|
205
205
|
toolUseBlocks.map(async (toolUse) => {
|
|
206
206
|
const start = Date.now();
|
|
207
207
|
const result = await executeToolCall(toolUse, {
|
|
208
|
-
|
|
208
|
+
mem,
|
|
209
209
|
projectDir,
|
|
210
210
|
config,
|
|
211
211
|
mcpxClient: input.mcpxClient ?? null,
|
|
@@ -264,7 +264,7 @@ interface ToolCallResult {
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
interface ToolCallCtx {
|
|
267
|
-
|
|
267
|
+
mem: MembotClient;
|
|
268
268
|
projectDir: string;
|
|
269
269
|
config: Required<BotholomewConfig>;
|
|
270
270
|
mcpxClient: McpxClient | null;
|
|
@@ -298,10 +298,8 @@ async function executeToolCall(
|
|
|
298
298
|
|
|
299
299
|
let result: unknown;
|
|
300
300
|
try {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return tool.execute(parsed.data, ctx);
|
|
304
|
-
});
|
|
301
|
+
const ctx: ToolContext = baseCtx;
|
|
302
|
+
result = await tool.execute(parsed.data, ctx);
|
|
305
303
|
} catch (err) {
|
|
306
304
|
return {
|
|
307
305
|
output: `Tool ${toolUse.name} threw an error: ${err}. You may retry with different parameters or try an alternative approach.`,
|
package/src/worker/prompt.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { SERVER_INSTRUCTIONS as MEMBOT_INSTRUCTIONS } from "membot";
|
|
3
4
|
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
4
5
|
import { getPromptsDir } from "../constants.ts";
|
|
5
6
|
import type { Task } from "../tasks/schema.ts";
|
|
6
7
|
import { parsePromptFile } from "../utils/frontmatter.ts";
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Section header rendered above membot's upstream {@link MEMBOT_INSTRUCTIONS}
|
|
11
|
+
* blob in every system prompt. Pulling the body verbatim from the SDK keeps
|
|
12
|
+
* the agent's mental model of `membot_*` tools aligned with whatever the
|
|
13
|
+
* pinned membot version ships, with no per-bump prose edits on our side.
|
|
14
|
+
*/
|
|
15
|
+
export const MEMBOT_PROMPT_SECTION = `## Knowledge store (membot)
|
|
16
|
+
|
|
17
|
+
${MEMBOT_INSTRUCTIONS}
|
|
18
|
+
`;
|
|
19
|
+
|
|
8
20
|
const pkg = await Bun.file(
|
|
9
21
|
new URL("../../package.json", import.meta.url),
|
|
10
22
|
).json();
|
|
@@ -103,7 +115,6 @@ User: ${process.env.USER || process.env.USERNAME || "unknown"}
|
|
|
103
115
|
export async function buildSystemPrompt(
|
|
104
116
|
projectDir: string,
|
|
105
117
|
task?: Task,
|
|
106
|
-
dbPath?: string,
|
|
107
118
|
_config?: Required<BotholomewConfig>,
|
|
108
119
|
options?: { hasMcpTools?: boolean },
|
|
109
120
|
): Promise<string> {
|
|
@@ -115,11 +126,10 @@ export async function buildSystemPrompt(
|
|
|
115
126
|
|
|
116
127
|
prompt += await loadPersistentContext(projectDir, taskKeywords);
|
|
117
128
|
|
|
118
|
-
// The agent finds task-relevant content via the `
|
|
119
|
-
// rather than having chunks pre-stuffed into the system prompt —
|
|
120
|
-
// prompt small and lets the model decide what
|
|
129
|
+
// The agent finds task-relevant content via the `membot_search` tool on
|
|
130
|
+
// demand rather than having chunks pre-stuffed into the system prompt —
|
|
131
|
+
// keeps the prompt small and lets the model decide what to read.
|
|
121
132
|
void task;
|
|
122
|
-
void dbPath;
|
|
123
133
|
void _config;
|
|
124
134
|
|
|
125
135
|
prompt += `## Instructions
|
|
@@ -128,26 +138,28 @@ You are Botholomew, a wise-owl worker that works through tasks. Use available to
|
|
|
128
138
|
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.
|
|
129
139
|
`;
|
|
130
140
|
|
|
141
|
+
prompt += `\n${MEMBOT_PROMPT_SECTION}`;
|
|
142
|
+
|
|
131
143
|
if (options?.hasMcpTools) {
|
|
132
144
|
prompt += `
|
|
133
145
|
## External Tools (MCP)
|
|
134
146
|
|
|
135
|
-
### Local
|
|
147
|
+
### Local knowledge store first
|
|
136
148
|
|
|
137
|
-
**Before any MCP read, search
|
|
149
|
+
**Before any MCP read, search the membot knowledge store.** Prior ingests (Gmail dumps, GitHub fetches, URL captures, prior agent outputs) are usually already there — refetching is slower, costs tokens, and risks rate limits.
|
|
138
150
|
|
|
139
151
|
Workflow for any "look up / find / read" intent:
|
|
140
152
|
|
|
141
|
-
1. \`
|
|
142
|
-
2. If freshness matters, call \`
|
|
153
|
+
1. \`membot_search\` (hybrid semantic + BM25) over the store, then \`membot_read\` / \`membot_tree\` to drill in.
|
|
154
|
+
2. If freshness matters, call \`membot_info\` and check the source mtime / refresh status. To re-pull stale content, call \`membot_refresh\` for URL-backed entries, or \`membot_pipe\` from an \`mcp_exec\` call for fresh captures.
|
|
143
155
|
3. Only call \`mcp_exec\` for reads when the data is genuinely missing locally **or** must be real-time (e.g., "what's on my calendar right now").
|
|
144
156
|
|
|
145
|
-
Writes always go through MCP — sending an email, creating an issue, posting to Slack. Don't search
|
|
157
|
+
Writes to external systems always go through MCP — sending an email, creating an issue, posting to Slack. Don't search membot first for those.
|
|
146
158
|
|
|
147
159
|
Examples:
|
|
148
|
-
- "What does doc X say?" → \`
|
|
149
|
-
- "Any new emails from Y?" → \`
|
|
150
|
-
- "Send an email to Y" → MCP write directly; no
|
|
160
|
+
- "What does doc X say?" → \`membot_search\` first.
|
|
161
|
+
- "Any new emails from Y?" → \`membot_search\` for the sender's name before hitting Gmail MCP.
|
|
162
|
+
- "Send an email to Y" → MCP write directly; no membot lookup.
|
|
151
163
|
|
|
152
164
|
### Calling MCP tools
|
|
153
165
|
|
package/src/worker/spawn.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mkdir } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { getConfigPath, getWorkerLogPath } from "../constants.ts";
|
|
4
|
-
import { uuidv7 } from "../db/uuid.ts";
|
|
5
4
|
import { logger } from "../utils/logger.ts";
|
|
5
|
+
import { uuidv7 } from "../utils/uuid.ts";
|
|
6
6
|
import { dateForId } from "../utils/v7-date.ts";
|
|
7
7
|
import type { WorkerMode } from "./index.ts";
|
|
8
8
|
|
package/src/worker/tick.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpxClient } from "@evantahler/mcpx";
|
|
2
|
+
import type { MembotClient } from "membot";
|
|
2
3
|
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
3
4
|
import type { Task } from "../tasks/schema.ts";
|
|
4
5
|
import {
|
|
@@ -18,7 +19,7 @@ import { processSchedules } from "./schedules.ts";
|
|
|
18
19
|
|
|
19
20
|
export interface TickOptions {
|
|
20
21
|
projectDir: string;
|
|
21
|
-
|
|
22
|
+
mem: MembotClient;
|
|
22
23
|
config: Required<BotholomewConfig>;
|
|
23
24
|
workerId: string;
|
|
24
25
|
mcpxClient?: McpxClient | null;
|
|
@@ -34,7 +35,7 @@ export interface TickOptions {
|
|
|
34
35
|
export async function tick(opts: TickOptions): Promise<boolean> {
|
|
35
36
|
const {
|
|
36
37
|
projectDir,
|
|
37
|
-
|
|
38
|
+
mem,
|
|
38
39
|
config,
|
|
39
40
|
workerId,
|
|
40
41
|
mcpxClient,
|
|
@@ -75,7 +76,7 @@ export async function tick(opts: TickOptions): Promise<boolean> {
|
|
|
75
76
|
|
|
76
77
|
await runClaimedTask({
|
|
77
78
|
projectDir,
|
|
78
|
-
|
|
79
|
+
mem,
|
|
79
80
|
config,
|
|
80
81
|
workerId,
|
|
81
82
|
mcpxClient,
|
|
@@ -94,7 +95,7 @@ export async function tick(opts: TickOptions): Promise<boolean> {
|
|
|
94
95
|
*/
|
|
95
96
|
export async function runSpecificTask(opts: {
|
|
96
97
|
projectDir: string;
|
|
97
|
-
|
|
98
|
+
mem: MembotClient;
|
|
98
99
|
config: Required<BotholomewConfig>;
|
|
99
100
|
workerId: string;
|
|
100
101
|
taskId: string;
|
|
@@ -114,7 +115,7 @@ export async function runSpecificTask(opts: {
|
|
|
114
115
|
}
|
|
115
116
|
await runClaimedTask({
|
|
116
117
|
projectDir: opts.projectDir,
|
|
117
|
-
|
|
118
|
+
mem: opts.mem,
|
|
118
119
|
config: opts.config,
|
|
119
120
|
workerId: opts.workerId,
|
|
120
121
|
mcpxClient: opts.mcpxClient,
|
|
@@ -126,14 +127,14 @@ export async function runSpecificTask(opts: {
|
|
|
126
127
|
|
|
127
128
|
async function runClaimedTask(opts: {
|
|
128
129
|
projectDir: string;
|
|
129
|
-
|
|
130
|
+
mem: MembotClient;
|
|
130
131
|
config: Required<BotholomewConfig>;
|
|
131
132
|
workerId: string;
|
|
132
133
|
mcpxClient?: McpxClient | null;
|
|
133
134
|
callbacks?: WorkerStreamCallbacks;
|
|
134
135
|
task: Task;
|
|
135
136
|
}): Promise<void> {
|
|
136
|
-
const { projectDir,
|
|
137
|
+
const { projectDir, mem, config, workerId, mcpxClient, callbacks, task } =
|
|
137
138
|
opts;
|
|
138
139
|
|
|
139
140
|
logger.info(`Claimed task: ${task.name} (${task.id})`);
|
|
@@ -151,7 +152,7 @@ async function runClaimedTask(opts: {
|
|
|
151
152
|
|
|
152
153
|
let systemPrompt: string;
|
|
153
154
|
try {
|
|
154
|
-
systemPrompt = await buildSystemPrompt(projectDir, task,
|
|
155
|
+
systemPrompt = await buildSystemPrompt(projectDir, task, config, {
|
|
155
156
|
hasMcpTools: mcpxClient != null,
|
|
156
157
|
});
|
|
157
158
|
} catch (err) {
|
|
@@ -171,7 +172,7 @@ async function runClaimedTask(opts: {
|
|
|
171
172
|
systemPrompt,
|
|
172
173
|
task,
|
|
173
174
|
config,
|
|
174
|
-
|
|
175
|
+
mem,
|
|
175
176
|
threadId,
|
|
176
177
|
projectDir,
|
|
177
178
|
workerId,
|
package/src/commands/db.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import ansis from "ansis";
|
|
2
|
-
import type { Command } from "commander";
|
|
3
|
-
import { getDbPath } from "../constants.ts";
|
|
4
|
-
import {
|
|
5
|
-
isPidAlive,
|
|
6
|
-
type ProbeResult,
|
|
7
|
-
probeAllTables,
|
|
8
|
-
repairDatabase,
|
|
9
|
-
} from "../db/doctor.ts";
|
|
10
|
-
import { logger } from "../utils/logger.ts";
|
|
11
|
-
import { listWorkers, type Worker } from "../workers/store.ts";
|
|
12
|
-
|
|
13
|
-
function statusBadge(status: ProbeResult["status"]): string {
|
|
14
|
-
switch (status) {
|
|
15
|
-
case "ok":
|
|
16
|
-
return ansis.green("ok");
|
|
17
|
-
case "empty":
|
|
18
|
-
return ansis.dim("empty");
|
|
19
|
-
case "missing":
|
|
20
|
-
return ansis.dim("missing");
|
|
21
|
-
case "corrupt":
|
|
22
|
-
return ansis.red.bold("corrupt");
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function printResults(results: ProbeResult[]) {
|
|
27
|
-
const nameWidth = Math.max(...results.map((r) => r.table.length));
|
|
28
|
-
for (const r of results) {
|
|
29
|
-
const name = r.table.padEnd(nameWidth + 2);
|
|
30
|
-
const detail = r.message ? ansis.dim(` ${r.message.slice(0, 200)}`) : "";
|
|
31
|
-
console.log(` ${name}${statusBadge(r.status)}${detail}`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function registerDbCommand(program: Command) {
|
|
36
|
-
const db = program
|
|
37
|
-
.command("db")
|
|
38
|
-
.description("Inspect and repair the project database");
|
|
39
|
-
|
|
40
|
-
db.command("doctor")
|
|
41
|
-
.description(
|
|
42
|
-
"Probe every table for primary-key index corruption and optionally repair via EXPORT/IMPORT",
|
|
43
|
-
)
|
|
44
|
-
.option(
|
|
45
|
-
"-r, --repair",
|
|
46
|
-
"Rebuild the database file from an export when corruption is detected",
|
|
47
|
-
)
|
|
48
|
-
.action((opts) => doctor(program, opts.repair === true));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function doctor(program: Command, repair: boolean): Promise<void> {
|
|
52
|
-
const dir = program.opts().dir as string;
|
|
53
|
-
const dbPath = getDbPath(dir);
|
|
54
|
-
|
|
55
|
-
logger.info(`Probing tables in ${dbPath}`);
|
|
56
|
-
const results = await probeAllTables(dbPath);
|
|
57
|
-
printResults(results);
|
|
58
|
-
|
|
59
|
-
const corrupt = results.filter((r) => r.status === "corrupt");
|
|
60
|
-
if (corrupt.length === 0) {
|
|
61
|
-
logger.success("No corruption detected.");
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
logger.error(
|
|
66
|
-
`${corrupt.length} table(s) have corrupted indexes: ${corrupt
|
|
67
|
-
.map((r) => r.table)
|
|
68
|
-
.join(", ")}`,
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
if (!repair) {
|
|
72
|
-
console.log("");
|
|
73
|
-
console.log(
|
|
74
|
-
ansis.yellow(
|
|
75
|
-
"Re-run with --repair to rebuild the database file (creates a timestamped backup).",
|
|
76
|
-
),
|
|
77
|
-
);
|
|
78
|
-
process.exit(1);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Repair requires exclusive access — refuse if any worker is actually
|
|
82
|
-
// running, otherwise the EXPORT would race with the worker's writes.
|
|
83
|
-
// Stale 'running' worker JSON files whose PID is dead are reported but
|
|
84
|
-
// don't block repair.
|
|
85
|
-
let running: Worker[];
|
|
86
|
-
try {
|
|
87
|
-
running = await listWorkers(dir, { status: "running" });
|
|
88
|
-
} catch {
|
|
89
|
-
running = [];
|
|
90
|
-
}
|
|
91
|
-
const live = running.filter((w) => isPidAlive(w.pid));
|
|
92
|
-
const stale = running.filter((w) => !isPidAlive(w.pid));
|
|
93
|
-
if (live.length > 0) {
|
|
94
|
-
logger.error(
|
|
95
|
-
`${live.length} worker(s) actually running. Stop them first: botholomew worker stop <id>`,
|
|
96
|
-
);
|
|
97
|
-
for (const w of live) {
|
|
98
|
-
logger.dim(` ${w.id} (pid ${w.pid}, mode=${w.mode})`);
|
|
99
|
-
}
|
|
100
|
-
process.exit(1);
|
|
101
|
-
}
|
|
102
|
-
if (stale.length > 0) {
|
|
103
|
-
logger.warn(
|
|
104
|
-
`${stale.length} worker row(s) marked 'running' but PID is dead — proceeding (rows will be carried through repair, then reapable):`,
|
|
105
|
-
);
|
|
106
|
-
for (const w of stale) {
|
|
107
|
-
logger.dim(` ${w.id} (pid ${w.pid}, mode=${w.mode})`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
logger.phase("repair", "EXPORT DATABASE → swap files → IMPORT DATABASE");
|
|
112
|
-
const result = await repairDatabase(dbPath);
|
|
113
|
-
logger.success(
|
|
114
|
-
`Repaired in ${result.durationMs}ms. Backup: ${result.backupDbPath}`,
|
|
115
|
-
);
|
|
116
|
-
logger.dim(
|
|
117
|
-
" Re-run `botholomew db doctor` to confirm. Delete the backup once you're sure.",
|
|
118
|
-
);
|
|
119
|
-
}
|
package/src/commands/with-db.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
import { getDbPath } from "../constants.ts";
|
|
3
|
-
import type { DbConnection } from "../db/connection.ts";
|
|
4
|
-
import { withDb as coreWithDb } from "../db/connection.ts";
|
|
5
|
-
import { migrate } from "../db/schema.ts";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Open a migrated DB connection from the CLI --dir flag, run the callback,
|
|
9
|
-
* and guarantee the connection is closed afterward. Retries on lock
|
|
10
|
-
* conflicts so CLI invocations cooperate with running workers or chat.
|
|
11
|
-
*/
|
|
12
|
-
export async function withDb<T>(
|
|
13
|
-
program: Command,
|
|
14
|
-
fn: (conn: DbConnection, dir: string) => Promise<T>,
|
|
15
|
-
): Promise<T> {
|
|
16
|
-
const dir = program.opts().dir;
|
|
17
|
-
const dbPath = getDbPath(dir);
|
|
18
|
-
return coreWithDb(dbPath, async (conn) => {
|
|
19
|
-
await migrate(conn);
|
|
20
|
-
return fn(conn, dir);
|
|
21
|
-
});
|
|
22
|
-
}
|