botholomew 0.20.1 → 0.21.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 +12 -2
- package/package.json +2 -1
- package/src/cli-standalone.ts +76 -0
- package/src/cli.ts +19 -1
- package/src/commands/check-update.ts +1 -4
- package/src/commands/mcpx.ts +38 -11
- package/src/commands/membot.ts +48 -14
- package/src/commands/upgrade.ts +1 -4
- package/src/pkg.ts +10 -0
- package/src/runtime.ts +25 -0
- package/src/update/background.ts +1 -4
- package/src/update/checker.ts +1 -4
- package/src/worker/prompt.ts +1 -4
- package/src/worker/run.ts +46 -27
- package/src/worker/sentinel.ts +7 -0
- package/src/worker/spawn.ts +9 -9
package/README.md
CHANGED
|
@@ -76,13 +76,23 @@ the message queue, tool-call visualization, and the live workers panel:
|
|
|
76
76
|
|
|
77
77
|
## Install
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
**Prebuilt binary** (no Bun required — one self-contained file per platform):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
curl -fsSL https://raw.githubusercontent.com/evantahler/botholomew/main/install.sh | sh
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
macOS (arm64) and Linux (x64); on Windows grab `botholomew-windows-x64.exe`
|
|
86
|
+
from the [releases](https://github.com/evantahler/botholomew/releases/latest). Then
|
|
87
|
+
`botholomew upgrade` updates it in place.
|
|
88
|
+
|
|
89
|
+
**With Bun** (requires [Bun](https://bun.sh) 1.1+):
|
|
80
90
|
|
|
81
91
|
```bash
|
|
82
92
|
bun install -g botholomew
|
|
83
93
|
```
|
|
84
94
|
|
|
85
|
-
|
|
95
|
+
Either way the CLI installs as both `botholomew` and `bothy` — the same binary, two names.
|
|
86
96
|
|
|
87
97
|
Or run the dev build from a checkout:
|
|
88
98
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botholomew",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"dev:demo": "bun run src/cli.ts chat -p 'learn everything you can about me from the connected MCP services and then save what you'\\''ve learned about me to context'",
|
|
22
22
|
"test": "bun test",
|
|
23
23
|
"lint": "tsc --noEmit && biome check .",
|
|
24
|
+
"build": "bun run scripts/build.ts",
|
|
24
25
|
"capture": "bun run scripts/capture.ts",
|
|
25
26
|
"docs:dev": "vitepress dev docs",
|
|
26
27
|
"docs:build": "vitepress build docs",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Virtual specifiers supplied + embedded (via the "file" loader) by
|
|
3
|
+
// scripts/build.ts at compile time; each resolves to its $bunfs path string.
|
|
4
|
+
// @ts-expect-error: virtual module resolved + embedded by scripts/build.ts at compile time
|
|
5
|
+
import dylibPath from "bothy:duckdb-dylib";
|
|
6
|
+
// @ts-expect-error: virtual module resolved + embedded by scripts/build.ts at compile time
|
|
7
|
+
import ortMjsPath from "bothy:ort-wasm-mjs";
|
|
8
|
+
// @ts-expect-error: virtual module resolved + embedded by scripts/build.ts at compile time
|
|
9
|
+
import ortWasmPath from "bothy:ort-wasm-wasm";
|
|
10
|
+
// Build-only entrypoint for the compiled binary (`bun run build`; see
|
|
11
|
+
// scripts/build.ts). Not used under `bun run` in dev.
|
|
12
|
+
//
|
|
13
|
+
// A `bun build --compile` binary has no node_modules on disk, and Bun resolves
|
|
14
|
+
// neither externalized native packages nor `import.meta.resolve(...)` against
|
|
15
|
+
// the real filesystem. So we EMBED the native assets that can't be bundled —
|
|
16
|
+
// DuckDB's shared library and onnxruntime-web's WASM runtime — and stage them
|
|
17
|
+
// in a temp dir BEFORE the code that loads them runs:
|
|
18
|
+
// • DuckDB: the embedded `duckdb.node` is extracted by Bun into os.tmpdir()
|
|
19
|
+
// and dlopens its library via an `@loader_path` rpath, so the library must
|
|
20
|
+
// sit in that same dir.
|
|
21
|
+
// • Embeddings: membot's embedder reads BOTHOLOMEW_ORT_WASM_{MJS,WASM} (the
|
|
22
|
+
// build rewrites its `import.meta.resolve(...)` calls to use these) instead
|
|
23
|
+
// of resolving onnxruntime-web from a (missing) node_modules.
|
|
24
|
+
// The dynamic `import("./cli.ts")` guarantees all of this is in place before the
|
|
25
|
+
// real CLI — and its transitive duckdb/transformers imports — are evaluated.
|
|
26
|
+
import {
|
|
27
|
+
existsSync,
|
|
28
|
+
readFileSync,
|
|
29
|
+
realpathSync,
|
|
30
|
+
statSync,
|
|
31
|
+
writeFileSync,
|
|
32
|
+
} from "node:fs";
|
|
33
|
+
import { tmpdir } from "node:os";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
import { pathToFileURL } from "node:url";
|
|
36
|
+
import { MCPX_CLI_SENTINEL, MEMBOT_CLI_SENTINEL } from "./runtime.ts";
|
|
37
|
+
|
|
38
|
+
const stageDir = realpathSync(tmpdir());
|
|
39
|
+
|
|
40
|
+
/** Copy an embedded asset into the temp dir (skip if an identical one exists). */
|
|
41
|
+
function stage(embedded: string, name: string): string {
|
|
42
|
+
const dest = join(stageDir, name);
|
|
43
|
+
if (!existsSync(dest) || statSync(dest).size !== statSync(embedded).size) {
|
|
44
|
+
writeFileSync(dest, readFileSync(embedded));
|
|
45
|
+
}
|
|
46
|
+
return dest;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// DuckDB shared library — must land where Bun extracts the addon (os.tmpdir())
|
|
50
|
+
// under exactly the name the addon's rpath expects. scripts/build.ts globs that
|
|
51
|
+
// name from the binding package and injects it here via --define.
|
|
52
|
+
declare const BOTHOLOMEW_DUCKDB_LIB: string;
|
|
53
|
+
stage(dylibPath, BOTHOLOMEW_DUCKDB_LIB);
|
|
54
|
+
|
|
55
|
+
// onnxruntime-web WASM runtime — staged side by side; membot's embedder reads
|
|
56
|
+
// these env vars (the build rewrites its import.meta.resolve calls).
|
|
57
|
+
const mjs = stage(ortMjsPath, "ort-wasm-simd-threaded.asyncify.mjs");
|
|
58
|
+
const wasm = stage(ortWasmPath, "ort-wasm-simd-threaded.asyncify.wasm");
|
|
59
|
+
process.env.BOTHOLOMEW_ORT_WASM_MJS = pathToFileURL(mjs).href;
|
|
60
|
+
process.env.BOTHOLOMEW_ORT_WASM_WASM = pathToFileURL(wasm).href;
|
|
61
|
+
|
|
62
|
+
// The membot/mcpx passthroughs re-exec this binary with a sentinel arg; hand
|
|
63
|
+
// off to the bundled upstream CLI in this dedicated process (commands/membot.ts,
|
|
64
|
+
// commands/mcpx.ts). Otherwise run the normal Botholomew CLI.
|
|
65
|
+
const argv = process.argv;
|
|
66
|
+
const membotIdx = argv.indexOf(MEMBOT_CLI_SENTINEL);
|
|
67
|
+
const mcpxIdx = argv.indexOf(MCPX_CLI_SENTINEL);
|
|
68
|
+
if (membotIdx !== -1) {
|
|
69
|
+
process.argv = [process.execPath, "membot", ...argv.slice(membotIdx + 1)];
|
|
70
|
+
await import("membot/cli");
|
|
71
|
+
} else if (mcpxIdx !== -1) {
|
|
72
|
+
process.argv = [process.execPath, "mcpx", ...argv.slice(mcpxIdx + 1)];
|
|
73
|
+
await import("@evantahler/mcpx/cli");
|
|
74
|
+
} else {
|
|
75
|
+
await import("./cli.ts");
|
|
76
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -17,9 +17,27 @@ import { registerTaskCommand } from "./commands/task.ts";
|
|
|
17
17
|
import { registerThreadCommand } from "./commands/thread.ts";
|
|
18
18
|
import { registerUpgradeCommand } from "./commands/upgrade.ts";
|
|
19
19
|
import { registerWorkerCommand } from "./commands/worker.ts";
|
|
20
|
+
import { pkg } from "./pkg.ts";
|
|
21
|
+
import { IS_COMPILED_BINARY } from "./runtime.ts";
|
|
20
22
|
import { maybeCheckForUpdate } from "./update/background.ts";
|
|
23
|
+
import { runWorkerFromArgv } from "./worker/run.ts";
|
|
24
|
+
import { WORKER_RUN_SENTINEL } from "./worker/sentinel.ts";
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
// In a compiled binary there is no source tree to `bun run`, so a backgrounded
|
|
27
|
+
// worker re-execs this binary with a sentinel arg (see worker/spawn.ts). Handle
|
|
28
|
+
// it before commander sees it. Also force the embedder to stay in-process —
|
|
29
|
+
// membot's subprocess pool would re-exec the binary with its own sentinel,
|
|
30
|
+
// which our CLI doesn't understand.
|
|
31
|
+
if (IS_COMPILED_BINARY) {
|
|
32
|
+
process.env.MEMBOT_EMBEDDING_WORKERS ??= "1";
|
|
33
|
+
// Find the sentinel by value rather than position — Bun's compiled-binary
|
|
34
|
+
// argv layout differs from `bun run`, so we slice everything after it.
|
|
35
|
+
const sentinelIdx = process.argv.indexOf(WORKER_RUN_SENTINEL);
|
|
36
|
+
if (sentinelIdx !== -1) {
|
|
37
|
+
await runWorkerFromArgv(process.argv.slice(sentinelIdx + 1));
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
23
41
|
|
|
24
42
|
program
|
|
25
43
|
.name("botholomew")
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { cyan, dim, green, yellow } from "ansis";
|
|
2
2
|
import type { Command } from "commander";
|
|
3
|
+
import { pkg } from "../pkg.ts";
|
|
3
4
|
import { saveUpdateCache } from "../update/cache.ts";
|
|
4
5
|
import type { UpdateCache } from "../update/checker.ts";
|
|
5
6
|
import { checkForUpdate } from "../update/checker.ts";
|
|
6
7
|
|
|
7
|
-
const pkg = await Bun.file(
|
|
8
|
-
new URL("../../package.json", import.meta.url),
|
|
9
|
-
).json();
|
|
10
|
-
|
|
11
8
|
export function registerCheckUpdateCommand(program: Command) {
|
|
12
9
|
program
|
|
13
10
|
.command("check-update")
|
package/src/commands/mcpx.ts
CHANGED
|
@@ -8,22 +8,44 @@ import { createSpinner } from "nanospinner";
|
|
|
8
8
|
import { loadConfig } from "../config/loader.ts";
|
|
9
9
|
import { getMcpxDir } from "../constants.ts";
|
|
10
10
|
import { createMcpxClient, resolveMcpxDir } from "../mcpx/client.ts";
|
|
11
|
+
import { pkg as ourPkg } from "../pkg.ts";
|
|
11
12
|
import { writeCapabilitiesFile } from "../prompts/capabilities.ts";
|
|
13
|
+
import { IS_COMPILED_BINARY, MCPX_CLI_SENTINEL } from "../runtime.ts";
|
|
12
14
|
import { registerAllTools } from "../tools/registry.ts";
|
|
13
15
|
import { logger } from "../utils/logger.ts";
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
// Resolve the upstream mcpx CLI lazily — see commands/membot.ts for why this
|
|
18
|
+
// can't run at module load (no node_modules in a `bun build --compile` binary).
|
|
19
|
+
let mcpxCli: string | null | undefined;
|
|
20
|
+
function resolveMcpxCli(): string {
|
|
21
|
+
if (mcpxCli === undefined) {
|
|
22
|
+
mcpxCli = null;
|
|
23
|
+
try {
|
|
24
|
+
const cli = fileURLToPath(import.meta.resolve("@evantahler/mcpx/cli"));
|
|
25
|
+
if (existsSync(cli)) mcpxCli = cli;
|
|
26
|
+
} catch {
|
|
27
|
+
// unresolvable (e.g. standalone binary) — handled below
|
|
28
|
+
}
|
|
29
|
+
if (mcpxCli) {
|
|
30
|
+
// Strict version pin — only checkable when mcpx is actually reachable.
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const mcpxPkg = require("@evantahler/mcpx/package.json");
|
|
33
|
+
if (mcpxPkg.version !== ourPkg.dependencies["@evantahler/mcpx"]) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`@evantahler/mcpx version mismatch: installed ${mcpxPkg.version}, expected ${ourPkg.dependencies["@evantahler/mcpx"]}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (!mcpxCli) {
|
|
41
|
+
logger.error(
|
|
42
|
+
"The `botholomew mcpx` passthrough requires a reachable @evantahler/mcpx install, which the standalone binary doesn't bundle. Install botholomew via npm/Bun, or run `mcpx` directly.",
|
|
43
|
+
);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
return mcpxCli;
|
|
23
47
|
}
|
|
24
48
|
|
|
25
|
-
const MCPX_CLI = fileURLToPath(import.meta.resolve("@evantahler/mcpx/cli"));
|
|
26
|
-
|
|
27
49
|
export async function runMcpx(
|
|
28
50
|
projectDir: string,
|
|
29
51
|
args: (string | undefined)[],
|
|
@@ -35,7 +57,12 @@ export async function runMcpx(
|
|
|
35
57
|
const config = await loadConfig(projectDir);
|
|
36
58
|
const mcpxDir = resolveMcpxDir(projectDir, config);
|
|
37
59
|
const filteredArgs = args.filter((a): a is string => a !== undefined);
|
|
38
|
-
|
|
60
|
+
// In the compiled binary, re-exec ourselves with the sentinel so the bundled
|
|
61
|
+
// mcpx CLI runs; under Bun, spawn the resolved on-disk mcpx CLI.
|
|
62
|
+
const cmd = IS_COMPILED_BINARY
|
|
63
|
+
? [process.execPath, MCPX_CLI_SENTINEL, ...filteredArgs, "-c", mcpxDir]
|
|
64
|
+
: ["bun", resolveMcpxCli(), ...filteredArgs, "-c", mcpxDir];
|
|
65
|
+
const proc = Bun.spawn(cmd, {
|
|
39
66
|
stdout: opts?.inherit ? "inherit" : "pipe",
|
|
40
67
|
stderr: opts?.inherit ? "inherit" : "pipe",
|
|
41
68
|
stdin: opts?.inherit ? "inherit" : undefined,
|
package/src/commands/membot.ts
CHANGED
|
@@ -7,23 +7,52 @@ import type { Command } from "commander";
|
|
|
7
7
|
import { defaultCliName, OPERATIONS } from "membot";
|
|
8
8
|
import { loadConfig } from "../config/loader.ts";
|
|
9
9
|
import { resolveMembotDir } from "../mem/client.ts";
|
|
10
|
+
import { pkg as ourPkg } from "../pkg.ts";
|
|
11
|
+
import { IS_COMPILED_BINARY, MEMBOT_CLI_SENTINEL } from "../runtime.ts";
|
|
10
12
|
import { logger } from "../utils/logger.ts";
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
// Resolve the upstream membot CLI lazily. `import.meta.resolve` walks
|
|
15
|
+
// node_modules on disk, which a `bun build --compile` binary doesn't have, so
|
|
16
|
+
// resolving at module load would crash *every* command. On first use we resolve
|
|
17
|
+
// it, soft-check version drift, and fail this passthrough alone with a clear
|
|
18
|
+
// message when membot isn't reachable (e.g. inside the standalone binary).
|
|
19
|
+
let membotCli: string | null | undefined;
|
|
20
|
+
function resolveMembotCli(): string {
|
|
21
|
+
if (membotCli === undefined) {
|
|
22
|
+
membotCli = null;
|
|
23
|
+
try {
|
|
24
|
+
const cli = fileURLToPath(import.meta.resolve("membot/cli"));
|
|
25
|
+
if (existsSync(cli)) {
|
|
26
|
+
membotCli = cli;
|
|
27
|
+
try {
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const membotPkg = require("membot/package.json");
|
|
30
|
+
const requested = (ourPkg.dependencies.membot as string).replace(
|
|
31
|
+
/^[\^~]/,
|
|
32
|
+
"",
|
|
33
|
+
);
|
|
34
|
+
if (!membotPkg.version.startsWith(requested.split(".")[0])) {
|
|
35
|
+
logger.warn(
|
|
36
|
+
`membot version drift: installed ${membotPkg.version}, expected ${ourPkg.dependencies.membot}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// best-effort version check — ignore if package.json isn't readable
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// unresolvable (e.g. standalone binary) — handled below
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!membotCli) {
|
|
48
|
+
logger.error(
|
|
49
|
+
"The `botholomew membot` passthrough requires a reachable membot install, which the standalone binary doesn't bundle. Install botholomew via npm/Bun, or run `membot` directly.",
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
return membotCli;
|
|
23
54
|
}
|
|
24
55
|
|
|
25
|
-
const MEMBOT_CLI = fileURLToPath(import.meta.resolve("membot/cli"));
|
|
26
|
-
|
|
27
56
|
function getDir(program: Command): string {
|
|
28
57
|
return program.opts().dir;
|
|
29
58
|
}
|
|
@@ -45,7 +74,12 @@ async function runMembot(projectDir: string, args: string[]): Promise<number> {
|
|
|
45
74
|
// `membot` directly.
|
|
46
75
|
const config = await loadConfig(projectDir);
|
|
47
76
|
const membotDir = resolveMembotDir(projectDir, config);
|
|
48
|
-
|
|
77
|
+
// In the compiled binary, re-exec ourselves with the sentinel so the bundled
|
|
78
|
+
// membot CLI runs; under Bun, spawn the resolved on-disk membot CLI.
|
|
79
|
+
const cmd = IS_COMPILED_BINARY
|
|
80
|
+
? [process.execPath, MEMBOT_CLI_SENTINEL, "--config", membotDir, ...args]
|
|
81
|
+
: ["bun", resolveMembotCli(), "--config", membotDir, ...args];
|
|
82
|
+
const proc = Bun.spawn(cmd, {
|
|
49
83
|
stdout: "inherit",
|
|
50
84
|
stderr: "inherit",
|
|
51
85
|
stdin: "inherit",
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { dim, green, red, yellow } from "ansis";
|
|
4
4
|
import { $ } from "bun";
|
|
5
5
|
import type { Command } from "commander";
|
|
6
|
+
import { pkg } from "../pkg.ts";
|
|
6
7
|
import {
|
|
7
8
|
clearUpdateCache,
|
|
8
9
|
loadUpdateCache,
|
|
@@ -16,10 +17,6 @@ import {
|
|
|
16
17
|
needsCheck,
|
|
17
18
|
} from "../update/checker.ts";
|
|
18
19
|
|
|
19
|
-
const pkg = await Bun.file(
|
|
20
|
-
new URL("../../package.json", import.meta.url),
|
|
21
|
-
).json();
|
|
22
|
-
|
|
23
20
|
const GITHUB_REPO = (pkg.repository.url as string)
|
|
24
21
|
.replace(/^https:\/\/github\.com\//, "")
|
|
25
22
|
.replace(/\.git$/, "");
|
package/src/pkg.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Single source of truth for our own package.json metadata.
|
|
2
|
+
//
|
|
3
|
+
// Imported statically (not read at runtime via `Bun.file(new URL(...))`) so the
|
|
4
|
+
// values are inlined at `bun build --compile` time. A compiled standalone
|
|
5
|
+
// binary has no package.json on disk beside it, so a runtime read would throw
|
|
6
|
+
// at startup — even for `--version` / `--help`. The static import works in both
|
|
7
|
+
// `bun run` (read from disk) and the compiled binary (inlined).
|
|
8
|
+
import pkg from "../package.json" with { type: "json" };
|
|
9
|
+
|
|
10
|
+
export { pkg };
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Runtime-environment detection shared across the CLI.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* True when running as a `bun build --compile` standalone binary rather than
|
|
5
|
+
* under the `bun` runtime (`bun run …` in dev, or the `bun install -g` shim).
|
|
6
|
+
*
|
|
7
|
+
* In a compiled binary `process.execPath` is the binary itself (e.g.
|
|
8
|
+
* `…/dist/bothy`); under Bun it is the `bun`/`bunx` executable. Several code
|
|
9
|
+
* paths must behave differently in a binary: there is no source tree or
|
|
10
|
+
* `node_modules` on disk, so we can't `bun run` a `.ts` worker entry and must
|
|
11
|
+
* instead re-exec the binary, and the embedder must stay in-process (no
|
|
12
|
+
* subprocess pool) since the pool would re-exec an unknown sentinel arg.
|
|
13
|
+
*/
|
|
14
|
+
export const IS_COMPILED_BINARY = !/[\\/]bunx?(\.exe)?$/i.test(
|
|
15
|
+
process.execPath,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sentinels the compiled binary re-execs itself with to run a bundled upstream
|
|
20
|
+
* CLI in a fresh process (the binary embeds membot's and mcpx's CLIs, so the
|
|
21
|
+
* `botholomew membot`/`botholomew mcpx` passthroughs don't need those CLIs
|
|
22
|
+
* resolvable on disk). See cli-standalone.ts, commands/membot.ts, commands/mcpx.ts.
|
|
23
|
+
*/
|
|
24
|
+
export const MEMBOT_CLI_SENTINEL = "__botholomew_membot_cli__";
|
|
25
|
+
export const MCPX_CLI_SENTINEL = "__botholomew_mcpx_cli__";
|
package/src/update/background.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { cyan, dim, yellow } from "ansis";
|
|
2
2
|
import { DEFAULTS, ENV } from "../constants.ts";
|
|
3
|
+
import { pkg } from "../pkg.ts";
|
|
3
4
|
import { loadUpdateCache, saveUpdateCache } from "./cache.ts";
|
|
4
5
|
import { checkForUpdate, needsCheck, type UpdateCache } from "./checker.ts";
|
|
5
6
|
|
|
6
|
-
const pkg = await Bun.file(
|
|
7
|
-
new URL("../../package.json", import.meta.url),
|
|
8
|
-
).json();
|
|
9
|
-
|
|
10
7
|
/** Format an update notice for stderr output. */
|
|
11
8
|
function formatNotice(
|
|
12
9
|
currentVersion: string,
|
package/src/update/checker.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { DEFAULTS } from "../constants.ts";
|
|
2
|
-
|
|
3
|
-
const pkg = await Bun.file(
|
|
4
|
-
new URL("../../package.json", import.meta.url),
|
|
5
|
-
).json();
|
|
2
|
+
import { pkg } from "../pkg.ts";
|
|
6
3
|
|
|
7
4
|
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${pkg.name}/latest`;
|
|
8
5
|
const GITHUB_REPO = (pkg.repository.url as string)
|
package/src/worker/prompt.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { SERVER_INSTRUCTIONS as MEMBOT_INSTRUCTIONS } from "membot";
|
|
4
4
|
import type { BotholomewConfig } from "../config/schemas.ts";
|
|
5
5
|
import { getPromptsDir } from "../constants.ts";
|
|
6
|
+
import { pkg } from "../pkg.ts";
|
|
6
7
|
import type { Task } from "../tasks/schema.ts";
|
|
7
8
|
import { parsePromptFile } from "../utils/frontmatter.ts";
|
|
8
9
|
|
|
@@ -17,10 +18,6 @@ export const MEMBOT_PROMPT_SECTION = `## Knowledge store (membot)
|
|
|
17
18
|
${MEMBOT_INSTRUCTIONS}
|
|
18
19
|
`;
|
|
19
20
|
|
|
20
|
-
const pkg = await Bun.file(
|
|
21
|
-
new URL("../../package.json", import.meta.url),
|
|
22
|
-
).json();
|
|
23
|
-
|
|
24
21
|
export const STYLE_RULES = `## Style
|
|
25
22
|
- Open with the result, action, or next step. Skip preambles like "Great question", "You're absolutely right", "Let me…", "I'll go ahead and…".
|
|
26
23
|
- Don't flatter the user or their ideas. If a request is wrong, ambiguous, or risky, say so plainly with the reason.
|
package/src/worker/run.ts
CHANGED
|
@@ -1,34 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// Worker entry when spawned as a detached process.
|
|
4
|
+
// - Dev / `bun install -g`: `bun run src/worker/run.ts <projectDir> [flags]`
|
|
5
|
+
// - Compiled binary: the parent re-execs the binary with WORKER_RUN_SENTINEL
|
|
6
|
+
// as argv[2] (see ../cli.ts and ./spawn.ts), then calls runWorkerFromArgv.
|
|
7
|
+
// Flags: [--worker-id=<uuid>] [--log-path=<path>] [--persist] [--task-id=<uuid>] [--no-eval-schedules]
|
|
5
8
|
|
|
6
9
|
import { startWorker } from "./index.ts";
|
|
7
10
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
const USAGE =
|
|
12
|
+
"Usage: bun run src/worker/run.ts <projectDir> [--worker-id=<uuid>] [--log-path=<path>] [--persist] [--task-id=<uuid>] [--no-eval-schedules]";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse worker args (`<projectDir> [flags]`) and start the worker. Shared by
|
|
16
|
+
* the standalone `run.ts` invocation (dev) and the binary's sentinel branch.
|
|
17
|
+
*/
|
|
18
|
+
export async function runWorkerFromArgv(args: string[]): Promise<void> {
|
|
19
|
+
const projectDir = args[0];
|
|
20
|
+
if (!projectDir) {
|
|
21
|
+
console.error(USAGE);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
15
24
|
|
|
16
|
-
const
|
|
17
|
-
const persist =
|
|
18
|
-
const noEvalSchedules =
|
|
19
|
-
const taskIdArg =
|
|
20
|
-
const taskId = taskIdArg ? taskIdArg.slice("--task-id=".length) : undefined;
|
|
21
|
-
const workerIdArg =
|
|
22
|
-
const workerId = workerIdArg
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const logPathArg =
|
|
26
|
-
const logPath = logPathArg
|
|
25
|
+
const flags = args.slice(1);
|
|
26
|
+
const persist = flags.includes("--persist");
|
|
27
|
+
const noEvalSchedules = flags.includes("--no-eval-schedules");
|
|
28
|
+
const taskIdArg = flags.find((a) => a.startsWith("--task-id="));
|
|
29
|
+
const taskId = taskIdArg ? taskIdArg.slice("--task-id=".length) : undefined;
|
|
30
|
+
const workerIdArg = flags.find((a) => a.startsWith("--worker-id="));
|
|
31
|
+
const workerId = workerIdArg
|
|
32
|
+
? workerIdArg.slice("--worker-id=".length)
|
|
33
|
+
: undefined;
|
|
34
|
+
const logPathArg = flags.find((a) => a.startsWith("--log-path="));
|
|
35
|
+
const logPath = logPathArg
|
|
36
|
+
? logPathArg.slice("--log-path=".length)
|
|
37
|
+
: undefined;
|
|
27
38
|
|
|
28
|
-
await startWorker(projectDir, {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
});
|
|
39
|
+
await startWorker(projectDir, {
|
|
40
|
+
mode: persist ? "persist" : "once",
|
|
41
|
+
taskId,
|
|
42
|
+
workerId,
|
|
43
|
+
logPath,
|
|
44
|
+
evalSchedules: noEvalSchedules ? false : undefined,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Only run when invoked directly (`bun run …/run.ts`), not when imported by
|
|
49
|
+
// cli.ts (the compiled binary's entrypoint), which would otherwise execute a
|
|
50
|
+
// worker on every CLI invocation.
|
|
51
|
+
if (import.meta.main) {
|
|
52
|
+
await runWorkerFromArgv(process.argv.slice(2));
|
|
53
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel arg the compiled binary re-execs itself with to run a background
|
|
3
|
+
* worker (see ../cli.ts and ./spawn.ts). Lives in its own leaf module — with no
|
|
4
|
+
* imports — so both the entrypoint and the spawner can use it without dragging
|
|
5
|
+
* the worker stack into a circular import.
|
|
6
|
+
*/
|
|
7
|
+
export const WORKER_RUN_SENTINEL = "__botholomew_worker_run__";
|
package/src/worker/spawn.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { mkdir } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { getConfigPath, getWorkerLogPath } from "../constants.ts";
|
|
4
|
+
import { IS_COMPILED_BINARY } from "../runtime.ts";
|
|
4
5
|
import { logger } from "../utils/logger.ts";
|
|
5
6
|
import { uuidv7 } from "../utils/uuid.ts";
|
|
6
7
|
import { dateForId } from "../utils/v7-date.ts";
|
|
7
8
|
import type { WorkerMode } from "./index.ts";
|
|
9
|
+
import { WORKER_RUN_SENTINEL } from "./sentinel.ts";
|
|
8
10
|
|
|
9
11
|
export interface SpawnWorkerOptions {
|
|
10
12
|
mode?: WorkerMode;
|
|
@@ -56,15 +58,13 @@ export async function spawnWorker(
|
|
|
56
58
|
await mkdir(dirname(logPath), { recursive: true });
|
|
57
59
|
const logFile = Bun.file(logPath);
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
projectDir
|
|
65
|
-
|
|
66
|
-
`--log-path=${logPath}`,
|
|
67
|
-
];
|
|
61
|
+
// In a compiled binary there is no `run.ts` on disk to `bun run`, so re-exec
|
|
62
|
+
// this binary with the sentinel arg that cli.ts intercepts. Under Bun (dev /
|
|
63
|
+
// global install) spawn the worker entry directly.
|
|
64
|
+
const args = IS_COMPILED_BINARY
|
|
65
|
+
? [process.execPath, WORKER_RUN_SENTINEL, projectDir]
|
|
66
|
+
: ["bun", "run", new URL("./run.ts", import.meta.url).pathname, projectDir];
|
|
67
|
+
args.push(`--worker-id=${workerId}`, `--log-path=${logPath}`);
|
|
68
68
|
if (options.mode === "persist") args.push("--persist");
|
|
69
69
|
if (options.taskId) args.push(`--task-id=${options.taskId}`);
|
|
70
70
|
|