@beevibe/daemon 0.1.2 → 0.1.3

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 +823 -36
  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",
@@ -845,6 +876,9 @@ class ClaudeCodeRuntime {
845
876
  args.push("--max-turns", String(maxTurns));
846
877
  if (context.resume_session_id)
847
878
  args.push("--resume", context.resume_session_id);
879
+ if (context.disallowed_tools?.length) {
880
+ args.push("--disallowedTools", context.disallowed_tools.join(","));
881
+ }
848
882
  if (context.system_prompt_append.length > 0) {
849
883
  args.push("--append-system-prompt", context.system_prompt_append);
850
884
  }
@@ -937,10 +971,705 @@ class ClaudeCodeRuntime {
937
971
  }
938
972
  }
939
973
 
974
+ // ../core/dist/adapters/codex/runtime.js
975
+ import { existsSync as existsSync3, readFileSync as readFileSync3, unlinkSync } from "node:fs";
976
+ import { tmpdir as tmpdir2 } from "node:os";
977
+ import { join as join4 } from "node:path";
978
+
979
+ // ../core/dist/adapters/codex/stream-json.js
980
+ var CODEX_EVENT_TYPE = {
981
+ ThreadStarted: "thread.started",
982
+ TurnStarted: "turn.started",
983
+ TurnCompleted: "turn.completed",
984
+ TurnFailed: "turn.failed",
985
+ ItemStarted: "item.started",
986
+ ItemUpdated: "item.updated",
987
+ ItemCompleted: "item.completed",
988
+ Error: "error"
989
+ };
990
+ var CODEX_ITEM_TYPE = {
991
+ AgentMessage: "agent_message",
992
+ Reasoning: "reasoning",
993
+ CommandExecution: "command_execution",
994
+ FileChange: "file_change",
995
+ McpToolCall: "mcp_tool_call",
996
+ CollabToolCall: "collab_tool_call",
997
+ WebSearch: "web_search",
998
+ TodoList: "todo_list",
999
+ Error: "error"
1000
+ };
1001
+ function parseCodexEventLine(line) {
1002
+ const trimmed = line.trim();
1003
+ if (!trimmed || !trimmed.startsWith("{"))
1004
+ return null;
1005
+ try {
1006
+ return JSON.parse(trimmed);
1007
+ } catch {
1008
+ return null;
1009
+ }
1010
+ }
1011
+ function extractCodexStepEvents(evt) {
1012
+ const now = new Date().toISOString();
1013
+ if (evt.type !== CODEX_EVENT_TYPE.ItemStarted && evt.type !== CODEX_EVENT_TYPE.ItemCompleted) {
1014
+ return [];
1015
+ }
1016
+ const item = evt.item;
1017
+ if (!item || !item.type)
1018
+ return [];
1019
+ const isCompletion = evt.type === CODEX_EVENT_TYPE.ItemCompleted;
1020
+ if (item.type === CODEX_ITEM_TYPE.AgentMessage) {
1021
+ if (!isCompletion)
1022
+ return [];
1023
+ const text = item.text?.trim();
1024
+ if (!text)
1025
+ return [];
1026
+ return [{ kind: "agent", description: text, timestamp: now }];
1027
+ }
1028
+ if (item.type === CODEX_ITEM_TYPE.McpToolCall) {
1029
+ return [
1030
+ {
1031
+ kind: isCompletion ? "tool_result" : "tool_call",
1032
+ tool: item.tool ?? "unknown",
1033
+ description: describeCodexInput(item.arguments),
1034
+ timestamp: now
1035
+ }
1036
+ ];
1037
+ }
1038
+ if (item.type === CODEX_ITEM_TYPE.CommandExecution) {
1039
+ return [
1040
+ {
1041
+ kind: isCompletion ? "tool_result" : "tool_call",
1042
+ tool: "shell",
1043
+ description: (item.command ?? "").slice(0, 200),
1044
+ timestamp: now
1045
+ }
1046
+ ];
1047
+ }
1048
+ if (item.type === CODEX_ITEM_TYPE.FileChange) {
1049
+ const summary = (item.changes ?? []).map((c) => `${c.kind ?? "?"} ${c.path ?? ""}`).join(", ").slice(0, 200);
1050
+ return [
1051
+ {
1052
+ kind: isCompletion ? "tool_result" : "tool_call",
1053
+ tool: "file_change",
1054
+ description: summary,
1055
+ timestamp: now
1056
+ }
1057
+ ];
1058
+ }
1059
+ if (item.type === CODEX_ITEM_TYPE.WebSearch) {
1060
+ return [
1061
+ {
1062
+ kind: isCompletion ? "tool_result" : "tool_call",
1063
+ tool: "web_search",
1064
+ description: (item.query ?? "").slice(0, 200),
1065
+ timestamp: now
1066
+ }
1067
+ ];
1068
+ }
1069
+ return [];
1070
+ }
1071
+ var PREFERRED_CODEX_FIELDS = [
1072
+ "file_path",
1073
+ "path",
1074
+ "command",
1075
+ "cmd",
1076
+ "query",
1077
+ "pattern",
1078
+ "url",
1079
+ "intent"
1080
+ ];
1081
+ function describeCodexInput(input) {
1082
+ if (typeof input === "string")
1083
+ return input.slice(0, 200);
1084
+ if (!input || typeof input !== "object" || Array.isArray(input))
1085
+ return "";
1086
+ const obj = input;
1087
+ for (const key of PREFERRED_CODEX_FIELDS) {
1088
+ const v = obj[key];
1089
+ if (typeof v === "string" && v.length > 0)
1090
+ return v.slice(0, 200);
1091
+ }
1092
+ return JSON.stringify(input).slice(0, 200);
1093
+ }
1094
+ function parseCodexEvents(events, exitCode, lastMessage) {
1095
+ let threadId;
1096
+ let usage;
1097
+ let assistantText = "";
1098
+ let turnFailed;
1099
+ let topLevelError;
1100
+ const transcriptParts = [];
1101
+ for (const evt of events) {
1102
+ switch (evt.type) {
1103
+ case CODEX_EVENT_TYPE.ThreadStarted:
1104
+ if (evt.thread_id)
1105
+ threadId = evt.thread_id;
1106
+ break;
1107
+ case CODEX_EVENT_TYPE.TurnCompleted:
1108
+ if (evt.usage)
1109
+ usage = evt.usage;
1110
+ break;
1111
+ case CODEX_EVENT_TYPE.TurnFailed:
1112
+ turnFailed = evt.error?.message ?? turnFailed;
1113
+ break;
1114
+ case CODEX_EVENT_TYPE.Error:
1115
+ topLevelError = evt.message ?? topLevelError;
1116
+ if (evt.message)
1117
+ transcriptParts.push(`[error] ${evt.message}
1118
+ `);
1119
+ break;
1120
+ case CODEX_EVENT_TYPE.ItemCompleted: {
1121
+ const item = evt.item;
1122
+ if (!item || !item.type)
1123
+ break;
1124
+ if (item.type === CODEX_ITEM_TYPE.AgentMessage && item.text) {
1125
+ assistantText = item.text;
1126
+ transcriptParts.push(`[assistant] ${item.text}
1127
+ `);
1128
+ } else if (item.type === CODEX_ITEM_TYPE.McpToolCall) {
1129
+ const tool = item.tool ?? "unknown";
1130
+ transcriptParts.push(`[tool_call] ${tool}
1131
+ `);
1132
+ const resultSummary = summarizeMcpResult(item.result);
1133
+ if (resultSummary || item.error?.message) {
1134
+ transcriptParts.push(`[tool_result from ${tool}] ${item.error?.message ?? resultSummary}
1135
+ `);
1136
+ }
1137
+ } else if (item.type === CODEX_ITEM_TYPE.CommandExecution) {
1138
+ transcriptParts.push(`[tool_call] shell ${(item.command ?? "").slice(0, 200)}
1139
+ `);
1140
+ if (item.aggregated_output) {
1141
+ transcriptParts.push(`[tool_result from shell] ${item.aggregated_output.slice(0, 200).replace(/\n/g, " ")}
1142
+ `);
1143
+ }
1144
+ }
1145
+ break;
1146
+ }
1147
+ default:
1148
+ break;
1149
+ }
1150
+ }
1151
+ const failed = exitCode !== 0 || !!turnFailed || !!topLevelError;
1152
+ const trimmedLast = lastMessage.trim();
1153
+ const failureMessage = turnFailed ?? topLevelError;
1154
+ const output = failed ? failureMessage || assistantText || bareCliExitMessage(exitCode) : trimmedLast || assistantText || "Session completed.";
1155
+ return {
1156
+ status: failed ? "failed" : "completed",
1157
+ output,
1158
+ transcript: transcriptParts.join("") || undefined,
1159
+ cli_session_id: threadId,
1160
+ usage: usage ? {
1161
+ input_tokens: usage.input_tokens ?? 0,
1162
+ output_tokens: (usage.output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0),
1163
+ cache_creation_input_tokens: 0,
1164
+ cache_read_input_tokens: usage.cached_input_tokens ?? 0
1165
+ } : undefined
1166
+ };
1167
+ }
1168
+ function summarizeMcpResult(result) {
1169
+ if (!result || !Array.isArray(result.content))
1170
+ return "";
1171
+ for (const block of result.content) {
1172
+ if (block && typeof block === "object" && block.type === "text") {
1173
+ const text = block.text;
1174
+ if (typeof text === "string")
1175
+ return text.slice(0, 200).replace(/\n/g, " ");
1176
+ }
1177
+ }
1178
+ return JSON.stringify(result.content).slice(0, 200);
1179
+ }
1180
+
1181
+ // ../core/dist/adapters/codex/runtime.js
1182
+ var OPENAI_AUTH_VARS = ["OPENAI_API_KEY", "OPENAI_AUTH_TOKEN"];
1183
+
1184
+ class CodexRuntime {
1185
+ config;
1186
+ type = "codex";
1187
+ prepared = new Map;
1188
+ constructor(config = {}) {
1189
+ this.config = config;
1190
+ }
1191
+ async execute(context) {
1192
+ const prepared = this.prepared.get(context.workspace.path);
1193
+ const sid = context.env?.BEEVIBE_SESSION_ID;
1194
+ const lastMessagePath = join4(context.workspace.path, `.beevibe-codex-last-message-${Date.now()}.txt`);
1195
+ const globalArgs = buildGlobalArgs(context, this.config);
1196
+ const execArgs = [
1197
+ "--json",
1198
+ "--skip-git-repo-check",
1199
+ "--output-last-message",
1200
+ lastMessagePath
1201
+ ];
1202
+ 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")}`);
1204
+ }
1205
+ const args = context.resume_session_id ? [
1206
+ ...globalArgs,
1207
+ "exec",
1208
+ "resume",
1209
+ ...execArgs,
1210
+ context.resume_session_id,
1211
+ composePrompt(context)
1212
+ ] : [...globalArgs, "exec", ...execArgs, composePrompt(context)];
1213
+ const env2 = { ...process.env };
1214
+ for (const key of OPENAI_AUTH_VARS)
1215
+ delete env2[key];
1216
+ if (context.env)
1217
+ Object.assign(env2, context.env);
1218
+ if (prepared)
1219
+ env2.BEEVIBE_AGENT_API_KEY = prepared.agentApiKey;
1220
+ const events = [];
1221
+ let pending = "";
1222
+ const handleLine = (line) => {
1223
+ const evt = parseCodexEventLine(line);
1224
+ if (!evt)
1225
+ return;
1226
+ events.push(evt);
1227
+ if (!context.onStep)
1228
+ return;
1229
+ for (const step of extractCodexStepEvents(evt)) {
1230
+ context.onStep(step);
1231
+ }
1232
+ };
1233
+ const result = await runCliProcess({
1234
+ command: this.config.command ?? "codex",
1235
+ args,
1236
+ cwd: context.workspace.path,
1237
+ env: env2,
1238
+ abortSignal: context.abort_signal,
1239
+ onSpawn: ({ pid, process_group_id }) => {
1240
+ context.onSpawn?.({ process_pid: pid, process_group_id });
1241
+ },
1242
+ onLog: (stream, chunk) => {
1243
+ if (stream !== "stdout")
1244
+ return;
1245
+ pending += chunk;
1246
+ let nl;
1247
+ while ((nl = pending.indexOf(`
1248
+ `)) !== -1) {
1249
+ handleLine(pending.slice(0, nl));
1250
+ pending = pending.slice(nl + 1);
1251
+ }
1252
+ }
1253
+ });
1254
+ if (pending)
1255
+ handleLine(pending);
1256
+ if (result.truncated) {
1257
+ console.warn("[CodexRuntime] stdout truncated at 4MB — result parsing may be incomplete");
1258
+ }
1259
+ if (result.aborted) {
1260
+ removeIfExists(lastMessagePath);
1261
+ return {
1262
+ status: "cancelled",
1263
+ output: "Session cancelled.",
1264
+ process_pid: result.pid ?? undefined,
1265
+ process_group_id: result.process_group_id ?? undefined
1266
+ };
1267
+ }
1268
+ const lastMessage = readIfExists(lastMessagePath);
1269
+ removeIfExists(lastMessagePath);
1270
+ const parsed = parseCodexEvents(events, result.exitCode, lastMessage);
1271
+ const STDERR_TAIL_BYTES = 4096;
1272
+ const stderrTail = parsed.status === "failed" && result.stderr ? result.stderr.slice(-STDERR_TAIL_BYTES) : undefined;
1273
+ return {
1274
+ ...parsed,
1275
+ process_pid: result.pid ?? undefined,
1276
+ process_group_id: result.process_group_id ?? undefined,
1277
+ exit_code: result.exitCode,
1278
+ ...stderrTail ? { stderr: stderrTail } : {}
1279
+ };
1280
+ }
1281
+ async healthCheck() {
1282
+ try {
1283
+ const result = await runCliProcess({
1284
+ command: this.config.command ?? "codex",
1285
+ args: ["--version"],
1286
+ cwd: tmpdir2(),
1287
+ timeoutMs: 5000,
1288
+ graceMs: 0
1289
+ });
1290
+ return {
1291
+ healthy: result.exitCode === 0,
1292
+ error: result.exitCode === 0 ? undefined : result.stderr.slice(-500)
1293
+ };
1294
+ } catch {
1295
+ return {
1296
+ healthy: false,
1297
+ error: `Command not found: ${this.config.command ?? "codex"}`
1298
+ };
1299
+ }
1300
+ }
1301
+ async shutdown() {}
1302
+ skillsDir(workspace2) {
1303
+ return join4(workspace2.path, ".codex", "skills");
1304
+ }
1305
+ prepareWorkspace(context) {
1306
+ this.prepared.set(context.workspace.path, {
1307
+ agentApiKey: context.agentApiKey,
1308
+ mcpServerUrl: context.mcpServerUrl
1309
+ });
1310
+ }
1311
+ }
1312
+ function buildGlobalArgs(context, config) {
1313
+ const args = [
1314
+ "--sandbox",
1315
+ "workspace-write",
1316
+ "--ask-for-approval",
1317
+ "never",
1318
+ "--cd",
1319
+ context.workspace.path
1320
+ ];
1321
+ const model = context.model ?? config.model;
1322
+ if (model)
1323
+ args.push("--model", model);
1324
+ return args;
1325
+ }
1326
+ function composePrompt(context) {
1327
+ if (context.system_prompt_append.length === 0)
1328
+ return context.intent;
1329
+ return [
1330
+ "<beevibe_system_context>",
1331
+ context.system_prompt_append,
1332
+ "</beevibe_system_context>",
1333
+ "",
1334
+ context.intent
1335
+ ].join(`
1336
+ `);
1337
+ }
1338
+ function withBeevibeSession(mcpServerUrl, sid) {
1339
+ const url = new URL(mcpServerUrl);
1340
+ url.searchParams.set("beevibe_session", sid);
1341
+ return url.toString();
1342
+ }
1343
+ function tomlString(value) {
1344
+ return JSON.stringify(value);
1345
+ }
1346
+ function readIfExists(path2) {
1347
+ try {
1348
+ return existsSync3(path2) ? readFileSync3(path2, "utf8") : "";
1349
+ } catch {
1350
+ return "";
1351
+ }
1352
+ }
1353
+ function removeIfExists(path2) {
1354
+ try {
1355
+ if (existsSync3(path2))
1356
+ unlinkSync(path2);
1357
+ } catch {}
1358
+ }
1359
+
1360
+ // ../core/dist/adapters/opencode/runtime.js
1361
+ import { existsSync as existsSync4, writeFileSync as writeFileSync3 } from "node:fs";
1362
+ import { tmpdir as tmpdir3 } from "node:os";
1363
+ import { join as join5 } from "node:path";
1364
+
1365
+ // ../core/dist/adapters/opencode/stream-json.js
1366
+ var OPENCODE_EVENT_TYPE = {
1367
+ Text: "text",
1368
+ Reasoning: "reasoning",
1369
+ ToolUse: "tool_use",
1370
+ StepStart: "step_start",
1371
+ StepFinish: "step_finish",
1372
+ Error: "error"
1373
+ };
1374
+ var OPENCODE_TOOL_STATUS = {
1375
+ Pending: "pending",
1376
+ Running: "running",
1377
+ Completed: "completed",
1378
+ Error: "error"
1379
+ };
1380
+ function parseOpenCodeEventLine(line) {
1381
+ const trimmed = line.trim();
1382
+ if (!trimmed || !trimmed.startsWith("{"))
1383
+ return null;
1384
+ try {
1385
+ return JSON.parse(trimmed);
1386
+ } catch {
1387
+ return null;
1388
+ }
1389
+ }
1390
+ function extractOpenCodeStepEvents(evt) {
1391
+ const now = new Date().toISOString();
1392
+ if (evt.type === OPENCODE_EVENT_TYPE.Text) {
1393
+ const text = evt.part?.text?.trim();
1394
+ if (!text)
1395
+ return [];
1396
+ return [{ kind: "agent", description: text, timestamp: now }];
1397
+ }
1398
+ if (evt.type === OPENCODE_EVENT_TYPE.ToolUse) {
1399
+ const part = evt.part;
1400
+ if (!part)
1401
+ return [];
1402
+ const status = part.state?.status;
1403
+ const isTerminal = status === OPENCODE_TOOL_STATUS.Completed || status === OPENCODE_TOOL_STATUS.Error;
1404
+ return [
1405
+ {
1406
+ kind: isTerminal ? "tool_result" : "tool_call",
1407
+ tool: part.tool ?? "unknown",
1408
+ description: describeOpenCodeInput(part.state?.input),
1409
+ timestamp: now
1410
+ }
1411
+ ];
1412
+ }
1413
+ return [];
1414
+ }
1415
+ var PREFERRED_OPENCODE_FIELDS = [
1416
+ "file_path",
1417
+ "path",
1418
+ "command",
1419
+ "cmd",
1420
+ "query",
1421
+ "pattern",
1422
+ "url",
1423
+ "intent"
1424
+ ];
1425
+ function describeOpenCodeInput(input) {
1426
+ if (typeof input === "string")
1427
+ return input.slice(0, 200);
1428
+ if (!input || typeof input !== "object" || Array.isArray(input))
1429
+ return "";
1430
+ const obj = input;
1431
+ for (const key of PREFERRED_OPENCODE_FIELDS) {
1432
+ const v = obj[key];
1433
+ if (typeof v === "string" && v.length > 0)
1434
+ return v.slice(0, 200);
1435
+ }
1436
+ return JSON.stringify(input).slice(0, 200);
1437
+ }
1438
+ function parseOpenCodeEvents(events, exitCode) {
1439
+ let sessionId;
1440
+ let sawUsage = false;
1441
+ let totalInput = 0;
1442
+ let totalOutput = 0;
1443
+ let totalReasoning = 0;
1444
+ let totalCacheRead = 0;
1445
+ let totalCacheWrite = 0;
1446
+ let totalCost = 0;
1447
+ const assistantTexts = [];
1448
+ const transcriptParts = [];
1449
+ let errorMessage;
1450
+ for (const evt of events) {
1451
+ if (evt.sessionID)
1452
+ sessionId = evt.sessionID;
1453
+ switch (evt.type) {
1454
+ case OPENCODE_EVENT_TYPE.StepFinish: {
1455
+ const part = evt.part;
1456
+ if (!part)
1457
+ break;
1458
+ sawUsage = true;
1459
+ totalCost += part.cost ?? 0;
1460
+ totalInput += part.tokens?.input ?? 0;
1461
+ totalOutput += part.tokens?.output ?? 0;
1462
+ totalReasoning += part.tokens?.reasoning ?? 0;
1463
+ totalCacheRead += part.tokens?.cache?.read ?? 0;
1464
+ totalCacheWrite += part.tokens?.cache?.write ?? 0;
1465
+ break;
1466
+ }
1467
+ case OPENCODE_EVENT_TYPE.Text: {
1468
+ const text = evt.part?.text;
1469
+ if (text) {
1470
+ assistantTexts.push(text);
1471
+ transcriptParts.push(`[assistant] ${text}
1472
+ `);
1473
+ }
1474
+ break;
1475
+ }
1476
+ case OPENCODE_EVENT_TYPE.ToolUse: {
1477
+ const part = evt.part;
1478
+ if (!part)
1479
+ break;
1480
+ const tool = part.tool ?? "unknown";
1481
+ const status = part.state?.status;
1482
+ if (status === OPENCODE_TOOL_STATUS.Completed || status === OPENCODE_TOOL_STATUS.Error) {
1483
+ const detail = (part.state?.error ?? part.state?.output ?? "").slice(0, 200).replace(/\n/g, " ");
1484
+ transcriptParts.push(detail ? `[tool_result from ${tool}] ${detail}
1485
+ ` : `[tool_result from ${tool}]
1486
+ `);
1487
+ } else {
1488
+ transcriptParts.push(`[tool_call] ${tool}
1489
+ `);
1490
+ }
1491
+ break;
1492
+ }
1493
+ case OPENCODE_EVENT_TYPE.Error:
1494
+ errorMessage = evt.error?.message ?? evt.result?.error?.message ?? errorMessage;
1495
+ if (errorMessage)
1496
+ transcriptParts.push(`[error] ${errorMessage}
1497
+ `);
1498
+ break;
1499
+ default:
1500
+ break;
1501
+ }
1502
+ }
1503
+ const assistantText = assistantTexts.join(`
1504
+ `).trim();
1505
+ const failed = exitCode !== 0 || !!errorMessage;
1506
+ const output = failed ? errorMessage || assistantText || bareCliExitMessage(exitCode) : assistantText || "Session completed.";
1507
+ const usage = sawUsage ? {
1508
+ input_tokens: totalInput,
1509
+ output_tokens: totalOutput + totalReasoning,
1510
+ cache_creation_input_tokens: totalCacheWrite,
1511
+ cache_read_input_tokens: totalCacheRead,
1512
+ cost_usd: totalCost
1513
+ } : undefined;
1514
+ return {
1515
+ status: failed ? "failed" : "completed",
1516
+ output,
1517
+ transcript: transcriptParts.join("") || undefined,
1518
+ cli_session_id: sessionId,
1519
+ usage
1520
+ };
1521
+ }
1522
+
1523
+ // ../core/dist/adapters/opencode/runtime.js
1524
+ class OpenCodeRuntime {
1525
+ config;
1526
+ type = "opencode";
1527
+ constructor(config = {}) {
1528
+ this.config = config;
1529
+ }
1530
+ async execute(context) {
1531
+ const args = [
1532
+ "run",
1533
+ "--format",
1534
+ "json",
1535
+ "--dangerously-skip-permissions",
1536
+ "--dir",
1537
+ context.workspace.path
1538
+ ];
1539
+ const model = context.model ?? this.config.model;
1540
+ if (model)
1541
+ args.push("--model", model);
1542
+ if (context.resume_session_id)
1543
+ args.push("--session", context.resume_session_id);
1544
+ args.push(composePrompt2(context));
1545
+ const env2 = { ...process.env };
1546
+ if (context.env)
1547
+ Object.assign(env2, context.env);
1548
+ const events = [];
1549
+ let pending = "";
1550
+ const handleLine = (line) => {
1551
+ const evt = parseOpenCodeEventLine(line);
1552
+ if (!evt)
1553
+ return;
1554
+ events.push(evt);
1555
+ if (!context.onStep)
1556
+ return;
1557
+ for (const step of extractOpenCodeStepEvents(evt)) {
1558
+ context.onStep(step);
1559
+ }
1560
+ };
1561
+ const result = await runCliProcess({
1562
+ command: this.config.command ?? "opencode",
1563
+ args,
1564
+ cwd: context.workspace.path,
1565
+ env: env2,
1566
+ abortSignal: context.abort_signal,
1567
+ onSpawn: ({ pid, process_group_id }) => {
1568
+ context.onSpawn?.({ process_pid: pid, process_group_id });
1569
+ },
1570
+ onLog: (stream, chunk) => {
1571
+ if (stream !== "stdout")
1572
+ return;
1573
+ pending += chunk;
1574
+ let nl;
1575
+ while ((nl = pending.indexOf(`
1576
+ `)) !== -1) {
1577
+ handleLine(pending.slice(0, nl));
1578
+ pending = pending.slice(nl + 1);
1579
+ }
1580
+ }
1581
+ });
1582
+ if (pending)
1583
+ handleLine(pending);
1584
+ if (result.truncated) {
1585
+ console.warn("[OpenCodeRuntime] stdout truncated at 4MB — result parsing may be incomplete");
1586
+ }
1587
+ if (result.aborted) {
1588
+ return {
1589
+ status: "cancelled",
1590
+ output: "Session cancelled.",
1591
+ process_pid: result.pid ?? undefined,
1592
+ process_group_id: result.process_group_id ?? undefined
1593
+ };
1594
+ }
1595
+ const parsed = parseOpenCodeEvents(events, result.exitCode);
1596
+ const STDERR_TAIL_BYTES = 4096;
1597
+ const stderrTail = parsed.status === "failed" && result.stderr ? result.stderr.slice(-STDERR_TAIL_BYTES) : undefined;
1598
+ return {
1599
+ ...parsed,
1600
+ process_pid: result.pid ?? undefined,
1601
+ process_group_id: result.process_group_id ?? undefined,
1602
+ exit_code: result.exitCode,
1603
+ ...stderrTail ? { stderr: stderrTail } : {}
1604
+ };
1605
+ }
1606
+ async healthCheck() {
1607
+ try {
1608
+ const result = await runCliProcess({
1609
+ command: this.config.command ?? "opencode",
1610
+ args: ["--version"],
1611
+ cwd: tmpdir3(),
1612
+ timeoutMs: 5000,
1613
+ graceMs: 0
1614
+ });
1615
+ return { healthy: result.exitCode === 0 };
1616
+ } catch {
1617
+ return {
1618
+ healthy: false,
1619
+ error: `Command not found: ${this.config.command ?? "opencode"}`
1620
+ };
1621
+ }
1622
+ }
1623
+ async shutdown() {}
1624
+ skillsDir(workspace2) {
1625
+ return join5(workspace2.path, ".opencode", "skills");
1626
+ }
1627
+ prepareWorkspace(context) {
1628
+ const configPath = join5(context.workspace.path, "opencode.json");
1629
+ if (existsSync4(configPath))
1630
+ return;
1631
+ writeFileSync3(configPath, buildOpenCodeConfig(context.agentApiKey, context.mcpServerUrl), {
1632
+ mode: 384
1633
+ });
1634
+ }
1635
+ }
1636
+ function composePrompt2(context) {
1637
+ if (context.system_prompt_append.length === 0)
1638
+ return context.intent;
1639
+ return [
1640
+ "<beevibe_system_context>",
1641
+ context.system_prompt_append,
1642
+ "</beevibe_system_context>",
1643
+ "",
1644
+ context.intent
1645
+ ].join(`
1646
+ `);
1647
+ }
1648
+ function buildOpenCodeConfig(apiKey, mcpServerUrl) {
1649
+ return JSON.stringify({
1650
+ $schema: "https://opencode.ai/config.json",
1651
+ mcp: {
1652
+ beevibe: {
1653
+ type: "remote",
1654
+ url: mcpServerUrl,
1655
+ enabled: true,
1656
+ oauth: false,
1657
+ headers: {
1658
+ Authorization: `Bearer ${apiKey}`,
1659
+ "X-Beevibe-Session": "{env:BEEVIBE_SESSION_ID}"
1660
+ }
1661
+ }
1662
+ }
1663
+ }, null, 2) + `
1664
+ `;
1665
+ }
1666
+
940
1667
  // ../core/dist/adapters/runtime-registry.js
941
1668
  function createDefaultRuntimeRegistry() {
942
1669
  return {
943
- claude: new ClaudeCodeRuntime({})
1670
+ claude: new ClaudeCodeRuntime({}),
1671
+ codex: new CodexRuntime({}),
1672
+ opencode: new OpenCodeRuntime({})
944
1673
  };
945
1674
  }
946
1675
 
@@ -1002,17 +1731,22 @@ class ApiClient {
1002
1731
  return `${this.cfg.apiUrl}${path2}`;
1003
1732
  }
1004
1733
  }
1734
+
1005
1735
  // src/spawner.ts
1006
1736
  async function runDispatch(deps, payload, abortSignal) {
1007
1737
  const syntheticAgent = {
1008
1738
  id: payload.agent_id,
1009
1739
  api_key: payload.agent_api_key,
1010
1740
  hierarchy_level: payload.agent_hierarchy_level,
1011
- runtime_config: { type: "claude" }
1741
+ runtime_config: { type: payload.runtime_type }
1012
1742
  };
1013
1743
  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;
1744
+ console.log(`[daemon/spawn] sess=${payload.session_id} agent=${payload.agent_id} runtime=${payload.runtime_type} type=${payload.type} cwd=${ws.path}`);
1745
+ const registry = deps.runtimeRegistry ?? createDefaultRuntimeRegistry();
1746
+ const runtime3 = registry[payload.runtime_type];
1747
+ if (!runtime3) {
1748
+ throw new Error(`No runtime registered for dispatch payload type '${payload.runtime_type}'`);
1749
+ }
1016
1750
  const buffer = [];
1017
1751
  let flushTimer;
1018
1752
  const flush = async () => {
@@ -1054,6 +1788,7 @@ async function runDispatch(deps, payload, abortSignal) {
1054
1788
  system_prompt_append: payload.system_prompt_append,
1055
1789
  model: payload.model,
1056
1790
  max_turns: payload.max_turns,
1791
+ disallowed_tools: payload.disallowed_tools,
1057
1792
  env: payload.env,
1058
1793
  resume_session_id: payload.resume_session_id,
1059
1794
  abort_signal: abortSignal,
@@ -1206,12 +1941,22 @@ class Claimer {
1206
1941
  }
1207
1942
  async pollRuntime(runtimeId) {
1208
1943
  while (this.running && this.cfg.supervisor.hasCapacity()) {
1209
- const payload = await this.cfg.api.claim(runtimeId);
1944
+ let payload;
1945
+ try {
1946
+ payload = await this.cfg.api.claim(runtimeId);
1947
+ } catch (err) {
1948
+ console.warn(`[daemon] claim failed for runtime=${runtimeId}:`, err instanceof Error ? err.message : String(err));
1949
+ return;
1950
+ }
1210
1951
  if (!payload)
1211
1952
  return;
1212
1953
  console.log(`[daemon/claim] sess=${payload.session_id} agent=${payload.agent_id} runtime=${runtimeId}`);
1213
1954
  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));
1955
+ runDispatch({
1956
+ api: this.cfg.api,
1957
+ workspaceManager: this.cfg.workspaceManager,
1958
+ runtimeRegistry: this.cfg.runtimeRegistry
1959
+ }, 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
1960
  }
1216
1961
  }
1217
1962
  }
@@ -1219,14 +1964,14 @@ class Claimer {
1219
1964
  // src/skills-cache.ts
1220
1965
  import { promises as fs2 } from "node:fs";
1221
1966
  import { homedir as homedir3 } from "node:os";
1222
- import { join as join4 } from "node:path";
1967
+ import { join as join6 } from "node:path";
1223
1968
  function skillsCacheDir() {
1224
- return join4(homedir3(), ".beevibe", "skills");
1969
+ return join6(homedir3(), ".beevibe", "skills");
1225
1970
  }
1226
1971
  var VERSION_FILE = ".version";
1227
1972
  async function readCachedVersion() {
1228
1973
  try {
1229
- return (await fs2.readFile(join4(skillsCacheDir(), VERSION_FILE), "utf8")).trim();
1974
+ return (await fs2.readFile(join6(skillsCacheDir(), VERSION_FILE), "utf8")).trim();
1230
1975
  } catch {
1231
1976
  return;
1232
1977
  }
@@ -1247,19 +1992,19 @@ async function syncSkillsCache(api) {
1247
1992
  if (!dirent.isDirectory())
1248
1993
  continue;
1249
1994
  if (dirent.name === "beevibe" || dirent.name.startsWith("beevibe-")) {
1250
- await fs2.rm(join4(cache, dirent.name), { recursive: true, force: true });
1995
+ await fs2.rm(join6(cache, dirent.name), { recursive: true, force: true });
1251
1996
  }
1252
1997
  }
1253
1998
  for (const skill of res.skills) {
1254
- const skillDir = join4(cache, skill.name);
1999
+ const skillDir = join6(cache, skill.name);
1255
2000
  await fs2.mkdir(skillDir, { recursive: true, mode: 448 });
1256
2001
  for (const file of skill.files) {
1257
- const filePath = join4(skillDir, file.path);
1258
- await fs2.mkdir(join4(filePath, ".."), { recursive: true });
2002
+ const filePath = join6(skillDir, file.path);
2003
+ await fs2.mkdir(join6(filePath, ".."), { recursive: true });
1259
2004
  await fs2.writeFile(filePath, file.content, { mode: 384 });
1260
2005
  }
1261
2006
  }
1262
- await fs2.writeFile(join4(cache, VERSION_FILE), res.version, { mode: 384 });
2007
+ await fs2.writeFile(join6(cache, VERSION_FILE), res.version, { mode: 384 });
1263
2008
  return cache;
1264
2009
  }
1265
2010
 
@@ -1338,6 +2083,7 @@ async function runStart() {
1338
2083
  api,
1339
2084
  supervisor,
1340
2085
  workspaceManager,
2086
+ runtimeRegistry,
1341
2087
  runtimeIds: cfg.runtimes.map((r) => r.id)
1342
2088
  });
1343
2089
  claimer.start();
@@ -1353,16 +2099,46 @@ async function runStart() {
1353
2099
  };
1354
2100
  process.on("SIGINT", () => void stop("SIGINT"));
1355
2101
  process.on("SIGTERM", () => void stop("SIGTERM"));
2102
+ process.on("unhandledRejection", (reason) => {
2103
+ console.warn("[daemon] unhandledRejection (continuing):", reason instanceof Error ? reason.message : String(reason));
2104
+ });
1356
2105
  await new Promise(() => {
1357
2106
  return;
1358
2107
  });
1359
2108
  }
1360
2109
 
2110
+ // src/sync.ts
2111
+ async function runSync() {
2112
+ const config = loadConfig();
2113
+ if (!config) {
2114
+ throw new Error(`No daemon config at ${CONFIG_PATH}. Run 'beevibe-daemon setup' first.`);
2115
+ }
2116
+ const detected = await detectClis();
2117
+ if (detected.length === 0) {
2118
+ throw new Error(`No supported CLIs detected on PATH. beevibe currently looks for: ${KNOWN_CLIS.join(", ")}`);
2119
+ }
2120
+ const api = new ApiClient({
2121
+ apiUrl: config.api_url,
2122
+ daemonToken: config.daemon_token
2123
+ });
2124
+ const { status, body } = await api.post("/runtime/sync", {
2125
+ runtimes: detected
2126
+ });
2127
+ if (status !== 200 || !body) {
2128
+ throw new Error(`/runtime/sync failed: ${status}`);
2129
+ }
2130
+ const before = new Set(config.runtimes.map((r) => r.cli));
2131
+ const added = body.runtimes.filter((r) => !before.has(r.cli));
2132
+ const next = { ...config, runtimes: body.runtimes };
2133
+ saveConfig(next);
2134
+ return { added, runtimes: body.runtimes };
2135
+ }
2136
+
1361
2137
  // src/update.ts
1362
2138
  import { createHash } from "node:crypto";
1363
2139
  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";
2140
+ import { tmpdir as tmpdir4 } from "node:os";
2141
+ import { join as join7 } from "node:path";
1366
2142
  import { Readable } from "node:stream";
1367
2143
  import { pipeline } from "node:stream/promises";
1368
2144
  import { createInterface } from "node:readline/promises";
@@ -1376,7 +2152,7 @@ var PLATFORM_ASSETS = {
1376
2152
  "linux-arm64": "beevibe-daemon-linux-arm64"
1377
2153
  };
1378
2154
  function currentVersion() {
1379
- return "0.1.2";
2155
+ return "0.1.3";
1380
2156
  }
1381
2157
  function isCompiledBinary() {
1382
2158
  if (!process.versions.bun)
@@ -1489,8 +2265,8 @@ async function runUpdate(opts = {}) {
1489
2265
  return;
1490
2266
  }
1491
2267
  }
1492
- const stagingDir = mkdtempSync(join5(tmpdir2(), "beevibe-daemon-update-"));
1493
- const stagingPath = join5(stagingDir, asset);
2268
+ const stagingDir = mkdtempSync(join7(tmpdir4(), "beevibe-daemon-update-"));
2269
+ const stagingPath = join7(stagingDir, asset);
1494
2270
  try {
1495
2271
  console.log(`Downloading ${asset}…`);
1496
2272
  const downloadUrl = `${DOWNLOAD_BASE}/${latest}/${asset}`;
@@ -1548,6 +2324,7 @@ function printHelp() {
1548
2324
  "Commands:",
1549
2325
  " setup Register this machine with a beevibe api server.",
1550
2326
  " start Run the daemon: claim pending sessions and spawn the CLI.",
2327
+ " sync Re-detect CLIs on PATH and register newly-installed ones.",
1551
2328
  " update Check for and install a newer daemon binary (brew/curl installs).",
1552
2329
  "",
1553
2330
  "setup flags:",
@@ -1589,6 +2366,16 @@ async function main() {
1589
2366
  await runStart();
1590
2367
  return;
1591
2368
  }
2369
+ if (command === "sync") {
2370
+ const result = await runSync();
2371
+ if (result.added.length === 0) {
2372
+ console.log("No new CLIs detected.");
2373
+ } else {
2374
+ console.log(`Added ${result.added.length} runtime(s): ${result.added.map((r) => `${r.cli} (${r.id})`).join(", ")}.`);
2375
+ console.log("Restart the daemon to pick up the new runtime(s).");
2376
+ }
2377
+ return;
2378
+ }
1592
2379
  if (command === "update") {
1593
2380
  const skipPrompt = rest.includes("--yes") || rest.includes("-y");
1594
2381
  await runUpdate({ skipPrompt });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beevibe/daemon",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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",