@beevibe/daemon 0.1.5 → 0.1.6
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 +23 -0
- package/dist/main.js +171 -117
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,6 +93,29 @@ Each workspace contains `mcp-config.json` (mode `0600`) with `Bearer <bv_a_…>`
|
|
|
93
93
|
|
|
94
94
|
Skills cache: `~/.beevibe/skills/`, version-gated via `.version`. Refreshed on every `start`.
|
|
95
95
|
|
|
96
|
+
## Multi-instance (dev only)
|
|
97
|
+
|
|
98
|
+
Two daemons can run on one machine, authenticated as two different `bv_u_` accounts, by giving each its own `--config-root`. The api already supports this — the `daemon` table is keyed by `(owner_person_id, external_id)`, so two daemons with the same hostname but different owners coexist as separate rows.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Terminal 1 — account A
|
|
102
|
+
pnpm dev setup --config-root $HOME/.beevibe-A --api http://localhost:3000 --user-token bv_u_A…
|
|
103
|
+
pnpm dev start --config-root $HOME/.beevibe-A
|
|
104
|
+
|
|
105
|
+
# Terminal 2 — account B
|
|
106
|
+
pnpm dev setup --config-root $HOME/.beevibe-B --api http://localhost:3000 --user-token bv_u_B…
|
|
107
|
+
pnpm dev start --config-root $HOME/.beevibe-B
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`BEEVIBE_CONFIG_ROOT` is an equivalent env-var entry point. Each instance writes its own `config.json`, skills cache, and workspaces under the override; defaults are unchanged (`~/.beevibe/...`) when neither the flag nor the env is set.
|
|
111
|
+
|
|
112
|
+
**Hard restrictions:**
|
|
113
|
+
|
|
114
|
+
- **Dev/source builds only.** Compiled binaries (brew/curl install path) and the npm-published bundle reject both `--config-root` and `BEEVIBE_CONFIG_ROOT` with exit code 2. The gate is the `__DEV_BUILD__` compile-time define set by `scripts/build-binaries.sh` and `scripts/prepare-publish.sh`, backed by a runtime check in `main.ts`. To run multi-instance, use `pnpm dev` against a source checkout.
|
|
115
|
+
- **`update` operates on the shared binary.** `beevibe-daemon update` rewrites `process.execPath`, which is the same file both instances spawn from. Running `update` from one instance affects the other on its next launch. Acceptable for dev testing of the update flow; the running daemon keeps its open inode on POSIX so it doesn't crash mid-session.
|
|
116
|
+
- **Device name is not auto-suffixed.** Both daemons register as `<user>@<hostname>` in the Runtimes panel by default. Use `--device-name "MBP (account-B)"` at `setup` time if you want them visually distinct.
|
|
117
|
+
- **Same OS user only.** Different OS users on one machine already isolate via `homedir()`; this flag is for running two accounts as the SAME OS user.
|
|
118
|
+
|
|
96
119
|
## What it doesn't do
|
|
97
120
|
|
|
98
121
|
- **No MCP proxy.** The CLI calls `/mcp` directly using the `bv_a_` token in `mcp-config.json`. The daemon's job stops at writing the file and supervising the subprocess.
|
package/dist/main.js
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/config.ts
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
var CONFIG_ROOT_ENV = "BEEVIBE_CONFIG_ROOT";
|
|
8
|
+
function isDevBuild() {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
function getConfigRoot(override) {
|
|
12
|
+
if (override && override.length > 0)
|
|
13
|
+
return override;
|
|
14
|
+
const fromEnv = process.env[CONFIG_ROOT_ENV];
|
|
15
|
+
if (fromEnv && fromEnv.length > 0)
|
|
16
|
+
return fromEnv;
|
|
17
|
+
return join(homedir(), ".beevibe");
|
|
18
|
+
}
|
|
19
|
+
function getConfigPath(override) {
|
|
20
|
+
return join(getConfigRoot(override), "config.json");
|
|
21
|
+
}
|
|
22
|
+
function loadConfig(configRoot) {
|
|
23
|
+
const path = getConfigPath(configRoot);
|
|
24
|
+
if (!existsSync(path))
|
|
25
|
+
return;
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
28
|
+
} catch (err) {
|
|
29
|
+
throw new Error(`Daemon config at ${path} is malformed: ${err instanceof Error ? err.message : String(err)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function saveConfig(cfg, configRoot) {
|
|
33
|
+
const path = getConfigPath(configRoot);
|
|
34
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
35
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2) + `
|
|
36
|
+
`, { mode: 384 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/logger.ts
|
|
40
|
+
function log(...args) {
|
|
41
|
+
console.log(new Date().toISOString(), ...args);
|
|
42
|
+
}
|
|
43
|
+
function warn(...args) {
|
|
44
|
+
console.warn(new Date().toISOString(), ...args);
|
|
45
|
+
}
|
|
46
|
+
function error(...args) {
|
|
47
|
+
console.error(new Date().toISOString(), ...args);
|
|
48
|
+
}
|
|
49
|
+
|
|
3
50
|
// src/setup.ts
|
|
4
51
|
import { hostname, userInfo } from "node:os";
|
|
5
52
|
|
|
@@ -202,27 +249,6 @@ var scryptAsync = promisify(scrypt);
|
|
|
202
249
|
var SCRYPT_N = 16384;
|
|
203
250
|
var SCRYPT_R = 8;
|
|
204
251
|
var SCRYPT_MAXMEM = 128 * SCRYPT_N * SCRYPT_R * 2;
|
|
205
|
-
// src/config.ts
|
|
206
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
207
|
-
import { homedir } from "node:os";
|
|
208
|
-
import { dirname, join } from "node:path";
|
|
209
|
-
var CONFIG_DIR = join(homedir(), ".beevibe");
|
|
210
|
-
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
211
|
-
function loadConfig() {
|
|
212
|
-
if (!existsSync(CONFIG_PATH))
|
|
213
|
-
return;
|
|
214
|
-
try {
|
|
215
|
-
return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
216
|
-
} catch (err) {
|
|
217
|
-
throw new Error(`Daemon config at ${CONFIG_PATH} is malformed: ${err instanceof Error ? err.message : String(err)}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
function saveConfig(cfg) {
|
|
221
|
-
mkdirSync(dirname(CONFIG_PATH), { recursive: true, mode: 448 });
|
|
222
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + `
|
|
223
|
-
`, { mode: 384 });
|
|
224
|
-
}
|
|
225
|
-
|
|
226
252
|
// src/detect-clis.ts
|
|
227
253
|
import { execFile } from "node:child_process";
|
|
228
254
|
import { promisify as promisify2 } from "node:util";
|
|
@@ -283,10 +309,13 @@ async function runSetup(options) {
|
|
|
283
309
|
daemon_token: body.daemon_token,
|
|
284
310
|
runtimes: body.runtimes
|
|
285
311
|
};
|
|
286
|
-
saveConfig(config);
|
|
312
|
+
saveConfig(config, options.configRoot);
|
|
287
313
|
return config;
|
|
288
314
|
}
|
|
289
315
|
|
|
316
|
+
// src/start.ts
|
|
317
|
+
import { join as join9 } from "node:path";
|
|
318
|
+
|
|
290
319
|
// ../core/dist/adapters/local-workspace/manager.js
|
|
291
320
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
292
321
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -468,12 +497,14 @@ class LocalWorkspaceManager {
|
|
|
468
497
|
rmSync(workspace2.path, { recursive: true, force: true });
|
|
469
498
|
}
|
|
470
499
|
}
|
|
500
|
+
var MCP_TOOL_TIMEOUT_MS = 10 * 60000;
|
|
471
501
|
function buildMcpConfig(apiKey, mcpServerUrl) {
|
|
472
502
|
return JSON.stringify({
|
|
473
503
|
mcpServers: {
|
|
474
504
|
beevibe: {
|
|
475
505
|
type: "http",
|
|
476
506
|
url: mcpServerUrl,
|
|
507
|
+
timeout: MCP_TOOL_TIMEOUT_MS,
|
|
477
508
|
headers: {
|
|
478
509
|
Authorization: `Bearer ${apiKey}`,
|
|
479
510
|
"X-Beevibe-Session": "${BEEVIBE_SESSION_ID}"
|
|
@@ -1200,7 +1231,7 @@ class CodexRuntime {
|
|
|
1200
1231
|
lastMessagePath
|
|
1201
1232
|
];
|
|
1202
1233
|
if (prepared && sid) {
|
|
1203
|
-
globalArgs.push("-c", `mcp_servers.beevibe.url=${tomlString(withBeevibeSession(prepared.mcpServerUrl, sid))}`, "-c", `mcp_servers.beevibe.bearer_token_env_var=${tomlString("BEEVIBE_AGENT_API_KEY")}`, "-c", `mcp_servers.beevibe.default_tools_approval_mode=${tomlString("approve")}`);
|
|
1234
|
+
globalArgs.push("-c", `mcp_servers.beevibe.url=${tomlString(withBeevibeSession(prepared.mcpServerUrl, sid))}`, "-c", `mcp_servers.beevibe.bearer_token_env_var=${tomlString("BEEVIBE_AGENT_API_KEY")}`, "-c", `mcp_servers.beevibe.default_tools_approval_mode=${tomlString("approve")}`, "-c", `mcp_servers.beevibe.tool_timeout_sec=${MCP_TOOL_TIMEOUT_MS / 1000}`);
|
|
1204
1235
|
}
|
|
1205
1236
|
const args = context.resume_session_id ? [
|
|
1206
1237
|
...globalArgs,
|
|
@@ -1941,7 +1972,7 @@ async function runRepoAgent(opts) {
|
|
|
1941
1972
|
artifacts: []
|
|
1942
1973
|
};
|
|
1943
1974
|
const emit = () => opts.on_state?.({ ...state, transcript: state.transcript.slice(), artifacts: state.artifacts.slice() });
|
|
1944
|
-
const
|
|
1975
|
+
const log2 = (kind, text) => {
|
|
1945
1976
|
const trimmed = text.length > MAX_EVENT_TEXT_BYTES ? text.slice(0, MAX_EVENT_TEXT_BYTES) + `
|
|
1946
1977
|
…[truncated ${text.length - MAX_EVENT_TEXT_BYTES} bytes]…` : text;
|
|
1947
1978
|
state.transcript.push({ at: nowIso(), kind, text: trimmed });
|
|
@@ -1961,7 +1992,7 @@ async function runRepoAgent(opts) {
|
|
|
1961
1992
|
};
|
|
1962
1993
|
let sandbox = null;
|
|
1963
1994
|
try {
|
|
1964
|
-
|
|
1995
|
+
log2("log", "Creating sandbox container…");
|
|
1965
1996
|
state.status = "preparing";
|
|
1966
1997
|
emit();
|
|
1967
1998
|
try {
|
|
@@ -1970,18 +2001,18 @@ async function runRepoAgent(opts) {
|
|
|
1970
2001
|
throw new Error(classifyStartupError(err));
|
|
1971
2002
|
}
|
|
1972
2003
|
state.sandbox_id = sandbox.id;
|
|
1973
|
-
|
|
1974
|
-
|
|
2004
|
+
log2("log", `Sandbox ${sandbox.id} created (image ${sandbox.image}).`);
|
|
2005
|
+
log2("log", "Installing git + curl in the sandbox base image…");
|
|
1975
2006
|
await prepareBaseEnvironment(sandbox);
|
|
1976
|
-
|
|
2007
|
+
log2("log", "Base environment ready.");
|
|
1977
2008
|
if (opts.input_url) {
|
|
1978
2009
|
const filename = opts.input_filename ?? "input.bin";
|
|
1979
|
-
|
|
2010
|
+
log2("log", `Fetching input into /sandbox/inputs/${filename}…`);
|
|
1980
2011
|
const r = await exec(sandbox, `mkdir -p /sandbox/inputs && curl -fsSL ${shellQuote(opts.input_url)} -o /sandbox/inputs/${shellQuote(filename)}`, { timeout_seconds: 120 });
|
|
1981
2012
|
if (r.exit_code !== 0) {
|
|
1982
2013
|
throw new Error(`input fetch failed: ${r.stderr.trim().slice(0, 400)}`);
|
|
1983
2014
|
}
|
|
1984
|
-
|
|
2015
|
+
log2("log", "Input file ready.");
|
|
1985
2016
|
}
|
|
1986
2017
|
const mcpServerCommand = opts.mcp_server_command ?? defaultMcpServerCommand();
|
|
1987
2018
|
const mcpConfigPath = join7(sandbox.artifact_dir, "mcp-config.json");
|
|
@@ -1998,11 +2029,11 @@ async function runRepoAgent(opts) {
|
|
|
1998
2029
|
}
|
|
1999
2030
|
};
|
|
2000
2031
|
await writeFile2(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
|
|
2001
|
-
|
|
2032
|
+
log2("log", `MCP config written: ${mcpConfigPath}`);
|
|
2002
2033
|
const userPrompt = buildUserPrompt(opts);
|
|
2003
2034
|
const systemPromptAppend = DEFAULT_PROMPT_HEADER;
|
|
2004
2035
|
state.status = "running";
|
|
2005
|
-
|
|
2036
|
+
log2("log", "Spawning child claude session…");
|
|
2006
2037
|
emit();
|
|
2007
2038
|
const claudeResult = await runClaude({
|
|
2008
2039
|
claudeBin: opts.claude_bin ?? "claude",
|
|
@@ -2011,18 +2042,18 @@ async function runRepoAgent(opts) {
|
|
|
2011
2042
|
userPrompt,
|
|
2012
2043
|
maxBudgetUsd: opts.max_budget_usd ?? 2,
|
|
2013
2044
|
timeoutSeconds: opts.max_runtime_seconds ?? 600,
|
|
2014
|
-
onTranscript: (kind, text) =>
|
|
2045
|
+
onTranscript: (kind, text) => log2(kind, text)
|
|
2015
2046
|
});
|
|
2016
2047
|
if (claudeResult.exit_code !== 0 && claudeResult.exit_code !== null) {
|
|
2017
2048
|
const tail = claudeResult.stderr.slice(-500);
|
|
2018
|
-
|
|
2049
|
+
log2("error", `Child claude exited with code ${claudeResult.exit_code}: ${tail}`);
|
|
2019
2050
|
state.status = claudeResult.timed_out ? "blocked" : "failed";
|
|
2020
2051
|
state.error = claudeResult.timed_out ? `Run hit the ${opts.max_runtime_seconds ?? 600}s wall-clock budget — agent didn't finish in time.` : `Claude exited ${claudeResult.exit_code}.${tail ? " " + tail.slice(-200) : ""}`;
|
|
2021
2052
|
state.finished_at = nowIso();
|
|
2022
2053
|
emit();
|
|
2023
2054
|
return state;
|
|
2024
2055
|
}
|
|
2025
|
-
|
|
2056
|
+
log2("log", "Child claude exited cleanly. Collecting artifacts…");
|
|
2026
2057
|
state.artifacts = await collectArtifacts(sandbox);
|
|
2027
2058
|
if (state.artifacts.length === 0) {
|
|
2028
2059
|
state.status = "blocked";
|
|
@@ -2034,7 +2065,7 @@ async function runRepoAgent(opts) {
|
|
|
2034
2065
|
emit();
|
|
2035
2066
|
return state;
|
|
2036
2067
|
} catch (err) {
|
|
2037
|
-
|
|
2068
|
+
log2("error", err instanceof Error ? err.message : String(err));
|
|
2038
2069
|
state.status = "failed";
|
|
2039
2070
|
state.error = err instanceof Error ? err.message : String(err);
|
|
2040
2071
|
state.finished_at = nowIso();
|
|
@@ -2043,10 +2074,10 @@ async function runRepoAgent(opts) {
|
|
|
2043
2074
|
} finally {
|
|
2044
2075
|
if (sandbox) {
|
|
2045
2076
|
try {
|
|
2046
|
-
|
|
2077
|
+
log2("log", `Destroying sandbox ${sandbox.id}…`);
|
|
2047
2078
|
await destroySandbox(sandbox);
|
|
2048
2079
|
} catch (err) {
|
|
2049
|
-
|
|
2080
|
+
log2("log", `Sandbox cleanup note: ${err instanceof Error ? err.message : String(err)}. Run with \`docker ps -a --filter name=bv-run-\` to inspect.`);
|
|
2050
2081
|
}
|
|
2051
2082
|
}
|
|
2052
2083
|
}
|
|
@@ -2267,7 +2298,7 @@ async function runRepoDispatch(deps, payload, abortSignal) {
|
|
|
2267
2298
|
if (!rr) {
|
|
2268
2299
|
throw new Error("run_repo dispatch missing payload.run_repo");
|
|
2269
2300
|
}
|
|
2270
|
-
|
|
2301
|
+
log(`[daemon/repo-run] sess=${payload.session_id} repo_run=${rr.repo_run_id} repo=${rr.repo_url}`);
|
|
2271
2302
|
const buffer = [];
|
|
2272
2303
|
let flushTimer;
|
|
2273
2304
|
const flush = async () => {
|
|
@@ -2277,7 +2308,7 @@ async function runRepoDispatch(deps, payload, abortSignal) {
|
|
|
2277
2308
|
try {
|
|
2278
2309
|
await deps.api.post("/runtime/events", { events });
|
|
2279
2310
|
} catch (err) {
|
|
2280
|
-
|
|
2311
|
+
warn("[daemon/repo-run] /runtime/events POST failed:", err instanceof Error ? err.message : String(err));
|
|
2281
2312
|
}
|
|
2282
2313
|
};
|
|
2283
2314
|
const scheduleFlush = () => {
|
|
@@ -2374,9 +2405,9 @@ async function runRepoDispatch(deps, payload, abortSignal) {
|
|
|
2374
2405
|
}
|
|
2375
2406
|
};
|
|
2376
2407
|
if (status === "succeeded") {
|
|
2377
|
-
|
|
2408
|
+
log(`[daemon/repo-run] sess=${payload.session_id} succeeded artifacts=${artifacts.length}`);
|
|
2378
2409
|
} else {
|
|
2379
|
-
|
|
2410
|
+
error(`[daemon/repo-run] sess=${payload.session_id} status=${status}` + (result.error ? `
|
|
2380
2411
|
error:
|
|
2381
2412
|
${result.error.split(`
|
|
2382
2413
|
`).join(`
|
|
@@ -2385,7 +2416,7 @@ async function runRepoDispatch(deps, payload, abortSignal) {
|
|
|
2385
2416
|
try {
|
|
2386
2417
|
await deps.api.post("/runtime/done", done);
|
|
2387
2418
|
} catch (err) {
|
|
2388
|
-
|
|
2419
|
+
error("[daemon/repo-run] /runtime/done POST failed:", err instanceof Error ? err.message : String(err));
|
|
2389
2420
|
}
|
|
2390
2421
|
}
|
|
2391
2422
|
function mapKind(kind) {
|
|
@@ -2417,7 +2448,7 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
2417
2448
|
runtime_config: { type: payload.runtime_type }
|
|
2418
2449
|
};
|
|
2419
2450
|
const ws = await deps.workspaceManager.ensureWorkspace({ agent: syntheticAgent });
|
|
2420
|
-
|
|
2451
|
+
log(`[daemon/spawn] sess=${payload.session_id} agent=${payload.agent_id} runtime=${payload.runtime_type} type=${payload.type} cwd=${ws.path}`);
|
|
2421
2452
|
const registry = deps.runtimeRegistry ?? createDefaultRuntimeRegistry();
|
|
2422
2453
|
const runtime3 = registry[payload.runtime_type];
|
|
2423
2454
|
if (!runtime3) {
|
|
@@ -2432,7 +2463,7 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
2432
2463
|
try {
|
|
2433
2464
|
await deps.api.post("/runtime/events", { events });
|
|
2434
2465
|
} catch (err) {
|
|
2435
|
-
|
|
2466
|
+
warn("[daemon/spawner] /runtime/events POST failed; events dropped:", err instanceof Error ? err.message : String(err));
|
|
2436
2467
|
}
|
|
2437
2468
|
};
|
|
2438
2469
|
const scheduleFlush = () => {
|
|
@@ -2489,9 +2520,9 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
2489
2520
|
usage: result?.usage
|
|
2490
2521
|
};
|
|
2491
2522
|
if (status === "succeeded") {
|
|
2492
|
-
|
|
2523
|
+
log(`[daemon/spawn] sess=${payload.session_id} exit=0`);
|
|
2493
2524
|
} else {
|
|
2494
|
-
|
|
2525
|
+
error(`[daemon/spawn] sess=${payload.session_id} status=${status} exit=${done.exit_code}` + (errorDetail ? `
|
|
2495
2526
|
error:
|
|
2496
2527
|
${errorDetail.split(`
|
|
2497
2528
|
`).join(`
|
|
@@ -2500,7 +2531,7 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
2500
2531
|
try {
|
|
2501
2532
|
await deps.api.post("/runtime/done", done);
|
|
2502
2533
|
} catch (err) {
|
|
2503
|
-
|
|
2534
|
+
error("[daemon/spawner] /runtime/done POST failed:", err instanceof Error ? err.message : String(err));
|
|
2504
2535
|
}
|
|
2505
2536
|
}
|
|
2506
2537
|
|
|
@@ -2563,7 +2594,7 @@ class Claimer {
|
|
|
2563
2594
|
this.ws = ws;
|
|
2564
2595
|
ws.on("open", () => {
|
|
2565
2596
|
this.wsReconnectAttempts = 0;
|
|
2566
|
-
|
|
2597
|
+
log(`[daemon] connected: ${this.cfg.runtimeIds.length} runtime(s) subscribed`);
|
|
2567
2598
|
});
|
|
2568
2599
|
ws.on("message", (raw) => {
|
|
2569
2600
|
let msg;
|
|
@@ -2584,7 +2615,7 @@ class Claimer {
|
|
|
2584
2615
|
this.scheduleWsReconnect();
|
|
2585
2616
|
});
|
|
2586
2617
|
ws.on("error", (err) => {
|
|
2587
|
-
|
|
2618
|
+
warn("[daemon] ws error:", err.message);
|
|
2588
2619
|
});
|
|
2589
2620
|
}
|
|
2590
2621
|
scheduleWsReconnect() {
|
|
@@ -2592,7 +2623,7 @@ class Claimer {
|
|
|
2592
2623
|
return;
|
|
2593
2624
|
this.wsReconnectAttempts += 1;
|
|
2594
2625
|
const delay = Math.min(1000 * Math.pow(2, this.wsReconnectAttempts - 1), this.wsReconnectMaxDelayMs);
|
|
2595
|
-
|
|
2626
|
+
warn(`[daemon] ws disconnected; reconnecting in ${delay}ms`);
|
|
2596
2627
|
this.wsReconnectTimer = setTimeout(() => {
|
|
2597
2628
|
this.wsReconnectTimer = undefined;
|
|
2598
2629
|
this.connectWs();
|
|
@@ -2606,7 +2637,7 @@ class Claimer {
|
|
|
2606
2637
|
runtime_ids: this.cfg.runtimeIds
|
|
2607
2638
|
});
|
|
2608
2639
|
} catch (err) {
|
|
2609
|
-
|
|
2640
|
+
warn("[daemon] heartbeat failed:", err instanceof Error ? err.message : String(err));
|
|
2610
2641
|
}
|
|
2611
2642
|
}
|
|
2612
2643
|
async pollAll() {
|
|
@@ -2620,40 +2651,39 @@ class Claimer {
|
|
|
2620
2651
|
try {
|
|
2621
2652
|
payload = await this.cfg.api.claim(runtimeId);
|
|
2622
2653
|
} catch (err) {
|
|
2623
|
-
|
|
2654
|
+
warn(`[daemon] claim failed for runtime=${runtimeId}:`, err instanceof Error ? err.message : String(err));
|
|
2624
2655
|
return;
|
|
2625
2656
|
}
|
|
2626
2657
|
if (!payload)
|
|
2627
2658
|
return;
|
|
2628
|
-
|
|
2659
|
+
log(`[daemon/claim] sess=${payload.session_id} agent=${payload.agent_id} runtime=${runtimeId}`);
|
|
2629
2660
|
const ctrl = this.cfg.supervisor.start(payload.session_id);
|
|
2630
2661
|
runDispatch({
|
|
2631
2662
|
api: this.cfg.api,
|
|
2632
2663
|
workspaceManager: this.cfg.workspaceManager,
|
|
2633
2664
|
runtimeRegistry: this.cfg.runtimeRegistry
|
|
2634
|
-
}, payload, ctrl.signal).catch((err) =>
|
|
2665
|
+
}, payload, ctrl.signal).catch((err) => error(`[daemon] dispatch ${payload.session_id} failed:`, err instanceof Error ? err.message : String(err))).finally(() => this.cfg.supervisor.finish(payload.session_id));
|
|
2635
2666
|
}
|
|
2636
2667
|
}
|
|
2637
2668
|
}
|
|
2638
2669
|
|
|
2639
2670
|
// src/skills-cache.ts
|
|
2640
2671
|
import { promises as fs2 } from "node:fs";
|
|
2641
|
-
import { homedir as homedir3 } from "node:os";
|
|
2642
2672
|
import { join as join8 } from "node:path";
|
|
2643
|
-
function skillsCacheDir() {
|
|
2644
|
-
return join8(
|
|
2673
|
+
function skillsCacheDir(configRoot) {
|
|
2674
|
+
return join8(getConfigRoot(configRoot), "skills");
|
|
2645
2675
|
}
|
|
2646
2676
|
var VERSION_FILE = ".version";
|
|
2647
|
-
async function readCachedVersion() {
|
|
2677
|
+
async function readCachedVersion(configRoot) {
|
|
2648
2678
|
try {
|
|
2649
|
-
return (await fs2.readFile(join8(skillsCacheDir(), VERSION_FILE), "utf8")).trim();
|
|
2679
|
+
return (await fs2.readFile(join8(skillsCacheDir(configRoot), VERSION_FILE), "utf8")).trim();
|
|
2650
2680
|
} catch {
|
|
2651
2681
|
return;
|
|
2652
2682
|
}
|
|
2653
2683
|
}
|
|
2654
|
-
async function syncSkillsCache(api) {
|
|
2655
|
-
const cache = skillsCacheDir();
|
|
2656
|
-
const cached = await readCachedVersion();
|
|
2684
|
+
async function syncSkillsCache(api, configRoot) {
|
|
2685
|
+
const cache = skillsCacheDir(configRoot);
|
|
2686
|
+
const cached = await readCachedVersion(configRoot);
|
|
2657
2687
|
const res = await api.get("/runtime/skills");
|
|
2658
2688
|
if (!res) {
|
|
2659
2689
|
if (cached)
|
|
@@ -2733,8 +2763,8 @@ function readMaxFromEnv() {
|
|
|
2733
2763
|
}
|
|
2734
2764
|
|
|
2735
2765
|
// src/start.ts
|
|
2736
|
-
async function runStart() {
|
|
2737
|
-
const cfg = loadConfig();
|
|
2766
|
+
async function runStart(options = {}) {
|
|
2767
|
+
const cfg = loadConfig(options.configRoot);
|
|
2738
2768
|
if (!cfg) {
|
|
2739
2769
|
throw new Error("No daemon config found. Run `beevibe-daemon setup --api <url> --user-token <bv_u_…>` first.");
|
|
2740
2770
|
}
|
|
@@ -2742,8 +2772,8 @@ async function runStart() {
|
|
|
2742
2772
|
apiUrl: cfg.api_url,
|
|
2743
2773
|
daemonToken: cfg.daemon_token
|
|
2744
2774
|
});
|
|
2745
|
-
const skillsSourceDir = await syncSkillsCache(api).catch((err) => {
|
|
2746
|
-
|
|
2775
|
+
const skillsSourceDir = await syncSkillsCache(api, options.configRoot).catch((err) => {
|
|
2776
|
+
warn("[daemon] skills sync failed; continuing without skills:", err instanceof Error ? err.message : String(err));
|
|
2747
2777
|
return;
|
|
2748
2778
|
});
|
|
2749
2779
|
const runtimeRegistry = createDefaultRuntimeRegistry();
|
|
@@ -2751,7 +2781,7 @@ async function runStart() {
|
|
|
2751
2781
|
mcpServerUrl: `${cfg.api_url}/mcp`,
|
|
2752
2782
|
runtimeRegistry,
|
|
2753
2783
|
skillsSourceDir: skillsSourceDir ?? "/dev/null",
|
|
2754
|
-
workspaceRoot: process.env.WORKSPACE_ROOT
|
|
2784
|
+
workspaceRoot: process.env.WORKSPACE_ROOT && process.env.WORKSPACE_ROOT.length > 0 ? process.env.WORKSPACE_ROOT : join9(getConfigRoot(options.configRoot), "workspaces")
|
|
2755
2785
|
});
|
|
2756
2786
|
const supervisor = new Supervisor;
|
|
2757
2787
|
const claimer = new Claimer({
|
|
@@ -2762,20 +2792,20 @@ async function runStart() {
|
|
|
2762
2792
|
runtimeIds: cfg.runtimes.map((r) => r.id)
|
|
2763
2793
|
});
|
|
2764
2794
|
claimer.start();
|
|
2765
|
-
|
|
2795
|
+
log(`[daemon] started (${cfg.daemon_id} → ${cfg.api_url}, ${cfg.runtimes.length} runtime(s))`);
|
|
2766
2796
|
let stopped = false;
|
|
2767
2797
|
const stop = async (signal) => {
|
|
2768
2798
|
if (stopped)
|
|
2769
2799
|
return;
|
|
2770
2800
|
stopped = true;
|
|
2771
|
-
|
|
2801
|
+
log(`[daemon] received ${signal}; stopping`);
|
|
2772
2802
|
await claimer.stop();
|
|
2773
2803
|
process.exit(0);
|
|
2774
2804
|
};
|
|
2775
2805
|
process.on("SIGINT", () => void stop("SIGINT"));
|
|
2776
2806
|
process.on("SIGTERM", () => void stop("SIGTERM"));
|
|
2777
2807
|
process.on("unhandledRejection", (reason) => {
|
|
2778
|
-
|
|
2808
|
+
warn("[daemon] unhandledRejection (continuing):", reason instanceof Error ? reason.message : String(reason));
|
|
2779
2809
|
});
|
|
2780
2810
|
await new Promise(() => {
|
|
2781
2811
|
return;
|
|
@@ -2783,10 +2813,10 @@ async function runStart() {
|
|
|
2783
2813
|
}
|
|
2784
2814
|
|
|
2785
2815
|
// src/sync.ts
|
|
2786
|
-
async function runSync() {
|
|
2787
|
-
const config = loadConfig();
|
|
2816
|
+
async function runSync(options = {}) {
|
|
2817
|
+
const config = loadConfig(options.configRoot);
|
|
2788
2818
|
if (!config) {
|
|
2789
|
-
throw new Error(`No daemon config at ${
|
|
2819
|
+
throw new Error(`No daemon config at ${getConfigPath(options.configRoot)}. Run 'beevibe-daemon setup' first.`);
|
|
2790
2820
|
}
|
|
2791
2821
|
const detected = await detectClis();
|
|
2792
2822
|
if (detected.length === 0) {
|
|
@@ -2805,7 +2835,7 @@ async function runSync() {
|
|
|
2805
2835
|
const before = new Set(config.runtimes.map((r) => r.cli));
|
|
2806
2836
|
const added = body.runtimes.filter((r) => !before.has(r.cli));
|
|
2807
2837
|
const next = { ...config, runtimes: body.runtimes };
|
|
2808
|
-
saveConfig(next);
|
|
2838
|
+
saveConfig(next, options.configRoot);
|
|
2809
2839
|
return { added, runtimes: body.runtimes };
|
|
2810
2840
|
}
|
|
2811
2841
|
|
|
@@ -2813,7 +2843,7 @@ async function runSync() {
|
|
|
2813
2843
|
import { createHash } from "node:crypto";
|
|
2814
2844
|
import { createWriteStream, mkdtempSync, rmSync as rmSync2, chmodSync, renameSync } from "node:fs";
|
|
2815
2845
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
2816
|
-
import { join as
|
|
2846
|
+
import { join as join10 } from "node:path";
|
|
2817
2847
|
import { Readable } from "node:stream";
|
|
2818
2848
|
import { pipeline } from "node:stream/promises";
|
|
2819
2849
|
import { createInterface } from "node:readline/promises";
|
|
@@ -2827,7 +2857,7 @@ var PLATFORM_ASSETS = {
|
|
|
2827
2857
|
"linux-arm64": "beevibe-daemon-linux-arm64"
|
|
2828
2858
|
};
|
|
2829
2859
|
function currentVersion() {
|
|
2830
|
-
return "0.1.
|
|
2860
|
+
return "0.1.6";
|
|
2831
2861
|
}
|
|
2832
2862
|
function isCompiledBinary() {
|
|
2833
2863
|
if (!process.versions.bun)
|
|
@@ -2901,58 +2931,58 @@ async function promptYesNo(question) {
|
|
|
2901
2931
|
async function runUpdate(opts = {}) {
|
|
2902
2932
|
const current = currentVersion();
|
|
2903
2933
|
if (!current) {
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2934
|
+
log("Could not determine current daemon version.");
|
|
2935
|
+
log("If you installed via npm: npm update -g @beevibe/daemon");
|
|
2936
|
+
log("If you installed from source: git pull && pnpm install && pnpm build");
|
|
2907
2937
|
process.exit(2);
|
|
2908
2938
|
}
|
|
2909
2939
|
if (!isCompiledBinary()) {
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2940
|
+
log(`Current version: ${current}`);
|
|
2941
|
+
log("This binary was not produced by the standalone build path.");
|
|
2942
|
+
log("If you installed via npm: npm update -g @beevibe/daemon");
|
|
2943
|
+
log("If you installed from source: git pull && pnpm install && pnpm build");
|
|
2914
2944
|
return;
|
|
2915
2945
|
}
|
|
2916
2946
|
const asset = platformAsset();
|
|
2917
2947
|
if (!asset) {
|
|
2918
|
-
|
|
2919
|
-
|
|
2948
|
+
error(`Unsupported platform: ${process.platform}/${process.arch}`);
|
|
2949
|
+
error("Pre-built binaries are only published for darwin-arm64, darwin-x64, linux-x64, linux-arm64.");
|
|
2920
2950
|
process.exit(2);
|
|
2921
2951
|
}
|
|
2922
|
-
|
|
2923
|
-
|
|
2952
|
+
log(`Current version: ${current}`);
|
|
2953
|
+
log("Checking for updates…");
|
|
2924
2954
|
const release = await fetchLatestRelease();
|
|
2925
2955
|
if (!release) {
|
|
2926
|
-
|
|
2956
|
+
log("No releases published yet — nothing to update to.");
|
|
2927
2957
|
return;
|
|
2928
2958
|
}
|
|
2929
2959
|
const latest = release.tag_name;
|
|
2930
|
-
|
|
2960
|
+
log(`Latest version: ${latest}`);
|
|
2931
2961
|
if (compareSemver(current, latest) >= 0) {
|
|
2932
|
-
|
|
2962
|
+
log("Already on the latest version.");
|
|
2933
2963
|
return;
|
|
2934
2964
|
}
|
|
2935
|
-
|
|
2965
|
+
log(`Update available: ${current} → ${latest}`);
|
|
2936
2966
|
if (!opts.skipPrompt) {
|
|
2937
2967
|
const proceed = await promptYesNo("Install this update now?");
|
|
2938
2968
|
if (!proceed) {
|
|
2939
|
-
|
|
2969
|
+
log("Update cancelled.");
|
|
2940
2970
|
return;
|
|
2941
2971
|
}
|
|
2942
2972
|
}
|
|
2943
|
-
const stagingDir = mkdtempSync(
|
|
2944
|
-
const stagingPath =
|
|
2973
|
+
const stagingDir = mkdtempSync(join10(tmpdir5(), "beevibe-daemon-update-"));
|
|
2974
|
+
const stagingPath = join10(stagingDir, asset);
|
|
2945
2975
|
try {
|
|
2946
|
-
|
|
2976
|
+
log(`Downloading ${asset}…`);
|
|
2947
2977
|
const downloadUrl = `${DOWNLOAD_BASE}/${latest}/${asset}`;
|
|
2948
2978
|
const [actualSha, expectedSha] = await Promise.all([
|
|
2949
2979
|
downloadAndHash(downloadUrl, stagingPath),
|
|
2950
2980
|
downloadChecksum(latest, asset)
|
|
2951
2981
|
]);
|
|
2952
2982
|
if (actualSha !== expectedSha) {
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2983
|
+
error(`Checksum mismatch — refusing to install.`);
|
|
2984
|
+
error(` expected: ${expectedSha}`);
|
|
2985
|
+
error(` actual: ${actualSha}`);
|
|
2956
2986
|
process.exit(3);
|
|
2957
2987
|
}
|
|
2958
2988
|
chmodSync(stagingPath, 493);
|
|
@@ -2960,11 +2990,11 @@ async function runUpdate(opts = {}) {
|
|
|
2960
2990
|
renameSync(stagingPath, process.execPath);
|
|
2961
2991
|
} catch (err) {
|
|
2962
2992
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2963
|
-
|
|
2964
|
-
|
|
2993
|
+
error(`Failed to replace ${process.execPath}: ${msg}`);
|
|
2994
|
+
error(`The new binary is at ${stagingPath} — install manually if needed.`);
|
|
2965
2995
|
return;
|
|
2966
2996
|
}
|
|
2967
|
-
|
|
2997
|
+
log(`Updated to ${latest}. Restart the daemon to pick up the new binary.`);
|
|
2968
2998
|
} finally {
|
|
2969
2999
|
rmSync2(stagingDir, { recursive: true, force: true });
|
|
2970
3000
|
}
|
|
@@ -2988,12 +3018,28 @@ function parseFlags(argv) {
|
|
|
2988
3018
|
} else if (arg === "--external-id" && next) {
|
|
2989
3019
|
flags.externalId = next;
|
|
2990
3020
|
i += 1;
|
|
3021
|
+
} else if (arg === "--config-root" && next) {
|
|
3022
|
+
flags.configRoot = next;
|
|
3023
|
+
i += 1;
|
|
2991
3024
|
}
|
|
2992
3025
|
}
|
|
2993
3026
|
return flags;
|
|
2994
3027
|
}
|
|
3028
|
+
function resolveConfigRoot(flag) {
|
|
3029
|
+
const env2 = process.env[CONFIG_ROOT_ENV];
|
|
3030
|
+
const hasFlag = flag && flag.length > 0;
|
|
3031
|
+
const hasEnv = env2 && env2.length > 0;
|
|
3032
|
+
if (!hasFlag && !hasEnv)
|
|
3033
|
+
return;
|
|
3034
|
+
if (!isDevBuild()) {
|
|
3035
|
+
const source = hasFlag ? "--config-root" : CONFIG_ROOT_ENV;
|
|
3036
|
+
error(`${source} is a dev-only knob and is not available in this build. ` + `Reinstall via npm/curl without the override, or use a source checkout ` + `(pnpm dev) if you need multi-instance.`);
|
|
3037
|
+
process.exit(2);
|
|
3038
|
+
}
|
|
3039
|
+
return hasFlag ? flag : env2;
|
|
3040
|
+
}
|
|
2995
3041
|
function printHelp() {
|
|
2996
|
-
|
|
3042
|
+
log([
|
|
2997
3043
|
"Usage: beevibe-daemon <command> [flags]",
|
|
2998
3044
|
"",
|
|
2999
3045
|
"Commands:",
|
|
@@ -3009,7 +3055,13 @@ function printHelp() {
|
|
|
3009
3055
|
" --external-id <id> optional stable per-machine id (defaults to hostname)",
|
|
3010
3056
|
"",
|
|
3011
3057
|
"update flags:",
|
|
3012
|
-
" --yes, -y skip the install-this-update prompt"
|
|
3058
|
+
" --yes, -y skip the install-this-update prompt",
|
|
3059
|
+
"",
|
|
3060
|
+
"dev-only flags (source builds only — rejected in compiled binaries):",
|
|
3061
|
+
" --config-root <path> shift the daemon's on-disk root from ~/.beevibe.",
|
|
3062
|
+
" Lets two daemons coexist on one machine as",
|
|
3063
|
+
" different accounts. Also settable via the",
|
|
3064
|
+
" BEEVIBE_CONFIG_ROOT env var."
|
|
3013
3065
|
].join(`
|
|
3014
3066
|
`));
|
|
3015
3067
|
}
|
|
@@ -3019,10 +3071,11 @@ async function main() {
|
|
|
3019
3071
|
printHelp();
|
|
3020
3072
|
return;
|
|
3021
3073
|
}
|
|
3074
|
+
const flags = parseFlags(rest);
|
|
3075
|
+
const configRoot = resolveConfigRoot(flags.configRoot);
|
|
3022
3076
|
if (command === "setup") {
|
|
3023
|
-
const flags = parseFlags(rest);
|
|
3024
3077
|
if (!flags.api || !flags.userToken) {
|
|
3025
|
-
|
|
3078
|
+
error("setup requires --api and --user-token");
|
|
3026
3079
|
printHelp();
|
|
3027
3080
|
process.exit(2);
|
|
3028
3081
|
}
|
|
@@ -3030,24 +3083,25 @@ async function main() {
|
|
|
3030
3083
|
apiUrl: flags.api,
|
|
3031
3084
|
userToken: flags.userToken,
|
|
3032
3085
|
deviceName: flags.deviceName,
|
|
3033
|
-
externalId: flags.externalId
|
|
3086
|
+
externalId: flags.externalId,
|
|
3087
|
+
configRoot
|
|
3034
3088
|
});
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3089
|
+
log(`Registered as ${cfg.daemon_id}`);
|
|
3090
|
+
log(`Runtimes: ${cfg.runtimes.map((r) => `${r.cli} (${r.id})`).join(", ")}`);
|
|
3091
|
+
log(`Config saved to ${getConfigPath(configRoot)}`);
|
|
3038
3092
|
return;
|
|
3039
3093
|
}
|
|
3040
3094
|
if (command === "start") {
|
|
3041
|
-
await runStart();
|
|
3095
|
+
await runStart({ configRoot });
|
|
3042
3096
|
return;
|
|
3043
3097
|
}
|
|
3044
3098
|
if (command === "sync") {
|
|
3045
|
-
const result = await runSync();
|
|
3099
|
+
const result = await runSync({ configRoot });
|
|
3046
3100
|
if (result.added.length === 0) {
|
|
3047
|
-
|
|
3101
|
+
log("No new CLIs detected.");
|
|
3048
3102
|
} else {
|
|
3049
|
-
|
|
3050
|
-
|
|
3103
|
+
log(`Added ${result.added.length} runtime(s): ${result.added.map((r) => `${r.cli} (${r.id})`).join(", ")}.`);
|
|
3104
|
+
log("Restart the daemon to pick up the new runtime(s).");
|
|
3051
3105
|
}
|
|
3052
3106
|
return;
|
|
3053
3107
|
}
|
|
@@ -3056,11 +3110,11 @@ async function main() {
|
|
|
3056
3110
|
await runUpdate({ skipPrompt });
|
|
3057
3111
|
return;
|
|
3058
3112
|
}
|
|
3059
|
-
|
|
3113
|
+
error(`Unknown command: ${command}`);
|
|
3060
3114
|
printHelp();
|
|
3061
3115
|
process.exit(2);
|
|
3062
3116
|
}
|
|
3063
3117
|
main().catch((err) => {
|
|
3064
|
-
|
|
3118
|
+
error(err instanceof Error ? err.stack ?? err.message : String(err));
|
|
3065
3119
|
process.exit(1);
|
|
3066
3120
|
});
|
package/package.json
CHANGED