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/commands/context.ts
CHANGED
|
@@ -1,245 +1,157 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { homedir } from "node:os";
|
|
2
4
|
import { join } from "node:path";
|
|
3
|
-
import
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
4
6
|
import type { Command } from "commander";
|
|
5
|
-
import {
|
|
7
|
+
import { defaultCliName, OPERATIONS } from "membot";
|
|
6
8
|
import { loadConfig } from "../config/loader.ts";
|
|
7
|
-
import {
|
|
8
|
-
import { fetchUrl } from "../context/fetcher.ts";
|
|
9
|
-
import { reindexContext } from "../context/reindex.ts";
|
|
10
|
-
import {
|
|
11
|
-
buildTree,
|
|
12
|
-
fileExists,
|
|
13
|
-
listContextDir,
|
|
14
|
-
type TreeNode,
|
|
15
|
-
writeContextFile,
|
|
16
|
-
} from "../context/store.ts";
|
|
17
|
-
import { withDb } from "../db/connection.ts";
|
|
18
|
-
import { indexStats } from "../db/embeddings.ts";
|
|
19
|
-
import { migrate } from "../db/schema.ts";
|
|
20
|
-
import { createMcpxClient } from "../mcpx/client.ts";
|
|
21
|
-
import {
|
|
22
|
-
type ContextFileMeta,
|
|
23
|
-
serializeContextFile,
|
|
24
|
-
} from "../utils/frontmatter.ts";
|
|
9
|
+
import { resolveMembotDir } from "../mem/client.ts";
|
|
25
10
|
import { logger } from "../utils/logger.ts";
|
|
26
11
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.description(
|
|
31
|
-
"Inspect and manage the on-disk context/ tree (the agent's knowledge store)",
|
|
32
|
-
);
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const ourPkg = require("../../package.json");
|
|
14
|
+
const membotPkg = require("membot/package.json");
|
|
33
15
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"destination path under context/ (default: derived from the URL)",
|
|
43
|
-
)
|
|
44
|
-
.option(
|
|
45
|
-
"--prompt <text>",
|
|
46
|
-
"extra guidance passed to the LLM-driven fetcher (e.g. 'export as markdown')",
|
|
47
|
-
)
|
|
48
|
-
.option("--overwrite", "replace an existing file at the destination path")
|
|
49
|
-
.action(async (url: string, opts) => {
|
|
50
|
-
const dir = program.opts().dir;
|
|
51
|
-
const config = await loadConfig(dir);
|
|
52
|
-
const mcpxClient = await createMcpxClient(dir);
|
|
53
|
-
logger.info(`importing ${url}`);
|
|
54
|
-
try {
|
|
55
|
-
const fetched = await fetchUrl(url, config, mcpxClient, opts.prompt);
|
|
56
|
-
const dest = opts.path ?? deriveContextPath(url, fetched.source);
|
|
57
|
-
const meta: ContextFileMeta = {
|
|
58
|
-
source_url: url,
|
|
59
|
-
imported_at: new Date().toISOString(),
|
|
60
|
-
};
|
|
61
|
-
// Title falls back to the URL when fetcher couldn't extract one —
|
|
62
|
-
// skip it in that case to avoid duplicating source_url.
|
|
63
|
-
if (fetched.title && fetched.title !== url) {
|
|
64
|
-
meta.title = fetched.title;
|
|
65
|
-
}
|
|
66
|
-
const body = serializeContextFile(meta, fetched.content);
|
|
67
|
-
await writeContextFile(dir, dest, body, {
|
|
68
|
-
onConflict: opts.overwrite ? "overwrite" : "error",
|
|
69
|
-
});
|
|
70
|
-
logger.success(
|
|
71
|
-
`imported ${body.length} bytes → ${ansis.bold(`context/${dest}`)} (source: ${fetched.source ?? "http"})`,
|
|
72
|
-
);
|
|
16
|
+
// Soft warning rather than a hard error — membot's SDK API is stable within a
|
|
17
|
+
// minor version, and dev workspaces sometimes pin a newer copy.
|
|
18
|
+
const requested = (ourPkg.dependencies.membot as string).replace(/^[\^~]/, "");
|
|
19
|
+
if (!membotPkg.version.startsWith(requested.split(".")[0])) {
|
|
20
|
+
logger.warn(
|
|
21
|
+
`membot version drift: installed ${membotPkg.version}, expected ${ourPkg.dependencies.membot}`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
73
24
|
|
|
74
|
-
|
|
75
|
-
// incremental — files whose content_hash matches the index are
|
|
76
|
-
// skipped, so this only embeds the file we just wrote.
|
|
77
|
-
const dbPath = getDbPath(dir);
|
|
78
|
-
await withDb(dbPath, migrate);
|
|
79
|
-
const summary = await reindexContext(dir, config, dbPath, {
|
|
80
|
-
onProgress: (msg) => logger.dim(` ${msg}`),
|
|
81
|
-
});
|
|
82
|
-
logger.success(
|
|
83
|
-
`indexed: ${summary.added} added, ${summary.updated} updated, ${summary.unchanged} unchanged, ${summary.chunksWritten} chunks written`,
|
|
84
|
-
);
|
|
85
|
-
} catch (err) {
|
|
86
|
-
logger.error(
|
|
87
|
-
`import failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
88
|
-
);
|
|
89
|
-
process.exit(1);
|
|
90
|
-
} finally {
|
|
91
|
-
await mcpxClient?.close();
|
|
92
|
-
}
|
|
93
|
-
});
|
|
25
|
+
const MEMBOT_CLI = fileURLToPath(import.meta.resolve("membot/cli"));
|
|
94
26
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.description(
|
|
99
|
-
"Walk context/ and reconcile the search index: embed new files, re-embed changed ones, drop rows for removed ones.",
|
|
100
|
-
)
|
|
101
|
-
.action(async () => {
|
|
102
|
-
const dir = program.opts().dir;
|
|
103
|
-
const config = await loadConfig(dir);
|
|
104
|
-
const dbPath = getDbPath(dir);
|
|
105
|
-
// The migrate() call ensures the index DB is initialized, including
|
|
106
|
-
// the context_index table from migration 19, before we try to write.
|
|
107
|
-
await withDb(dbPath, migrate);
|
|
108
|
-
const spinner = createSpinner("reindexing").start();
|
|
109
|
-
const summary = await reindexContext(dir, config, dbPath, {
|
|
110
|
-
onProgress: (msg) => spinner.update({ text: msg }),
|
|
111
|
-
});
|
|
112
|
-
const parts = [
|
|
113
|
-
`${summary.added} added`,
|
|
114
|
-
`${summary.updated} updated`,
|
|
115
|
-
`${summary.unchanged} unchanged`,
|
|
116
|
-
`${summary.removed} removed`,
|
|
117
|
-
`${summary.chunksWritten} chunks written`,
|
|
118
|
-
];
|
|
119
|
-
spinner.success({ text: parts.join(", ") });
|
|
120
|
-
});
|
|
27
|
+
function getDir(program: Command): string {
|
|
28
|
+
return program.opts().dir;
|
|
29
|
+
}
|
|
121
30
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Slice process.argv from the token after "context" so flags (including
|
|
33
|
+
* --help) and positional args flow through to upstream membot verbatim.
|
|
34
|
+
*/
|
|
35
|
+
function getRawContextArgs(): string[] {
|
|
36
|
+
const idx = process.argv.indexOf("context");
|
|
37
|
+
return idx === -1 ? [] : process.argv.slice(idx + 1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function runMembot(projectDir: string, args: string[]): Promise<number> {
|
|
41
|
+
// Resolve membot's data dir from `membot_scope`:
|
|
42
|
+
// - "global" → ~/.membot (default, shared)
|
|
43
|
+
// - "project" → <projectDir>
|
|
44
|
+
// Forward stdio so the user sees the same output they would running
|
|
45
|
+
// `membot` directly.
|
|
46
|
+
const config = await loadConfig(projectDir);
|
|
47
|
+
const membotDir = resolveMembotDir(projectDir, config);
|
|
48
|
+
const proc = Bun.spawn(["bun", MEMBOT_CLI, "--config", membotDir, ...args], {
|
|
49
|
+
stdout: "inherit",
|
|
50
|
+
stderr: "inherit",
|
|
51
|
+
stdin: "inherit",
|
|
52
|
+
});
|
|
53
|
+
return await proc.exited;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Copy the system-wide `~/.membot` data dir into the project, mirroring
|
|
58
|
+
* `botholomew mcpx import-global`. Useful when the user has built up a
|
|
59
|
+
* personal knowledge base globally and wants to seed a new project with it.
|
|
60
|
+
*
|
|
61
|
+
* Refuses to overwrite a non-empty project membot store unless `--force` is
|
|
62
|
+
* passed — accidentally clobbering an active project's index is much worse
|
|
63
|
+
* than re-running the import.
|
|
64
|
+
*/
|
|
65
|
+
function registerImportGlobal(parent: Command, program: Command): void {
|
|
66
|
+
parent
|
|
67
|
+
.command("import-global")
|
|
68
|
+
.description("Copy system-wide membot data (~/.membot) into this project")
|
|
126
69
|
.option(
|
|
127
|
-
"-
|
|
128
|
-
"
|
|
129
|
-
|
|
130
|
-
10,
|
|
70
|
+
"-f, --force",
|
|
71
|
+
"Overwrite an existing index.duckdb in the project",
|
|
72
|
+
false,
|
|
131
73
|
)
|
|
132
|
-
.action(async (
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
74
|
+
.action(async (opts: { force: boolean }) => {
|
|
75
|
+
const globalDir = join(homedir(), ".membot");
|
|
76
|
+
if (!existsSync(globalDir)) {
|
|
77
|
+
logger.error("No global membot data found at ~/.membot");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
137
80
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
.action(async () => {
|
|
145
|
-
const dir = program.opts().dir;
|
|
146
|
-
const dbPath = getDbPath(dir);
|
|
147
|
-
const exists = await fileExists(dir, "");
|
|
148
|
-
if (!exists) {
|
|
149
|
-
logger.dim(`context/ does not exist under ${dir}`);
|
|
150
|
-
return;
|
|
81
|
+
const projectDir = getDir(program);
|
|
82
|
+
const config = await loadConfig(projectDir);
|
|
83
|
+
if (config.membot_scope !== "project") {
|
|
84
|
+
logger.warn(
|
|
85
|
+
`membot_scope is "${config.membot_scope}" — Botholomew currently reads from ~/.membot. After this import, set membot_scope to "project" in ${getDir(program)}/config/config.json for the project-local copy to take effect.`,
|
|
86
|
+
);
|
|
151
87
|
}
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
bytes += st.size;
|
|
163
|
-
} catch {
|
|
164
|
-
// file vanished mid-walk — skip
|
|
88
|
+
const dest = (name: string) => join(projectDir, name);
|
|
89
|
+
const destDb = dest("index.duckdb");
|
|
90
|
+
|
|
91
|
+
if (existsSync(destDb) && !opts.force) {
|
|
92
|
+
const stat = statSync(destDb);
|
|
93
|
+
if (stat.size > 0) {
|
|
94
|
+
logger.error(
|
|
95
|
+
`Refusing to overwrite ${destDb} (${stat.size} bytes). Pass --force to replace it.`,
|
|
96
|
+
);
|
|
97
|
+
process.exit(1);
|
|
165
98
|
}
|
|
166
99
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
` ${ansis.dim((label ?? "").padEnd(labelWidth))} ${value}`,
|
|
184
|
-
);
|
|
100
|
+
|
|
101
|
+
mkdirSync(projectDir, { recursive: true });
|
|
102
|
+
|
|
103
|
+
const filesToCopy = ["index.duckdb", "config.json"];
|
|
104
|
+
let copied = 0;
|
|
105
|
+
for (const file of filesToCopy) {
|
|
106
|
+
const src = join(globalDir, file);
|
|
107
|
+
if (!existsSync(src)) continue;
|
|
108
|
+
copyFileSync(src, dest(file));
|
|
109
|
+
logger.success(`Copied ${file}`);
|
|
110
|
+
copied++;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (copied === 0) {
|
|
114
|
+
logger.warn("No files found in ~/.membot to copy.");
|
|
115
|
+
return;
|
|
185
116
|
}
|
|
117
|
+
|
|
118
|
+
logger.success(
|
|
119
|
+
`Imported ${copied} file(s) from ~/.membot into ${projectDir}`,
|
|
120
|
+
);
|
|
186
121
|
});
|
|
187
122
|
}
|
|
188
123
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
function deriveContextPath(url: string, source: string | null): string {
|
|
196
|
-
const slug = slugifyUrl(url);
|
|
197
|
-
const root = source ?? "url";
|
|
198
|
-
return `${root}/${slug}.md`;
|
|
199
|
-
}
|
|
124
|
+
export function registerContextCommand(program: Command) {
|
|
125
|
+
const context = program
|
|
126
|
+
.command("context")
|
|
127
|
+
.description(
|
|
128
|
+
"Manage the project's knowledge store via membot (add, search, ls, read, …)",
|
|
129
|
+
);
|
|
200
130
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
parsed = new URL(url);
|
|
205
|
-
} catch {
|
|
206
|
-
return url.replace(/[^a-z0-9]+/gi, "-").slice(0, 80);
|
|
207
|
-
}
|
|
208
|
-
const path = parsed.pathname.replace(/^\/+|\/+$/g, "").replace(/\//g, "_");
|
|
209
|
-
const base = path || parsed.hostname;
|
|
210
|
-
return `${parsed.hostname}_${base}`
|
|
211
|
-
.replace(/[^a-z0-9._-]+/gi, "-")
|
|
212
|
-
.replace(/-+/g, "-")
|
|
213
|
-
.slice(0, 80);
|
|
214
|
-
}
|
|
131
|
+
// Botholomew-specific helpers first so they show up before the membot
|
|
132
|
+
// passthrough subcommands in --help.
|
|
133
|
+
registerImportGlobal(context, program);
|
|
215
134
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const children = node.children;
|
|
232
|
-
children.forEach((c, i) => {
|
|
233
|
-
const last = i === children.length - 1;
|
|
234
|
-
lines.push(renderTreeAnsi(c, childPrefix, last, false));
|
|
235
|
-
});
|
|
135
|
+
// One Commander subcommand per membot Operation. We don't redeclare any
|
|
136
|
+
// flags — Commander hands the raw argv slice to membot, which owns the
|
|
137
|
+
// canonical schema.
|
|
138
|
+
for (const op of OPERATIONS) {
|
|
139
|
+
const name = defaultCliName(op);
|
|
140
|
+
context
|
|
141
|
+
.command(name)
|
|
142
|
+
.description(op.description.split("\n")[0] ?? op.description)
|
|
143
|
+
.allowUnknownOption(true)
|
|
144
|
+
.helpOption(false)
|
|
145
|
+
.argument("[args...]", "arguments forwarded to membot")
|
|
146
|
+
.action(async () => {
|
|
147
|
+
const exitCode = await runMembot(getDir(program), getRawContextArgs());
|
|
148
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
149
|
+
});
|
|
236
150
|
}
|
|
237
|
-
return lines.join("\n");
|
|
238
|
-
}
|
|
239
151
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
152
|
+
// `botholomew context` (no subcommand) → membot's default action.
|
|
153
|
+
context.action(async () => {
|
|
154
|
+
const exitCode = await runMembot(getDir(program), getRawContextArgs());
|
|
155
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
156
|
+
});
|
|
245
157
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -2,6 +2,13 @@ import type { Command } from "commander";
|
|
|
2
2
|
import { initProject } from "../init/index.ts";
|
|
3
3
|
import { logger } from "../utils/logger.ts";
|
|
4
4
|
|
|
5
|
+
function parseScope(value: string): "global" | "project" {
|
|
6
|
+
if (value !== "global" && value !== "project") {
|
|
7
|
+
throw new Error(`scope must be "global" or "project" (got "${value}")`);
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
export function registerInitCommand(program: Command) {
|
|
6
13
|
program
|
|
7
14
|
.command("init")
|
|
@@ -10,10 +17,24 @@ export function registerInitCommand(program: Command) {
|
|
|
10
17
|
"--force",
|
|
11
18
|
"overwrite existing project files; also bypass the unsupported-filesystem check (iCloud/Dropbox/etc)",
|
|
12
19
|
)
|
|
20
|
+
.option(
|
|
21
|
+
"--membot-scope <scope>",
|
|
22
|
+
'where this project reads/writes its knowledge store: "global" (default; shared ~/.membot) or "project" (per-project index.duckdb)',
|
|
23
|
+
parseScope,
|
|
24
|
+
)
|
|
25
|
+
.option(
|
|
26
|
+
"--mcpx-scope <scope>",
|
|
27
|
+
'where this project reads its MCPX config: "global" (default; shared ~/.mcpx) or "project" (per-project mcpx/)',
|
|
28
|
+
parseScope,
|
|
29
|
+
)
|
|
13
30
|
.action(async (opts) => {
|
|
14
31
|
const dir = program.opts().dir;
|
|
15
32
|
try {
|
|
16
|
-
await initProject(dir, {
|
|
33
|
+
await initProject(dir, {
|
|
34
|
+
force: opts.force,
|
|
35
|
+
membotScope: opts.membotScope,
|
|
36
|
+
mcpxScope: opts.mcpxScope,
|
|
37
|
+
});
|
|
17
38
|
} catch (err) {
|
|
18
39
|
logger.error(String(err instanceof Error ? err.message : err));
|
|
19
40
|
process.exit(1);
|
package/src/commands/mcpx.ts
CHANGED
|
@@ -7,8 +7,8 @@ import type { Command } from "commander";
|
|
|
7
7
|
import { createSpinner } from "nanospinner";
|
|
8
8
|
import { loadConfig } from "../config/loader.ts";
|
|
9
9
|
import { getMcpxDir } from "../constants.ts";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { createMcpxClient, resolveMcpxDir } from "../mcpx/client.ts";
|
|
11
|
+
import { writeCapabilitiesFile } from "../prompts/capabilities.ts";
|
|
12
12
|
import { registerAllTools } from "../tools/registry.ts";
|
|
13
13
|
import { logger } from "../utils/logger.ts";
|
|
14
14
|
|
|
@@ -29,7 +29,11 @@ export async function runMcpx(
|
|
|
29
29
|
args: (string | undefined)[],
|
|
30
30
|
opts?: { inherit?: boolean },
|
|
31
31
|
): Promise<string> {
|
|
32
|
-
|
|
32
|
+
// Resolve mcpx config dir from `mcpx_scope`:
|
|
33
|
+
// - "global" → ~/.mcpx (default, shared)
|
|
34
|
+
// - "project" → <projectDir>/mcpx
|
|
35
|
+
const config = await loadConfig(projectDir);
|
|
36
|
+
const mcpxDir = resolveMcpxDir(projectDir, config);
|
|
33
37
|
const filteredArgs = args.filter((a): a is string => a !== undefined);
|
|
34
38
|
const proc = Bun.spawn(["bun", MCPX_CLI, ...filteredArgs, "-c", mcpxDir], {
|
|
35
39
|
stdout: opts?.inherit ? "inherit" : "pipe",
|
|
@@ -124,7 +128,15 @@ export function registerMcpxCommand(program: Command) {
|
|
|
124
128
|
process.exit(1);
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
const
|
|
131
|
+
const projectDir = getDir(program);
|
|
132
|
+
const cfg = await loadConfig(projectDir);
|
|
133
|
+
if (cfg.mcpx_scope !== "project") {
|
|
134
|
+
logger.warn(
|
|
135
|
+
`mcpx_scope is "${cfg.mcpx_scope}" — Botholomew currently reads from ~/.mcpx. After this import, set mcpx_scope to "project" in ${projectDir}/config/config.json for the project-local copy to take effect.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const projectMcpxDir = getMcpxDir(projectDir);
|
|
128
140
|
if (!existsSync(projectMcpxDir)) {
|
|
129
141
|
mkdirSync(projectMcpxDir, { recursive: true });
|
|
130
142
|
}
|
|
@@ -149,16 +161,17 @@ export function registerMcpxCommand(program: Command) {
|
|
|
149
161
|
`Imported ${copied} file(s) from ~/.mcpx into ${projectMcpxDir}`,
|
|
150
162
|
);
|
|
151
163
|
|
|
152
|
-
const projectDir = getDir(program);
|
|
153
164
|
registerAllTools();
|
|
154
|
-
|
|
155
|
-
|
|
165
|
+
// After import-global, the freshly-copied files live in the project's
|
|
166
|
+
// mcpx dir — read them from there so `capabilities.md` reflects what was
|
|
167
|
+
// just imported, regardless of the user's current `mcpx_scope` setting.
|
|
168
|
+
const mcpxClient = await createMcpxClient(projectMcpxDir);
|
|
156
169
|
const spinner = createSpinner("Rebuilding capabilities.md").start();
|
|
157
170
|
try {
|
|
158
171
|
const result = await writeCapabilitiesFile(
|
|
159
172
|
projectDir,
|
|
160
173
|
mcpxClient,
|
|
161
|
-
|
|
174
|
+
cfg,
|
|
162
175
|
(phase) => spinner.update({ text: phase }),
|
|
163
176
|
);
|
|
164
177
|
spinner.success({
|
package/src/commands/nuke.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import ansis from "ansis";
|
|
3
4
|
import type { Command } from "commander";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
SCHEDULES_DIR,
|
|
8
|
-
TASKS_DIR,
|
|
9
|
-
THREADS_DIR,
|
|
10
|
-
} from "../constants.ts";
|
|
5
|
+
import { loadConfig } from "../config/loader.ts";
|
|
6
|
+
import { SCHEDULES_DIR, TASKS_DIR, THREADS_DIR } from "../constants.ts";
|
|
7
|
+
import { openMembot, resolveMembotDir } from "../mem/client.ts";
|
|
11
8
|
import { deleteAllSchedules } from "../schedules/store.ts";
|
|
12
9
|
import { deleteAllTasks } from "../tasks/store.ts";
|
|
13
10
|
import { deleteAllThreads } from "../threads/store.ts";
|
|
14
11
|
import { logger } from "../utils/logger.ts";
|
|
15
12
|
import { listWorkers } from "../workers/store.ts";
|
|
16
13
|
|
|
17
|
-
type NukeScope = "
|
|
14
|
+
type NukeScope = "knowledge" | "tasks" | "schedules" | "threads" | "all";
|
|
18
15
|
|
|
19
16
|
async function ensureNoRunningWorkers(projectDir: string): Promise<boolean> {
|
|
20
17
|
const running = await listWorkers(projectDir, { status: "running" });
|
|
@@ -30,10 +27,50 @@ async function ensureNoRunningWorkers(projectDir: string): Promise<boolean> {
|
|
|
30
27
|
return true;
|
|
31
28
|
}
|
|
32
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Erase the membot knowledge store: tombstone every current file and prune
|
|
32
|
+
* non-current versions. We do this through the SDK rather than `rm`ing
|
|
33
|
+
* `index.duckdb` directly so a concurrent open client (e.g. a tool call in
|
|
34
|
+
* mid-flight) doesn't lose data unpredictably; if the file is empty / missing
|
|
35
|
+
* we fall back to deleting it outright.
|
|
36
|
+
*/
|
|
37
|
+
async function nukeKnowledge(projectDir: string): Promise<void> {
|
|
38
|
+
const config = await loadConfig(projectDir);
|
|
39
|
+
const membotDir = resolveMembotDir(projectDir, config);
|
|
40
|
+
const indexPath = join(membotDir, "index.duckdb");
|
|
41
|
+
if (config.membot_scope !== "project") {
|
|
42
|
+
logger.warn(
|
|
43
|
+
`membot_scope is "${config.membot_scope}"; this will erase the SHARED knowledge store at ${membotDir}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const mem = openMembot(membotDir);
|
|
48
|
+
try {
|
|
49
|
+
const list = await mem.list({ limit: 100_000 });
|
|
50
|
+
const paths = list.entries.map((e) => e.logical_path);
|
|
51
|
+
if (paths.length > 0) {
|
|
52
|
+
await mem.remove({ paths });
|
|
53
|
+
}
|
|
54
|
+
// Drop all versions older than now (i.e. everything we just tombstoned).
|
|
55
|
+
await mem.prune({ before: new Date().toISOString() });
|
|
56
|
+
logger.success(
|
|
57
|
+
`Cleared the membot knowledge store (${paths.length} entries removed, history pruned)`,
|
|
58
|
+
);
|
|
59
|
+
} finally {
|
|
60
|
+
await mem.close();
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
logger.warn(
|
|
64
|
+
`membot prune failed (${(err as Error).message}); removing ${indexPath} instead`,
|
|
65
|
+
);
|
|
66
|
+
await rm(indexPath, { force: true });
|
|
67
|
+
logger.success(`Removed ${indexPath}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
33
71
|
async function runNuke(projectDir: string, scope: NukeScope): Promise<void> {
|
|
34
|
-
if (scope === "
|
|
35
|
-
await
|
|
36
|
-
logger.success(`Removed ${CONTEXT_DIR}/ directory`);
|
|
72
|
+
if (scope === "knowledge" || scope === "all") {
|
|
73
|
+
await nukeKnowledge(projectDir);
|
|
37
74
|
}
|
|
38
75
|
if (scope === "tasks" || scope === "all") {
|
|
39
76
|
const n = await deleteAllTasks(projectDir);
|
|
@@ -71,7 +108,7 @@ function registerScope(
|
|
|
71
108
|
console.log(ansis.red.bold(`Nuke scope: ${scope}`));
|
|
72
109
|
console.log(
|
|
73
110
|
ansis.yellow(
|
|
74
|
-
`Re-run with --yes to confirm. This will delete
|
|
111
|
+
`Re-run with --yes to confirm. This will delete data; cannot be undone.`,
|
|
75
112
|
),
|
|
76
113
|
);
|
|
77
114
|
process.exit(1);
|
|
@@ -89,8 +126,8 @@ export function registerNukeCommand(program: Command) {
|
|
|
89
126
|
registerScope(
|
|
90
127
|
program,
|
|
91
128
|
nuke,
|
|
92
|
-
"
|
|
93
|
-
|
|
129
|
+
"knowledge",
|
|
130
|
+
"Erase the entire membot knowledge store (every current entry tombstoned and pruned)",
|
|
94
131
|
);
|
|
95
132
|
registerScope(
|
|
96
133
|
program,
|
|
@@ -114,6 +151,6 @@ export function registerNukeCommand(program: Command) {
|
|
|
114
151
|
program,
|
|
115
152
|
nuke,
|
|
116
153
|
"all",
|
|
117
|
-
"Erase all agent-writable data:
|
|
154
|
+
"Erase all agent-writable data: membot store, tasks/, schedules/, threads/",
|
|
118
155
|
);
|
|
119
156
|
}
|
package/src/commands/prepare.ts
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { loadConfig } from "../config/loader.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { openMembot, resolveMembotDir } from "../mem/client.ts";
|
|
4
4
|
import { logger } from "../utils/logger.ts";
|
|
5
|
-
import { withDb } from "./with-db.ts";
|
|
6
5
|
|
|
7
6
|
export function registerPrepareCommand(program: Command) {
|
|
8
7
|
program
|
|
9
8
|
.command("prepare")
|
|
10
|
-
.description(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
.description(
|
|
10
|
+
"Verify the project is healthy: load config and open the membot knowledge store (triggers any first-run migration / model download).",
|
|
11
|
+
)
|
|
12
|
+
.action(async () => {
|
|
13
|
+
const projectDir = program.opts().dir as string;
|
|
14
|
+
logger.info("Preparing Botholomew...");
|
|
15
|
+
const config = await loadConfig(projectDir);
|
|
16
|
+
const mem = openMembot(resolveMembotDir(projectDir, config));
|
|
17
|
+
try {
|
|
18
|
+
await mem.connect();
|
|
19
|
+
logger.success("membot knowledge store opened successfully");
|
|
20
|
+
} finally {
|
|
21
|
+
await mem.close();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
21
24
|
}
|
package/src/config/loader.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getConfigPath, getModelsDir } from "../constants.ts";
|
|
1
|
+
import { getConfigPath } from "../constants.ts";
|
|
3
2
|
import { setLogLevel } from "../utils/logger.ts";
|
|
4
3
|
import { type BotholomewConfig, DEFAULT_CONFIG } from "./schemas.ts";
|
|
5
4
|
|
|
@@ -23,12 +22,6 @@ export async function loadConfig(
|
|
|
23
22
|
|
|
24
23
|
setLogLevel(config.log_level);
|
|
25
24
|
|
|
26
|
-
const modelsDir = getModelsDir(projectDir);
|
|
27
|
-
mkdirSync(modelsDir, { recursive: true });
|
|
28
|
-
// Dynamic import keeps @huggingface/transformers (heavy, pulls ONNX runtime) out of commands that never embed.
|
|
29
|
-
const { setEmbeddingCacheDir } = await import("../context/embedder-impl.ts");
|
|
30
|
-
setEmbeddingCacheDir(modelsDir);
|
|
31
|
-
|
|
32
25
|
return config;
|
|
33
26
|
}
|
|
34
27
|
|