@beevibe/daemon 0.1.2 → 0.1.4

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 (2) hide show
  1. package/dist/main.js +1578 -119
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  // src/setup.ts
4
4
  import { hostname, userInfo } from "node:os";
5
- import { spawnSync } from "node:child_process";
6
5
 
7
6
  // ../../node_modules/.pnpm/nanoid@5.1.9/node_modules/nanoid/index.js
8
7
  import { webcrypto as crypto } from "node:crypto";
@@ -224,6 +223,29 @@ function saveConfig(cfg) {
224
223
  `, { mode: 384 });
225
224
  }
226
225
 
226
+ // src/detect-clis.ts
227
+ import { execFile } from "node:child_process";
228
+ import { promisify as promisify2 } from "node:util";
229
+ var execFileAsync = promisify2(execFile);
230
+ async function probeOne(cli) {
231
+ try {
232
+ await execFileAsync("which", [cli]);
233
+ } catch {
234
+ return null;
235
+ }
236
+ let cli_version;
237
+ try {
238
+ const { stdout } = await execFileAsync(cli, ["--version"]);
239
+ cli_version = stdout.trim().split(`
240
+ `)[0];
241
+ } catch {}
242
+ return { cli, cli_version };
243
+ }
244
+ async function detectClis() {
245
+ const results = await Promise.all(KNOWN_CLIS.map((cli) => probeOne(cli)));
246
+ return results.filter((r) => r !== null);
247
+ }
248
+
227
249
  // src/setup.ts
228
250
  async function runSetup(options) {
229
251
  if (!/^https?:\/\//.test(options.apiUrl)) {
@@ -234,7 +256,7 @@ async function runSetup(options) {
234
256
  }
235
257
  const externalId = options.externalId ?? hostname();
236
258
  const deviceName = options.deviceName ?? `${userInfo().username}@${hostname()}`;
237
- const runtimes = options.detectedClis ?? detectClis();
259
+ const runtimes = options.detectedClis ?? await detectClis();
238
260
  if (runtimes.length === 0) {
239
261
  throw new Error(`No supported CLIs detected on PATH. beevibe currently looks for: ${KNOWN_CLIS.join(", ")}`);
240
262
  }
@@ -264,21 +286,6 @@ async function runSetup(options) {
264
286
  saveConfig(config);
265
287
  return config;
266
288
  }
267
- function detectClis() {
268
- const out = [];
269
- for (const cli of KNOWN_CLIS) {
270
- const which = spawnSync("which", [cli], { encoding: "utf8" });
271
- if (which.status !== 0 || !which.stdout.trim())
272
- continue;
273
- const version = spawnSync(cli, ["--version"], { encoding: "utf8" });
274
- out.push({
275
- cli,
276
- cli_version: version.status === 0 ? version.stdout.trim().split(`
277
- `)[0] : undefined
278
- });
279
- }
280
- return out;
281
- }
282
289
 
283
290
  // ../core/dist/adapters/local-workspace/manager.js
284
291
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
@@ -436,6 +443,16 @@ class LocalWorkspaceManager {
436
443
  if (!runtime3) {
437
444
  throw new Error(`No runtime registered for agent ${agent2.id} (runtime_config.type='${agent2.runtime_config.type}')`);
438
445
  }
446
+ if (runtime3.prepareWorkspace) {
447
+ if (!agent2.api_key) {
448
+ throw new Error(`Cannot prepare workspace for agent ${agent2.id}: agent.api_key is missing`);
449
+ }
450
+ await runtime3.prepareWorkspace({
451
+ workspace: workspace2,
452
+ agentApiKey: agent2.api_key,
453
+ mcpServerUrl: this.config.mcpServerUrl
454
+ });
455
+ }
439
456
  await syncSkills({
440
457
  sourceDir: this.config.skillsSourceDir,
441
458
  targetDir: runtime3.skillsDir(workspace2),
@@ -648,6 +665,15 @@ function extractStepEvents(msg) {
648
665
  }
649
666
  ];
650
667
  }
668
+ if (msg.type === STREAM_TYPE.ToolResult) {
669
+ return [
670
+ {
671
+ kind: "tool_result",
672
+ description: describeToolResult(msg.content, msg.is_error === true),
673
+ timestamp: now
674
+ }
675
+ ];
676
+ }
651
677
  if (msg.type === STREAM_TYPE.ContentBlockStart && msg.content_block?.type === BLOCK_TYPE.ToolUse) {
652
678
  const block = msg.content_block;
653
679
  return [
@@ -681,6 +707,11 @@ function extractStepEvents(msg) {
681
707
  }
682
708
  return [];
683
709
  }
710
+ function describeToolResult(content, isError) {
711
+ const text = typeof content === "string" ? content : JSON.stringify(content ?? "");
712
+ const collapsed = text.replace(/\s+/g, " ").trim();
713
+ return isError ? `[error] ${collapsed}` : collapsed;
714
+ }
684
715
  var PREFERRED_INPUT_FIELDS = [
685
716
  "file_path",
686
717
  "command",
@@ -937,110 +968,1486 @@ class ClaudeCodeRuntime {
937
968
  }
938
969
  }
939
970
 
940
- // ../core/dist/adapters/runtime-registry.js
941
- function createDefaultRuntimeRegistry() {
971
+ // ../core/dist/adapters/codex/runtime.js
972
+ import { existsSync as existsSync3, readFileSync as readFileSync3, unlinkSync } from "node:fs";
973
+ import { tmpdir as tmpdir2 } from "node:os";
974
+ import { join as join4 } from "node:path";
975
+
976
+ // ../core/dist/adapters/codex/stream-json.js
977
+ var CODEX_EVENT_TYPE = {
978
+ ThreadStarted: "thread.started",
979
+ TurnStarted: "turn.started",
980
+ TurnCompleted: "turn.completed",
981
+ TurnFailed: "turn.failed",
982
+ ItemStarted: "item.started",
983
+ ItemUpdated: "item.updated",
984
+ ItemCompleted: "item.completed",
985
+ Error: "error"
986
+ };
987
+ var CODEX_ITEM_TYPE = {
988
+ AgentMessage: "agent_message",
989
+ Reasoning: "reasoning",
990
+ CommandExecution: "command_execution",
991
+ FileChange: "file_change",
992
+ McpToolCall: "mcp_tool_call",
993
+ CollabToolCall: "collab_tool_call",
994
+ WebSearch: "web_search",
995
+ TodoList: "todo_list",
996
+ Error: "error"
997
+ };
998
+ function parseCodexEventLine(line) {
999
+ const trimmed = line.trim();
1000
+ if (!trimmed || !trimmed.startsWith("{"))
1001
+ return null;
1002
+ try {
1003
+ return JSON.parse(trimmed);
1004
+ } catch {
1005
+ return null;
1006
+ }
1007
+ }
1008
+ function extractCodexStepEvents(evt) {
1009
+ const now = new Date().toISOString();
1010
+ if (evt.type !== CODEX_EVENT_TYPE.ItemStarted && evt.type !== CODEX_EVENT_TYPE.ItemCompleted) {
1011
+ return [];
1012
+ }
1013
+ const item = evt.item;
1014
+ if (!item || !item.type)
1015
+ return [];
1016
+ const isCompletion = evt.type === CODEX_EVENT_TYPE.ItemCompleted;
1017
+ if (item.type === CODEX_ITEM_TYPE.AgentMessage) {
1018
+ if (!isCompletion)
1019
+ return [];
1020
+ const text = item.text?.trim();
1021
+ if (!text)
1022
+ return [];
1023
+ return [{ kind: "agent", description: text, timestamp: now }];
1024
+ }
1025
+ if (item.type === CODEX_ITEM_TYPE.McpToolCall) {
1026
+ return [
1027
+ {
1028
+ kind: isCompletion ? "tool_result" : "tool_call",
1029
+ tool: item.tool ?? "unknown",
1030
+ description: describeCodexInput(item.arguments),
1031
+ timestamp: now
1032
+ }
1033
+ ];
1034
+ }
1035
+ if (item.type === CODEX_ITEM_TYPE.CommandExecution) {
1036
+ return [
1037
+ {
1038
+ kind: isCompletion ? "tool_result" : "tool_call",
1039
+ tool: "shell",
1040
+ description: (item.command ?? "").slice(0, 200),
1041
+ timestamp: now
1042
+ }
1043
+ ];
1044
+ }
1045
+ if (item.type === CODEX_ITEM_TYPE.FileChange) {
1046
+ const summary = (item.changes ?? []).map((c) => `${c.kind ?? "?"} ${c.path ?? ""}`).join(", ").slice(0, 200);
1047
+ return [
1048
+ {
1049
+ kind: isCompletion ? "tool_result" : "tool_call",
1050
+ tool: "file_change",
1051
+ description: summary,
1052
+ timestamp: now
1053
+ }
1054
+ ];
1055
+ }
1056
+ if (item.type === CODEX_ITEM_TYPE.WebSearch) {
1057
+ return [
1058
+ {
1059
+ kind: isCompletion ? "tool_result" : "tool_call",
1060
+ tool: "web_search",
1061
+ description: (item.query ?? "").slice(0, 200),
1062
+ timestamp: now
1063
+ }
1064
+ ];
1065
+ }
1066
+ return [];
1067
+ }
1068
+ var PREFERRED_CODEX_FIELDS = [
1069
+ "file_path",
1070
+ "path",
1071
+ "command",
1072
+ "cmd",
1073
+ "query",
1074
+ "pattern",
1075
+ "url",
1076
+ "intent"
1077
+ ];
1078
+ function describeCodexInput(input) {
1079
+ if (typeof input === "string")
1080
+ return input.slice(0, 200);
1081
+ if (!input || typeof input !== "object" || Array.isArray(input))
1082
+ return "";
1083
+ const obj = input;
1084
+ for (const key of PREFERRED_CODEX_FIELDS) {
1085
+ const v = obj[key];
1086
+ if (typeof v === "string" && v.length > 0)
1087
+ return v.slice(0, 200);
1088
+ }
1089
+ return JSON.stringify(input).slice(0, 200);
1090
+ }
1091
+ function parseCodexEvents(events, exitCode, lastMessage) {
1092
+ let threadId;
1093
+ let usage;
1094
+ let assistantText = "";
1095
+ let turnFailed;
1096
+ let topLevelError;
1097
+ const transcriptParts = [];
1098
+ for (const evt of events) {
1099
+ switch (evt.type) {
1100
+ case CODEX_EVENT_TYPE.ThreadStarted:
1101
+ if (evt.thread_id)
1102
+ threadId = evt.thread_id;
1103
+ break;
1104
+ case CODEX_EVENT_TYPE.TurnCompleted:
1105
+ if (evt.usage)
1106
+ usage = evt.usage;
1107
+ break;
1108
+ case CODEX_EVENT_TYPE.TurnFailed:
1109
+ turnFailed = evt.error?.message ?? turnFailed;
1110
+ break;
1111
+ case CODEX_EVENT_TYPE.Error:
1112
+ topLevelError = evt.message ?? topLevelError;
1113
+ if (evt.message)
1114
+ transcriptParts.push(`[error] ${evt.message}
1115
+ `);
1116
+ break;
1117
+ case CODEX_EVENT_TYPE.ItemCompleted: {
1118
+ const item = evt.item;
1119
+ if (!item || !item.type)
1120
+ break;
1121
+ if (item.type === CODEX_ITEM_TYPE.AgentMessage && item.text) {
1122
+ assistantText = item.text;
1123
+ transcriptParts.push(`[assistant] ${item.text}
1124
+ `);
1125
+ } else if (item.type === CODEX_ITEM_TYPE.McpToolCall) {
1126
+ const tool = item.tool ?? "unknown";
1127
+ transcriptParts.push(`[tool_call] ${tool}
1128
+ `);
1129
+ const resultSummary = summarizeMcpResult(item.result);
1130
+ if (resultSummary || item.error?.message) {
1131
+ transcriptParts.push(`[tool_result from ${tool}] ${item.error?.message ?? resultSummary}
1132
+ `);
1133
+ }
1134
+ } else if (item.type === CODEX_ITEM_TYPE.CommandExecution) {
1135
+ transcriptParts.push(`[tool_call] shell ${(item.command ?? "").slice(0, 200)}
1136
+ `);
1137
+ if (item.aggregated_output) {
1138
+ transcriptParts.push(`[tool_result from shell] ${item.aggregated_output.slice(0, 200).replace(/\n/g, " ")}
1139
+ `);
1140
+ }
1141
+ }
1142
+ break;
1143
+ }
1144
+ default:
1145
+ break;
1146
+ }
1147
+ }
1148
+ const failed = exitCode !== 0 || !!turnFailed || !!topLevelError;
1149
+ const trimmedLast = lastMessage.trim();
1150
+ const failureMessage = turnFailed ?? topLevelError;
1151
+ const output = failed ? failureMessage || assistantText || bareCliExitMessage(exitCode) : trimmedLast || assistantText || "Session completed.";
942
1152
  return {
943
- claude: new ClaudeCodeRuntime({})
1153
+ status: failed ? "failed" : "completed",
1154
+ output,
1155
+ transcript: transcriptParts.join("") || undefined,
1156
+ cli_session_id: threadId,
1157
+ usage: usage ? {
1158
+ input_tokens: usage.input_tokens ?? 0,
1159
+ output_tokens: (usage.output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0),
1160
+ cache_creation_input_tokens: 0,
1161
+ cache_read_input_tokens: usage.cached_input_tokens ?? 0
1162
+ } : undefined
944
1163
  };
945
1164
  }
1165
+ function summarizeMcpResult(result) {
1166
+ if (!result || !Array.isArray(result.content))
1167
+ return "";
1168
+ for (const block of result.content) {
1169
+ if (block && typeof block === "object" && block.type === "text") {
1170
+ const text = block.text;
1171
+ if (typeof text === "string")
1172
+ return text.slice(0, 200).replace(/\n/g, " ");
1173
+ }
1174
+ }
1175
+ return JSON.stringify(result.content).slice(0, 200);
1176
+ }
946
1177
 
947
- // src/api-client.ts
948
- import WebSocket from "ws";
1178
+ // ../core/dist/adapters/codex/runtime.js
1179
+ var OPENAI_AUTH_VARS = ["OPENAI_API_KEY", "OPENAI_AUTH_TOKEN"];
949
1180
 
950
- class ApiClient {
951
- cfg;
952
- constructor(cfg) {
953
- this.cfg = cfg;
954
- }
955
- async get(path2) {
956
- const res = await fetch(this.url(path2), {
957
- headers: { authorization: `Bearer ${this.cfg.daemonToken}` }
958
- });
959
- if (res.status === 204 || res.status >= 400)
960
- return;
961
- return await res.json();
1181
+ class CodexRuntime {
1182
+ config;
1183
+ type = "codex";
1184
+ prepared = new Map;
1185
+ constructor(config = {}) {
1186
+ this.config = config;
962
1187
  }
963
- async post(path2, body) {
964
- const res = await fetch(this.url(path2), {
965
- method: "POST",
966
- headers: {
967
- "content-type": "application/json",
968
- authorization: `Bearer ${this.cfg.daemonToken}`
1188
+ async execute(context) {
1189
+ const prepared = this.prepared.get(context.workspace.path);
1190
+ const sid = context.env?.BEEVIBE_SESSION_ID;
1191
+ const lastMessagePath = join4(context.workspace.path, `.beevibe-codex-last-message-${Date.now()}.txt`);
1192
+ const globalArgs = buildGlobalArgs(context, this.config);
1193
+ const execArgs = [
1194
+ "--json",
1195
+ "--skip-git-repo-check",
1196
+ "--output-last-message",
1197
+ lastMessagePath
1198
+ ];
1199
+ 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")}`);
1201
+ }
1202
+ const args = context.resume_session_id ? [
1203
+ ...globalArgs,
1204
+ "exec",
1205
+ "resume",
1206
+ ...execArgs,
1207
+ context.resume_session_id,
1208
+ composePrompt(context)
1209
+ ] : [...globalArgs, "exec", ...execArgs, composePrompt(context)];
1210
+ const env2 = { ...process.env };
1211
+ for (const key of OPENAI_AUTH_VARS)
1212
+ delete env2[key];
1213
+ if (context.env)
1214
+ Object.assign(env2, context.env);
1215
+ if (prepared)
1216
+ env2.BEEVIBE_AGENT_API_KEY = prepared.agentApiKey;
1217
+ const events = [];
1218
+ let pending = "";
1219
+ const handleLine = (line) => {
1220
+ const evt = parseCodexEventLine(line);
1221
+ if (!evt)
1222
+ return;
1223
+ events.push(evt);
1224
+ if (!context.onStep)
1225
+ return;
1226
+ for (const step of extractCodexStepEvents(evt)) {
1227
+ context.onStep(step);
1228
+ }
1229
+ };
1230
+ const result = await runCliProcess({
1231
+ command: this.config.command ?? "codex",
1232
+ args,
1233
+ cwd: context.workspace.path,
1234
+ env: env2,
1235
+ abortSignal: context.abort_signal,
1236
+ onSpawn: ({ pid, process_group_id }) => {
1237
+ context.onSpawn?.({ process_pid: pid, process_group_id });
969
1238
  },
970
- body: JSON.stringify(body)
1239
+ onLog: (stream, chunk) => {
1240
+ if (stream !== "stdout")
1241
+ return;
1242
+ pending += chunk;
1243
+ let nl;
1244
+ while ((nl = pending.indexOf(`
1245
+ `)) !== -1) {
1246
+ handleLine(pending.slice(0, nl));
1247
+ pending = pending.slice(nl + 1);
1248
+ }
1249
+ }
971
1250
  });
972
- if (res.status === 204)
973
- return { status: 204, body: undefined };
974
- const text = await res.text();
975
- if (!text)
976
- return { status: res.status, body: undefined };
1251
+ if (pending)
1252
+ handleLine(pending);
1253
+ if (result.truncated) {
1254
+ console.warn("[CodexRuntime] stdout truncated at 4MB — result parsing may be incomplete");
1255
+ }
1256
+ if (result.aborted) {
1257
+ removeIfExists(lastMessagePath);
1258
+ return {
1259
+ status: "cancelled",
1260
+ output: "Session cancelled.",
1261
+ process_pid: result.pid ?? undefined,
1262
+ process_group_id: result.process_group_id ?? undefined
1263
+ };
1264
+ }
1265
+ const lastMessage = readIfExists(lastMessagePath);
1266
+ removeIfExists(lastMessagePath);
1267
+ const parsed = parseCodexEvents(events, result.exitCode, lastMessage);
1268
+ const STDERR_TAIL_BYTES = 4096;
1269
+ const stderrTail = parsed.status === "failed" && result.stderr ? result.stderr.slice(-STDERR_TAIL_BYTES) : undefined;
1270
+ return {
1271
+ ...parsed,
1272
+ process_pid: result.pid ?? undefined,
1273
+ process_group_id: result.process_group_id ?? undefined,
1274
+ exit_code: result.exitCode,
1275
+ ...stderrTail ? { stderr: stderrTail } : {}
1276
+ };
1277
+ }
1278
+ async healthCheck() {
977
1279
  try {
978
- return { status: res.status, body: JSON.parse(text) };
1280
+ const result = await runCliProcess({
1281
+ command: this.config.command ?? "codex",
1282
+ args: ["--version"],
1283
+ cwd: tmpdir2(),
1284
+ timeoutMs: 5000,
1285
+ graceMs: 0
1286
+ });
1287
+ return {
1288
+ healthy: result.exitCode === 0,
1289
+ error: result.exitCode === 0 ? undefined : result.stderr.slice(-500)
1290
+ };
979
1291
  } catch {
980
- return { status: res.status, body: undefined };
1292
+ return {
1293
+ healthy: false,
1294
+ error: `Command not found: ${this.config.command ?? "codex"}`
1295
+ };
981
1296
  }
982
1297
  }
983
- async claim(runtimeId) {
984
- const res = await fetch(`${this.url("/runtime/claim")}?runtime_id=${encodeURIComponent(runtimeId)}`, {
985
- method: "POST",
986
- headers: { authorization: `Bearer ${this.cfg.daemonToken}` }
987
- });
988
- if (res.status === 204)
989
- return;
990
- if (res.status >= 400)
991
- return;
992
- return await res.json();
1298
+ async shutdown() {}
1299
+ skillsDir(workspace2) {
1300
+ return join4(workspace2.path, ".codex", "skills");
993
1301
  }
994
- openWebSocket(runtimeIds) {
995
- const wsUrl = this.cfg.apiUrl.replace(/^http/, "ws");
996
- const url = `${wsUrl}/runtime/ws?runtime_ids=${runtimeIds.map(encodeURIComponent).join(",")}`;
997
- return new WebSocket(url, {
998
- headers: { authorization: `Bearer ${this.cfg.daemonToken}` }
1302
+ prepareWorkspace(context) {
1303
+ this.prepared.set(context.workspace.path, {
1304
+ agentApiKey: context.agentApiKey,
1305
+ mcpServerUrl: context.mcpServerUrl
999
1306
  });
1000
1307
  }
1001
- url(path2) {
1002
- return `${this.cfg.apiUrl}${path2}`;
1308
+ }
1309
+ function buildGlobalArgs(context, config) {
1310
+ const args = [
1311
+ "--sandbox",
1312
+ "workspace-write",
1313
+ "--ask-for-approval",
1314
+ "never",
1315
+ "--cd",
1316
+ context.workspace.path
1317
+ ];
1318
+ const model = context.model ?? config.model;
1319
+ if (model)
1320
+ args.push("--model", model);
1321
+ return args;
1322
+ }
1323
+ function composePrompt(context) {
1324
+ if (context.system_prompt_append.length === 0)
1325
+ return context.intent;
1326
+ return [
1327
+ "<beevibe_system_context>",
1328
+ context.system_prompt_append,
1329
+ "</beevibe_system_context>",
1330
+ "",
1331
+ context.intent
1332
+ ].join(`
1333
+ `);
1334
+ }
1335
+ function withBeevibeSession(mcpServerUrl, sid) {
1336
+ const url = new URL(mcpServerUrl);
1337
+ url.searchParams.set("beevibe_session", sid);
1338
+ return url.toString();
1339
+ }
1340
+ function tomlString(value) {
1341
+ return JSON.stringify(value);
1342
+ }
1343
+ function readIfExists(path2) {
1344
+ try {
1345
+ return existsSync3(path2) ? readFileSync3(path2, "utf8") : "";
1346
+ } catch {
1347
+ return "";
1003
1348
  }
1004
1349
  }
1005
- // src/spawner.ts
1006
- async function runDispatch(deps, payload, abortSignal) {
1007
- const syntheticAgent = {
1008
- id: payload.agent_id,
1009
- api_key: payload.agent_api_key,
1010
- hierarchy_level: payload.agent_hierarchy_level,
1011
- runtime_config: { type: "claude" }
1012
- };
1013
- const ws = await deps.workspaceManager.ensureWorkspace({ agent: syntheticAgent });
1014
- console.log(`[daemon/spawn] sess=${payload.session_id} agent=${payload.agent_id} type=${payload.type} cwd=${ws.path}`);
1015
- const runtime3 = deps.runtime ?? new ClaudeCodeRuntime;
1016
- const buffer = [];
1017
- let flushTimer;
1018
- const flush = async () => {
1019
- if (buffer.length === 0)
1020
- return;
1021
- const events = buffer.splice(0);
1022
- try {
1023
- await deps.api.post("/runtime/events", { events });
1024
- } catch (err) {
1025
- console.warn("[daemon/spawner] /runtime/events POST failed; events dropped:", err instanceof Error ? err.message : String(err));
1026
- }
1027
- };
1028
- const scheduleFlush = () => {
1029
- if (flushTimer)
1030
- return;
1031
- flushTimer = setTimeout(() => {
1032
- flushTimer = undefined;
1033
- flush();
1034
- }, 250);
1035
- };
1036
- const onStep = (step) => {
1037
- buffer.push({
1038
- session_id: payload.session_id,
1039
- kind: step.kind,
1040
- content: step.description,
1041
- tool_name: step.tool
1042
- });
1043
- if (buffer.length >= 16)
1350
+ function removeIfExists(path2) {
1351
+ try {
1352
+ if (existsSync3(path2))
1353
+ unlinkSync(path2);
1354
+ } catch {}
1355
+ }
1356
+
1357
+ // ../core/dist/adapters/opencode/runtime.js
1358
+ import { existsSync as existsSync4, writeFileSync as writeFileSync3 } from "node:fs";
1359
+ import { tmpdir as tmpdir3 } from "node:os";
1360
+ import { join as join5 } from "node:path";
1361
+
1362
+ // ../core/dist/adapters/opencode/stream-json.js
1363
+ var OPENCODE_EVENT_TYPE = {
1364
+ Text: "text",
1365
+ Reasoning: "reasoning",
1366
+ ToolUse: "tool_use",
1367
+ StepStart: "step_start",
1368
+ StepFinish: "step_finish",
1369
+ Error: "error"
1370
+ };
1371
+ var OPENCODE_TOOL_STATUS = {
1372
+ Pending: "pending",
1373
+ Running: "running",
1374
+ Completed: "completed",
1375
+ Error: "error"
1376
+ };
1377
+ function parseOpenCodeEventLine(line) {
1378
+ const trimmed = line.trim();
1379
+ if (!trimmed || !trimmed.startsWith("{"))
1380
+ return null;
1381
+ try {
1382
+ return JSON.parse(trimmed);
1383
+ } catch {
1384
+ return null;
1385
+ }
1386
+ }
1387
+ function extractOpenCodeStepEvents(evt) {
1388
+ const now = new Date().toISOString();
1389
+ if (evt.type === OPENCODE_EVENT_TYPE.Text) {
1390
+ const text = evt.part?.text?.trim();
1391
+ if (!text)
1392
+ return [];
1393
+ return [{ kind: "agent", description: text, timestamp: now }];
1394
+ }
1395
+ if (evt.type === OPENCODE_EVENT_TYPE.ToolUse) {
1396
+ const part = evt.part;
1397
+ if (!part)
1398
+ return [];
1399
+ const status = part.state?.status;
1400
+ const isTerminal = status === OPENCODE_TOOL_STATUS.Completed || status === OPENCODE_TOOL_STATUS.Error;
1401
+ return [
1402
+ {
1403
+ kind: isTerminal ? "tool_result" : "tool_call",
1404
+ tool: part.tool ?? "unknown",
1405
+ description: describeOpenCodeInput(part.state?.input),
1406
+ timestamp: now
1407
+ }
1408
+ ];
1409
+ }
1410
+ return [];
1411
+ }
1412
+ var PREFERRED_OPENCODE_FIELDS = [
1413
+ "file_path",
1414
+ "path",
1415
+ "command",
1416
+ "cmd",
1417
+ "query",
1418
+ "pattern",
1419
+ "url",
1420
+ "intent"
1421
+ ];
1422
+ function describeOpenCodeInput(input) {
1423
+ if (typeof input === "string")
1424
+ return input.slice(0, 200);
1425
+ if (!input || typeof input !== "object" || Array.isArray(input))
1426
+ return "";
1427
+ const obj = input;
1428
+ for (const key of PREFERRED_OPENCODE_FIELDS) {
1429
+ const v = obj[key];
1430
+ if (typeof v === "string" && v.length > 0)
1431
+ return v.slice(0, 200);
1432
+ }
1433
+ return JSON.stringify(input).slice(0, 200);
1434
+ }
1435
+ function parseOpenCodeEvents(events, exitCode) {
1436
+ let sessionId;
1437
+ let sawUsage = false;
1438
+ let totalInput = 0;
1439
+ let totalOutput = 0;
1440
+ let totalReasoning = 0;
1441
+ let totalCacheRead = 0;
1442
+ let totalCacheWrite = 0;
1443
+ let totalCost = 0;
1444
+ const assistantTexts = [];
1445
+ const transcriptParts = [];
1446
+ let errorMessage;
1447
+ for (const evt of events) {
1448
+ if (evt.sessionID)
1449
+ sessionId = evt.sessionID;
1450
+ switch (evt.type) {
1451
+ case OPENCODE_EVENT_TYPE.StepFinish: {
1452
+ const part = evt.part;
1453
+ if (!part)
1454
+ break;
1455
+ sawUsage = true;
1456
+ totalCost += part.cost ?? 0;
1457
+ totalInput += part.tokens?.input ?? 0;
1458
+ totalOutput += part.tokens?.output ?? 0;
1459
+ totalReasoning += part.tokens?.reasoning ?? 0;
1460
+ totalCacheRead += part.tokens?.cache?.read ?? 0;
1461
+ totalCacheWrite += part.tokens?.cache?.write ?? 0;
1462
+ break;
1463
+ }
1464
+ case OPENCODE_EVENT_TYPE.Text: {
1465
+ const text = evt.part?.text;
1466
+ if (text) {
1467
+ assistantTexts.push(text);
1468
+ transcriptParts.push(`[assistant] ${text}
1469
+ `);
1470
+ }
1471
+ break;
1472
+ }
1473
+ case OPENCODE_EVENT_TYPE.ToolUse: {
1474
+ const part = evt.part;
1475
+ if (!part)
1476
+ break;
1477
+ const tool = part.tool ?? "unknown";
1478
+ const status = part.state?.status;
1479
+ if (status === OPENCODE_TOOL_STATUS.Completed || status === OPENCODE_TOOL_STATUS.Error) {
1480
+ const detail = (part.state?.error ?? part.state?.output ?? "").slice(0, 200).replace(/\n/g, " ");
1481
+ transcriptParts.push(detail ? `[tool_result from ${tool}] ${detail}
1482
+ ` : `[tool_result from ${tool}]
1483
+ `);
1484
+ } else {
1485
+ transcriptParts.push(`[tool_call] ${tool}
1486
+ `);
1487
+ }
1488
+ break;
1489
+ }
1490
+ case OPENCODE_EVENT_TYPE.Error:
1491
+ errorMessage = evt.error?.message ?? evt.result?.error?.message ?? errorMessage;
1492
+ if (errorMessage)
1493
+ transcriptParts.push(`[error] ${errorMessage}
1494
+ `);
1495
+ break;
1496
+ default:
1497
+ break;
1498
+ }
1499
+ }
1500
+ const assistantText = assistantTexts.join(`
1501
+ `).trim();
1502
+ const failed = exitCode !== 0 || !!errorMessage;
1503
+ const output = failed ? errorMessage || assistantText || bareCliExitMessage(exitCode) : assistantText || "Session completed.";
1504
+ const usage = sawUsage ? {
1505
+ input_tokens: totalInput,
1506
+ output_tokens: totalOutput + totalReasoning,
1507
+ cache_creation_input_tokens: totalCacheWrite,
1508
+ cache_read_input_tokens: totalCacheRead,
1509
+ cost_usd: totalCost
1510
+ } : undefined;
1511
+ return {
1512
+ status: failed ? "failed" : "completed",
1513
+ output,
1514
+ transcript: transcriptParts.join("") || undefined,
1515
+ cli_session_id: sessionId,
1516
+ usage
1517
+ };
1518
+ }
1519
+
1520
+ // ../core/dist/adapters/opencode/runtime.js
1521
+ class OpenCodeRuntime {
1522
+ config;
1523
+ type = "opencode";
1524
+ constructor(config = {}) {
1525
+ this.config = config;
1526
+ }
1527
+ async execute(context) {
1528
+ const args = [
1529
+ "run",
1530
+ "--format",
1531
+ "json",
1532
+ "--dangerously-skip-permissions",
1533
+ "--dir",
1534
+ context.workspace.path
1535
+ ];
1536
+ const model = context.model ?? this.config.model;
1537
+ if (model)
1538
+ args.push("--model", model);
1539
+ if (context.resume_session_id)
1540
+ args.push("--session", context.resume_session_id);
1541
+ args.push(composePrompt2(context));
1542
+ const env2 = { ...process.env };
1543
+ if (context.env)
1544
+ Object.assign(env2, context.env);
1545
+ const events = [];
1546
+ let pending = "";
1547
+ const handleLine = (line) => {
1548
+ const evt = parseOpenCodeEventLine(line);
1549
+ if (!evt)
1550
+ return;
1551
+ events.push(evt);
1552
+ if (!context.onStep)
1553
+ return;
1554
+ for (const step of extractOpenCodeStepEvents(evt)) {
1555
+ context.onStep(step);
1556
+ }
1557
+ };
1558
+ const result = await runCliProcess({
1559
+ command: this.config.command ?? "opencode",
1560
+ args,
1561
+ cwd: context.workspace.path,
1562
+ env: env2,
1563
+ abortSignal: context.abort_signal,
1564
+ onSpawn: ({ pid, process_group_id }) => {
1565
+ context.onSpawn?.({ process_pid: pid, process_group_id });
1566
+ },
1567
+ onLog: (stream, chunk) => {
1568
+ if (stream !== "stdout")
1569
+ return;
1570
+ pending += chunk;
1571
+ let nl;
1572
+ while ((nl = pending.indexOf(`
1573
+ `)) !== -1) {
1574
+ handleLine(pending.slice(0, nl));
1575
+ pending = pending.slice(nl + 1);
1576
+ }
1577
+ }
1578
+ });
1579
+ if (pending)
1580
+ handleLine(pending);
1581
+ if (result.truncated) {
1582
+ console.warn("[OpenCodeRuntime] stdout truncated at 4MB — result parsing may be incomplete");
1583
+ }
1584
+ if (result.aborted) {
1585
+ return {
1586
+ status: "cancelled",
1587
+ output: "Session cancelled.",
1588
+ process_pid: result.pid ?? undefined,
1589
+ process_group_id: result.process_group_id ?? undefined
1590
+ };
1591
+ }
1592
+ const parsed = parseOpenCodeEvents(events, result.exitCode);
1593
+ const STDERR_TAIL_BYTES = 4096;
1594
+ const stderrTail = parsed.status === "failed" && result.stderr ? result.stderr.slice(-STDERR_TAIL_BYTES) : undefined;
1595
+ return {
1596
+ ...parsed,
1597
+ process_pid: result.pid ?? undefined,
1598
+ process_group_id: result.process_group_id ?? undefined,
1599
+ exit_code: result.exitCode,
1600
+ ...stderrTail ? { stderr: stderrTail } : {}
1601
+ };
1602
+ }
1603
+ async healthCheck() {
1604
+ try {
1605
+ const result = await runCliProcess({
1606
+ command: this.config.command ?? "opencode",
1607
+ args: ["--version"],
1608
+ cwd: tmpdir3(),
1609
+ timeoutMs: 5000,
1610
+ graceMs: 0
1611
+ });
1612
+ return { healthy: result.exitCode === 0 };
1613
+ } catch {
1614
+ return {
1615
+ healthy: false,
1616
+ error: `Command not found: ${this.config.command ?? "opencode"}`
1617
+ };
1618
+ }
1619
+ }
1620
+ async shutdown() {}
1621
+ skillsDir(workspace2) {
1622
+ return join5(workspace2.path, ".opencode", "skills");
1623
+ }
1624
+ prepareWorkspace(context) {
1625
+ const configPath = join5(context.workspace.path, "opencode.json");
1626
+ if (existsSync4(configPath))
1627
+ return;
1628
+ writeFileSync3(configPath, buildOpenCodeConfig(context.agentApiKey, context.mcpServerUrl), {
1629
+ mode: 384
1630
+ });
1631
+ }
1632
+ }
1633
+ function composePrompt2(context) {
1634
+ if (context.system_prompt_append.length === 0)
1635
+ return context.intent;
1636
+ return [
1637
+ "<beevibe_system_context>",
1638
+ context.system_prompt_append,
1639
+ "</beevibe_system_context>",
1640
+ "",
1641
+ context.intent
1642
+ ].join(`
1643
+ `);
1644
+ }
1645
+ function buildOpenCodeConfig(apiKey, mcpServerUrl) {
1646
+ return JSON.stringify({
1647
+ $schema: "https://opencode.ai/config.json",
1648
+ mcp: {
1649
+ beevibe: {
1650
+ type: "remote",
1651
+ url: mcpServerUrl,
1652
+ enabled: true,
1653
+ oauth: false,
1654
+ headers: {
1655
+ Authorization: `Bearer ${apiKey}`,
1656
+ "X-Beevibe-Session": "{env:BEEVIBE_SESSION_ID}"
1657
+ }
1658
+ }
1659
+ }
1660
+ }, null, 2) + `
1661
+ `;
1662
+ }
1663
+
1664
+ // ../core/dist/adapters/runtime-registry.js
1665
+ function createDefaultRuntimeRegistry() {
1666
+ return {
1667
+ claude: new ClaudeCodeRuntime({}),
1668
+ codex: new CodexRuntime({}),
1669
+ opencode: new OpenCodeRuntime({})
1670
+ };
1671
+ }
1672
+ function runtimeMissingError(cli) {
1673
+ return `No runtime registered for dispatch payload type '${cli}'`;
1674
+ }
1675
+
1676
+ // src/api-client.ts
1677
+ import WebSocket from "ws";
1678
+
1679
+ class ApiClient {
1680
+ cfg;
1681
+ constructor(cfg) {
1682
+ this.cfg = cfg;
1683
+ }
1684
+ async get(path2) {
1685
+ const res = await fetch(this.url(path2), {
1686
+ headers: { authorization: `Bearer ${this.cfg.daemonToken}` }
1687
+ });
1688
+ if (res.status === 204 || res.status >= 400)
1689
+ return;
1690
+ return await res.json();
1691
+ }
1692
+ async post(path2, body) {
1693
+ const res = await fetch(this.url(path2), {
1694
+ method: "POST",
1695
+ headers: {
1696
+ "content-type": "application/json",
1697
+ authorization: `Bearer ${this.cfg.daemonToken}`
1698
+ },
1699
+ body: JSON.stringify(body)
1700
+ });
1701
+ if (res.status === 204)
1702
+ return { status: 204, body: undefined };
1703
+ const text = await res.text();
1704
+ if (!text)
1705
+ return { status: res.status, body: undefined };
1706
+ try {
1707
+ return { status: res.status, body: JSON.parse(text) };
1708
+ } catch {
1709
+ return { status: res.status, body: undefined };
1710
+ }
1711
+ }
1712
+ async claim(runtimeId) {
1713
+ const res = await fetch(`${this.url("/runtime/claim")}?runtime_id=${encodeURIComponent(runtimeId)}`, {
1714
+ method: "POST",
1715
+ headers: { authorization: `Bearer ${this.cfg.daemonToken}` }
1716
+ });
1717
+ if (res.status === 204)
1718
+ return;
1719
+ if (res.status >= 400)
1720
+ return;
1721
+ return await res.json();
1722
+ }
1723
+ openWebSocket(runtimeIds) {
1724
+ const wsUrl = this.cfg.apiUrl.replace(/^http/, "ws");
1725
+ const url = `${wsUrl}/runtime/ws?runtime_ids=${runtimeIds.map(encodeURIComponent).join(",")}`;
1726
+ return new WebSocket(url, {
1727
+ headers: { authorization: `Bearer ${this.cfg.daemonToken}` }
1728
+ });
1729
+ }
1730
+ url(path2) {
1731
+ return `${this.cfg.apiUrl}${path2}`;
1732
+ }
1733
+ }
1734
+
1735
+ // ../sandbox/dist/orchestrator.js
1736
+ import { spawn as spawn3 } from "node:child_process";
1737
+ import { mkdir as mkdir2, readFile as readFile2, readdir, writeFile as writeFile2 } from "node:fs/promises";
1738
+ import { dirname as dirname2, join as join7, resolve } from "node:path";
1739
+ import { fileURLToPath } from "node:url";
1740
+
1741
+ // ../sandbox/dist/docker.js
1742
+ import { spawn as spawn2 } from "node:child_process";
1743
+ import { randomBytes as randomBytes2 } from "node:crypto";
1744
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
1745
+ import { tmpdir as tmpdir4 } from "node:os";
1746
+ import { join as join6 } from "node:path";
1747
+ var DEFAULT_IMAGE = "python:3.12-slim";
1748
+ var DEFAULT_LIMITS = {
1749
+ cmd_timeout_seconds: 300,
1750
+ cpus: 2,
1751
+ memory: "2g",
1752
+ storage: "4g",
1753
+ network: true
1754
+ };
1755
+ async function createSandbox(opts = {}) {
1756
+ const image = opts.image ?? DEFAULT_IMAGE;
1757
+ const limits = { ...DEFAULT_LIMITS, ...opts.limits ?? {} };
1758
+ const id = generateId(opts.label ?? "bv-sbx");
1759
+ const artifact_dir = await mkdtemp(join6(tmpdir4(), `${id}-artifacts-`));
1760
+ const args = [
1761
+ "run",
1762
+ "--detach",
1763
+ "--name",
1764
+ id,
1765
+ `--cpus=${limits.cpus}`,
1766
+ `--memory=${limits.memory}`,
1767
+ `--storage-opt=size=${limits.storage}`,
1768
+ "--user",
1769
+ "1000:1000",
1770
+ "--tmpfs",
1771
+ "/tmp:rw,size=512m",
1772
+ "-v",
1773
+ `${artifact_dir}:/sandbox/artifacts:rw`,
1774
+ "--workdir",
1775
+ "/sandbox",
1776
+ "--entrypoint",
1777
+ "tail"
1778
+ ];
1779
+ if (!limits.network) {
1780
+ args.push("--network=none");
1781
+ }
1782
+ args.push(image, "-f", "/dev/null");
1783
+ const result = await runDocker(args, { timeoutMs: 30000 });
1784
+ if (result.exit_code !== 0) {
1785
+ await rm(artifact_dir, { recursive: true, force: true }).catch(() => {});
1786
+ throw new SandboxError(`Failed to create sandbox: ${result.stderr.trim() || "docker run exited " + result.exit_code}`);
1787
+ }
1788
+ return {
1789
+ id,
1790
+ image,
1791
+ artifact_dir,
1792
+ created_at: new Date
1793
+ };
1794
+ }
1795
+ async function exec(sandbox, cmd, opts = {}) {
1796
+ const cwd = opts.cwd ?? "/sandbox";
1797
+ const timeoutMs = (opts.timeout_seconds ?? DEFAULT_LIMITS.cmd_timeout_seconds) * 1000;
1798
+ const args = ["exec", "--workdir", cwd];
1799
+ for (const [k, v] of Object.entries(opts.env ?? {})) {
1800
+ args.push("--env", `${k}=${v}`);
1801
+ }
1802
+ args.push(sandbox.id, "sh", "-c", cmd);
1803
+ const startedAt = Date.now();
1804
+ const r = await runDocker(args, { timeoutMs });
1805
+ return {
1806
+ stdout: r.stdout,
1807
+ stderr: r.stderr,
1808
+ exit_code: r.exit_code,
1809
+ timed_out: r.timed_out,
1810
+ duration_seconds: (Date.now() - startedAt) / 1000
1811
+ };
1812
+ }
1813
+ async function destroySandbox(sandbox) {
1814
+ await runDocker(["rm", "-f", sandbox.id], { timeoutMs: 30000 }).catch(() => {});
1815
+ }
1816
+ async function prepareBaseEnvironment(sandbox) {
1817
+ const r = await runDocker([
1818
+ "exec",
1819
+ "--user",
1820
+ "0",
1821
+ sandbox.id,
1822
+ "sh",
1823
+ "-c",
1824
+ "apt-get update -qq && apt-get install -y -qq --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/* && mkdir -p /sandbox && chown -R 1000:1000 /sandbox"
1825
+ ], { timeoutMs: 180000 });
1826
+ if (r.exit_code !== 0) {
1827
+ throw new SandboxError(`base prep failed (exit ${r.exit_code}): ${r.stderr.trim().slice(0, 500)}`);
1828
+ }
1829
+ }
1830
+
1831
+ class SandboxError extends Error {
1832
+ constructor(message) {
1833
+ super(message);
1834
+ this.name = "SandboxError";
1835
+ }
1836
+ }
1837
+ function runDocker(args, opts) {
1838
+ return new Promise((resolve) => {
1839
+ const proc = spawn2("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
1840
+ let stdout = "";
1841
+ let stderr = "";
1842
+ let timedOut = false;
1843
+ const timer = setTimeout(() => {
1844
+ timedOut = true;
1845
+ proc.kill("SIGKILL");
1846
+ }, opts.timeoutMs);
1847
+ proc.stdout?.on("data", (c) => {
1848
+ stdout += c.toString("utf8");
1849
+ });
1850
+ proc.stderr?.on("data", (c) => {
1851
+ stderr += c.toString("utf8");
1852
+ });
1853
+ proc.on("error", (err) => {
1854
+ clearTimeout(timer);
1855
+ resolve({
1856
+ stdout,
1857
+ stderr: stderr + `
1858
+ <spawn-error>${err.message}</spawn-error>`,
1859
+ exit_code: -1,
1860
+ timed_out: timedOut
1861
+ });
1862
+ });
1863
+ proc.on("close", (code) => {
1864
+ clearTimeout(timer);
1865
+ resolve({
1866
+ stdout,
1867
+ stderr,
1868
+ exit_code: code ?? -1,
1869
+ timed_out: timedOut
1870
+ });
1871
+ });
1872
+ });
1873
+ }
1874
+ function generateId(label) {
1875
+ const suffix = randomBytes2(4).toString("hex");
1876
+ const safe = label.toLowerCase().replace(/[^a-z0-9_-]/g, "-").slice(0, 32);
1877
+ return `${safe}-${suffix}`;
1878
+ }
1879
+
1880
+ // ../sandbox/dist/orchestrator.js
1881
+ var DEFAULT_PROMPT_HEADER = `You are a Beevibe sandbox child agent running inside a fresh Docker container.
1882
+
1883
+ You have ONLY these five MCP tools to touch the container. They are in your
1884
+ tool list as deferred MCP tools — you MUST load each one via ToolSearch
1885
+ before using it. ToolSearch's "select:" form only accepts ONE tool name per
1886
+ call, so make five separate calls at the very start of your work:
1887
+
1888
+ ToolSearch({ "query": "select:mcp__beevibe-sandbox__sandbox_exec", "max_results": 1 })
1889
+ ToolSearch({ "query": "select:mcp__beevibe-sandbox__sandbox_read_file", "max_results": 1 })
1890
+ ToolSearch({ "query": "select:mcp__beevibe-sandbox__sandbox_write_file", "max_results": 1 })
1891
+ ToolSearch({ "query": "select:mcp__beevibe-sandbox__sandbox_list", "max_results": 1 })
1892
+ ToolSearch({ "query": "select:mcp__beevibe-sandbox__sandbox_export_artifact", "max_results": 1 })
1893
+
1894
+ After those five ToolSearch calls, you may invoke the MCP tools directly:
1895
+
1896
+ mcp__beevibe-sandbox__sandbox_exec(cmd, cwd?, timeout_seconds?)
1897
+ mcp__beevibe-sandbox__sandbox_read_file(path, max_bytes?)
1898
+ mcp__beevibe-sandbox__sandbox_write_file(path, content)
1899
+ mcp__beevibe-sandbox__sandbox_list(path)
1900
+ mcp__beevibe-sandbox__sandbox_export_artifact(sandbox_path, title?)
1901
+
1902
+ You have NO Bash, NO Read, NO Edit, NO Write tool — those will return errors.
1903
+
1904
+ The container has Python 3.12, git, and curl pre-installed. Operate inside
1905
+ /sandbox. Use a project-local venv at /sandbox/venv.
1906
+
1907
+ Your job: use the external repo the user gives you to produce a real artifact
1908
+ for the goal. Plan, install, run, verify, export. You succeed by exporting at
1909
+ least one artifact under /sandbox/artifacts/ via sandbox_export_artifact. Stop
1910
+ as soon as you've exported something useful — don't keep exploring. If you
1911
+ can't, write REASON.txt explaining why and export that.`;
1912
+ function nowIso() {
1913
+ return new Date().toISOString();
1914
+ }
1915
+ var MAX_TRANSCRIPT_EVENTS = 500;
1916
+ var MAX_EVENT_TEXT_BYTES = 4000;
1917
+ function classifyStartupError(err) {
1918
+ const raw = err instanceof Error ? err.message : String(err);
1919
+ if (/Cannot connect to the Docker daemon/i.test(raw)) {
1920
+ return "Docker isn't running. Start Docker Desktop and try the run again.";
1921
+ }
1922
+ if (/ENOENT.*docker/i.test(raw)) {
1923
+ return "The `docker` CLI isn't on PATH. Install Docker Desktop (or set DOCKER_HOST).";
1924
+ }
1925
+ if (/no space left on device/i.test(raw)) {
1926
+ return "Docker is out of disk. Prune containers/images or expand Docker Desktop's allotment.";
1927
+ }
1928
+ return raw;
1929
+ }
1930
+ async function runRepoAgent(opts) {
1931
+ const state = {
1932
+ run_id: opts.run_id,
1933
+ status: "starting",
1934
+ repo_url: opts.repo_url,
1935
+ goal: opts.goal,
1936
+ started_at: nowIso(),
1937
+ transcript: [],
1938
+ artifacts: []
1939
+ };
1940
+ const emit = () => opts.on_state?.({ ...state, transcript: state.transcript.slice(), artifacts: state.artifacts.slice() });
1941
+ const log = (kind, text) => {
1942
+ const trimmed = text.length > MAX_EVENT_TEXT_BYTES ? text.slice(0, MAX_EVENT_TEXT_BYTES) + `
1943
+ …[truncated ${text.length - MAX_EVENT_TEXT_BYTES} bytes]…` : text;
1944
+ state.transcript.push({ at: nowIso(), kind, text: trimmed });
1945
+ if (state.transcript.length > MAX_TRANSCRIPT_EVENTS) {
1946
+ const overflow = state.transcript.length - MAX_TRANSCRIPT_EVENTS;
1947
+ state.transcript.splice(1, overflow);
1948
+ const alreadyMarked = state.transcript[1]?.text.startsWith("[log truncated:");
1949
+ if (!alreadyMarked) {
1950
+ state.transcript.splice(1, 0, {
1951
+ at: nowIso(),
1952
+ kind: "log",
1953
+ text: `[log truncated: dropped ${overflow} earlier event${overflow === 1 ? "" : "s"}]`
1954
+ });
1955
+ }
1956
+ }
1957
+ emit();
1958
+ };
1959
+ let sandbox = null;
1960
+ try {
1961
+ log("log", "Creating sandbox container…");
1962
+ state.status = "preparing";
1963
+ emit();
1964
+ try {
1965
+ sandbox = await createSandbox({ label: `bv-run-${opts.run_id}` });
1966
+ } catch (err) {
1967
+ throw new Error(classifyStartupError(err));
1968
+ }
1969
+ 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…");
1972
+ await prepareBaseEnvironment(sandbox);
1973
+ log("log", "Base environment ready.");
1974
+ if (opts.input_url) {
1975
+ const filename = opts.input_filename ?? "input.bin";
1976
+ log("log", `Fetching input into /sandbox/inputs/${filename}…`);
1977
+ const r = await exec(sandbox, `mkdir -p /sandbox/inputs && curl -fsSL ${shellQuote(opts.input_url)} -o /sandbox/inputs/${shellQuote(filename)}`, { timeout_seconds: 120 });
1978
+ if (r.exit_code !== 0) {
1979
+ throw new Error(`input fetch failed: ${r.stderr.trim().slice(0, 400)}`);
1980
+ }
1981
+ log("log", "Input file ready.");
1982
+ }
1983
+ const mcpServerCommand = opts.mcp_server_command ?? defaultMcpServerCommand();
1984
+ const mcpConfigPath = join7(sandbox.artifact_dir, "mcp-config.json");
1985
+ const mcpConfig = {
1986
+ mcpServers: {
1987
+ "beevibe-sandbox": {
1988
+ command: mcpServerCommand.command,
1989
+ args: mcpServerCommand.args,
1990
+ env: {
1991
+ BEEVIBE_SANDBOX_ID: sandbox.id,
1992
+ BEEVIBE_SANDBOX_ARTIFACTS: sandbox.artifact_dir
1993
+ }
1994
+ }
1995
+ }
1996
+ };
1997
+ await writeFile2(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
1998
+ log("log", `MCP config written: ${mcpConfigPath}`);
1999
+ const userPrompt = buildUserPrompt(opts);
2000
+ const systemPromptAppend = DEFAULT_PROMPT_HEADER;
2001
+ state.status = "running";
2002
+ log("log", "Spawning child claude session…");
2003
+ emit();
2004
+ const claudeResult = await runClaude({
2005
+ claudeBin: opts.claude_bin ?? "claude",
2006
+ mcpConfigPath,
2007
+ systemPromptAppend,
2008
+ userPrompt,
2009
+ maxBudgetUsd: opts.max_budget_usd ?? 2,
2010
+ timeoutSeconds: opts.max_runtime_seconds ?? 600,
2011
+ onTranscript: (kind, text) => log(kind, text)
2012
+ });
2013
+ if (claudeResult.exit_code !== 0 && claudeResult.exit_code !== null) {
2014
+ const tail = claudeResult.stderr.slice(-500);
2015
+ log("error", `Child claude exited with code ${claudeResult.exit_code}: ${tail}`);
2016
+ state.status = claudeResult.timed_out ? "blocked" : "failed";
2017
+ 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
+ state.finished_at = nowIso();
2019
+ emit();
2020
+ return state;
2021
+ }
2022
+ log("log", "Child claude exited cleanly. Collecting artifacts…");
2023
+ state.artifacts = await collectArtifacts(sandbox);
2024
+ if (state.artifacts.length === 0) {
2025
+ state.status = "blocked";
2026
+ state.error = "agent produced no artifacts";
2027
+ } else {
2028
+ state.status = "succeeded";
2029
+ }
2030
+ state.finished_at = nowIso();
2031
+ emit();
2032
+ return state;
2033
+ } catch (err) {
2034
+ log("error", err instanceof Error ? err.message : String(err));
2035
+ state.status = "failed";
2036
+ state.error = err instanceof Error ? err.message : String(err);
2037
+ state.finished_at = nowIso();
2038
+ emit();
2039
+ return state;
2040
+ } finally {
2041
+ if (sandbox) {
2042
+ try {
2043
+ log("log", `Destroying sandbox ${sandbox.id}…`);
2044
+ await destroySandbox(sandbox);
2045
+ } 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.`);
2047
+ }
2048
+ }
2049
+ }
2050
+ }
2051
+ function buildUserPrompt(opts) {
2052
+ const lines = [
2053
+ `Goal: ${opts.goal}`,
2054
+ `Repo: ${opts.repo_url}`
2055
+ ];
2056
+ if (opts.input_url) {
2057
+ lines.push(`Input file (pre-fetched): /sandbox/inputs/${opts.input_filename ?? "input.bin"}`);
2058
+ }
2059
+ lines.push("");
2060
+ lines.push("Work inside /sandbox. Clone the repo to /sandbox/repo. Install in a venv. ");
2061
+ lines.push("Produce at least one artifact under /sandbox/artifacts/, then call ");
2062
+ lines.push("sandbox_export_artifact() on each one with a short, human-readable title. ");
2063
+ lines.push("Stop as soon as you've exported a useful artifact — don't keep exploring.");
2064
+ return lines.join(`
2065
+ `);
2066
+ }
2067
+ var ALLOWED_TOOLS = [
2068
+ "mcp__beevibe-sandbox__sandbox_exec",
2069
+ "mcp__beevibe-sandbox__sandbox_read_file",
2070
+ "mcp__beevibe-sandbox__sandbox_write_file",
2071
+ "mcp__beevibe-sandbox__sandbox_list",
2072
+ "mcp__beevibe-sandbox__sandbox_export_artifact"
2073
+ ];
2074
+ var DISALLOWED_TOOLS = ["Bash", "Read", "Edit", "Write", "BashOutput", "KillBash"];
2075
+ function runClaude(args) {
2076
+ return new Promise((resolve2) => {
2077
+ const cliArgs = [
2078
+ "--print",
2079
+ "--mcp-config",
2080
+ args.mcpConfigPath,
2081
+ "--allowed-tools",
2082
+ ALLOWED_TOOLS.join(","),
2083
+ "--disallowed-tools",
2084
+ DISALLOWED_TOOLS.join(","),
2085
+ "--append-system-prompt",
2086
+ args.systemPromptAppend,
2087
+ "--max-budget-usd",
2088
+ String(args.maxBudgetUsd),
2089
+ "--output-format",
2090
+ "stream-json",
2091
+ "--verbose"
2092
+ ];
2093
+ const proc = spawn3(args.claudeBin, cliArgs, {
2094
+ stdio: ["pipe", "pipe", "pipe"],
2095
+ env: { ...process.env },
2096
+ detached: true
2097
+ });
2098
+ proc.unref();
2099
+ let stdout = "";
2100
+ let stderr = "";
2101
+ let timedOut = false;
2102
+ let stdoutBuffer = "";
2103
+ const timer = setTimeout(() => {
2104
+ timedOut = true;
2105
+ proc.kill("SIGTERM");
2106
+ setTimeout(() => proc.kill("SIGKILL"), 5000);
2107
+ }, args.timeoutSeconds * 1000);
2108
+ proc.stdout.on("data", (chunk) => {
2109
+ const s = chunk.toString("utf8");
2110
+ stdout += s;
2111
+ stdoutBuffer += s;
2112
+ let nl;
2113
+ while ((nl = stdoutBuffer.indexOf(`
2114
+ `)) !== -1) {
2115
+ const line = stdoutBuffer.slice(0, nl);
2116
+ stdoutBuffer = stdoutBuffer.slice(nl + 1);
2117
+ if (line.trim().length === 0)
2118
+ continue;
2119
+ try {
2120
+ const evt = JSON.parse(line);
2121
+ if (evt && typeof evt === "object" && evt.type === "system" && evt.subtype === "init") {
2122
+ const init = evt;
2123
+ const servers = init.mcp_servers;
2124
+ if (servers) {
2125
+ for (const sv of servers) {
2126
+ args.onTranscript("log", `mcp server ${sv.name}: ${sv.status}`);
2127
+ }
2128
+ }
2129
+ const tools = init.tools;
2130
+ const mcpTools = (tools ?? []).filter((t) => t.startsWith("mcp__beevibe-sandbox__"));
2131
+ args.onTranscript("log", `mcp tools exposed: ${mcpTools.length ? mcpTools.join(", ") : "none"}`);
2132
+ }
2133
+ handleStreamEvent(evt, args.onTranscript);
2134
+ } catch {}
2135
+ }
2136
+ });
2137
+ proc.stderr.on("data", (chunk) => {
2138
+ stderr += chunk.toString("utf8");
2139
+ });
2140
+ proc.on("error", (err) => {
2141
+ clearTimeout(timer);
2142
+ const friendly = /ENOENT/i.test(err.message) ? `Claude CLI not found at ${args.claudeBin}. Set BEEVIBE_CLAUDE_BIN to the binary path.` : `claude spawn error: ${err.message}`;
2143
+ args.onTranscript("error", friendly);
2144
+ resolve2({ exit_code: -1, stdout, stderr, timed_out: timedOut });
2145
+ });
2146
+ proc.on("close", (code) => {
2147
+ clearTimeout(timer);
2148
+ if (code !== 0 && code !== null) {
2149
+ const tail = stderr.slice(-800).trim();
2150
+ if (tail) {
2151
+ args.onTranscript("error", `claude stderr tail: ${tail}`);
2152
+ }
2153
+ }
2154
+ resolve2({ exit_code: code, stdout, stderr, timed_out: timedOut });
2155
+ });
2156
+ proc.stdin.write(args.userPrompt);
2157
+ proc.stdin.end();
2158
+ });
2159
+ }
2160
+ function handleStreamEvent(evt, emit) {
2161
+ if (!evt || typeof evt !== "object")
2162
+ return;
2163
+ const e = evt;
2164
+ const t = e.type;
2165
+ if (t === "assistant" || t === "assistant_message") {
2166
+ const message = e.message;
2167
+ if (message && Array.isArray(message.content)) {
2168
+ for (const item of message.content) {
2169
+ if (!item || typeof item !== "object")
2170
+ continue;
2171
+ const it = item;
2172
+ if (it.type === "text" && typeof it.text === "string") {
2173
+ emit("agent", it.text);
2174
+ } else if (it.type === "tool_use") {
2175
+ const name = String(it.name ?? "unknown");
2176
+ const input = it.input ? compactJson(it.input) : "";
2177
+ emit("tool_call", `${name}(${input})`);
2178
+ }
2179
+ }
2180
+ } else if (typeof e.content === "string") {
2181
+ emit("agent", e.content);
2182
+ }
2183
+ } else if (t === "user" || t === "user_message") {
2184
+ const message = e.message;
2185
+ if (message && Array.isArray(message.content)) {
2186
+ for (const item of message.content) {
2187
+ if (!item || typeof item !== "object")
2188
+ continue;
2189
+ const it = item;
2190
+ if (it.type === "tool_result") {
2191
+ const content = it.content;
2192
+ let text;
2193
+ if (typeof content === "string") {
2194
+ text = content;
2195
+ } else if (Array.isArray(content)) {
2196
+ text = content.map((c) => c && typeof c === "object" && typeof c.text === "string" ? c.text : "").filter(Boolean).join(" ");
2197
+ } else {
2198
+ text = JSON.stringify(content ?? null);
2199
+ }
2200
+ const isErr = it.is_error === true;
2201
+ emit(isErr ? "error" : "tool_call", `→ ${text.slice(0, 300)}${text.length > 300 ? "…" : ""}`);
2202
+ }
2203
+ }
2204
+ }
2205
+ } else if (t === "result" && typeof e.result === "string") {
2206
+ emit("agent", e.result);
2207
+ } else if (t === "error") {
2208
+ emit("error", typeof e.message === "string" ? e.message : "claude error");
2209
+ }
2210
+ }
2211
+ function compactJson(v) {
2212
+ try {
2213
+ const s = JSON.stringify(v);
2214
+ return s.length > 200 ? s.slice(0, 200) + "…" : s;
2215
+ } catch {
2216
+ return "?";
2217
+ }
2218
+ }
2219
+ async function collectArtifacts(sandbox) {
2220
+ const out = [];
2221
+ await mkdir2(sandbox.artifact_dir, { recursive: true });
2222
+ const entries = await readdir(sandbox.artifact_dir);
2223
+ const sidecars = new Map;
2224
+ for (const e of entries) {
2225
+ if (e.endsWith(".meta.json")) {
2226
+ try {
2227
+ const raw = await readFile2(join7(sandbox.artifact_dir, e), "utf8");
2228
+ sidecars.set(e.replace(/\.meta\.json$/, ""), JSON.parse(raw));
2229
+ } catch {}
2230
+ }
2231
+ }
2232
+ for (const e of entries) {
2233
+ if (e.endsWith(".meta.json") || e === "mcp-config.json")
2234
+ continue;
2235
+ const hostPath = join7(sandbox.artifact_dir, e);
2236
+ const stat = await readFile2(hostPath).then((b) => ({ size: b.byteLength }), () => null);
2237
+ if (!stat)
2238
+ continue;
2239
+ const meta = sidecars.get(e);
2240
+ out.push({
2241
+ filename: e,
2242
+ title: typeof meta?.title === "string" && meta.title || e.replace(/\.[^.]+$/, ""),
2243
+ size_bytes: stat.size,
2244
+ host_path: hostPath,
2245
+ sandbox_path: typeof meta?.sandbox_path === "string" && meta.sandbox_path || undefined
2246
+ });
2247
+ }
2248
+ return out;
2249
+ }
2250
+ function defaultMcpServerCommand() {
2251
+ const here = dirname2(fileURLToPath(import.meta.url));
2252
+ const isTsxRun = here.endsWith("/src");
2253
+ const mcpServerPath = resolve(here, isTsxRun ? "./mcp-server.ts" : "./mcp-server.js");
2254
+ return isTsxRun ? { command: "npx", args: ["--no-install", "tsx", mcpServerPath] } : { command: "node", args: ["--enable-source-maps", mcpServerPath] };
2255
+ }
2256
+ function shellQuote(s) {
2257
+ return `'${s.replace(/'/g, `'\\''`)}'`;
2258
+ }
2259
+
2260
+ // src/repo-runs.ts
2261
+ var CLAUDE_BIN = process.env.BEEVIBE_CLAUDE_BIN ?? "claude";
2262
+ async function runRepoDispatch(deps, payload, abortSignal) {
2263
+ const rr = payload.run_repo;
2264
+ if (!rr) {
2265
+ throw new Error("run_repo dispatch missing payload.run_repo");
2266
+ }
2267
+ console.log(`[daemon/repo-run] sess=${payload.session_id} repo_run=${rr.repo_run_id} repo=${rr.repo_url}`);
2268
+ const buffer = [];
2269
+ let flushTimer;
2270
+ const flush = async () => {
2271
+ if (buffer.length === 0)
2272
+ return;
2273
+ const events = buffer.splice(0);
2274
+ try {
2275
+ await deps.api.post("/runtime/events", { events });
2276
+ } catch (err) {
2277
+ console.warn("[daemon/repo-run] /runtime/events POST failed:", err instanceof Error ? err.message : String(err));
2278
+ }
2279
+ };
2280
+ const scheduleFlush = () => {
2281
+ if (flushTimer)
2282
+ return;
2283
+ flushTimer = setTimeout(() => {
2284
+ flushTimer = undefined;
2285
+ flush();
2286
+ }, 250);
2287
+ };
2288
+ let pushedUpTo = 0;
2289
+ const pushNew = (transcript) => {
2290
+ for (let i = pushedUpTo;i < transcript.length; i++) {
2291
+ const ev = transcript[i];
2292
+ if (!ev)
2293
+ continue;
2294
+ buffer.push({
2295
+ session_id: payload.session_id,
2296
+ kind: mapKind(ev.kind),
2297
+ content: ev.text
2298
+ });
2299
+ }
2300
+ pushedUpTo = transcript.length;
2301
+ if (buffer.length >= 16)
2302
+ flush();
2303
+ else
2304
+ scheduleFlush();
2305
+ };
2306
+ const installLines = [];
2307
+ let invocation;
2308
+ const noteCommandFromToolCall = (text) => {
2309
+ const m = text.match(/sandbox_exec\(\{?"?cmd"?:?\s*"([^"]+)"/);
2310
+ if (!m)
2311
+ return;
2312
+ const cmd = m[1] ?? "";
2313
+ if (!cmd)
2314
+ return;
2315
+ if (isInstallCommand(cmd))
2316
+ installLines.push(cmd);
2317
+ else
2318
+ invocation = cmd;
2319
+ };
2320
+ const result = await runRepoAgent({
2321
+ run_id: rr.repo_run_id,
2322
+ repo_url: rr.repo_url,
2323
+ goal: rr.goal,
2324
+ input_url: rr.input_url,
2325
+ input_filename: rr.input_filename,
2326
+ claude_bin: CLAUDE_BIN,
2327
+ max_runtime_seconds: (rr.limits?.wall_clock_minutes ?? 20) * 60,
2328
+ on_state: (s) => {
2329
+ pushNew(s.transcript);
2330
+ for (let i = pushedUpTo - (s.transcript.length - 0);i < s.transcript.length; i++) {
2331
+ if (i < 0)
2332
+ continue;
2333
+ const ev = s.transcript[i];
2334
+ if (ev?.kind === "tool_call")
2335
+ noteCommandFromToolCall(ev.text);
2336
+ }
2337
+ if (abortSignal?.aborted) {
2338
+ buffer.push({
2339
+ session_id: payload.session_id,
2340
+ kind: "summary",
2341
+ content: "[run cancelled by user]"
2342
+ });
2343
+ flush();
2344
+ }
2345
+ }
2346
+ });
2347
+ if (flushTimer) {
2348
+ clearTimeout(flushTimer);
2349
+ flushTimer = undefined;
2350
+ }
2351
+ await flush();
2352
+ const status = result.status === "succeeded" ? "succeeded" : result.status === "blocked" ? "failed" : result.status === "failed" ? "failed" : "failed";
2353
+ const artifacts = result.artifacts.map((a) => ({
2354
+ filename: a.filename,
2355
+ title: a.title,
2356
+ size_bytes: a.size_bytes,
2357
+ host_path: a.host_path,
2358
+ sandbox_path: a.sandbox_path
2359
+ }));
2360
+ const done = {
2361
+ session_id: payload.session_id,
2362
+ status,
2363
+ result_summary: result.status === "succeeded" ? `Exported ${artifacts.length} artifact${artifacts.length === 1 ? "" : "s"}.` : result.error ?? "Run did not produce an artifact.",
2364
+ error: result.error,
2365
+ run_repo: {
2366
+ repo_run_id: rr.repo_run_id,
2367
+ install_log: installLines.length ? installLines.join(`
2368
+ `) : undefined,
2369
+ invocation,
2370
+ artifacts
2371
+ }
2372
+ };
2373
+ if (status === "succeeded") {
2374
+ console.log(`[daemon/repo-run] sess=${payload.session_id} succeeded artifacts=${artifacts.length}`);
2375
+ } else {
2376
+ console.error(`[daemon/repo-run] sess=${payload.session_id} status=${status}` + (result.error ? `
2377
+ error:
2378
+ ${result.error.split(`
2379
+ `).join(`
2380
+ `)}` : ""));
2381
+ }
2382
+ try {
2383
+ await deps.api.post("/runtime/done", done);
2384
+ } catch (err) {
2385
+ console.error("[daemon/repo-run] /runtime/done POST failed:", err instanceof Error ? err.message : String(err));
2386
+ }
2387
+ }
2388
+ function mapKind(kind) {
2389
+ switch (kind) {
2390
+ case "agent":
2391
+ return "agent";
2392
+ case "tool_call":
2393
+ return "tool_call";
2394
+ case "log":
2395
+ return "summary";
2396
+ case "error":
2397
+ return "tool_result";
2398
+ }
2399
+ }
2400
+ function isInstallCommand(cmd) {
2401
+ return /^(pip\s|pip3\s|apt-get\s|apt\s|brew\s|npm\s+(install|i)|yarn\s+(add|install)|pnpm\s+(add|install)|git\s+clone)/.test(cmd.trim());
2402
+ }
2403
+
2404
+ // src/spawner.ts
2405
+ async function runDispatch(deps, payload, abortSignal) {
2406
+ if (payload.type === "run_repo") {
2407
+ await runRepoDispatch({ api: deps.api }, payload, abortSignal);
2408
+ return;
2409
+ }
2410
+ const syntheticAgent = {
2411
+ id: payload.agent_id,
2412
+ api_key: payload.agent_api_key,
2413
+ hierarchy_level: payload.agent_hierarchy_level,
2414
+ runtime_config: { type: payload.runtime_type }
2415
+ };
2416
+ 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}`);
2418
+ const registry = deps.runtimeRegistry ?? createDefaultRuntimeRegistry();
2419
+ const runtime3 = registry[payload.runtime_type];
2420
+ if (!runtime3) {
2421
+ throw new Error(runtimeMissingError(payload.runtime_type));
2422
+ }
2423
+ const buffer = [];
2424
+ let flushTimer;
2425
+ const flush = async () => {
2426
+ if (buffer.length === 0)
2427
+ return;
2428
+ const events = buffer.splice(0);
2429
+ try {
2430
+ await deps.api.post("/runtime/events", { events });
2431
+ } catch (err) {
2432
+ console.warn("[daemon/spawner] /runtime/events POST failed; events dropped:", err instanceof Error ? err.message : String(err));
2433
+ }
2434
+ };
2435
+ const scheduleFlush = () => {
2436
+ if (flushTimer)
2437
+ return;
2438
+ flushTimer = setTimeout(() => {
2439
+ flushTimer = undefined;
2440
+ flush();
2441
+ }, 250);
2442
+ };
2443
+ const onStep = (step) => {
2444
+ buffer.push({
2445
+ session_id: payload.session_id,
2446
+ kind: step.kind,
2447
+ content: step.description,
2448
+ tool_name: step.tool
2449
+ });
2450
+ if (buffer.length >= 16)
1044
2451
  flush();
1045
2452
  else
1046
2453
  scheduleFlush();
@@ -1206,12 +2613,22 @@ class Claimer {
1206
2613
  }
1207
2614
  async pollRuntime(runtimeId) {
1208
2615
  while (this.running && this.cfg.supervisor.hasCapacity()) {
1209
- const payload = await this.cfg.api.claim(runtimeId);
2616
+ let payload;
2617
+ try {
2618
+ payload = await this.cfg.api.claim(runtimeId);
2619
+ } catch (err) {
2620
+ console.warn(`[daemon] claim failed for runtime=${runtimeId}:`, err instanceof Error ? err.message : String(err));
2621
+ return;
2622
+ }
1210
2623
  if (!payload)
1211
2624
  return;
1212
2625
  console.log(`[daemon/claim] sess=${payload.session_id} agent=${payload.agent_id} runtime=${runtimeId}`);
1213
2626
  const ctrl = this.cfg.supervisor.start(payload.session_id);
1214
- runDispatch({ api: this.cfg.api, workspaceManager: this.cfg.workspaceManager }, 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));
2627
+ runDispatch({
2628
+ api: this.cfg.api,
2629
+ workspaceManager: this.cfg.workspaceManager,
2630
+ 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));
1215
2632
  }
1216
2633
  }
1217
2634
  }
@@ -1219,14 +2636,14 @@ class Claimer {
1219
2636
  // src/skills-cache.ts
1220
2637
  import { promises as fs2 } from "node:fs";
1221
2638
  import { homedir as homedir3 } from "node:os";
1222
- import { join as join4 } from "node:path";
2639
+ import { join as join8 } from "node:path";
1223
2640
  function skillsCacheDir() {
1224
- return join4(homedir3(), ".beevibe", "skills");
2641
+ return join8(homedir3(), ".beevibe", "skills");
1225
2642
  }
1226
2643
  var VERSION_FILE = ".version";
1227
2644
  async function readCachedVersion() {
1228
2645
  try {
1229
- return (await fs2.readFile(join4(skillsCacheDir(), VERSION_FILE), "utf8")).trim();
2646
+ return (await fs2.readFile(join8(skillsCacheDir(), VERSION_FILE), "utf8")).trim();
1230
2647
  } catch {
1231
2648
  return;
1232
2649
  }
@@ -1247,19 +2664,19 @@ async function syncSkillsCache(api) {
1247
2664
  if (!dirent.isDirectory())
1248
2665
  continue;
1249
2666
  if (dirent.name === "beevibe" || dirent.name.startsWith("beevibe-")) {
1250
- await fs2.rm(join4(cache, dirent.name), { recursive: true, force: true });
2667
+ await fs2.rm(join8(cache, dirent.name), { recursive: true, force: true });
1251
2668
  }
1252
2669
  }
1253
2670
  for (const skill of res.skills) {
1254
- const skillDir = join4(cache, skill.name);
2671
+ const skillDir = join8(cache, skill.name);
1255
2672
  await fs2.mkdir(skillDir, { recursive: true, mode: 448 });
1256
2673
  for (const file of skill.files) {
1257
- const filePath = join4(skillDir, file.path);
1258
- await fs2.mkdir(join4(filePath, ".."), { recursive: true });
2674
+ const filePath = join8(skillDir, file.path);
2675
+ await fs2.mkdir(join8(filePath, ".."), { recursive: true });
1259
2676
  await fs2.writeFile(filePath, file.content, { mode: 384 });
1260
2677
  }
1261
2678
  }
1262
- await fs2.writeFile(join4(cache, VERSION_FILE), res.version, { mode: 384 });
2679
+ await fs2.writeFile(join8(cache, VERSION_FILE), res.version, { mode: 384 });
1263
2680
  return cache;
1264
2681
  }
1265
2682
 
@@ -1338,6 +2755,7 @@ async function runStart() {
1338
2755
  api,
1339
2756
  supervisor,
1340
2757
  workspaceManager,
2758
+ runtimeRegistry,
1341
2759
  runtimeIds: cfg.runtimes.map((r) => r.id)
1342
2760
  });
1343
2761
  claimer.start();
@@ -1353,16 +2771,46 @@ async function runStart() {
1353
2771
  };
1354
2772
  process.on("SIGINT", () => void stop("SIGINT"));
1355
2773
  process.on("SIGTERM", () => void stop("SIGTERM"));
2774
+ process.on("unhandledRejection", (reason) => {
2775
+ console.warn("[daemon] unhandledRejection (continuing):", reason instanceof Error ? reason.message : String(reason));
2776
+ });
1356
2777
  await new Promise(() => {
1357
2778
  return;
1358
2779
  });
1359
2780
  }
1360
2781
 
2782
+ // src/sync.ts
2783
+ async function runSync() {
2784
+ const config = loadConfig();
2785
+ if (!config) {
2786
+ throw new Error(`No daemon config at ${CONFIG_PATH}. Run 'beevibe-daemon setup' first.`);
2787
+ }
2788
+ const detected = await detectClis();
2789
+ if (detected.length === 0) {
2790
+ throw new Error(`No supported CLIs detected on PATH. beevibe currently looks for: ${KNOWN_CLIS.join(", ")}`);
2791
+ }
2792
+ const api = new ApiClient({
2793
+ apiUrl: config.api_url,
2794
+ daemonToken: config.daemon_token
2795
+ });
2796
+ const { status, body } = await api.post("/runtime/sync", {
2797
+ runtimes: detected
2798
+ });
2799
+ if (status !== 200 || !body) {
2800
+ throw new Error(`/runtime/sync failed: ${status}`);
2801
+ }
2802
+ const before = new Set(config.runtimes.map((r) => r.cli));
2803
+ const added = body.runtimes.filter((r) => !before.has(r.cli));
2804
+ const next = { ...config, runtimes: body.runtimes };
2805
+ saveConfig(next);
2806
+ return { added, runtimes: body.runtimes };
2807
+ }
2808
+
1361
2809
  // src/update.ts
1362
2810
  import { createHash } from "node:crypto";
1363
2811
  import { createWriteStream, mkdtempSync, rmSync as rmSync2, chmodSync, renameSync } from "node:fs";
1364
- import { tmpdir as tmpdir2 } from "node:os";
1365
- import { join as join5 } from "node:path";
2812
+ import { tmpdir as tmpdir5 } from "node:os";
2813
+ import { join as join9 } from "node:path";
1366
2814
  import { Readable } from "node:stream";
1367
2815
  import { pipeline } from "node:stream/promises";
1368
2816
  import { createInterface } from "node:readline/promises";
@@ -1376,7 +2824,7 @@ var PLATFORM_ASSETS = {
1376
2824
  "linux-arm64": "beevibe-daemon-linux-arm64"
1377
2825
  };
1378
2826
  function currentVersion() {
1379
- return "0.1.2";
2827
+ return "0.1.4";
1380
2828
  }
1381
2829
  function isCompiledBinary() {
1382
2830
  if (!process.versions.bun)
@@ -1489,8 +2937,8 @@ async function runUpdate(opts = {}) {
1489
2937
  return;
1490
2938
  }
1491
2939
  }
1492
- const stagingDir = mkdtempSync(join5(tmpdir2(), "beevibe-daemon-update-"));
1493
- const stagingPath = join5(stagingDir, asset);
2940
+ const stagingDir = mkdtempSync(join9(tmpdir5(), "beevibe-daemon-update-"));
2941
+ const stagingPath = join9(stagingDir, asset);
1494
2942
  try {
1495
2943
  console.log(`Downloading ${asset}…`);
1496
2944
  const downloadUrl = `${DOWNLOAD_BASE}/${latest}/${asset}`;
@@ -1548,6 +2996,7 @@ function printHelp() {
1548
2996
  "Commands:",
1549
2997
  " setup Register this machine with a beevibe api server.",
1550
2998
  " start Run the daemon: claim pending sessions and spawn the CLI.",
2999
+ " sync Re-detect CLIs on PATH and register newly-installed ones.",
1551
3000
  " update Check for and install a newer daemon binary (brew/curl installs).",
1552
3001
  "",
1553
3002
  "setup flags:",
@@ -1589,6 +3038,16 @@ async function main() {
1589
3038
  await runStart();
1590
3039
  return;
1591
3040
  }
3041
+ if (command === "sync") {
3042
+ const result = await runSync();
3043
+ if (result.added.length === 0) {
3044
+ console.log("No new CLIs detected.");
3045
+ } 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).");
3048
+ }
3049
+ return;
3050
+ }
1592
3051
  if (command === "update") {
1593
3052
  const skipPrompt = rest.includes("--yes") || rest.includes("-y");
1594
3053
  await runUpdate({ skipPrompt });