botholomew 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/chat/session.ts +4 -4
- package/src/cli.ts +6 -2
- package/src/commands/capabilities.ts +4 -2
- package/src/commands/context.ts +17 -6
- package/src/commands/init.ts +22 -1
- package/src/commands/mcpx.ts +20 -7
- package/src/commands/nuke.ts +11 -3
- package/src/commands/prepare.ts +2 -3
- package/src/config/schemas.ts +6 -0
- package/src/init/index.ts +47 -19
- package/src/mcpx/client.ts +21 -5
- package/src/mem/client.ts +26 -10
- package/src/worker/index.ts +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botholomew",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "An autonomous AI agent for knowledge work — works your task queue while you sleep.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sdk": "^0.92.0",
|
|
31
|
-
"@evantahler/mcpx": "0.21.
|
|
31
|
+
"@evantahler/mcpx": "0.21.6",
|
|
32
32
|
"ansis": "^4.2.0",
|
|
33
33
|
"commander": "^14.0.0",
|
|
34
34
|
"gray-matter": "^4.0.3",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"ink-spinner": "^5.0.0",
|
|
37
37
|
"ink-text-input": "^6.0.0",
|
|
38
38
|
"istextorbinary": "^9.5.0",
|
|
39
|
-
"membot": "^0.
|
|
39
|
+
"membot": "^0.14.0",
|
|
40
40
|
"nanospinner": "^1.2.2",
|
|
41
41
|
"react": "^19.2.0",
|
|
42
42
|
"uuid": "^14.0.0",
|
package/src/chat/session.ts
CHANGED
|
@@ -3,8 +3,8 @@ import type { MessageParam } from "@anthropic-ai/sdk/resources/messages";
|
|
|
3
3
|
import type { MembotClient } from "membot";
|
|
4
4
|
import { loadConfig } from "../config/loader.ts";
|
|
5
5
|
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
6
|
-
import { createMcpxClient } from "../mcpx/client.ts";
|
|
7
|
-
import { openMembot } from "../mem/client.ts";
|
|
6
|
+
import { createMcpxClient, resolveMcpxDir } from "../mcpx/client.ts";
|
|
7
|
+
import { openMembot, resolveMembotDir } from "../mem/client.ts";
|
|
8
8
|
import { loadSkills } from "../skills/loader.ts";
|
|
9
9
|
import type { SkillDefinition } from "../skills/parser.ts";
|
|
10
10
|
import {
|
|
@@ -60,7 +60,7 @@ export async function startChatSession(
|
|
|
60
60
|
);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const mem = openMembot(projectDir);
|
|
63
|
+
const mem = openMembot(resolveMembotDir(projectDir, config));
|
|
64
64
|
await mem.connect();
|
|
65
65
|
await ensureThreadsDir(projectDir);
|
|
66
66
|
|
|
@@ -101,7 +101,7 @@ export async function startChatSession(
|
|
|
101
101
|
);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const mcpxClient = await createMcpxClient(projectDir);
|
|
104
|
+
const mcpxClient = await createMcpxClient(resolveMcpxDir(projectDir, config));
|
|
105
105
|
const skills = await loadSkills(projectDir);
|
|
106
106
|
|
|
107
107
|
const cleanup = async () => {
|
package/src/cli.ts
CHANGED
|
@@ -29,9 +29,13 @@ program
|
|
|
29
29
|
.configureHelp({
|
|
30
30
|
styleTitle: (str) => ansis.bold(str),
|
|
31
31
|
styleUsage: (str) => ansis.cyan(str),
|
|
32
|
-
styleCommandText: (str) => ansis.
|
|
32
|
+
styleCommandText: (str) => ansis.cyan.bold(str),
|
|
33
|
+
styleSubcommandTerm: (str) => ansis.green(str),
|
|
34
|
+
styleSubcommandDescription: (str) => ansis.dim(str),
|
|
33
35
|
styleOptionTerm: (str) => ansis.yellow(str),
|
|
34
|
-
|
|
36
|
+
styleOptionDescription: (str) => ansis.dim(str),
|
|
37
|
+
styleArgumentTerm: (str) => ansis.magenta(str),
|
|
38
|
+
styleArgumentDescription: (str) => ansis.dim(str),
|
|
35
39
|
});
|
|
36
40
|
|
|
37
41
|
registerInitCommand(program);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { createSpinner } from "nanospinner";
|
|
3
3
|
import { loadConfig } from "../config/loader.ts";
|
|
4
|
-
import { createMcpxClient } from "../mcpx/client.ts";
|
|
4
|
+
import { createMcpxClient, resolveMcpxDir } from "../mcpx/client.ts";
|
|
5
5
|
import { writeCapabilitiesFile } from "../prompts/capabilities.ts";
|
|
6
6
|
import { registerAllTools } from "../tools/registry.ts";
|
|
7
7
|
|
|
@@ -19,7 +19,9 @@ export function registerCapabilitiesCommand(program: Command) {
|
|
|
19
19
|
const spinner = createSpinner("Loading config").start();
|
|
20
20
|
const config = await loadConfig(dir);
|
|
21
21
|
spinner.update({ text: "Connecting to MCPX servers" });
|
|
22
|
-
const mcpxClient = includeMcp
|
|
22
|
+
const mcpxClient = includeMcp
|
|
23
|
+
? await createMcpxClient(resolveMcpxDir(dir, config))
|
|
24
|
+
: null;
|
|
23
25
|
try {
|
|
24
26
|
const result = await writeCapabilitiesFile(
|
|
25
27
|
dir,
|
package/src/commands/context.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { join } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import type { Command } from "commander";
|
|
7
7
|
import { defaultCliName, OPERATIONS } from "membot";
|
|
8
|
+
import { loadConfig } from "../config/loader.ts";
|
|
9
|
+
import { resolveMembotDir } from "../mem/client.ts";
|
|
8
10
|
import { logger } from "../utils/logger.ts";
|
|
9
11
|
|
|
10
12
|
const require = createRequire(import.meta.url);
|
|
@@ -36,11 +38,14 @@ function getRawContextArgs(): string[] {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
async function runMembot(projectDir: string, args: string[]): Promise<number> {
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// output they would running
|
|
43
|
-
|
|
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], {
|
|
44
49
|
stdout: "inherit",
|
|
45
50
|
stderr: "inherit",
|
|
46
51
|
stdin: "inherit",
|
|
@@ -66,7 +71,7 @@ function registerImportGlobal(parent: Command, program: Command): void {
|
|
|
66
71
|
"Overwrite an existing index.duckdb in the project",
|
|
67
72
|
false,
|
|
68
73
|
)
|
|
69
|
-
.action((opts: { force: boolean }) => {
|
|
74
|
+
.action(async (opts: { force: boolean }) => {
|
|
70
75
|
const globalDir = join(homedir(), ".membot");
|
|
71
76
|
if (!existsSync(globalDir)) {
|
|
72
77
|
logger.error("No global membot data found at ~/.membot");
|
|
@@ -74,6 +79,12 @@ function registerImportGlobal(parent: Command, program: Command): void {
|
|
|
74
79
|
}
|
|
75
80
|
|
|
76
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
|
+
);
|
|
87
|
+
}
|
|
77
88
|
const dest = (name: string) => join(projectDir, name);
|
|
78
89
|
const destDb = dest("index.duckdb");
|
|
79
90
|
|
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,7 +7,7 @@ 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 { createMcpxClient } from "../mcpx/client.ts";
|
|
10
|
+
import { createMcpxClient, resolveMcpxDir } from "../mcpx/client.ts";
|
|
11
11
|
import { writeCapabilitiesFile } from "../prompts/capabilities.ts";
|
|
12
12
|
import { registerAllTools } from "../tools/registry.ts";
|
|
13
13
|
import { logger } from "../utils/logger.ts";
|
|
@@ -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
|
@@ -2,8 +2,9 @@ import { rm } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import ansis from "ansis";
|
|
4
4
|
import type { Command } from "commander";
|
|
5
|
+
import { loadConfig } from "../config/loader.ts";
|
|
5
6
|
import { SCHEDULES_DIR, TASKS_DIR, THREADS_DIR } from "../constants.ts";
|
|
6
|
-
import { openMembot } from "../mem/client.ts";
|
|
7
|
+
import { openMembot, resolveMembotDir } from "../mem/client.ts";
|
|
7
8
|
import { deleteAllSchedules } from "../schedules/store.ts";
|
|
8
9
|
import { deleteAllTasks } from "../tasks/store.ts";
|
|
9
10
|
import { deleteAllThreads } from "../threads/store.ts";
|
|
@@ -34,9 +35,16 @@ async function ensureNoRunningWorkers(projectDir: string): Promise<boolean> {
|
|
|
34
35
|
* we fall back to deleting it outright.
|
|
35
36
|
*/
|
|
36
37
|
async function nukeKnowledge(projectDir: string): Promise<void> {
|
|
37
|
-
const
|
|
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
|
+
}
|
|
38
46
|
try {
|
|
39
|
-
const mem = openMembot(
|
|
47
|
+
const mem = openMembot(membotDir);
|
|
40
48
|
try {
|
|
41
49
|
const list = await mem.list({ limit: 100_000 });
|
|
42
50
|
const paths = list.entries.map((e) => e.logical_path);
|
package/src/commands/prepare.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { loadConfig } from "../config/loader.ts";
|
|
3
|
-
import { openMembot } from "../mem/client.ts";
|
|
3
|
+
import { openMembot, resolveMembotDir } from "../mem/client.ts";
|
|
4
4
|
import { logger } from "../utils/logger.ts";
|
|
5
5
|
|
|
6
6
|
export function registerPrepareCommand(program: Command) {
|
|
@@ -13,8 +13,7 @@ export function registerPrepareCommand(program: Command) {
|
|
|
13
13
|
const projectDir = program.opts().dir as string;
|
|
14
14
|
logger.info("Preparing Botholomew...");
|
|
15
15
|
const config = await loadConfig(projectDir);
|
|
16
|
-
|
|
17
|
-
const mem = openMembot(projectDir);
|
|
16
|
+
const mem = openMembot(resolveMembotDir(projectDir, config));
|
|
18
17
|
try {
|
|
19
18
|
await mem.connect();
|
|
20
19
|
logger.success("membot knowledge store opened successfully");
|
package/src/config/schemas.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type Scope = "global" | "project";
|
|
2
|
+
|
|
1
3
|
export interface BotholomewConfig {
|
|
2
4
|
anthropic_api_key?: string;
|
|
3
5
|
model?: string;
|
|
@@ -16,6 +18,8 @@ export interface BotholomewConfig {
|
|
|
16
18
|
schedule_claim_stale_seconds?: number;
|
|
17
19
|
tui_idle_timeout_seconds?: number;
|
|
18
20
|
log_level?: string;
|
|
21
|
+
membot_scope?: Scope;
|
|
22
|
+
mcpx_scope?: Scope;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export const DEFAULT_CONFIG: Required<BotholomewConfig> = {
|
|
@@ -36,4 +40,6 @@ export const DEFAULT_CONFIG: Required<BotholomewConfig> = {
|
|
|
36
40
|
schedule_claim_stale_seconds: 300,
|
|
37
41
|
tui_idle_timeout_seconds: 180,
|
|
38
42
|
log_level: "",
|
|
43
|
+
membot_scope: "global",
|
|
44
|
+
mcpx_scope: "global",
|
|
39
45
|
};
|
package/src/init/index.ts
CHANGED
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
TASKS_DIR,
|
|
22
22
|
} from "../constants.ts";
|
|
23
23
|
import { assertCompatibleFilesystem } from "../fs/compat.ts";
|
|
24
|
-
import { createMcpxClient } from "../mcpx/client.ts";
|
|
25
|
-
import { openMembot } from "../mem/client.ts";
|
|
24
|
+
import { createMcpxClient, resolveMcpxDir } from "../mcpx/client.ts";
|
|
25
|
+
import { openMembot, resolveMembotDir } from "../mem/client.ts";
|
|
26
26
|
import { writeCapabilitiesFile } from "../prompts/capabilities.ts";
|
|
27
27
|
import { registerAllTools } from "../tools/registry.ts";
|
|
28
28
|
import { logger } from "../utils/logger.ts";
|
|
@@ -37,9 +37,17 @@ import {
|
|
|
37
37
|
SUMMARIZE_SKILL,
|
|
38
38
|
} from "./templates.ts";
|
|
39
39
|
|
|
40
|
+
export interface InitOptions {
|
|
41
|
+
force?: boolean;
|
|
42
|
+
/** Override the default `membot_scope` written into config/config.json. */
|
|
43
|
+
membotScope?: "global" | "project";
|
|
44
|
+
/** Override the default `mcpx_scope` written into config/config.json. */
|
|
45
|
+
mcpxScope?: "global" | "project";
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
export async function initProject(
|
|
41
49
|
projectDir: string,
|
|
42
|
-
opts:
|
|
50
|
+
opts: InitOptions = {},
|
|
43
51
|
): Promise<void> {
|
|
44
52
|
// Refuse to operate inside iCloud/Dropbox/etc unless --force is passed.
|
|
45
53
|
// Sync overlays break atomic rename / O_EXCL semantics that tasks and
|
|
@@ -79,43 +87,63 @@ export async function initProject(
|
|
|
79
87
|
await Bun.write(join(skillsDir, "standup.md"), STANDUP_SKILL);
|
|
80
88
|
await Bun.write(join(skillsDir, "capabilities.md"), CAPABILITIES_SKILL);
|
|
81
89
|
|
|
82
|
-
// Config
|
|
83
|
-
|
|
90
|
+
// Config — apply scope overrides from caller (CLI flags / tests) on top of
|
|
91
|
+
// the seeded defaults so tests and `botholomew init --membot-scope=project`
|
|
92
|
+
// can pick a per-project layout up front.
|
|
93
|
+
const initialConfig = {
|
|
94
|
+
...DEFAULT_CONFIG,
|
|
95
|
+
...(opts.membotScope ? { membot_scope: opts.membotScope } : {}),
|
|
96
|
+
...(opts.mcpxScope ? { mcpx_scope: opts.mcpxScope } : {}),
|
|
97
|
+
};
|
|
98
|
+
await Bun.write(configPath, `${JSON.stringify(initialConfig, null, 2)}\n`);
|
|
99
|
+
const config = await loadConfig(projectDir);
|
|
84
100
|
|
|
85
|
-
// mcpx servers config
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
// mcpx servers config — only seed a project-local servers.json when the
|
|
102
|
+
// project is opting out of the shared `~/.mcpx`. The empty `mcpx/` directory
|
|
103
|
+
// is still created above so flipping `mcpx_scope` later is a one-line edit.
|
|
104
|
+
if (config.mcpx_scope === "project") {
|
|
105
|
+
await Bun.write(
|
|
106
|
+
join(getMcpxDir(projectDir), MCPX_SERVERS_FILENAME),
|
|
107
|
+
`${JSON.stringify(DEFAULT_MCPX_SERVERS, null, 2)}\n`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
90
110
|
|
|
91
111
|
// Initialize the membot knowledge store. Opening + closing the client
|
|
92
|
-
// triggers membot's first-run migration
|
|
93
|
-
// ready
|
|
94
|
-
|
|
112
|
+
// triggers membot's first-run migration. When `membot_scope` is "global"
|
|
113
|
+
// (the default) we point at `~/.membot` so the shared store is ready;
|
|
114
|
+
// when "project" we seed `<projectDir>/index.duckdb`.
|
|
115
|
+
const mem = openMembot(resolveMembotDir(projectDir, config));
|
|
95
116
|
await mem.connect();
|
|
96
117
|
await mem.close();
|
|
97
118
|
|
|
98
119
|
// Populate capabilities.md with the real tool inventory.
|
|
99
120
|
registerAllTools();
|
|
100
|
-
const
|
|
101
|
-
const mcpxClient = await createMcpxClient(projectDir);
|
|
121
|
+
const mcpxClient = await createMcpxClient(resolveMcpxDir(projectDir, config));
|
|
102
122
|
try {
|
|
103
123
|
await writeCapabilitiesFile(projectDir, mcpxClient, config);
|
|
104
124
|
} finally {
|
|
105
125
|
await mcpxClient?.close();
|
|
106
126
|
}
|
|
107
127
|
|
|
128
|
+
const membotScopeDesc =
|
|
129
|
+
config.membot_scope === "project"
|
|
130
|
+
? `${projectDir}/index.duckdb (project-local)`
|
|
131
|
+
: `~/.membot (shared across projects — set membot_scope to "project" in ${CONFIG_DIR}/${CONFIG_FILENAME} to isolate)`;
|
|
132
|
+
const mcpxScopeDesc =
|
|
133
|
+
config.mcpx_scope === "project"
|
|
134
|
+
? `${projectDir}/mcpx/ (project-local)`
|
|
135
|
+
: `~/.mcpx (shared across projects — set mcpx_scope to "project" in ${CONFIG_DIR}/${CONFIG_FILENAME} to isolate)`;
|
|
108
136
|
logger.success("Initialized Botholomew project");
|
|
109
|
-
logger.dim(` Project root:
|
|
110
|
-
logger.dim(` Config:
|
|
111
|
-
logger.dim(` Knowledge:
|
|
137
|
+
logger.dim(` Project root: ${projectDir}`);
|
|
138
|
+
logger.dim(` Config: ${CONFIG_DIR}/${CONFIG_FILENAME}`);
|
|
139
|
+
logger.dim(` Knowledge: ${membotScopeDesc}`);
|
|
140
|
+
logger.dim(` MCPX: ${mcpxScopeDesc}`);
|
|
112
141
|
logger.dim("");
|
|
113
142
|
logger.dim("Layout:");
|
|
114
143
|
logger.dim(` ${CONFIG_DIR}/ settings`);
|
|
115
144
|
logger.dim(
|
|
116
145
|
` prompts/ goals, beliefs, capabilities (and any you add)`,
|
|
117
146
|
);
|
|
118
|
-
logger.dim(` index.duckdb agent's knowledge store (membot)`);
|
|
119
147
|
logger.dim(` ${TASKS_DIR}/ one markdown file per task`);
|
|
120
148
|
logger.dim(` ${LOCKS_SUBDIR}/ worker claim lockfiles`);
|
|
121
149
|
logger.dim(` ${SCHEDULES_DIR}/ one markdown file per schedule`);
|
package/src/mcpx/client.ts
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { type CallToolResult, McpxClient } from "@evantahler/mcpx";
|
|
5
|
+
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
4
6
|
import { getMcpxDir, MCPX_SERVERS_FILENAME } from "../constants.ts";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
+
* Resolve the mcpx config directory for a project, honoring `mcpx_scope`:
|
|
10
|
+
* - "global" → `~/.mcpx` (shared across all Botholomew projects)
|
|
11
|
+
* - "project" → `<projectDir>/mcpx` (isolated per project)
|
|
9
12
|
*/
|
|
10
|
-
export
|
|
13
|
+
export function resolveMcpxDir(
|
|
11
14
|
projectDir: string,
|
|
15
|
+
config: Pick<BotholomewConfig, "mcpx_scope">,
|
|
16
|
+
): string {
|
|
17
|
+
return config.mcpx_scope === "project"
|
|
18
|
+
? getMcpxDir(projectDir)
|
|
19
|
+
: join(homedir(), ".mcpx");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create an McpxClient from `<mcpxDir>/servers.json`. Returns null if the
|
|
24
|
+
* file is missing or has no servers configured. The caller is responsible
|
|
25
|
+
* for resolving `mcpxDir` via `resolveMcpxDir`.
|
|
26
|
+
*/
|
|
27
|
+
export async function createMcpxClient(
|
|
28
|
+
mcpxDir: string,
|
|
12
29
|
): Promise<McpxClient | null> {
|
|
13
|
-
const serversPath = join(
|
|
30
|
+
const serversPath = join(mcpxDir, MCPX_SERVERS_FILENAME);
|
|
14
31
|
if (!existsSync(serversPath)) return null;
|
|
15
32
|
|
|
16
33
|
const raw = await Bun.file(serversPath).text();
|
|
@@ -20,7 +37,6 @@ export async function createMcpxClient(
|
|
|
20
37
|
return null;
|
|
21
38
|
}
|
|
22
39
|
|
|
23
|
-
const mcpxDir = getMcpxDir(projectDir);
|
|
24
40
|
const authPath = join(mcpxDir, "auth.json");
|
|
25
41
|
const auth = existsSync(authPath)
|
|
26
42
|
? JSON.parse(await Bun.file(authPath).text())
|
package/src/mem/client.ts
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { MembotClient } from "membot";
|
|
4
|
+
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
|
-
*
|
|
5
|
-
* membot
|
|
6
|
-
*
|
|
7
|
-
* on shutdown.
|
|
7
|
+
* Resolve the membot data directory for a project, honoring `membot_scope`:
|
|
8
|
+
* - "global" → `~/.membot` (shared across all Botholomew projects)
|
|
9
|
+
* - "project" → `<projectDir>` (isolated per project)
|
|
8
10
|
*
|
|
9
11
|
* Membot's `configFlag` doubles as its data-dir flag (see
|
|
10
|
-
* `membot/src/config/loader.ts::resolveDataDir`): an explicit
|
|
11
|
-
* `$MEMBOT_HOME` and the `~/.membot` default. We pass
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* `node_modules/membot/src/config/loader.ts::resolveDataDir`): an explicit
|
|
13
|
+
* value wins over `$MEMBOT_HOME` and the `~/.membot` default. We always pass
|
|
14
|
+
* an explicit value so a stray `MEMBOT_HOME` cannot redirect Botholomew at a
|
|
15
|
+
* different store.
|
|
14
16
|
*/
|
|
15
|
-
export function
|
|
16
|
-
|
|
17
|
+
export function resolveMembotDir(
|
|
18
|
+
projectDir: string,
|
|
19
|
+
config: Pick<BotholomewConfig, "membot_scope">,
|
|
20
|
+
): string {
|
|
21
|
+
return config.membot_scope === "project"
|
|
22
|
+
? projectDir
|
|
23
|
+
: join(homedir(), ".membot");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Open a membot client rooted at `dataDir`. The caller is responsible for
|
|
28
|
+
* resolving the directory (via `resolveMembotDir`) and for `close()` on
|
|
29
|
+
* shutdown.
|
|
30
|
+
*/
|
|
31
|
+
export function openMembot(dataDir: string): MembotClient {
|
|
32
|
+
return new MembotClient({ configFlag: dataDir });
|
|
17
33
|
}
|
package/src/worker/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { hostname } from "node:os";
|
|
2
2
|
import ansis from "ansis";
|
|
3
3
|
import { loadConfig } from "../config/loader.ts";
|
|
4
|
-
import { createMcpxClient } from "../mcpx/client.ts";
|
|
5
|
-
import { openMembot } from "../mem/client.ts";
|
|
4
|
+
import { createMcpxClient, resolveMcpxDir } from "../mcpx/client.ts";
|
|
5
|
+
import { openMembot, resolveMembotDir } from "../mem/client.ts";
|
|
6
6
|
import { logger } from "../utils/logger.ts";
|
|
7
7
|
import { uuidv7 } from "../utils/uuid.ts";
|
|
8
8
|
import { markWorkerStopped, registerWorker } from "../workers/store.ts";
|
|
@@ -87,12 +87,12 @@ export async function startWorker(
|
|
|
87
87
|
const evalSchedules = options.evalSchedules ?? !taskId;
|
|
88
88
|
|
|
89
89
|
const config = await loadConfig(projectDir);
|
|
90
|
-
const mem = openMembot(projectDir);
|
|
90
|
+
const mem = openMembot(resolveMembotDir(projectDir, config));
|
|
91
91
|
// Surface init-time failures (bad config, locked DB) up front rather than
|
|
92
92
|
// letting the first tool call do it.
|
|
93
93
|
await mem.connect();
|
|
94
94
|
|
|
95
|
-
const mcpxClient = await createMcpxClient(projectDir);
|
|
95
|
+
const mcpxClient = await createMcpxClient(resolveMcpxDir(projectDir, config));
|
|
96
96
|
if (mcpxClient) {
|
|
97
97
|
logger.info("MCPX client initialized with external tools");
|
|
98
98
|
}
|