@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.
Files changed (3) hide show
  1. package/README.md +23 -0
  2. package/dist/main.js +175 -118
  3. 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 = ["beevibe-pre-task-setup"];
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 log = (kind, text) => {
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
- log("log", "Creating sandbox container…");
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
- log("log", `Sandbox ${sandbox.id} created (image ${sandbox.image}).`);
1971
- log("log", "Installing git + curl in the sandbox base image…");
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
- log("log", "Base environment ready.");
2007
+ log2("log", "Base environment ready.");
1974
2008
  if (opts.input_url) {
1975
2009
  const filename = opts.input_filename ?? "input.bin";
1976
- log("log", `Fetching input into /sandbox/inputs/${filename}…`);
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
- log("log", "Input file ready.");
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
- log("log", `MCP config written: ${mcpConfigPath}`);
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
- log("log", "Spawning child claude session…");
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) => log(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
- log("error", `Child claude exited with code ${claudeResult.exit_code}: ${tail}`);
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
- log("log", "Child claude exited cleanly. Collecting artifacts…");
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
- log("error", err instanceof Error ? err.message : String(err));
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
- log("log", `Destroying sandbox ${sandbox.id}…`);
2077
+ log2("log", `Destroying sandbox ${sandbox.id}…`);
2044
2078
  await destroySandbox(sandbox);
2045
2079
  } catch (err) {
2046
- log("log", `Sandbox cleanup note: ${err instanceof Error ? err.message : String(err)}. Run with \`docker ps -a --filter name=bv-run-\` to inspect.`);
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
- console.log(`[daemon/repo-run] sess=${payload.session_id} repo_run=${rr.repo_run_id} repo=${rr.repo_url}`);
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
- console.warn("[daemon/repo-run] /runtime/events POST failed:", err instanceof Error ? err.message : String(err));
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
- console.log(`[daemon/repo-run] sess=${payload.session_id} succeeded artifacts=${artifacts.length}`);
2408
+ log(`[daemon/repo-run] sess=${payload.session_id} succeeded artifacts=${artifacts.length}`);
2375
2409
  } else {
2376
- console.error(`[daemon/repo-run] sess=${payload.session_id} status=${status}` + (result.error ? `
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
- console.error("[daemon/repo-run] /runtime/done POST failed:", err instanceof Error ? err.message : String(err));
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
- console.log(`[daemon/spawn] sess=${payload.session_id} agent=${payload.agent_id} runtime=${payload.runtime_type} type=${payload.type} cwd=${ws.path}`);
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
- console.warn("[daemon/spawner] /runtime/events POST failed; events dropped:", err instanceof Error ? err.message : String(err));
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
- console.log(`[daemon/spawn] sess=${payload.session_id} exit=0`);
2523
+ log(`[daemon/spawn] sess=${payload.session_id} exit=0`);
2490
2524
  } else {
2491
- console.error(`[daemon/spawn] sess=${payload.session_id} status=${status} exit=${done.exit_code}` + (errorDetail ? `
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
- console.error("[daemon/spawner] /runtime/done POST failed:", err instanceof Error ? err.message : String(err));
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
- console.log(`[daemon] connected: ${this.cfg.runtimeIds.length} runtime(s) subscribed`);
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
- console.warn("[daemon] ws error:", err.message);
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
- console.warn(`[daemon] ws disconnected; reconnecting in ${delay}ms`);
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
- console.warn("[daemon] heartbeat failed:", err instanceof Error ? err.message : String(err));
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
- console.warn(`[daemon] claim failed for runtime=${runtimeId}:`, err instanceof Error ? err.message : String(err));
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
- console.log(`[daemon/claim] sess=${payload.session_id} agent=${payload.agent_id} runtime=${runtimeId}`);
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) => console.error(`[daemon] dispatch ${payload.session_id} failed:`, err instanceof Error ? err.message : String(err))).finally(() => this.cfg.supervisor.finish(payload.session_id));
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(homedir3(), ".beevibe", "skills");
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
- console.warn("[daemon] skills sync failed; continuing without skills:", err instanceof Error ? err.message : String(err));
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
- console.log(`[daemon] started (${cfg.daemon_id} → ${cfg.api_url}, ${cfg.runtimes.length} runtime(s))`);
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
- console.log(`[daemon] received ${signal}; stopping`);
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
- console.warn("[daemon] unhandledRejection (continuing):", reason instanceof Error ? reason.message : String(reason));
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 ${CONFIG_PATH}. Run 'beevibe-daemon setup' first.`);
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 join9 } from "node:path";
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.4";
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
- console.log("Could not determine current daemon version.");
2902
- console.log("If you installed via npm: npm update -g @beevibe/daemon");
2903
- console.log("If you installed from source: git pull && pnpm install && pnpm build");
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
- console.log(`Current version: ${current}`);
2908
- console.log("This binary was not produced by the standalone build path.");
2909
- console.log("If you installed via npm: npm update -g @beevibe/daemon");
2910
- console.log("If you installed from source: git pull && pnpm install && pnpm build");
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
- console.error(`Unsupported platform: ${process.platform}/${process.arch}`);
2916
- console.error("Pre-built binaries are only published for darwin-arm64, darwin-x64, linux-x64, linux-arm64.");
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
- console.log(`Current version: ${current}`);
2920
- console.log("Checking for updates…");
2952
+ log(`Current version: ${current}`);
2953
+ log("Checking for updates…");
2921
2954
  const release = await fetchLatestRelease();
2922
2955
  if (!release) {
2923
- console.log("No releases published yet — nothing to update to.");
2956
+ log("No releases published yet — nothing to update to.");
2924
2957
  return;
2925
2958
  }
2926
2959
  const latest = release.tag_name;
2927
- console.log(`Latest version: ${latest}`);
2960
+ log(`Latest version: ${latest}`);
2928
2961
  if (compareSemver(current, latest) >= 0) {
2929
- console.log("Already on the latest version.");
2962
+ log("Already on the latest version.");
2930
2963
  return;
2931
2964
  }
2932
- console.log(`Update available: ${current} → ${latest}`);
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
- console.log("Update cancelled.");
2969
+ log("Update cancelled.");
2937
2970
  return;
2938
2971
  }
2939
2972
  }
2940
- const stagingDir = mkdtempSync(join9(tmpdir5(), "beevibe-daemon-update-"));
2941
- const stagingPath = join9(stagingDir, asset);
2973
+ const stagingDir = mkdtempSync(join10(tmpdir5(), "beevibe-daemon-update-"));
2974
+ const stagingPath = join10(stagingDir, asset);
2942
2975
  try {
2943
- console.log(`Downloading ${asset}…`);
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
- console.error(`Checksum mismatch — refusing to install.`);
2951
- console.error(` expected: ${expectedSha}`);
2952
- console.error(` actual: ${actualSha}`);
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
- console.error(`Failed to replace ${process.execPath}: ${msg}`);
2961
- console.error(`The new binary is at ${stagingPath} — install manually if needed.`);
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
- console.log(`Updated to ${latest}. Restart the daemon to pick up the new binary.`);
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
- console.log([
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
- console.error("setup requires --api and --user-token");
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
- console.log(`Registered as ${cfg.daemon_id}`);
3033
- console.log(`Runtimes: ${cfg.runtimes.map((r) => `${r.cli} (${r.id})`).join(", ")}`);
3034
- console.log("Config saved to ~/.beevibe/config.json");
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
- console.log("No new CLIs detected.");
3101
+ log("No new CLIs detected.");
3045
3102
  } else {
3046
- console.log(`Added ${result.added.length} runtime(s): ${result.added.map((r) => `${r.cli} (${r.id})`).join(", ")}.`);
3047
- console.log("Restart the daemon to pick up the new runtime(s).");
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
- console.error(`Unknown command: ${command}`);
3113
+ error(`Unknown command: ${command}`);
3057
3114
  printHelp();
3058
3115
  process.exit(2);
3059
3116
  }
3060
3117
  main().catch((err) => {
3061
- console.error(err instanceof Error ? err.stack ?? err.message : String(err));
3118
+ error(err instanceof Error ? err.stack ?? err.message : String(err));
3062
3119
  process.exit(1);
3063
3120
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beevibe/daemon",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Beevibe daemon — runs on each user's machine, claims pending sessions and spawns the CLI locally",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Zhe Pang",