@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.
Files changed (3) hide show
  1. package/README.md +23 -0
  2. package/dist/main.js +171 -117
  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";
@@ -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 log = (kind, text) => {
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
- log("log", "Creating sandbox container…");
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
- log("log", `Sandbox ${sandbox.id} created (image ${sandbox.image}).`);
1974
- 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…");
1975
2006
  await prepareBaseEnvironment(sandbox);
1976
- log("log", "Base environment ready.");
2007
+ log2("log", "Base environment ready.");
1977
2008
  if (opts.input_url) {
1978
2009
  const filename = opts.input_filename ?? "input.bin";
1979
- log("log", `Fetching input into /sandbox/inputs/${filename}…`);
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
- log("log", "Input file ready.");
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
- log("log", `MCP config written: ${mcpConfigPath}`);
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
- log("log", "Spawning child claude session…");
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) => log(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
- log("error", `Child claude exited with code ${claudeResult.exit_code}: ${tail}`);
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
- log("log", "Child claude exited cleanly. Collecting artifacts…");
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
- log("error", err instanceof Error ? err.message : String(err));
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
- log("log", `Destroying sandbox ${sandbox.id}…`);
2077
+ log2("log", `Destroying sandbox ${sandbox.id}…`);
2047
2078
  await destroySandbox(sandbox);
2048
2079
  } catch (err) {
2049
- 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.`);
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
- 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}`);
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
- 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));
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
- 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}`);
2378
2409
  } else {
2379
- 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 ? `
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
- 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));
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
- 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}`);
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
- 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));
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
- console.log(`[daemon/spawn] sess=${payload.session_id} exit=0`);
2523
+ log(`[daemon/spawn] sess=${payload.session_id} exit=0`);
2493
2524
  } else {
2494
- 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 ? `
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
- 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));
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
- console.log(`[daemon] connected: ${this.cfg.runtimeIds.length} runtime(s) subscribed`);
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
- console.warn("[daemon] ws error:", err.message);
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
- console.warn(`[daemon] ws disconnected; reconnecting in ${delay}ms`);
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
- console.warn("[daemon] heartbeat failed:", err instanceof Error ? err.message : String(err));
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
- 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));
2624
2655
  return;
2625
2656
  }
2626
2657
  if (!payload)
2627
2658
  return;
2628
- 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}`);
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) => 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));
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(homedir3(), ".beevibe", "skills");
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
- 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));
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
- 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))`);
2766
2796
  let stopped = false;
2767
2797
  const stop = async (signal) => {
2768
2798
  if (stopped)
2769
2799
  return;
2770
2800
  stopped = true;
2771
- console.log(`[daemon] received ${signal}; stopping`);
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
- console.warn("[daemon] unhandledRejection (continuing):", reason instanceof Error ? reason.message : String(reason));
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 ${CONFIG_PATH}. Run 'beevibe-daemon setup' first.`);
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 join9 } from "node:path";
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.5";
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
- console.log("Could not determine current daemon version.");
2905
- console.log("If you installed via npm: npm update -g @beevibe/daemon");
2906
- 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");
2907
2937
  process.exit(2);
2908
2938
  }
2909
2939
  if (!isCompiledBinary()) {
2910
- console.log(`Current version: ${current}`);
2911
- console.log("This binary was not produced by the standalone build path.");
2912
- console.log("If you installed via npm: npm update -g @beevibe/daemon");
2913
- 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");
2914
2944
  return;
2915
2945
  }
2916
2946
  const asset = platformAsset();
2917
2947
  if (!asset) {
2918
- console.error(`Unsupported platform: ${process.platform}/${process.arch}`);
2919
- 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.");
2920
2950
  process.exit(2);
2921
2951
  }
2922
- console.log(`Current version: ${current}`);
2923
- console.log("Checking for updates…");
2952
+ log(`Current version: ${current}`);
2953
+ log("Checking for updates…");
2924
2954
  const release = await fetchLatestRelease();
2925
2955
  if (!release) {
2926
- console.log("No releases published yet — nothing to update to.");
2956
+ log("No releases published yet — nothing to update to.");
2927
2957
  return;
2928
2958
  }
2929
2959
  const latest = release.tag_name;
2930
- console.log(`Latest version: ${latest}`);
2960
+ log(`Latest version: ${latest}`);
2931
2961
  if (compareSemver(current, latest) >= 0) {
2932
- console.log("Already on the latest version.");
2962
+ log("Already on the latest version.");
2933
2963
  return;
2934
2964
  }
2935
- console.log(`Update available: ${current} → ${latest}`);
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
- console.log("Update cancelled.");
2969
+ log("Update cancelled.");
2940
2970
  return;
2941
2971
  }
2942
2972
  }
2943
- const stagingDir = mkdtempSync(join9(tmpdir5(), "beevibe-daemon-update-"));
2944
- const stagingPath = join9(stagingDir, asset);
2973
+ const stagingDir = mkdtempSync(join10(tmpdir5(), "beevibe-daemon-update-"));
2974
+ const stagingPath = join10(stagingDir, asset);
2945
2975
  try {
2946
- console.log(`Downloading ${asset}…`);
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
- console.error(`Checksum mismatch — refusing to install.`);
2954
- console.error(` expected: ${expectedSha}`);
2955
- console.error(` actual: ${actualSha}`);
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
- console.error(`Failed to replace ${process.execPath}: ${msg}`);
2964
- 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.`);
2965
2995
  return;
2966
2996
  }
2967
- 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.`);
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
- console.log([
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
- console.error("setup requires --api and --user-token");
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
- console.log(`Registered as ${cfg.daemon_id}`);
3036
- console.log(`Runtimes: ${cfg.runtimes.map((r) => `${r.cli} (${r.id})`).join(", ")}`);
3037
- 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)}`);
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
- console.log("No new CLIs detected.");
3101
+ log("No new CLIs detected.");
3048
3102
  } else {
3049
- console.log(`Added ${result.added.length} runtime(s): ${result.added.map((r) => `${r.cli} (${r.id})`).join(", ")}.`);
3050
- 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).");
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
- console.error(`Unknown command: ${command}`);
3113
+ error(`Unknown command: ${command}`);
3060
3114
  printHelp();
3061
3115
  process.exit(2);
3062
3116
  }
3063
3117
  main().catch((err) => {
3064
- console.error(err instanceof Error ? err.stack ?? err.message : String(err));
3118
+ error(err instanceof Error ? err.stack ?? err.message : String(err));
3065
3119
  process.exit(1);
3066
3120
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beevibe/daemon",
3
- "version": "0.1.5",
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",