botholomew 0.17.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botholomew",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
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.3",
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.13.3",
39
+ "membot": "^0.14.0",
40
40
  "nanospinner": "^1.2.2",
41
41
  "react": "^19.2.0",
42
42
  "uuid": "^14.0.0",
@@ -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 () => {
@@ -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 ? await createMcpxClient(dir) : null;
22
+ const mcpxClient = includeMcp
23
+ ? await createMcpxClient(resolveMcpxDir(dir, config))
24
+ : null;
23
25
  try {
24
26
  const result = await writeCapabilitiesFile(
25
27
  dir,
@@ -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
- // Point membot at <projectDir> as its data dir via the `--config` flag —
40
- // each Botholomew project gets its own membot store at
41
- // `<projectDir>/index.duckdb`. Forward stdio so the user sees the same
42
- // output they would running `membot` directly.
43
- const proc = Bun.spawn(["bun", MEMBOT_CLI, "--config", projectDir, ...args], {
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
 
@@ -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, { force: opts.force });
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);
@@ -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
- const mcpxDir = getMcpxDir(projectDir);
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 projectMcpxDir = getMcpxDir(getDir(program));
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
- const config = await loadConfig(projectDir);
155
- const mcpxClient = await createMcpxClient(projectDir);
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
- config,
174
+ cfg,
162
175
  (phase) => spinner.update({ text: phase }),
163
176
  );
164
177
  spinner.success({
@@ -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 indexPath = join(projectDir, "index.duckdb");
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(projectDir);
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);
@@ -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
- void config;
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");
@@ -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: { force?: boolean } = {},
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
- await Bun.write(configPath, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`);
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
- await Bun.write(
87
- join(getMcpxDir(projectDir), MCPX_SERVERS_FILENAME),
88
- `${JSON.stringify(DEFAULT_MCPX_SERVERS, null, 2)}\n`,
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 so the project ships with a
93
- // ready-to-use index.duckdb.
94
- const mem = openMembot(projectDir);
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 config = await loadConfig(projectDir);
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: ${projectDir}`);
110
- logger.dim(` Config: ${CONFIG_DIR}/${CONFIG_FILENAME}`);
111
- logger.dim(` Knowledge: index.duckdb (managed by membot)`);
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`);
@@ -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
- * Create an McpxClient from the project's mcpx/servers.json.
8
- * Returns null if the file is missing or has no servers configured.
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 async function createMcpxClient(
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(getMcpxDir(projectDir), MCPX_SERVERS_FILENAME);
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
- * Open a per-project membot client. Each Botholomew project gets its own
5
- * membot data dir (`<projectDir>/config.json` + `<projectDir>/index.duckdb`)
6
- * so projects don't share knowledge. The caller is responsible for `close()`
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 value wins over
11
- * `$MEMBOT_HOME` and the `~/.membot` default. We pass the project directory
12
- * unconditionally so a stray `MEMBOT_HOME` in the user's environment cannot
13
- * redirect Botholomew at a different store.
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 openMembot(projectDir: string): MembotClient {
16
- return new MembotClient({ configFlag: projectDir });
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
  }
@@ -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
  }