botholomew 0.1.1 → 0.2.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 +4 -1
- package/src/cli.ts +4 -0
- package/src/commands/context.ts +162 -8
- package/src/commands/mcpx.ts +224 -8
- package/src/commands/prepare.ts +16 -0
- package/src/commands/schedule.ts +186 -0
- package/src/commands/task.ts +102 -52
- package/src/commands/tools.ts +21 -26
- package/src/commands/with-db.ts +23 -0
- package/src/config/schemas.ts +3 -1
- package/src/constants.ts +3 -0
- package/src/context/chunker.ts +164 -0
- package/src/context/embedder.ts +78 -0
- package/src/context/ingest.ts +102 -0
- package/src/daemon/index.ts +13 -1
- package/src/daemon/llm.ts +8 -1
- package/src/daemon/prompt.ts +66 -2
- package/src/daemon/schedules.ts +142 -0
- package/src/daemon/tick.ts +31 -3
- package/src/db/connection.ts +34 -0
- package/src/db/context.ts +14 -34
- package/src/db/embeddings.ts +189 -7
- package/src/db/query.ts +45 -0
- package/src/db/schedules.ts +125 -3
- package/src/db/sql/1-core_tables.sql +1 -1
- package/src/db/tasks.ts +128 -15
- package/src/db/threads.ts +5 -14
- package/src/mcpx/client.ts +67 -0
- package/src/tools/file/edit.ts +3 -0
- package/src/tools/file/move.ts +6 -0
- package/src/tools/file/write.ts +3 -0
- package/src/tools/mcp/exec.ts +52 -0
- package/src/tools/mcp/info.ts +50 -0
- package/src/tools/mcp/list-tools.ts +42 -0
- package/src/tools/mcp/search.ts +58 -0
- package/src/tools/registry.ts +18 -0
- package/src/tools/schedule/create.ts +42 -0
- package/src/tools/schedule/list.ts +43 -0
- package/src/tools/search/semantic.ts +24 -4
- package/src/tools/tool.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botholomew",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "An AI agent for knowledge work",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@anthropic-ai/sdk": "^0.88.0",
|
|
26
|
+
"@evantahler/mcpx": "0.18.3",
|
|
27
|
+
"@huggingface/transformers": "^4.0.1",
|
|
28
|
+
"@sqliteai/sqlite-vector": "^0.9.95",
|
|
26
29
|
"ansis": "^4.2.0",
|
|
27
30
|
"commander": "^14.0.0",
|
|
28
31
|
"gray-matter": "^4.0.3",
|
package/src/cli.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { registerContextCommand } from "./commands/context.ts";
|
|
|
8
8
|
import { registerDaemonCommand } from "./commands/daemon.ts";
|
|
9
9
|
import { registerInitCommand } from "./commands/init.ts";
|
|
10
10
|
import { registerMcpxCommand } from "./commands/mcpx.ts";
|
|
11
|
+
import { registerPrepareCommand } from "./commands/prepare.ts";
|
|
12
|
+
import { registerScheduleCommand } from "./commands/schedule.ts";
|
|
11
13
|
import { registerTaskCommand } from "./commands/task.ts";
|
|
12
14
|
import { registerToolCommands } from "./commands/tools.ts";
|
|
13
15
|
import { registerUpgradeCommand } from "./commands/upgrade.ts";
|
|
@@ -31,10 +33,12 @@ program
|
|
|
31
33
|
registerInitCommand(program);
|
|
32
34
|
registerDaemonCommand(program);
|
|
33
35
|
registerTaskCommand(program);
|
|
36
|
+
registerScheduleCommand(program);
|
|
34
37
|
registerChatCommand(program);
|
|
35
38
|
registerContextCommand(program);
|
|
36
39
|
registerMcpxCommand(program);
|
|
37
40
|
registerToolCommands(program);
|
|
41
|
+
registerPrepareCommand(program);
|
|
38
42
|
registerCheckUpdateCommand(program);
|
|
39
43
|
registerUpgradeCommand(program);
|
|
40
44
|
|
package/src/commands/context.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import ansis from "ansis";
|
|
1
4
|
import type { Command } from "commander";
|
|
5
|
+
import { isText } from "istextorbinary";
|
|
6
|
+
import { loadConfig } from "../config/loader.ts";
|
|
7
|
+
import { embedSingle, warmupEmbedder } from "../context/embedder.ts";
|
|
8
|
+
import { ingestContextItem } from "../context/ingest.ts";
|
|
9
|
+
import type { DbConnection } from "../db/connection.ts";
|
|
10
|
+
import {
|
|
11
|
+
createContextItem,
|
|
12
|
+
listContextItems,
|
|
13
|
+
listContextItemsByPrefix,
|
|
14
|
+
} from "../db/context.ts";
|
|
15
|
+
import { hybridSearch, initVectorSearch } from "../db/embeddings.ts";
|
|
2
16
|
import { logger } from "../utils/logger.ts";
|
|
17
|
+
import { withDb } from "./with-db.ts";
|
|
3
18
|
|
|
4
19
|
export function registerContextCommand(program: Command) {
|
|
5
20
|
const ctx = program.command("context").description("Manage context items");
|
|
@@ -7,21 +22,160 @@ export function registerContextCommand(program: Command) {
|
|
|
7
22
|
ctx
|
|
8
23
|
.command("list")
|
|
9
24
|
.description("List context items")
|
|
10
|
-
.
|
|
11
|
-
|
|
12
|
-
|
|
25
|
+
.option("--path <prefix>", "filter by path prefix")
|
|
26
|
+
.option("-l, --limit <n>", "max number of items", Number.parseInt)
|
|
27
|
+
.action((opts) =>
|
|
28
|
+
withDb(program, async (conn) => {
|
|
29
|
+
const items = opts.path
|
|
30
|
+
? await listContextItemsByPrefix(conn, opts.path, {
|
|
31
|
+
recursive: true,
|
|
32
|
+
limit: opts.limit,
|
|
33
|
+
})
|
|
34
|
+
: await listContextItems(conn, { limit: opts.limit });
|
|
35
|
+
|
|
36
|
+
if (items.length === 0) {
|
|
37
|
+
logger.dim("No context items found.");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const header = `${ansis.bold("Path".padEnd(40))} ${"Title".padEnd(25)} ${"Type".padEnd(20)} Indexed`;
|
|
42
|
+
console.log(header);
|
|
43
|
+
console.log("-".repeat(header.length));
|
|
44
|
+
|
|
45
|
+
for (const item of items) {
|
|
46
|
+
const indexed = item.indexed_at
|
|
47
|
+
? ansis.green("yes")
|
|
48
|
+
: ansis.dim("no");
|
|
49
|
+
console.log(
|
|
50
|
+
`${item.context_path.padEnd(40)} ${item.title.slice(0, 24).padEnd(25)} ${item.mime_type.slice(0, 19).padEnd(20)} ${indexed}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(`\n${ansis.dim(`${items.length} item(s)`)}`);
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
13
57
|
|
|
14
58
|
ctx
|
|
15
59
|
.command("add <path>")
|
|
16
60
|
.description("Add a file or directory to context")
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
61
|
+
.option("--prefix <prefix>", "virtual path prefix", "/")
|
|
62
|
+
.action((path, opts) =>
|
|
63
|
+
withDb(program, async (conn, dir) => {
|
|
64
|
+
const config = await loadConfig(dir);
|
|
65
|
+
await warmupEmbedder();
|
|
66
|
+
|
|
67
|
+
const resolvedPath = resolve(path);
|
|
68
|
+
const info = await stat(resolvedPath);
|
|
69
|
+
|
|
70
|
+
let added = 0;
|
|
71
|
+
let chunks = 0;
|
|
72
|
+
|
|
73
|
+
if (info.isDirectory()) {
|
|
74
|
+
const entries = await walkDirectory(resolvedPath);
|
|
75
|
+
for (const filePath of entries) {
|
|
76
|
+
const relativePath = filePath.slice(resolvedPath.length);
|
|
77
|
+
const contextPath = join(opts.prefix, relativePath);
|
|
78
|
+
const count = await addFile(conn, config, filePath, contextPath);
|
|
79
|
+
if (count >= 0) {
|
|
80
|
+
added++;
|
|
81
|
+
chunks += count;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
const contextPath = join(opts.prefix, basename(resolvedPath));
|
|
86
|
+
const count = await addFile(conn, config, resolvedPath, contextPath);
|
|
87
|
+
if (count >= 0) {
|
|
88
|
+
added++;
|
|
89
|
+
chunks += count;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
logger.success(`Added ${added} file(s), ${chunks} chunk(s) indexed.`);
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
20
96
|
|
|
21
97
|
ctx
|
|
22
98
|
.command("search <query>")
|
|
23
99
|
.description("Search context items")
|
|
24
|
-
.
|
|
25
|
-
|
|
100
|
+
.option("-k, --top-k <n>", "max results", Number.parseInt, 10)
|
|
101
|
+
.action((query, opts) =>
|
|
102
|
+
withDb(program, async (conn) => {
|
|
103
|
+
await warmupEmbedder();
|
|
104
|
+
initVectorSearch(conn);
|
|
105
|
+
const queryVec = await embedSingle(query);
|
|
106
|
+
const results = hybridSearch(conn, query, queryVec, opts.topK);
|
|
107
|
+
|
|
108
|
+
if (results.length === 0) {
|
|
109
|
+
logger.dim("No results found.");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const [i, r] of results.entries()) {
|
|
114
|
+
const score = (r.score * 100).toFixed(1);
|
|
115
|
+
console.log(
|
|
116
|
+
`${ansis.bold(`${i + 1}.`)} ${ansis.cyan(r.title)} ${ansis.dim(`(${score}%)`)}`,
|
|
117
|
+
);
|
|
118
|
+
console.log(` ${ansis.dim(r.source_path || r.context_item_id)}`);
|
|
119
|
+
if (r.chunk_content) {
|
|
120
|
+
const snippet = r.chunk_content.slice(0, 120).replace(/\n/g, " ");
|
|
121
|
+
console.log(` ${snippet}...`);
|
|
122
|
+
}
|
|
123
|
+
console.log("");
|
|
124
|
+
}
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function addFile(
|
|
130
|
+
conn: DbConnection,
|
|
131
|
+
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
132
|
+
filePath: string,
|
|
133
|
+
contextPath: string,
|
|
134
|
+
): Promise<number> {
|
|
135
|
+
try {
|
|
136
|
+
const bunFile = Bun.file(filePath);
|
|
137
|
+
const mimeType = bunFile.type.split(";")[0] || "application/octet-stream";
|
|
138
|
+
const filename = basename(filePath);
|
|
139
|
+
const textual = isText(filename) !== false;
|
|
140
|
+
|
|
141
|
+
const content = textual ? await bunFile.text() : null;
|
|
142
|
+
|
|
143
|
+
const item = await createContextItem(conn, {
|
|
144
|
+
title: filename,
|
|
145
|
+
content: content ?? undefined,
|
|
146
|
+
mimeType,
|
|
147
|
+
sourcePath: filePath,
|
|
148
|
+
contextPath,
|
|
149
|
+
isTextual: textual,
|
|
26
150
|
});
|
|
151
|
+
|
|
152
|
+
if (textual && content) {
|
|
153
|
+
const count = await ingestContextItem(conn, item.id, config);
|
|
154
|
+
console.log(` + ${contextPath} (${count} chunks)`);
|
|
155
|
+
return count;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log(` + ${contextPath} (binary, not indexed)`);
|
|
159
|
+
return 0;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
logger.warn(` ! ${contextPath}: ${err}`);
|
|
162
|
+
return -1;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function walkDirectory(dirPath: string): Promise<string[]> {
|
|
167
|
+
const files: string[] = [];
|
|
168
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
169
|
+
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
const fullPath = join(dirPath, entry.name);
|
|
172
|
+
if (entry.isDirectory()) {
|
|
173
|
+
if (entry.name.startsWith(".")) continue; // skip hidden dirs
|
|
174
|
+
files.push(...(await walkDirectory(fullPath)));
|
|
175
|
+
} else if (entry.isFile()) {
|
|
176
|
+
files.push(fullPath);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return files;
|
|
27
181
|
}
|
package/src/commands/mcpx.ts
CHANGED
|
@@ -1,29 +1,245 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
1
6
|
import type { Command } from "commander";
|
|
7
|
+
import { getMcpxDir } from "../constants.ts";
|
|
2
8
|
import { logger } from "../utils/logger.ts";
|
|
3
9
|
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const ourPkg = require("../../package.json");
|
|
12
|
+
const mcpxPkg = require("@evantahler/mcpx/package.json");
|
|
13
|
+
|
|
14
|
+
if (mcpxPkg.version !== ourPkg.dependencies["@evantahler/mcpx"]) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`@evantahler/mcpx version mismatch: installed ${mcpxPkg.version}, expected ${ourPkg.dependencies["@evantahler/mcpx"]}`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const MCPX_CLI = fileURLToPath(import.meta.resolve("@evantahler/mcpx/cli"));
|
|
21
|
+
|
|
22
|
+
export async function runMcpx(
|
|
23
|
+
projectDir: string,
|
|
24
|
+
args: (string | undefined)[],
|
|
25
|
+
opts?: { inherit?: boolean },
|
|
26
|
+
): Promise<string> {
|
|
27
|
+
const mcpxDir = getMcpxDir(projectDir);
|
|
28
|
+
const filteredArgs = args.filter((a): a is string => a !== undefined);
|
|
29
|
+
const proc = Bun.spawn(["bun", MCPX_CLI, ...filteredArgs, "-c", mcpxDir], {
|
|
30
|
+
stdout: opts?.inherit ? "inherit" : "pipe",
|
|
31
|
+
stderr: opts?.inherit ? "inherit" : "pipe",
|
|
32
|
+
stdin: opts?.inherit ? "inherit" : undefined,
|
|
33
|
+
});
|
|
34
|
+
const exitCode = await proc.exited;
|
|
35
|
+
|
|
36
|
+
if (opts?.inherit) {
|
|
37
|
+
if (exitCode !== 0) process.exit(exitCode);
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const stdout = await new Response(proc.stdout).text();
|
|
42
|
+
const stderr = await new Response(proc.stderr).text();
|
|
43
|
+
if (exitCode !== 0) {
|
|
44
|
+
logger.error(stderr.trim() || stdout.trim());
|
|
45
|
+
process.exit(exitCode);
|
|
46
|
+
}
|
|
47
|
+
return stdout;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getDir(program: Command): string {
|
|
51
|
+
return program.opts().dir;
|
|
52
|
+
}
|
|
53
|
+
|
|
4
54
|
export function registerMcpxCommand(program: Command) {
|
|
5
55
|
const mcpx = program
|
|
6
56
|
.command("mcpx")
|
|
7
57
|
.description("Manage MCP servers via MCPX");
|
|
8
58
|
|
|
59
|
+
// --- servers ---
|
|
9
60
|
mcpx
|
|
10
|
-
.command("
|
|
11
|
-
.description("List configured MCP
|
|
61
|
+
.command("servers")
|
|
62
|
+
.description("List configured MCP server names")
|
|
12
63
|
.action(async () => {
|
|
13
|
-
|
|
64
|
+
const out = await runMcpx(getDir(program), ["servers"]);
|
|
65
|
+
process.stdout.write(out);
|
|
14
66
|
});
|
|
15
67
|
|
|
68
|
+
// --- info ---
|
|
16
69
|
mcpx
|
|
17
|
-
.command("
|
|
70
|
+
.command("info <first> [second]")
|
|
71
|
+
.description(
|
|
72
|
+
"Show server overview, or schema for a specific tool (server is optional if tool name is unambiguous)",
|
|
73
|
+
)
|
|
74
|
+
.action(async (first: string, second?: string) => {
|
|
75
|
+
const out = await runMcpx(getDir(program), ["info", first, second]);
|
|
76
|
+
process.stdout.write(out);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// --- search ---
|
|
80
|
+
mcpx
|
|
81
|
+
.command("search <terms...>")
|
|
82
|
+
.description("Search tools by keyword and/or semantic similarity")
|
|
83
|
+
.action(async (terms: string[]) => {
|
|
84
|
+
const out = await runMcpx(getDir(program), ["search", ...terms]);
|
|
85
|
+
process.stdout.write(out);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// --- exec ---
|
|
89
|
+
mcpx
|
|
90
|
+
.command("exec <first> [second] [third]")
|
|
91
|
+
.description(
|
|
92
|
+
"Execute a tool call (server is optional if tool name is unambiguous)",
|
|
93
|
+
)
|
|
94
|
+
.action(async (first: string, second?: string, third?: string) => {
|
|
95
|
+
const out = await runMcpx(getDir(program), [
|
|
96
|
+
"exec",
|
|
97
|
+
first,
|
|
98
|
+
second,
|
|
99
|
+
third,
|
|
100
|
+
]);
|
|
101
|
+
process.stdout.write(out);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// --- add ---
|
|
105
|
+
mcpx
|
|
106
|
+
.command("add <name>")
|
|
18
107
|
.description("Add an MCP server")
|
|
108
|
+
.option("--command <cmd>", "Stdio server command")
|
|
109
|
+
.option("--args <args...>", "Stdio server arguments")
|
|
110
|
+
.option("--url <url>", "HTTP server URL")
|
|
111
|
+
.option("--transport <type>", "HTTP transport: sse or streamable-http")
|
|
112
|
+
.option("--env <pairs...>", "Environment variables as KEY=VALUE pairs")
|
|
113
|
+
.action(
|
|
114
|
+
async (
|
|
115
|
+
name: string,
|
|
116
|
+
opts: {
|
|
117
|
+
command?: string;
|
|
118
|
+
args?: string[];
|
|
119
|
+
url?: string;
|
|
120
|
+
transport?: string;
|
|
121
|
+
env?: string[];
|
|
122
|
+
},
|
|
123
|
+
) => {
|
|
124
|
+
const cliArgs: string[] = ["add", name];
|
|
125
|
+
if (opts.command) cliArgs.push("--command", opts.command);
|
|
126
|
+
if (opts.args) {
|
|
127
|
+
for (const a of opts.args) cliArgs.push("--args", a);
|
|
128
|
+
}
|
|
129
|
+
if (opts.url) cliArgs.push("--url", opts.url);
|
|
130
|
+
if (opts.transport) cliArgs.push("--transport", opts.transport);
|
|
131
|
+
if (opts.env) {
|
|
132
|
+
for (const e of opts.env) cliArgs.push("--env", e);
|
|
133
|
+
}
|
|
134
|
+
const out = await runMcpx(getDir(program), cliArgs);
|
|
135
|
+
process.stdout.write(out);
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// --- remove ---
|
|
140
|
+
mcpx
|
|
141
|
+
.command("remove <name>")
|
|
142
|
+
.description("Remove an MCP server")
|
|
143
|
+
.action(async (name: string) => {
|
|
144
|
+
const out = await runMcpx(getDir(program), ["remove", name]);
|
|
145
|
+
process.stdout.write(out);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// --- ping ---
|
|
149
|
+
mcpx
|
|
150
|
+
.command("ping [servers...]")
|
|
151
|
+
.description("Check connectivity to MCP servers")
|
|
152
|
+
.action(async (servers: string[]) => {
|
|
153
|
+
const out = await runMcpx(getDir(program), ["ping", ...servers]);
|
|
154
|
+
process.stdout.write(out);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// --- auth ---
|
|
158
|
+
mcpx
|
|
159
|
+
.command("auth <server>")
|
|
160
|
+
.description("Authenticate with an HTTP MCP server")
|
|
161
|
+
.action(async (server: string) => {
|
|
162
|
+
await runMcpx(getDir(program), ["auth", server], { inherit: true });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// --- resource ---
|
|
166
|
+
mcpx
|
|
167
|
+
.command("resource [server] [uri]")
|
|
168
|
+
.description("List resources for a server, or read a specific resource")
|
|
169
|
+
.action(async (server?: string, uri?: string) => {
|
|
170
|
+
const out = await runMcpx(getDir(program), ["resource", server, uri]);
|
|
171
|
+
process.stdout.write(out);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// --- prompt ---
|
|
175
|
+
mcpx
|
|
176
|
+
.command("prompt [server] [name] [args]")
|
|
177
|
+
.description("List prompts for a server, or get a specific prompt")
|
|
178
|
+
.action(async (server?: string, name?: string, argsJson?: string) => {
|
|
179
|
+
const out = await runMcpx(getDir(program), [
|
|
180
|
+
"prompt",
|
|
181
|
+
server,
|
|
182
|
+
name,
|
|
183
|
+
argsJson,
|
|
184
|
+
]);
|
|
185
|
+
process.stdout.write(out);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// --- task ---
|
|
189
|
+
mcpx
|
|
190
|
+
.command("task <action> <server> [taskId]")
|
|
191
|
+
.description("Manage async tasks (actions: list, get, result, cancel)")
|
|
192
|
+
.action(async (action: string, server: string, taskId?: string) => {
|
|
193
|
+
const out = await runMcpx(getDir(program), [
|
|
194
|
+
"task",
|
|
195
|
+
action,
|
|
196
|
+
server,
|
|
197
|
+
taskId,
|
|
198
|
+
]);
|
|
199
|
+
process.stdout.write(out);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// --- import-global ---
|
|
203
|
+
mcpx
|
|
204
|
+
.command("import-global")
|
|
205
|
+
.description("Copy system-wide MCPX settings (~/.mcpx) into this project")
|
|
19
206
|
.action(async () => {
|
|
20
|
-
|
|
207
|
+
const globalDir = join(homedir(), ".mcpx");
|
|
208
|
+
if (!existsSync(globalDir)) {
|
|
209
|
+
logger.error("No global MCPX config found at ~/.mcpx");
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const projectMcpxDir = getMcpxDir(getDir(program));
|
|
214
|
+
if (!existsSync(projectMcpxDir)) {
|
|
215
|
+
mkdirSync(projectMcpxDir, { recursive: true });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const filesToCopy = ["servers.json", "auth.json", "search.json"];
|
|
219
|
+
let copied = 0;
|
|
220
|
+
for (const file of filesToCopy) {
|
|
221
|
+
const src = join(globalDir, file);
|
|
222
|
+
if (!existsSync(src)) continue;
|
|
223
|
+
const dest = join(projectMcpxDir, file);
|
|
224
|
+
copyFileSync(src, dest);
|
|
225
|
+
logger.success(`Copied ${file}`);
|
|
226
|
+
copied++;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (copied === 0) {
|
|
230
|
+
logger.warn("No config files found in ~/.mcpx to copy.");
|
|
231
|
+
} else {
|
|
232
|
+
logger.success(
|
|
233
|
+
`Imported ${copied} file(s) from ~/.mcpx into ${projectMcpxDir}`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
21
236
|
});
|
|
22
237
|
|
|
238
|
+
// --- index ---
|
|
23
239
|
mcpx
|
|
24
|
-
.command("
|
|
25
|
-
.description("
|
|
240
|
+
.command("index")
|
|
241
|
+
.description("Build the search index from all configured servers")
|
|
26
242
|
.action(async () => {
|
|
27
|
-
|
|
243
|
+
await runMcpx(getDir(program), ["index"], { inherit: true });
|
|
28
244
|
});
|
|
29
245
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { warmupEmbedder } from "../context/embedder.ts";
|
|
3
|
+
import { logger } from "../utils/logger.ts";
|
|
4
|
+
|
|
5
|
+
export function registerPrepareCommand(program: Command) {
|
|
6
|
+
program
|
|
7
|
+
.command("prepare")
|
|
8
|
+
.description(
|
|
9
|
+
"Download and cache required models. Run this in CI or on first setup.",
|
|
10
|
+
)
|
|
11
|
+
.action(async () => {
|
|
12
|
+
logger.info("Preparing Botholomew...");
|
|
13
|
+
await warmupEmbedder();
|
|
14
|
+
logger.success("All models downloaded and ready.");
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import ansis from "ansis";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import type { Schedule } from "../db/schedules.ts";
|
|
4
|
+
import {
|
|
5
|
+
createSchedule,
|
|
6
|
+
deleteSchedule,
|
|
7
|
+
getSchedule,
|
|
8
|
+
listSchedules,
|
|
9
|
+
updateSchedule,
|
|
10
|
+
} from "../db/schedules.ts";
|
|
11
|
+
import { logger } from "../utils/logger.ts";
|
|
12
|
+
import { withDb } from "./with-db.ts";
|
|
13
|
+
|
|
14
|
+
export function registerScheduleCommand(program: Command) {
|
|
15
|
+
const schedule = program.command("schedule").description("Manage schedules");
|
|
16
|
+
|
|
17
|
+
schedule
|
|
18
|
+
.command("list")
|
|
19
|
+
.description("List all schedules")
|
|
20
|
+
.option("--enabled", "show only enabled schedules")
|
|
21
|
+
.option("--disabled", "show only disabled schedules")
|
|
22
|
+
.action((opts) =>
|
|
23
|
+
withDb(program, async (conn) => {
|
|
24
|
+
const filters: { enabled?: boolean } = {};
|
|
25
|
+
if (opts.enabled) filters.enabled = true;
|
|
26
|
+
if (opts.disabled) filters.enabled = false;
|
|
27
|
+
|
|
28
|
+
const schedules = await listSchedules(conn, filters);
|
|
29
|
+
|
|
30
|
+
if (schedules.length === 0) {
|
|
31
|
+
logger.dim("No schedules found.");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const s of schedules) {
|
|
36
|
+
printSchedule(s);
|
|
37
|
+
}
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
schedule
|
|
42
|
+
.command("add <name>")
|
|
43
|
+
.description("Create a new schedule")
|
|
44
|
+
.requiredOption(
|
|
45
|
+
"-f, --frequency <text>",
|
|
46
|
+
"how often to run (e.g. 'every morning')",
|
|
47
|
+
)
|
|
48
|
+
.option("--description <text>", "schedule description", "")
|
|
49
|
+
.action((name, opts) =>
|
|
50
|
+
withDb(program, async (conn) => {
|
|
51
|
+
const s = await createSchedule(conn, {
|
|
52
|
+
name,
|
|
53
|
+
description: opts.description,
|
|
54
|
+
frequency: opts.frequency,
|
|
55
|
+
});
|
|
56
|
+
logger.success(`Created schedule: ${s.name} (${s.id})`);
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
schedule
|
|
61
|
+
.command("view <id>")
|
|
62
|
+
.description("View schedule details")
|
|
63
|
+
.action((id) =>
|
|
64
|
+
withDb(program, async (conn) => {
|
|
65
|
+
const s = await getSchedule(conn, id);
|
|
66
|
+
if (!s) {
|
|
67
|
+
logger.error(`Schedule not found: ${id}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
printScheduleDetail(s);
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
schedule
|
|
75
|
+
.command("enable <id>")
|
|
76
|
+
.description("Enable a schedule")
|
|
77
|
+
.action((id) =>
|
|
78
|
+
withDb(program, async (conn) => {
|
|
79
|
+
const s = await updateSchedule(conn, id, { enabled: true });
|
|
80
|
+
if (!s) {
|
|
81
|
+
logger.error(`Schedule not found: ${id}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
logger.success(`Enabled schedule: ${s.name}`);
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
schedule
|
|
89
|
+
.command("disable <id>")
|
|
90
|
+
.description("Disable a schedule")
|
|
91
|
+
.action((id) =>
|
|
92
|
+
withDb(program, async (conn) => {
|
|
93
|
+
const s = await updateSchedule(conn, id, { enabled: false });
|
|
94
|
+
if (!s) {
|
|
95
|
+
logger.error(`Schedule not found: ${id}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
logger.success(`Disabled schedule: ${s.name}`);
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
schedule
|
|
103
|
+
.command("delete <id>")
|
|
104
|
+
.description("Delete a schedule")
|
|
105
|
+
.action((id) =>
|
|
106
|
+
withDb(program, async (conn) => {
|
|
107
|
+
const deleted = await deleteSchedule(conn, id);
|
|
108
|
+
if (!deleted) {
|
|
109
|
+
logger.error(`Schedule not found: ${id}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
logger.success(`Deleted schedule: ${id}`);
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
schedule
|
|
117
|
+
.command("trigger <id>")
|
|
118
|
+
.description("Manually trigger a schedule (creates tasks immediately)")
|
|
119
|
+
.action((id) =>
|
|
120
|
+
withDb(program, async (conn, dir) => {
|
|
121
|
+
const s = await getSchedule(conn, id);
|
|
122
|
+
if (!s) {
|
|
123
|
+
logger.error(`Schedule not found: ${id}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Lazy import to avoid loading LLM deps for non-trigger commands
|
|
128
|
+
const { evaluateSchedule } = await import("../daemon/schedules.ts");
|
|
129
|
+
const { loadConfig } = await import("../config/loader.ts");
|
|
130
|
+
const { createTask } = await import("../db/tasks.ts");
|
|
131
|
+
const { markScheduleRun } = await import("../db/schedules.ts");
|
|
132
|
+
|
|
133
|
+
const config = await loadConfig(dir);
|
|
134
|
+
const evaluation = await evaluateSchedule(config, s);
|
|
135
|
+
|
|
136
|
+
if (evaluation.tasksToCreate.length === 0) {
|
|
137
|
+
logger.dim("Schedule evaluated but produced no tasks.");
|
|
138
|
+
} else {
|
|
139
|
+
const createdIds: string[] = [];
|
|
140
|
+
for (const taskDef of evaluation.tasksToCreate) {
|
|
141
|
+
const blockedBy = (taskDef.depends_on ?? [])
|
|
142
|
+
.map((i: number) => createdIds[i])
|
|
143
|
+
.filter(Boolean) as string[];
|
|
144
|
+
const t = await createTask(conn, {
|
|
145
|
+
name: taskDef.name,
|
|
146
|
+
description: taskDef.description,
|
|
147
|
+
priority: taskDef.priority,
|
|
148
|
+
blocked_by: blockedBy,
|
|
149
|
+
});
|
|
150
|
+
createdIds.push(t.id);
|
|
151
|
+
logger.success(`Created task: ${t.name} (${t.id})`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await markScheduleRun(conn, s.id);
|
|
156
|
+
logger.info(`Marked schedule "${s.name}" as run.`);
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function enabledColor(enabled: boolean): string {
|
|
162
|
+
return enabled ? ansis.green("enabled") : ansis.dim("disabled");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function printSchedule(s: Schedule) {
|
|
166
|
+
const id = ansis.dim(s.id.slice(0, 8));
|
|
167
|
+
const lastRun = s.last_run_at
|
|
168
|
+
? s.last_run_at.toISOString()
|
|
169
|
+
: ansis.dim("never");
|
|
170
|
+
console.log(
|
|
171
|
+
` ${id} ${enabledColor(s.enabled)} ${s.frequency} ${s.name} (last: ${lastRun})`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function printScheduleDetail(s: Schedule) {
|
|
176
|
+
console.log(ansis.bold(s.name));
|
|
177
|
+
console.log(` ID: ${s.id}`);
|
|
178
|
+
console.log(` Status: ${enabledColor(s.enabled)}`);
|
|
179
|
+
console.log(` Frequency: ${s.frequency}`);
|
|
180
|
+
if (s.description) console.log(` Description: ${s.description}`);
|
|
181
|
+
console.log(
|
|
182
|
+
` Last run: ${s.last_run_at ? s.last_run_at.toISOString() : ansis.dim("never")}`,
|
|
183
|
+
);
|
|
184
|
+
console.log(` Created: ${s.created_at.toISOString()}`);
|
|
185
|
+
console.log(` Updated: ${s.updated_at.toISOString()}`);
|
|
186
|
+
}
|