@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.
- package/dist/main.js +1578 -119
- 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
|
|
941
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
948
|
-
|
|
1178
|
+
// ../core/dist/adapters/codex/runtime.js
|
|
1179
|
+
var OPENAI_AUTH_VARS = ["OPENAI_API_KEY", "OPENAI_AUTH_TOKEN"];
|
|
949
1180
|
|
|
950
|
-
class
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
}
|
|
955
|
-
|
|
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
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
|
|
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 (
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
-
|
|
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 {
|
|
1292
|
+
return {
|
|
1293
|
+
healthy: false,
|
|
1294
|
+
error: `Command not found: ${this.config.command ?? "codex"}`
|
|
1295
|
+
};
|
|
981
1296
|
}
|
|
982
1297
|
}
|
|
983
|
-
async
|
|
984
|
-
|
|
985
|
-
|
|
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
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
|
2639
|
+
import { join as join8 } from "node:path";
|
|
1223
2640
|
function skillsCacheDir() {
|
|
1224
|
-
return
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
1258
|
-
await fs2.mkdir(
|
|
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(
|
|
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
|
|
1365
|
-
import { join as
|
|
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.
|
|
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(
|
|
1493
|
-
const stagingPath =
|
|
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 });
|