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.
Files changed (98) hide show
  1. package/README.md +46 -41
  2. package/package.json +4 -9
  3. package/src/chat/agent.ts +37 -40
  4. package/src/chat/session.ts +10 -10
  5. package/src/cli.ts +0 -2
  6. package/src/commands/capabilities.ts +35 -33
  7. package/src/commands/context.ts +133 -221
  8. package/src/commands/init.ts +22 -1
  9. package/src/commands/mcpx.ts +21 -8
  10. package/src/commands/nuke.ts +52 -15
  11. package/src/commands/prepare.ts +16 -13
  12. package/src/config/loader.ts +1 -8
  13. package/src/config/schemas.ts +6 -0
  14. package/src/constants.ts +16 -32
  15. package/src/init/index.ts +52 -27
  16. package/src/mcpx/client.ts +21 -5
  17. package/src/mem/client.ts +33 -0
  18. package/src/{context → prompts}/capabilities.ts +11 -7
  19. package/src/schedules/store.ts +1 -1
  20. package/src/tasks/store.ts +1 -1
  21. package/src/threads/store.ts +1 -1
  22. package/src/tools/capabilities/refresh.ts +1 -1
  23. package/src/tools/membot/adapter.ts +111 -0
  24. package/src/tools/membot/copy.ts +59 -0
  25. package/src/tools/membot/count_lines.ts +53 -0
  26. package/src/tools/membot/edit.ts +72 -0
  27. package/src/tools/membot/exists.ts +54 -0
  28. package/src/tools/membot/index.ts +26 -0
  29. package/src/tools/{context → membot}/pipe.ts +34 -32
  30. package/src/tools/registry.ts +6 -37
  31. package/src/tools/tool.ts +6 -8
  32. package/src/tui/App.tsx +3 -4
  33. package/src/tui/components/ContextPanel.tsx +109 -226
  34. package/src/tui/components/HelpPanel.tsx +2 -2
  35. package/src/tui/components/StatusBar.tsx +0 -6
  36. package/src/tui/components/ThreadPanel.tsx +8 -7
  37. package/src/tui/wrapDetail.ts +11 -0
  38. package/src/worker/heartbeat.ts +0 -20
  39. package/src/worker/index.ts +13 -13
  40. package/src/worker/llm.ts +7 -9
  41. package/src/worker/prompt.ts +25 -13
  42. package/src/worker/spawn.ts +1 -1
  43. package/src/worker/tick.ts +10 -9
  44. package/src/commands/db.ts +0 -119
  45. package/src/commands/with-db.ts +0 -22
  46. package/src/context/chunker.ts +0 -275
  47. package/src/context/embedder-impl.ts +0 -100
  48. package/src/context/embedder.ts +0 -9
  49. package/src/context/fetcher-errors.ts +0 -8
  50. package/src/context/fetcher.ts +0 -515
  51. package/src/context/locks.ts +0 -146
  52. package/src/context/markdown-converter.ts +0 -186
  53. package/src/context/reindex.ts +0 -198
  54. package/src/context/store.ts +0 -841
  55. package/src/context/url-utils.ts +0 -25
  56. package/src/db/connection.ts +0 -255
  57. package/src/db/doctor.ts +0 -235
  58. package/src/db/embeddings.ts +0 -317
  59. package/src/db/query.ts +0 -56
  60. package/src/db/schema.ts +0 -93
  61. package/src/db/sql/1-core_tables.sql +0 -53
  62. package/src/db/sql/10-dedupe_context_items.sql +0 -26
  63. package/src/db/sql/11-rebuild_hnsw.sql +0 -8
  64. package/src/db/sql/12-workers.sql +0 -66
  65. package/src/db/sql/13-drive-paths.sql +0 -47
  66. package/src/db/sql/14-drop_hnsw_index.sql +0 -8
  67. package/src/db/sql/15-fts_index.sql +0 -8
  68. package/src/db/sql/16-source_url.sql +0 -7
  69. package/src/db/sql/17-worker_log_path.sql +0 -3
  70. package/src/db/sql/18-reset_embeddings_for_local.sql +0 -39
  71. package/src/db/sql/19-disk_backed_index.sql +0 -36
  72. package/src/db/sql/2-logging_tables.sql +0 -24
  73. package/src/db/sql/20-drop_db_tables_for_files.sql +0 -19
  74. package/src/db/sql/3-daemon_state.sql +0 -5
  75. package/src/db/sql/4-unique_context_path.sql +0 -1
  76. package/src/db/sql/5-reset_embeddings_for_openai.sql +0 -1
  77. package/src/db/sql/6-vss_index.sql +0 -7
  78. package/src/db/sql/7-drop_embeddings_fk.sql +0 -23
  79. package/src/db/sql/8-task_output.sql +0 -1
  80. package/src/db/sql/9-source-type.sql +0 -1
  81. package/src/tools/context/read-large-result.ts +0 -33
  82. package/src/tools/dir/create.ts +0 -47
  83. package/src/tools/dir/size.ts +0 -77
  84. package/src/tools/dir/tree.ts +0 -124
  85. package/src/tools/file/copy.ts +0 -73
  86. package/src/tools/file/count-lines.ts +0 -54
  87. package/src/tools/file/delete.ts +0 -83
  88. package/src/tools/file/edit.ts +0 -76
  89. package/src/tools/file/exists.ts +0 -33
  90. package/src/tools/file/info.ts +0 -66
  91. package/src/tools/file/move.ts +0 -66
  92. package/src/tools/file/read.ts +0 -67
  93. package/src/tools/file/write.ts +0 -58
  94. package/src/tools/search/fuse.ts +0 -96
  95. package/src/tools/search/index.ts +0 -127
  96. package/src/tools/search/regexp.ts +0 -82
  97. package/src/tools/search/semantic.ts +0 -167
  98. /package/src/{db → utils}/uuid.ts +0 -0
@@ -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 { getDbPath } from "../constants.ts";
5
- import { withDb } from "../db/connection.ts";
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 dbPath = getDbPath(projectDir);
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
- // One short-lived connection to apply migrations, then release the lock.
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
- dbPath,
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
- dbPath,
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
- dbPath,
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
- dbPath: string;
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
- dbPath,
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
- dbPath,
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
- dbPath: string;
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
- result = await withDb(baseCtx.dbPath, (conn) => {
302
- const ctx: ToolContext = { ...baseCtx, conn };
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.`,
@@ -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 `search` tool on demand
119
- // rather than having chunks pre-stuffed into the system prompt — keeps the
120
- // prompt small and lets the model decide what it actually needs to read.
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 context first
147
+ ### Local knowledge store first
136
148
 
137
- **Before any MCP read, search local context.** Files in \`context/\` (Gmail dumps, GitHub fetches, URL ingests, prior agent outputs) are usually already there — refetching is slower, costs tokens, and risks rate limits.
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. \`search\` (hybrid regexp + semantic) over \`context/\`, then \`context_read\` / \`context_tree\` to drill in.
142
- 2. If freshness matters, call \`context_info\` and check the file's mtime. To re-pull stale content, write fresh into \`context/\` (\`pipe_to_context\` from an \`mcp_exec\` call is the typical path) rather than going to MCP for the whole document on every question.
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 context first for those.
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?" → \`search\` first.
149
- - "Any new emails from Y?" → \`search\` for the sender under \`context/gmail/\` (or wherever you've been ingesting mail) before hitting Gmail MCP.
150
- - "Send an email to Y" → MCP write directly; no context lookup.
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
 
@@ -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
 
@@ -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
- dbPath: string;
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
- dbPath,
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
- dbPath,
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
- dbPath: string;
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
- dbPath: opts.dbPath,
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
- dbPath: string;
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, dbPath, config, workerId, mcpxClient, callbacks, task } =
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, dbPath, config, {
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
- dbPath,
175
+ mem,
175
176
  threadId,
176
177
  projectDir,
177
178
  workerId,
@@ -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
- }
@@ -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
- }