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/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/constants.ts
CHANGED
|
@@ -6,19 +6,22 @@ import { join } from "node:path";
|
|
|
6
6
|
*
|
|
7
7
|
* <projectDir>/
|
|
8
8
|
* config/config.json
|
|
9
|
-
*
|
|
9
|
+
* config.json membot config (separate from ours)
|
|
10
|
+
* index.duckdb membot-owned knowledge store
|
|
11
|
+
* prompts/*.md init seeds goals/beliefs/capabilities
|
|
10
12
|
* skills/*.md
|
|
11
13
|
* mcpx/servers.json
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* tasks/.locks/<id>.lock O_EXCL claim files
|
|
16
|
-
* schedules/<id>.md schedules
|
|
14
|
+
* tasks/<id>.md tasks (status in frontmatter)
|
|
15
|
+
* tasks/.locks/<id>.lock O_EXCL claim files
|
|
16
|
+
* schedules/<id>.md
|
|
17
17
|
* schedules/.locks/<id>.lock
|
|
18
|
-
* threads/<YYYY-MM-DD>/<id>.csv
|
|
19
|
-
* workers/<id>.json
|
|
20
|
-
* logs/
|
|
21
|
-
*
|
|
18
|
+
* threads/<YYYY-MM-DD>/<id>.csv conversation history
|
|
19
|
+
* workers/<id>.json pidfile + heartbeat
|
|
20
|
+
* logs/ worker logs
|
|
21
|
+
*
|
|
22
|
+
* The agent's knowledge ("what used to be `context/`") now lives in
|
|
23
|
+
* `index.duckdb`, managed by the `membot` library. Tasks, schedules, threads,
|
|
24
|
+
* workers, prompts, and skills remain real files on disk.
|
|
22
25
|
*/
|
|
23
26
|
|
|
24
27
|
export const HOME_CONFIG_DIR = join(homedir(), ".botholomew");
|
|
@@ -32,14 +35,11 @@ export const DEFAULTS = {
|
|
|
32
35
|
UPDATE_CHECK_TIMEOUT_MS: 5_000,
|
|
33
36
|
} as const;
|
|
34
37
|
|
|
35
|
-
export const INDEX_DB_FILENAME = "index.duckdb";
|
|
36
38
|
export const CONFIG_DIR = "config";
|
|
37
39
|
export const CONFIG_FILENAME = "config.json";
|
|
38
40
|
export const PROMPTS_DIR = "prompts";
|
|
39
41
|
export const SKILLS_DIR = "skills";
|
|
40
42
|
export const MCPX_DIR = "mcpx";
|
|
41
|
-
export const MODELS_DIR = "models";
|
|
42
|
-
export const CONTEXT_DIR = "context";
|
|
43
43
|
export const TASKS_DIR = "tasks";
|
|
44
44
|
export const SCHEDULES_DIR = "schedules";
|
|
45
45
|
export const LOCKS_SUBDIR = ".locks";
|
|
@@ -47,24 +47,18 @@ export const LOGS_DIR = "logs";
|
|
|
47
47
|
export const WORKERS_DIR = "workers";
|
|
48
48
|
export const THREADS_DIR = "threads";
|
|
49
49
|
export const MCPX_SERVERS_FILENAME = "servers.json";
|
|
50
|
-
export const EMBEDDING_DIMENSION = 384;
|
|
51
|
-
export const EMBEDDING_MODEL = "Xenova/bge-small-en-v1.5";
|
|
52
50
|
|
|
53
51
|
/**
|
|
54
|
-
* Top-level areas tools must never touch directly.
|
|
55
|
-
*
|
|
52
|
+
* Top-level areas tools must never touch directly. Tasks/schedule lockfile
|
|
53
|
+
* dirs are kept off-limits because their `O_EXCL` claim semantics break if
|
|
54
|
+
* something else writes into them.
|
|
56
55
|
*/
|
|
57
56
|
export const PROTECTED_AREAS: ReadonlySet<string> = new Set([
|
|
58
|
-
MODELS_DIR,
|
|
59
57
|
LOGS_DIR,
|
|
60
58
|
`${TASKS_DIR}/${LOCKS_SUBDIR}`,
|
|
61
59
|
`${SCHEDULES_DIR}/${LOCKS_SUBDIR}`,
|
|
62
60
|
]);
|
|
63
61
|
|
|
64
|
-
export function getDbPath(projectDir: string): string {
|
|
65
|
-
return join(projectDir, INDEX_DB_FILENAME);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
62
|
export function getWorkerLogsDir(projectDir: string): string {
|
|
69
63
|
return join(projectDir, LOGS_DIR);
|
|
70
64
|
}
|
|
@@ -92,12 +86,6 @@ export function getMcpxDir(projectDir: string): string {
|
|
|
92
86
|
return join(projectDir, MCPX_DIR);
|
|
93
87
|
}
|
|
94
88
|
|
|
95
|
-
export function getModelsDir(projectDir: string): string {
|
|
96
|
-
return (
|
|
97
|
-
process.env.BOTHOLOMEW_MODELS_DIR_OVERRIDE ?? join(projectDir, MODELS_DIR)
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
89
|
export function getSkillsDir(projectDir: string): string {
|
|
102
90
|
return join(projectDir, SKILLS_DIR);
|
|
103
91
|
}
|
|
@@ -106,10 +94,6 @@ export function getPromptsDir(projectDir: string): string {
|
|
|
106
94
|
return join(projectDir, PROMPTS_DIR);
|
|
107
95
|
}
|
|
108
96
|
|
|
109
|
-
export function getContextDir(projectDir: string): string {
|
|
110
|
-
return join(projectDir, CONTEXT_DIR);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
97
|
export function getTasksDir(projectDir: string): string {
|
|
114
98
|
return join(projectDir, TASKS_DIR);
|
|
115
99
|
}
|
package/src/init/index.ts
CHANGED
|
@@ -4,9 +4,7 @@ import { loadConfig } from "../config/loader.ts";
|
|
|
4
4
|
import {
|
|
5
5
|
CONFIG_DIR,
|
|
6
6
|
CONFIG_FILENAME,
|
|
7
|
-
CONTEXT_DIR,
|
|
8
7
|
getConfigPath,
|
|
9
|
-
getDbPath,
|
|
10
8
|
getMcpxDir,
|
|
11
9
|
getPromptsDir,
|
|
12
10
|
getSchedulesDir,
|
|
@@ -22,11 +20,10 @@ import {
|
|
|
22
20
|
SCHEDULES_DIR,
|
|
23
21
|
TASKS_DIR,
|
|
24
22
|
} from "../constants.ts";
|
|
25
|
-
import { writeCapabilitiesFile } from "../context/capabilities.ts";
|
|
26
|
-
import { getConnection } from "../db/connection.ts";
|
|
27
|
-
import { migrate } from "../db/schema.ts";
|
|
28
23
|
import { assertCompatibleFilesystem } from "../fs/compat.ts";
|
|
29
|
-
import { createMcpxClient } from "../mcpx/client.ts";
|
|
24
|
+
import { createMcpxClient, resolveMcpxDir } from "../mcpx/client.ts";
|
|
25
|
+
import { openMembot, resolveMembotDir } from "../mem/client.ts";
|
|
26
|
+
import { writeCapabilitiesFile } from "../prompts/capabilities.ts";
|
|
30
27
|
import { registerAllTools } from "../tools/registry.ts";
|
|
31
28
|
import { logger } from "../utils/logger.ts";
|
|
32
29
|
import {
|
|
@@ -40,9 +37,17 @@ import {
|
|
|
40
37
|
SUMMARIZE_SKILL,
|
|
41
38
|
} from "./templates.ts";
|
|
42
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
|
+
|
|
43
48
|
export async function initProject(
|
|
44
49
|
projectDir: string,
|
|
45
|
-
opts:
|
|
50
|
+
opts: InitOptions = {},
|
|
46
51
|
): Promise<void> {
|
|
47
52
|
// Refuse to operate inside iCloud/Dropbox/etc unless --force is passed.
|
|
48
53
|
// Sync overlays break atomic rename / O_EXCL semantics that tasks and
|
|
@@ -62,7 +67,6 @@ export async function initProject(
|
|
|
62
67
|
await mkdir(getPromptsDir(projectDir), { recursive: true });
|
|
63
68
|
await mkdir(getSkillsDir(projectDir), { recursive: true });
|
|
64
69
|
await mkdir(getMcpxDir(projectDir), { recursive: true });
|
|
65
|
-
await mkdir(join(projectDir, CONTEXT_DIR), { recursive: true });
|
|
66
70
|
await mkdir(getTasksDir(projectDir), { recursive: true });
|
|
67
71
|
await mkdir(getTasksLockDir(projectDir), { recursive: true });
|
|
68
72
|
await mkdir(getSchedulesDir(projectDir), { recursive: true });
|
|
@@ -83,48 +87,69 @@ export async function initProject(
|
|
|
83
87
|
await Bun.write(join(skillsDir, "standup.md"), STANDUP_SKILL);
|
|
84
88
|
await Bun.write(join(skillsDir, "capabilities.md"), CAPABILITIES_SKILL);
|
|
85
89
|
|
|
86
|
-
// Config
|
|
87
|
-
|
|
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);
|
|
88
100
|
|
|
89
|
-
// mcpx servers config
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
}
|
|
94
110
|
|
|
95
|
-
// Initialize the
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
// Initialize the membot knowledge store. Opening + closing the client
|
|
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));
|
|
116
|
+
await mem.connect();
|
|
117
|
+
await mem.close();
|
|
100
118
|
|
|
101
119
|
// Populate capabilities.md with the real tool inventory.
|
|
102
120
|
registerAllTools();
|
|
103
|
-
const
|
|
104
|
-
const mcpxClient = await createMcpxClient(projectDir);
|
|
121
|
+
const mcpxClient = await createMcpxClient(resolveMcpxDir(projectDir, config));
|
|
105
122
|
try {
|
|
106
123
|
await writeCapabilitiesFile(projectDir, mcpxClient, config);
|
|
107
124
|
} finally {
|
|
108
125
|
await mcpxClient?.close();
|
|
109
126
|
}
|
|
110
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)`;
|
|
111
136
|
logger.success("Initialized Botholomew project");
|
|
112
|
-
logger.dim(` Project root:
|
|
113
|
-
logger.dim(` Config:
|
|
114
|
-
logger.dim(`
|
|
137
|
+
logger.dim(` Project root: ${projectDir}`);
|
|
138
|
+
logger.dim(` Config: ${CONFIG_DIR}/${CONFIG_FILENAME}`);
|
|
139
|
+
logger.dim(` Knowledge: ${membotScopeDesc}`);
|
|
140
|
+
logger.dim(` MCPX: ${mcpxScopeDesc}`);
|
|
115
141
|
logger.dim("");
|
|
116
142
|
logger.dim("Layout:");
|
|
117
143
|
logger.dim(` ${CONFIG_DIR}/ settings`);
|
|
118
144
|
logger.dim(
|
|
119
145
|
` prompts/ goals, beliefs, capabilities (and any you add)`,
|
|
120
146
|
);
|
|
121
|
-
logger.dim(` ${CONTEXT_DIR}/ agent-writable knowledge tree`);
|
|
122
147
|
logger.dim(` ${TASKS_DIR}/ one markdown file per task`);
|
|
123
148
|
logger.dim(` ${LOCKS_SUBDIR}/ worker claim lockfiles`);
|
|
124
149
|
logger.dim(` ${SCHEDULES_DIR}/ one markdown file per schedule`);
|
|
125
150
|
logger.dim(` threads/ one CSV per conversation, by UTC date`);
|
|
126
151
|
logger.dim(` workers/ one JSON pidfile per worker (heartbeats)`);
|
|
127
|
-
logger.dim(` skills/, mcpx/,
|
|
152
|
+
logger.dim(` skills/, mcpx/, logs/`);
|
|
128
153
|
logger.dim("");
|
|
129
154
|
logger.dim("Next steps:");
|
|
130
155
|
logger.dim(
|
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())
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { MembotClient } from "membot";
|
|
4
|
+
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
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)
|
|
10
|
+
*
|
|
11
|
+
* Membot's `configFlag` doubles as its data-dir flag (see
|
|
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.
|
|
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 });
|
|
33
|
+
}
|
|
@@ -27,8 +27,9 @@ type AnyTool = ToolDefinition<any, any>;
|
|
|
27
27
|
const GROUP_ORDER = [
|
|
28
28
|
"task",
|
|
29
29
|
"schedule",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
30
|
+
"membot",
|
|
31
|
+
"prompt",
|
|
32
|
+
"skill",
|
|
32
33
|
"thread",
|
|
33
34
|
"mcp",
|
|
34
35
|
"worker",
|
|
@@ -38,8 +39,9 @@ const GROUP_ORDER = [
|
|
|
38
39
|
const GROUP_HEADINGS: Record<string, string> = {
|
|
39
40
|
task: "Task management",
|
|
40
41
|
schedule: "Schedules",
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
membot: "Knowledge store (membot)",
|
|
43
|
+
prompt: "Prompts",
|
|
44
|
+
skill: "Skills",
|
|
43
45
|
thread: "Threads",
|
|
44
46
|
mcp: "MCPX meta-tools",
|
|
45
47
|
worker: "Workers",
|
|
@@ -367,9 +369,11 @@ function renderFallback(inv: RawInventory, now: Date): string {
|
|
|
367
369
|
task: "create, list, view, update, complete, fail, and wait on tasks in the agent's work queue",
|
|
368
370
|
schedule:
|
|
369
371
|
"create and list recurring schedules that automatically generate tasks",
|
|
370
|
-
|
|
371
|
-
"read, write, edit, move, copy, delete, and
|
|
372
|
-
|
|
372
|
+
membot:
|
|
373
|
+
"add, read, write, edit, move, copy, delete, and search content in the agent's knowledge store; track every version and refresh from URL sources",
|
|
374
|
+
prompt:
|
|
375
|
+
"list, read, create, edit, and delete the project's prompt files (goals, beliefs, capabilities, plus any agent-authored ones)",
|
|
376
|
+
skill: "list, read, write, edit, delete, and search slash-command skills",
|
|
373
377
|
thread: "list and view past conversation threads and tool interactions",
|
|
374
378
|
mcp: "search, list, inspect, and execute tools exposed by configured MCPX servers",
|
|
375
379
|
worker: "spawn background workers to run tasks asynchronously",
|
package/src/schedules/store.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { readdir, unlink } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import matter from "gray-matter";
|
|
4
4
|
import { getSchedulesDir, getSchedulesLockDir } from "../constants.ts";
|
|
5
|
-
import { uuidv7 } from "../db/uuid.ts";
|
|
6
5
|
import {
|
|
7
6
|
acquireLock,
|
|
8
7
|
atomicWrite,
|
|
@@ -13,6 +12,7 @@ import {
|
|
|
13
12
|
releaseLock,
|
|
14
13
|
} from "../fs/atomic.ts";
|
|
15
14
|
import { logger } from "../utils/logger.ts";
|
|
15
|
+
import { uuidv7 } from "../utils/uuid.ts";
|
|
16
16
|
import {
|
|
17
17
|
type Schedule,
|
|
18
18
|
type ScheduleFrontmatter,
|
package/src/tasks/store.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { readdir, stat, unlink } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import matter from "gray-matter";
|
|
4
4
|
import { getTasksDir, getTasksLockDir } from "../constants.ts";
|
|
5
|
-
import { uuidv7 } from "../db/uuid.ts";
|
|
6
5
|
import {
|
|
7
6
|
acquireLock,
|
|
8
7
|
atomicWrite,
|
|
@@ -13,6 +12,7 @@ import {
|
|
|
13
12
|
releaseLock,
|
|
14
13
|
} from "../fs/atomic.ts";
|
|
15
14
|
import { logger } from "../utils/logger.ts";
|
|
15
|
+
import { uuidv7 } from "../utils/uuid.ts";
|
|
16
16
|
import {
|
|
17
17
|
type Task,
|
|
18
18
|
type TaskFrontmatter,
|
package/src/threads/store.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { appendFile, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getThreadsDir } from "../constants.ts";
|
|
4
|
-
import { uuidv7 } from "../db/uuid.ts";
|
|
5
4
|
import { atomicWrite } from "../fs/atomic.ts";
|
|
5
|
+
import { uuidv7 } from "../utils/uuid.ts";
|
|
6
6
|
import { DATE_DIR_RE, dateForId } from "../utils/v7-date.ts";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Operation } from "membot";
|
|
2
|
+
import { composeDescription, isHelpfulError } from "membot";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import type { ToolContext, ToolDefinition } from "../tool.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Common output envelope for every membot-backed tool. The membot operation's
|
|
8
|
+
* own result is parked under `data` so the LLM gets a single, predictable
|
|
9
|
+
* shape across all 14 verbs. Errors flatten the HelpfulError's `kind`/`hint`
|
|
10
|
+
* into `error_type` / `next_action_hint`, which is the recovery cue Botholomew
|
|
11
|
+
* agents already know from the rest of the tool surface.
|
|
12
|
+
*/
|
|
13
|
+
export const membotOutputSchema = z.object({
|
|
14
|
+
is_error: z.boolean(),
|
|
15
|
+
data: z.unknown().optional(),
|
|
16
|
+
error_type: z.string().optional(),
|
|
17
|
+
message: z.string().optional(),
|
|
18
|
+
next_action_hint: z.string().optional(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type MembotOutput = z.infer<typeof membotOutputSchema>;
|
|
22
|
+
|
|
23
|
+
type MembotMethodName =
|
|
24
|
+
| "add"
|
|
25
|
+
| "list"
|
|
26
|
+
| "tree"
|
|
27
|
+
| "read"
|
|
28
|
+
| "search"
|
|
29
|
+
| "info"
|
|
30
|
+
| "stats"
|
|
31
|
+
| "versions"
|
|
32
|
+
| "diff"
|
|
33
|
+
| "write"
|
|
34
|
+
| "move"
|
|
35
|
+
| "remove"
|
|
36
|
+
| "refresh"
|
|
37
|
+
| "prune";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Map an Operation's exposed name (`membot_add`, `membot_delete`, …) to the
|
|
41
|
+
* `MembotClient` method that actually runs it. The two diverge in a couple
|
|
42
|
+
* of spots — `membot_delete` calls `client.remove`, `membot_move` calls
|
|
43
|
+
* `client.move` — so we keep the routing explicit rather than guessing.
|
|
44
|
+
*/
|
|
45
|
+
const METHOD_BY_OP_NAME: Record<string, MembotMethodName> = {
|
|
46
|
+
membot_add: "add",
|
|
47
|
+
membot_list: "list",
|
|
48
|
+
membot_tree: "tree",
|
|
49
|
+
membot_read: "read",
|
|
50
|
+
membot_search: "search",
|
|
51
|
+
membot_info: "info",
|
|
52
|
+
membot_stats: "stats",
|
|
53
|
+
membot_versions: "versions",
|
|
54
|
+
membot_diff: "diff",
|
|
55
|
+
membot_write: "write",
|
|
56
|
+
membot_move: "move",
|
|
57
|
+
membot_delete: "remove",
|
|
58
|
+
membot_refresh: "refresh",
|
|
59
|
+
membot_prune: "prune",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Adapt one membot {@link Operation} into a Botholomew {@link ToolDefinition}.
|
|
64
|
+
* The input schema passes through unchanged so the LLM sees membot's
|
|
65
|
+
* upstream prose/aliases verbatim. Success calls return `{ is_error: false,
|
|
66
|
+
* data: <op output> }`; HelpfulErrors return the flattened error envelope.
|
|
67
|
+
* Unknown errors are wrapped as `internal_error` so a thrown handler never
|
|
68
|
+
* crashes the agent loop.
|
|
69
|
+
*/
|
|
70
|
+
export function adaptOperation(
|
|
71
|
+
// biome-ignore lint/suspicious/noExplicitAny: Operation generic is heterogeneous across 14 verbs
|
|
72
|
+
op: Operation<any, any>,
|
|
73
|
+
): ToolDefinition<z.ZodObject<z.ZodRawShape>, typeof membotOutputSchema> {
|
|
74
|
+
const methodName = METHOD_BY_OP_NAME[op.name];
|
|
75
|
+
if (!methodName) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`adaptOperation: no MembotClient method registered for op '${op.name}'. ` +
|
|
78
|
+
`Add it to METHOD_BY_OP_NAME in src/tools/membot/adapter.ts.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
name: op.name,
|
|
83
|
+
description: composeDescription(op),
|
|
84
|
+
group: "membot",
|
|
85
|
+
inputSchema: op.inputSchema as z.ZodObject<z.ZodRawShape>,
|
|
86
|
+
outputSchema: membotOutputSchema,
|
|
87
|
+
execute: async (input, ctx: ToolContext) => {
|
|
88
|
+
try {
|
|
89
|
+
const method = ctx.mem[methodName] as (i: unknown) => Promise<unknown>;
|
|
90
|
+
const data = await method.call(ctx.mem, input);
|
|
91
|
+
return { is_error: false, data };
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (isHelpfulError(err)) {
|
|
94
|
+
return {
|
|
95
|
+
is_error: true,
|
|
96
|
+
error_type: err.kind,
|
|
97
|
+
message: err.message,
|
|
98
|
+
next_action_hint: err.hint,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
is_error: true,
|
|
103
|
+
error_type: "internal_error",
|
|
104
|
+
message: err instanceof Error ? err.message : String(err),
|
|
105
|
+
next_action_hint:
|
|
106
|
+
"Check the project's membot store (run `botholomew context stats`) and try again. If this persists, file a bug.",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { isHelpfulError } from "membot";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { ToolDefinition } from "../tool.ts";
|
|
4
|
+
|
|
5
|
+
const inputSchema = z.object({
|
|
6
|
+
from_logical_path: z.string().describe("Source path"),
|
|
7
|
+
to_logical_path: z.string().describe("Destination path"),
|
|
8
|
+
change_note: z.string().optional(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const outputSchema = z.object({
|
|
12
|
+
is_error: z.boolean(),
|
|
13
|
+
from_logical_path: z.string().optional(),
|
|
14
|
+
to_logical_path: z.string().optional(),
|
|
15
|
+
new_version_id: z.string().optional(),
|
|
16
|
+
error_type: z.string().optional(),
|
|
17
|
+
message: z.string().optional(),
|
|
18
|
+
next_action_hint: z.string().optional(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const membotCopyTool = {
|
|
22
|
+
name: "membot_copy",
|
|
23
|
+
description:
|
|
24
|
+
"[[ bash equivalent command: cp ]] Duplicate a file's current content under a new logical_path. The source is left untouched; the destination becomes a new inline-source version. Use membot_move to rename instead (the source is tombstoned in that case).",
|
|
25
|
+
group: "membot",
|
|
26
|
+
inputSchema,
|
|
27
|
+
outputSchema,
|
|
28
|
+
execute: async (input, ctx) => {
|
|
29
|
+
try {
|
|
30
|
+
const src = await ctx.mem.read({ logical_path: input.from_logical_path });
|
|
31
|
+
const written = await ctx.mem.write({
|
|
32
|
+
logical_path: input.to_logical_path,
|
|
33
|
+
content: src.content ?? "",
|
|
34
|
+
change_note:
|
|
35
|
+
input.change_note ?? `copied from ${input.from_logical_path}`,
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
is_error: false,
|
|
39
|
+
from_logical_path: input.from_logical_path,
|
|
40
|
+
to_logical_path: written.logical_path,
|
|
41
|
+
new_version_id: written.version_id,
|
|
42
|
+
};
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (isHelpfulError(err)) {
|
|
45
|
+
return {
|
|
46
|
+
is_error: true,
|
|
47
|
+
error_type: err.kind,
|
|
48
|
+
message: err.message,
|
|
49
|
+
next_action_hint: err.hint,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
is_error: true,
|
|
54
|
+
error_type: "internal_error",
|
|
55
|
+
message: err instanceof Error ? err.message : String(err),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { isHelpfulError } from "membot";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { ToolDefinition } from "../tool.ts";
|
|
4
|
+
|
|
5
|
+
const inputSchema = z.object({
|
|
6
|
+
logical_path: z.string().describe("Logical path of the file to count."),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const outputSchema = z.object({
|
|
10
|
+
is_error: z.boolean(),
|
|
11
|
+
logical_path: z.string().optional(),
|
|
12
|
+
line_count: z.number().optional(),
|
|
13
|
+
size_bytes: z.number().nullable().optional(),
|
|
14
|
+
error_type: z.string().optional(),
|
|
15
|
+
message: z.string().optional(),
|
|
16
|
+
next_action_hint: z.string().optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const membotCountLinesTool = {
|
|
20
|
+
name: "membot_count_lines",
|
|
21
|
+
description:
|
|
22
|
+
"[[ bash equivalent command: wc -l ]] Count lines in a stored file's markdown surrogate. Useful before a large membot_read or membot_edit to decide whether to fetch the whole body or page through it with `offset`/`limit`.",
|
|
23
|
+
group: "membot",
|
|
24
|
+
inputSchema,
|
|
25
|
+
outputSchema,
|
|
26
|
+
execute: async (input, ctx) => {
|
|
27
|
+
try {
|
|
28
|
+
const result = await ctx.mem.read({ logical_path: input.logical_path });
|
|
29
|
+
const content = result.content ?? "";
|
|
30
|
+
const lineCount = content === "" ? 0 : content.split("\n").length;
|
|
31
|
+
return {
|
|
32
|
+
is_error: false,
|
|
33
|
+
logical_path: result.logical_path,
|
|
34
|
+
line_count: lineCount,
|
|
35
|
+
size_bytes: result.size_bytes,
|
|
36
|
+
};
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (isHelpfulError(err)) {
|
|
39
|
+
return {
|
|
40
|
+
is_error: true,
|
|
41
|
+
error_type: err.kind,
|
|
42
|
+
message: err.message,
|
|
43
|
+
next_action_hint: err.hint,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
is_error: true,
|
|
48
|
+
error_type: "internal_error",
|
|
49
|
+
message: err instanceof Error ? err.message : String(err),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
} satisfies ToolDefinition<typeof inputSchema, typeof outputSchema>;
|