@beevibe/daemon 0.1.4 → 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 +175 -118
- 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";
|
|
@@ -408,7 +437,10 @@ async function listFilesRecursive(dir) {
|
|
|
408
437
|
return out;
|
|
409
438
|
}
|
|
410
439
|
// ../core/dist/services/skills/tier-filter.js
|
|
411
|
-
var UNIVERSAL_SKILLS = [
|
|
440
|
+
var UNIVERSAL_SKILLS = [
|
|
441
|
+
"beevibe-pre-task-setup",
|
|
442
|
+
"beevibe-verify-pr"
|
|
443
|
+
];
|
|
412
444
|
var TEAM_ONLY_SKILLS = ["beevibe-team-mesh-negotiation"];
|
|
413
445
|
function tierFilterFor(level) {
|
|
414
446
|
if (level === "ic")
|
|
@@ -465,12 +497,14 @@ class LocalWorkspaceManager {
|
|
|
465
497
|
rmSync(workspace2.path, { recursive: true, force: true });
|
|
466
498
|
}
|
|
467
499
|
}
|
|
500
|
+
var MCP_TOOL_TIMEOUT_MS = 10 * 60000;
|
|
468
501
|
function buildMcpConfig(apiKey, mcpServerUrl) {
|
|
469
502
|
return JSON.stringify({
|
|
470
503
|
mcpServers: {
|
|
471
504
|
beevibe: {
|
|
472
505
|
type: "http",
|
|
473
506
|
url: mcpServerUrl,
|
|
507
|
+
timeout: MCP_TOOL_TIMEOUT_MS,
|
|
474
508
|
headers: {
|
|
475
509
|
Authorization: `Bearer ${apiKey}`,
|
|
476
510
|
"X-Beevibe-Session": "${BEEVIBE_SESSION_ID}"
|
|
@@ -1197,7 +1231,7 @@ class CodexRuntime {
|
|
|
1197
1231
|
lastMessagePath
|
|
1198
1232
|
];
|
|
1199
1233
|
if (prepared && sid) {
|
|
1200
|
-
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}`);
|
|
1201
1235
|
}
|
|
1202
1236
|
const args = context.resume_session_id ? [
|
|
1203
1237
|
...globalArgs,
|
|
@@ -1938,7 +1972,7 @@ async function runRepoAgent(opts) {
|
|
|
1938
1972
|
artifacts: []
|
|
1939
1973
|
};
|
|
1940
1974
|
const emit = () => opts.on_state?.({ ...state, transcript: state.transcript.slice(), artifacts: state.artifacts.slice() });
|
|
1941
|
-
const
|
|
1975
|
+
const log2 = (kind, text) => {
|
|
1942
1976
|
const trimmed = text.length > MAX_EVENT_TEXT_BYTES ? text.slice(0, MAX_EVENT_TEXT_BYTES) + `
|
|
1943
1977
|
…[truncated ${text.length - MAX_EVENT_TEXT_BYTES} bytes]…` : text;
|
|
1944
1978
|
state.transcript.push({ at: nowIso(), kind, text: trimmed });
|
|
@@ -1958,7 +1992,7 @@ async function runRepoAgent(opts) {
|
|
|
1958
1992
|
};
|
|
1959
1993
|
let sandbox = null;
|
|
1960
1994
|
try {
|
|
1961
|
-
|
|
1995
|
+
log2("log", "Creating sandbox container…");
|
|
1962
1996
|
state.status = "preparing";
|
|
1963
1997
|
emit();
|
|
1964
1998
|
try {
|
|
@@ -1967,18 +2001,18 @@ async function runRepoAgent(opts) {
|
|
|
1967
2001
|
throw new Error(classifyStartupError(err));
|
|
1968
2002
|
}
|
|
1969
2003
|
state.sandbox_id = sandbox.id;
|
|
1970
|
-
|
|
1971
|
-
|
|
2004
|
+
log2("log", `Sandbox ${sandbox.id} created (image ${sandbox.image}).`);
|
|
2005
|
+
log2("log", "Installing git + curl in the sandbox base image…");
|
|
1972
2006
|
await prepareBaseEnvironment(sandbox);
|
|
1973
|
-
|
|
2007
|
+
log2("log", "Base environment ready.");
|
|
1974
2008
|
if (opts.input_url) {
|
|
1975
2009
|
const filename = opts.input_filename ?? "input.bin";
|
|
1976
|
-
|
|
2010
|
+
log2("log", `Fetching input into /sandbox/inputs/${filename}…`);
|
|
1977
2011
|
const r = await exec(sandbox, `mkdir -p /sandbox/inputs && curl -fsSL ${shellQuote(opts.input_url)} -o /sandbox/inputs/${shellQuote(filename)}`, { timeout_seconds: 120 });
|
|
1978
2012
|
if (r.exit_code !== 0) {
|
|
1979
2013
|
throw new Error(`input fetch failed: ${r.stderr.trim().slice(0, 400)}`);
|
|
1980
2014
|
}
|
|
1981
|
-
|
|
2015
|
+
log2("log", "Input file ready.");
|
|
1982
2016
|
}
|
|
1983
2017
|
const mcpServerCommand = opts.mcp_server_command ?? defaultMcpServerCommand();
|
|
1984
2018
|
const mcpConfigPath = join7(sandbox.artifact_dir, "mcp-config.json");
|
|
@@ -1995,11 +2029,11 @@ async function runRepoAgent(opts) {
|
|
|
1995
2029
|
}
|
|
1996
2030
|
};
|
|
1997
2031
|
await writeFile2(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
|
|
1998
|
-
|
|
2032
|
+
log2("log", `MCP config written: ${mcpConfigPath}`);
|
|
1999
2033
|
const userPrompt = buildUserPrompt(opts);
|
|
2000
2034
|
const systemPromptAppend = DEFAULT_PROMPT_HEADER;
|
|
2001
2035
|
state.status = "running";
|
|
2002
|
-
|
|
2036
|
+
log2("log", "Spawning child claude session…");
|
|
2003
2037
|
emit();
|
|
2004
2038
|
const claudeResult = await runClaude({
|
|
2005
2039
|
claudeBin: opts.claude_bin ?? "claude",
|
|
@@ -2008,18 +2042,18 @@ async function runRepoAgent(opts) {
|
|
|
2008
2042
|
userPrompt,
|
|
2009
2043
|
maxBudgetUsd: opts.max_budget_usd ?? 2,
|
|
2010
2044
|
timeoutSeconds: opts.max_runtime_seconds ?? 600,
|
|
2011
|
-
onTranscript: (kind, text) =>
|
|
2045
|
+
onTranscript: (kind, text) => log2(kind, text)
|
|
2012
2046
|
});
|
|
2013
2047
|
if (claudeResult.exit_code !== 0 && claudeResult.exit_code !== null) {
|
|
2014
2048
|
const tail = claudeResult.stderr.slice(-500);
|
|
2015
|
-
|
|
2049
|
+
log2("error", `Child claude exited with code ${claudeResult.exit_code}: ${tail}`);
|
|
2016
2050
|
state.status = claudeResult.timed_out ? "blocked" : "failed";
|
|
2017
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) : ""}`;
|
|
2018
2052
|
state.finished_at = nowIso();
|
|
2019
2053
|
emit();
|
|
2020
2054
|
return state;
|
|
2021
2055
|
}
|
|
2022
|
-
|
|
2056
|
+
log2("log", "Child claude exited cleanly. Collecting artifacts…");
|
|
2023
2057
|
state.artifacts = await collectArtifacts(sandbox);
|
|
2024
2058
|
if (state.artifacts.length === 0) {
|
|
2025
2059
|
state.status = "blocked";
|
|
@@ -2031,7 +2065,7 @@ async function runRepoAgent(opts) {
|
|
|
2031
2065
|
emit();
|
|
2032
2066
|
return state;
|
|
2033
2067
|
} catch (err) {
|
|
2034
|
-
|
|
2068
|
+
log2("error", err instanceof Error ? err.message : String(err));
|
|
2035
2069
|
state.status = "failed";
|
|
2036
2070
|
state.error = err instanceof Error ? err.message : String(err);
|
|
2037
2071
|
state.finished_at = nowIso();
|
|
@@ -2040,10 +2074,10 @@ async function runRepoAgent(opts) {
|
|
|
2040
2074
|
} finally {
|
|
2041
2075
|
if (sandbox) {
|
|
2042
2076
|
try {
|
|
2043
|
-
|
|
2077
|
+
log2("log", `Destroying sandbox ${sandbox.id}…`);
|
|
2044
2078
|
await destroySandbox(sandbox);
|
|
2045
2079
|
} catch (err) {
|
|
2046
|
-
|
|
2080
|
+
log2("log", `Sandbox cleanup note: ${err instanceof Error ? err.message : String(err)}. Run with \`docker ps -a --filter name=bv-run-\` to inspect.`);
|
|
2047
2081
|
}
|
|
2048
2082
|
}
|
|
2049
2083
|
}
|
|
@@ -2264,7 +2298,7 @@ async function runRepoDispatch(deps, payload, abortSignal) {
|
|
|
2264
2298
|
if (!rr) {
|
|
2265
2299
|
throw new Error("run_repo dispatch missing payload.run_repo");
|
|
2266
2300
|
}
|
|
2267
|
-
|
|
2301
|
+
log(`[daemon/repo-run] sess=${payload.session_id} repo_run=${rr.repo_run_id} repo=${rr.repo_url}`);
|
|
2268
2302
|
const buffer = [];
|
|
2269
2303
|
let flushTimer;
|
|
2270
2304
|
const flush = async () => {
|
|
@@ -2274,7 +2308,7 @@ async function runRepoDispatch(deps, payload, abortSignal) {
|
|
|
2274
2308
|
try {
|
|
2275
2309
|
await deps.api.post("/runtime/events", { events });
|
|
2276
2310
|
} catch (err) {
|
|
2277
|
-
|
|
2311
|
+
warn("[daemon/repo-run] /runtime/events POST failed:", err instanceof Error ? err.message : String(err));
|
|
2278
2312
|
}
|
|
2279
2313
|
};
|
|
2280
2314
|
const scheduleFlush = () => {
|
|
@@ -2371,9 +2405,9 @@ async function runRepoDispatch(deps, payload, abortSignal) {
|
|
|
2371
2405
|
}
|
|
2372
2406
|
};
|
|
2373
2407
|
if (status === "succeeded") {
|
|
2374
|
-
|
|
2408
|
+
log(`[daemon/repo-run] sess=${payload.session_id} succeeded artifacts=${artifacts.length}`);
|
|
2375
2409
|
} else {
|
|
2376
|
-
|
|
2410
|
+
error(`[daemon/repo-run] sess=${payload.session_id} status=${status}` + (result.error ? `
|
|
2377
2411
|
error:
|
|
2378
2412
|
${result.error.split(`
|
|
2379
2413
|
`).join(`
|
|
@@ -2382,7 +2416,7 @@ async function runRepoDispatch(deps, payload, abortSignal) {
|
|
|
2382
2416
|
try {
|
|
2383
2417
|
await deps.api.post("/runtime/done", done);
|
|
2384
2418
|
} catch (err) {
|
|
2385
|
-
|
|
2419
|
+
error("[daemon/repo-run] /runtime/done POST failed:", err instanceof Error ? err.message : String(err));
|
|
2386
2420
|
}
|
|
2387
2421
|
}
|
|
2388
2422
|
function mapKind(kind) {
|
|
@@ -2414,7 +2448,7 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
2414
2448
|
runtime_config: { type: payload.runtime_type }
|
|
2415
2449
|
};
|
|
2416
2450
|
const ws = await deps.workspaceManager.ensureWorkspace({ agent: syntheticAgent });
|
|
2417
|
-
|
|
2451
|
+
log(`[daemon/spawn] sess=${payload.session_id} agent=${payload.agent_id} runtime=${payload.runtime_type} type=${payload.type} cwd=${ws.path}`);
|
|
2418
2452
|
const registry = deps.runtimeRegistry ?? createDefaultRuntimeRegistry();
|
|
2419
2453
|
const runtime3 = registry[payload.runtime_type];
|
|
2420
2454
|
if (!runtime3) {
|
|
@@ -2429,7 +2463,7 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
2429
2463
|
try {
|
|
2430
2464
|
await deps.api.post("/runtime/events", { events });
|
|
2431
2465
|
} catch (err) {
|
|
2432
|
-
|
|
2466
|
+
warn("[daemon/spawner] /runtime/events POST failed; events dropped:", err instanceof Error ? err.message : String(err));
|
|
2433
2467
|
}
|
|
2434
2468
|
};
|
|
2435
2469
|
const scheduleFlush = () => {
|
|
@@ -2486,9 +2520,9 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
2486
2520
|
usage: result?.usage
|
|
2487
2521
|
};
|
|
2488
2522
|
if (status === "succeeded") {
|
|
2489
|
-
|
|
2523
|
+
log(`[daemon/spawn] sess=${payload.session_id} exit=0`);
|
|
2490
2524
|
} else {
|
|
2491
|
-
|
|
2525
|
+
error(`[daemon/spawn] sess=${payload.session_id} status=${status} exit=${done.exit_code}` + (errorDetail ? `
|
|
2492
2526
|
error:
|
|
2493
2527
|
${errorDetail.split(`
|
|
2494
2528
|
`).join(`
|
|
@@ -2497,7 +2531,7 @@ async function runDispatch(deps, payload, abortSignal) {
|
|
|
2497
2531
|
try {
|
|
2498
2532
|
await deps.api.post("/runtime/done", done);
|
|
2499
2533
|
} catch (err) {
|
|
2500
|
-
|
|
2534
|
+
error("[daemon/spawner] /runtime/done POST failed:", err instanceof Error ? err.message : String(err));
|
|
2501
2535
|
}
|
|
2502
2536
|
}
|
|
2503
2537
|
|
|
@@ -2560,7 +2594,7 @@ class Claimer {
|
|
|
2560
2594
|
this.ws = ws;
|
|
2561
2595
|
ws.on("open", () => {
|
|
2562
2596
|
this.wsReconnectAttempts = 0;
|
|
2563
|
-
|
|
2597
|
+
log(`[daemon] connected: ${this.cfg.runtimeIds.length} runtime(s) subscribed`);
|
|
2564
2598
|
});
|
|
2565
2599
|
ws.on("message", (raw) => {
|
|
2566
2600
|
let msg;
|
|
@@ -2581,7 +2615,7 @@ class Claimer {
|
|
|
2581
2615
|
this.scheduleWsReconnect();
|
|
2582
2616
|
});
|
|
2583
2617
|
ws.on("error", (err) => {
|
|
2584
|
-
|
|
2618
|
+
warn("[daemon] ws error:", err.message);
|
|
2585
2619
|
});
|
|
2586
2620
|
}
|
|
2587
2621
|
scheduleWsReconnect() {
|
|
@@ -2589,7 +2623,7 @@ class Claimer {
|
|
|
2589
2623
|
return;
|
|
2590
2624
|
this.wsReconnectAttempts += 1;
|
|
2591
2625
|
const delay = Math.min(1000 * Math.pow(2, this.wsReconnectAttempts - 1), this.wsReconnectMaxDelayMs);
|
|
2592
|
-
|
|
2626
|
+
warn(`[daemon] ws disconnected; reconnecting in ${delay}ms`);
|
|
2593
2627
|
this.wsReconnectTimer = setTimeout(() => {
|
|
2594
2628
|
this.wsReconnectTimer = undefined;
|
|
2595
2629
|
this.connectWs();
|
|
@@ -2603,7 +2637,7 @@ class Claimer {
|
|
|
2603
2637
|
runtime_ids: this.cfg.runtimeIds
|
|
2604
2638
|
});
|
|
2605
2639
|
} catch (err) {
|
|
2606
|
-
|
|
2640
|
+
warn("[daemon] heartbeat failed:", err instanceof Error ? err.message : String(err));
|
|
2607
2641
|
}
|
|
2608
2642
|
}
|
|
2609
2643
|
async pollAll() {
|
|
@@ -2617,40 +2651,39 @@ class Claimer {
|
|
|
2617
2651
|
try {
|
|
2618
2652
|
payload = await this.cfg.api.claim(runtimeId);
|
|
2619
2653
|
} catch (err) {
|
|
2620
|
-
|
|
2654
|
+
warn(`[daemon] claim failed for runtime=${runtimeId}:`, err instanceof Error ? err.message : String(err));
|
|
2621
2655
|
return;
|
|
2622
2656
|
}
|
|
2623
2657
|
if (!payload)
|
|
2624
2658
|
return;
|
|
2625
|
-
|
|
2659
|
+
log(`[daemon/claim] sess=${payload.session_id} agent=${payload.agent_id} runtime=${runtimeId}`);
|
|
2626
2660
|
const ctrl = this.cfg.supervisor.start(payload.session_id);
|
|
2627
2661
|
runDispatch({
|
|
2628
2662
|
api: this.cfg.api,
|
|
2629
2663
|
workspaceManager: this.cfg.workspaceManager,
|
|
2630
2664
|
runtimeRegistry: this.cfg.runtimeRegistry
|
|
2631
|
-
}, 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));
|
|
2632
2666
|
}
|
|
2633
2667
|
}
|
|
2634
2668
|
}
|
|
2635
2669
|
|
|
2636
2670
|
// src/skills-cache.ts
|
|
2637
2671
|
import { promises as fs2 } from "node:fs";
|
|
2638
|
-
import { homedir as homedir3 } from "node:os";
|
|
2639
2672
|
import { join as join8 } from "node:path";
|
|
2640
|
-
function skillsCacheDir() {
|
|
2641
|
-
return join8(
|
|
2673
|
+
function skillsCacheDir(configRoot) {
|
|
2674
|
+
return join8(getConfigRoot(configRoot), "skills");
|
|
2642
2675
|
}
|
|
2643
2676
|
var VERSION_FILE = ".version";
|
|
2644
|
-
async function readCachedVersion() {
|
|
2677
|
+
async function readCachedVersion(configRoot) {
|
|
2645
2678
|
try {
|
|
2646
|
-
return (await fs2.readFile(join8(skillsCacheDir(), VERSION_FILE), "utf8")).trim();
|
|
2679
|
+
return (await fs2.readFile(join8(skillsCacheDir(configRoot), VERSION_FILE), "utf8")).trim();
|
|
2647
2680
|
} catch {
|
|
2648
2681
|
return;
|
|
2649
2682
|
}
|
|
2650
2683
|
}
|
|
2651
|
-
async function syncSkillsCache(api) {
|
|
2652
|
-
const cache = skillsCacheDir();
|
|
2653
|
-
const cached = await readCachedVersion();
|
|
2684
|
+
async function syncSkillsCache(api, configRoot) {
|
|
2685
|
+
const cache = skillsCacheDir(configRoot);
|
|
2686
|
+
const cached = await readCachedVersion(configRoot);
|
|
2654
2687
|
const res = await api.get("/runtime/skills");
|
|
2655
2688
|
if (!res) {
|
|
2656
2689
|
if (cached)
|
|
@@ -2730,8 +2763,8 @@ function readMaxFromEnv() {
|
|
|
2730
2763
|
}
|
|
2731
2764
|
|
|
2732
2765
|
// src/start.ts
|
|
2733
|
-
async function runStart() {
|
|
2734
|
-
const cfg = loadConfig();
|
|
2766
|
+
async function runStart(options = {}) {
|
|
2767
|
+
const cfg = loadConfig(options.configRoot);
|
|
2735
2768
|
if (!cfg) {
|
|
2736
2769
|
throw new Error("No daemon config found. Run `beevibe-daemon setup --api <url> --user-token <bv_u_…>` first.");
|
|
2737
2770
|
}
|
|
@@ -2739,8 +2772,8 @@ async function runStart() {
|
|
|
2739
2772
|
apiUrl: cfg.api_url,
|
|
2740
2773
|
daemonToken: cfg.daemon_token
|
|
2741
2774
|
});
|
|
2742
|
-
const skillsSourceDir = await syncSkillsCache(api).catch((err) => {
|
|
2743
|
-
|
|
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));
|
|
2744
2777
|
return;
|
|
2745
2778
|
});
|
|
2746
2779
|
const runtimeRegistry = createDefaultRuntimeRegistry();
|
|
@@ -2748,7 +2781,7 @@ async function runStart() {
|
|
|
2748
2781
|
mcpServerUrl: `${cfg.api_url}/mcp`,
|
|
2749
2782
|
runtimeRegistry,
|
|
2750
2783
|
skillsSourceDir: skillsSourceDir ?? "/dev/null",
|
|
2751
|
-
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")
|
|
2752
2785
|
});
|
|
2753
2786
|
const supervisor = new Supervisor;
|
|
2754
2787
|
const claimer = new Claimer({
|
|
@@ -2759,20 +2792,20 @@ async function runStart() {
|
|
|
2759
2792
|
runtimeIds: cfg.runtimes.map((r) => r.id)
|
|
2760
2793
|
});
|
|
2761
2794
|
claimer.start();
|
|
2762
|
-
|
|
2795
|
+
log(`[daemon] started (${cfg.daemon_id} → ${cfg.api_url}, ${cfg.runtimes.length} runtime(s))`);
|
|
2763
2796
|
let stopped = false;
|
|
2764
2797
|
const stop = async (signal) => {
|
|
2765
2798
|
if (stopped)
|
|
2766
2799
|
return;
|
|
2767
2800
|
stopped = true;
|
|
2768
|
-
|
|
2801
|
+
log(`[daemon] received ${signal}; stopping`);
|
|
2769
2802
|
await claimer.stop();
|
|
2770
2803
|
process.exit(0);
|
|
2771
2804
|
};
|
|
2772
2805
|
process.on("SIGINT", () => void stop("SIGINT"));
|
|
2773
2806
|
process.on("SIGTERM", () => void stop("SIGTERM"));
|
|
2774
2807
|
process.on("unhandledRejection", (reason) => {
|
|
2775
|
-
|
|
2808
|
+
warn("[daemon] unhandledRejection (continuing):", reason instanceof Error ? reason.message : String(reason));
|
|
2776
2809
|
});
|
|
2777
2810
|
await new Promise(() => {
|
|
2778
2811
|
return;
|
|
@@ -2780,10 +2813,10 @@ async function runStart() {
|
|
|
2780
2813
|
}
|
|
2781
2814
|
|
|
2782
2815
|
// src/sync.ts
|
|
2783
|
-
async function runSync() {
|
|
2784
|
-
const config = loadConfig();
|
|
2816
|
+
async function runSync(options = {}) {
|
|
2817
|
+
const config = loadConfig(options.configRoot);
|
|
2785
2818
|
if (!config) {
|
|
2786
|
-
throw new Error(`No daemon config at ${
|
|
2819
|
+
throw new Error(`No daemon config at ${getConfigPath(options.configRoot)}. Run 'beevibe-daemon setup' first.`);
|
|
2787
2820
|
}
|
|
2788
2821
|
const detected = await detectClis();
|
|
2789
2822
|
if (detected.length === 0) {
|
|
@@ -2802,7 +2835,7 @@ async function runSync() {
|
|
|
2802
2835
|
const before = new Set(config.runtimes.map((r) => r.cli));
|
|
2803
2836
|
const added = body.runtimes.filter((r) => !before.has(r.cli));
|
|
2804
2837
|
const next = { ...config, runtimes: body.runtimes };
|
|
2805
|
-
saveConfig(next);
|
|
2838
|
+
saveConfig(next, options.configRoot);
|
|
2806
2839
|
return { added, runtimes: body.runtimes };
|
|
2807
2840
|
}
|
|
2808
2841
|
|
|
@@ -2810,7 +2843,7 @@ async function runSync() {
|
|
|
2810
2843
|
import { createHash } from "node:crypto";
|
|
2811
2844
|
import { createWriteStream, mkdtempSync, rmSync as rmSync2, chmodSync, renameSync } from "node:fs";
|
|
2812
2845
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
2813
|
-
import { join as
|
|
2846
|
+
import { join as join10 } from "node:path";
|
|
2814
2847
|
import { Readable } from "node:stream";
|
|
2815
2848
|
import { pipeline } from "node:stream/promises";
|
|
2816
2849
|
import { createInterface } from "node:readline/promises";
|
|
@@ -2824,7 +2857,7 @@ var PLATFORM_ASSETS = {
|
|
|
2824
2857
|
"linux-arm64": "beevibe-daemon-linux-arm64"
|
|
2825
2858
|
};
|
|
2826
2859
|
function currentVersion() {
|
|
2827
|
-
return "0.1.
|
|
2860
|
+
return "0.1.6";
|
|
2828
2861
|
}
|
|
2829
2862
|
function isCompiledBinary() {
|
|
2830
2863
|
if (!process.versions.bun)
|
|
@@ -2898,58 +2931,58 @@ async function promptYesNo(question) {
|
|
|
2898
2931
|
async function runUpdate(opts = {}) {
|
|
2899
2932
|
const current = currentVersion();
|
|
2900
2933
|
if (!current) {
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
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");
|
|
2904
2937
|
process.exit(2);
|
|
2905
2938
|
}
|
|
2906
2939
|
if (!isCompiledBinary()) {
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
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");
|
|
2911
2944
|
return;
|
|
2912
2945
|
}
|
|
2913
2946
|
const asset = platformAsset();
|
|
2914
2947
|
if (!asset) {
|
|
2915
|
-
|
|
2916
|
-
|
|
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.");
|
|
2917
2950
|
process.exit(2);
|
|
2918
2951
|
}
|
|
2919
|
-
|
|
2920
|
-
|
|
2952
|
+
log(`Current version: ${current}`);
|
|
2953
|
+
log("Checking for updates…");
|
|
2921
2954
|
const release = await fetchLatestRelease();
|
|
2922
2955
|
if (!release) {
|
|
2923
|
-
|
|
2956
|
+
log("No releases published yet — nothing to update to.");
|
|
2924
2957
|
return;
|
|
2925
2958
|
}
|
|
2926
2959
|
const latest = release.tag_name;
|
|
2927
|
-
|
|
2960
|
+
log(`Latest version: ${latest}`);
|
|
2928
2961
|
if (compareSemver(current, latest) >= 0) {
|
|
2929
|
-
|
|
2962
|
+
log("Already on the latest version.");
|
|
2930
2963
|
return;
|
|
2931
2964
|
}
|
|
2932
|
-
|
|
2965
|
+
log(`Update available: ${current} → ${latest}`);
|
|
2933
2966
|
if (!opts.skipPrompt) {
|
|
2934
2967
|
const proceed = await promptYesNo("Install this update now?");
|
|
2935
2968
|
if (!proceed) {
|
|
2936
|
-
|
|
2969
|
+
log("Update cancelled.");
|
|
2937
2970
|
return;
|
|
2938
2971
|
}
|
|
2939
2972
|
}
|
|
2940
|
-
const stagingDir = mkdtempSync(
|
|
2941
|
-
const stagingPath =
|
|
2973
|
+
const stagingDir = mkdtempSync(join10(tmpdir5(), "beevibe-daemon-update-"));
|
|
2974
|
+
const stagingPath = join10(stagingDir, asset);
|
|
2942
2975
|
try {
|
|
2943
|
-
|
|
2976
|
+
log(`Downloading ${asset}…`);
|
|
2944
2977
|
const downloadUrl = `${DOWNLOAD_BASE}/${latest}/${asset}`;
|
|
2945
2978
|
const [actualSha, expectedSha] = await Promise.all([
|
|
2946
2979
|
downloadAndHash(downloadUrl, stagingPath),
|
|
2947
2980
|
downloadChecksum(latest, asset)
|
|
2948
2981
|
]);
|
|
2949
2982
|
if (actualSha !== expectedSha) {
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2983
|
+
error(`Checksum mismatch — refusing to install.`);
|
|
2984
|
+
error(` expected: ${expectedSha}`);
|
|
2985
|
+
error(` actual: ${actualSha}`);
|
|
2953
2986
|
process.exit(3);
|
|
2954
2987
|
}
|
|
2955
2988
|
chmodSync(stagingPath, 493);
|
|
@@ -2957,11 +2990,11 @@ async function runUpdate(opts = {}) {
|
|
|
2957
2990
|
renameSync(stagingPath, process.execPath);
|
|
2958
2991
|
} catch (err) {
|
|
2959
2992
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2960
|
-
|
|
2961
|
-
|
|
2993
|
+
error(`Failed to replace ${process.execPath}: ${msg}`);
|
|
2994
|
+
error(`The new binary is at ${stagingPath} — install manually if needed.`);
|
|
2962
2995
|
return;
|
|
2963
2996
|
}
|
|
2964
|
-
|
|
2997
|
+
log(`Updated to ${latest}. Restart the daemon to pick up the new binary.`);
|
|
2965
2998
|
} finally {
|
|
2966
2999
|
rmSync2(stagingDir, { recursive: true, force: true });
|
|
2967
3000
|
}
|
|
@@ -2985,12 +3018,28 @@ function parseFlags(argv) {
|
|
|
2985
3018
|
} else if (arg === "--external-id" && next) {
|
|
2986
3019
|
flags.externalId = next;
|
|
2987
3020
|
i += 1;
|
|
3021
|
+
} else if (arg === "--config-root" && next) {
|
|
3022
|
+
flags.configRoot = next;
|
|
3023
|
+
i += 1;
|
|
2988
3024
|
}
|
|
2989
3025
|
}
|
|
2990
3026
|
return flags;
|
|
2991
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
|
+
}
|
|
2992
3041
|
function printHelp() {
|
|
2993
|
-
|
|
3042
|
+
log([
|
|
2994
3043
|
"Usage: beevibe-daemon <command> [flags]",
|
|
2995
3044
|
"",
|
|
2996
3045
|
"Commands:",
|
|
@@ -3006,7 +3055,13 @@ function printHelp() {
|
|
|
3006
3055
|
" --external-id <id> optional stable per-machine id (defaults to hostname)",
|
|
3007
3056
|
"",
|
|
3008
3057
|
"update flags:",
|
|
3009
|
-
" --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."
|
|
3010
3065
|
].join(`
|
|
3011
3066
|
`));
|
|
3012
3067
|
}
|
|
@@ -3016,10 +3071,11 @@ async function main() {
|
|
|
3016
3071
|
printHelp();
|
|
3017
3072
|
return;
|
|
3018
3073
|
}
|
|
3074
|
+
const flags = parseFlags(rest);
|
|
3075
|
+
const configRoot = resolveConfigRoot(flags.configRoot);
|
|
3019
3076
|
if (command === "setup") {
|
|
3020
|
-
const flags = parseFlags(rest);
|
|
3021
3077
|
if (!flags.api || !flags.userToken) {
|
|
3022
|
-
|
|
3078
|
+
error("setup requires --api and --user-token");
|
|
3023
3079
|
printHelp();
|
|
3024
3080
|
process.exit(2);
|
|
3025
3081
|
}
|
|
@@ -3027,24 +3083,25 @@ async function main() {
|
|
|
3027
3083
|
apiUrl: flags.api,
|
|
3028
3084
|
userToken: flags.userToken,
|
|
3029
3085
|
deviceName: flags.deviceName,
|
|
3030
|
-
externalId: flags.externalId
|
|
3086
|
+
externalId: flags.externalId,
|
|
3087
|
+
configRoot
|
|
3031
3088
|
});
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
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)}`);
|
|
3035
3092
|
return;
|
|
3036
3093
|
}
|
|
3037
3094
|
if (command === "start") {
|
|
3038
|
-
await runStart();
|
|
3095
|
+
await runStart({ configRoot });
|
|
3039
3096
|
return;
|
|
3040
3097
|
}
|
|
3041
3098
|
if (command === "sync") {
|
|
3042
|
-
const result = await runSync();
|
|
3099
|
+
const result = await runSync({ configRoot });
|
|
3043
3100
|
if (result.added.length === 0) {
|
|
3044
|
-
|
|
3101
|
+
log("No new CLIs detected.");
|
|
3045
3102
|
} else {
|
|
3046
|
-
|
|
3047
|
-
|
|
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).");
|
|
3048
3105
|
}
|
|
3049
3106
|
return;
|
|
3050
3107
|
}
|
|
@@ -3053,11 +3110,11 @@ async function main() {
|
|
|
3053
3110
|
await runUpdate({ skipPrompt });
|
|
3054
3111
|
return;
|
|
3055
3112
|
}
|
|
3056
|
-
|
|
3113
|
+
error(`Unknown command: ${command}`);
|
|
3057
3114
|
printHelp();
|
|
3058
3115
|
process.exit(2);
|
|
3059
3116
|
}
|
|
3060
3117
|
main().catch((err) => {
|
|
3061
|
-
|
|
3118
|
+
error(err instanceof Error ? err.stack ?? err.message : String(err));
|
|
3062
3119
|
process.exit(1);
|
|
3063
3120
|
});
|
package/package.json
CHANGED