@taewooopark/agent-blackbox 0.45.0 → 0.46.0
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.
|
@@ -81,6 +81,7 @@ function createTraceEvent(seq, input) {
|
|
|
81
81
|
runId: input.runId,
|
|
82
82
|
sessionId: input.sessionId,
|
|
83
83
|
...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
|
|
84
|
+
...input.cwd ? { cwd: input.cwd } : {},
|
|
84
85
|
...input.agentId ? { agentId: input.agentId } : {},
|
|
85
86
|
...input.agentRole ? { agentRole: input.agentRole } : {},
|
|
86
87
|
...input.turnId ? { turnId: input.turnId } : {},
|
|
@@ -111,6 +112,9 @@ function validateTraceEvent(event) {
|
|
|
111
112
|
requireEnum(event, "host", traceHosts, errors);
|
|
112
113
|
requireString(event, "runId", errors);
|
|
113
114
|
requireString(event, "sessionId", errors);
|
|
115
|
+
if (event.cwd !== void 0 && typeof event.cwd !== "string") {
|
|
116
|
+
errors.push("cwd must be a string when present");
|
|
117
|
+
}
|
|
114
118
|
requireEnum(event, "kind", traceEventKinds, errors);
|
|
115
119
|
requireEnum(event, "sensitivity", dataSensitivities, errors);
|
|
116
120
|
if (!isRecord(event.payload)) {
|
|
@@ -168,16 +172,16 @@ var defaultRedactionRules = [
|
|
|
168
172
|
pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/g,
|
|
169
173
|
replacement: "[REDACTED_GITHUB_TOKEN]"
|
|
170
174
|
},
|
|
171
|
-
{
|
|
172
|
-
name: "openai-key",
|
|
173
|
-
pattern: /\bsk-[A-Za-z0-9_-]{20,}\b/g,
|
|
174
|
-
replacement: "[REDACTED_OPENAI_KEY]"
|
|
175
|
-
},
|
|
176
175
|
{
|
|
177
176
|
name: "anthropic-key",
|
|
178
177
|
pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g,
|
|
179
178
|
replacement: "[REDACTED_ANTHROPIC_KEY]"
|
|
180
179
|
},
|
|
180
|
+
{
|
|
181
|
+
name: "openai-key",
|
|
182
|
+
pattern: /\bsk-[A-Za-z0-9_-]{20,}\b/g,
|
|
183
|
+
replacement: "[REDACTED_OPENAI_KEY]"
|
|
184
|
+
},
|
|
181
185
|
{
|
|
182
186
|
name: "private-key",
|
|
183
187
|
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
@@ -195,12 +199,12 @@ function redactJsonValue(value, options = {}) {
|
|
|
195
199
|
const visit = (current) => {
|
|
196
200
|
if (typeof current === "string") {
|
|
197
201
|
let next = current;
|
|
198
|
-
if (options.homeDir) {
|
|
199
|
-
next = replaceLiteral(next, options.homeDir, "~", "home-dir", applied);
|
|
200
|
-
}
|
|
201
202
|
if (options.projectDir) {
|
|
202
203
|
next = replaceLiteral(next, options.projectDir, "$PROJECT", "project-dir", applied);
|
|
203
204
|
}
|
|
205
|
+
if (options.homeDir) {
|
|
206
|
+
next = replaceLiteral(next, options.homeDir, "~", "home-dir", applied);
|
|
207
|
+
}
|
|
204
208
|
for (const rule of rules) {
|
|
205
209
|
if (rule.pattern.test(next)) {
|
|
206
210
|
applied.add(rule.name);
|
|
@@ -618,7 +622,9 @@ function sanitizeJson(value, seen) {
|
|
|
618
622
|
return "[Circular]";
|
|
619
623
|
}
|
|
620
624
|
seen.add(value);
|
|
621
|
-
|
|
625
|
+
const mapped = value.map((item) => sanitizeJson(item, seen));
|
|
626
|
+
seen.delete(value);
|
|
627
|
+
return mapped;
|
|
622
628
|
}
|
|
623
629
|
if (typeof value === "object") {
|
|
624
630
|
if (seen.has(value)) {
|
|
@@ -632,6 +638,7 @@ function sanitizeJson(value, seen) {
|
|
|
632
638
|
}
|
|
633
639
|
output[key] = sanitizeJson(nested, seen);
|
|
634
640
|
}
|
|
641
|
+
seen.delete(value);
|
|
635
642
|
return output;
|
|
636
643
|
}
|
|
637
644
|
return String(value);
|
|
@@ -851,18 +858,24 @@ async function appendTraceEvent(filePath, event) {
|
|
|
851
858
|
// packages/opencode-adapter/dist/sink.js
|
|
852
859
|
import { join } from "node:path";
|
|
853
860
|
function createTraceSink(options) {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
+
const base = options.sink ? options.sink : options.daemonUrl ? createHttpTraceSink(options.daemonUrl) : createFileTraceSink(options.eventsFile ?? join(options.directory, ".agent-blackbox", "events.ndjson"));
|
|
862
|
+
return {
|
|
863
|
+
async write(event) {
|
|
864
|
+
if (!event.cwd && options.directory) {
|
|
865
|
+
event.cwd = options.directory;
|
|
866
|
+
}
|
|
867
|
+
await base.write(event);
|
|
868
|
+
}
|
|
869
|
+
};
|
|
861
870
|
}
|
|
862
871
|
function createFileTraceSink(eventsFile) {
|
|
863
872
|
return {
|
|
864
873
|
async write(event) {
|
|
865
|
-
|
|
874
|
+
try {
|
|
875
|
+
await appendTraceEvent(eventsFile, event);
|
|
876
|
+
} catch (error) {
|
|
877
|
+
console.warn(`[agent-blackbox] could not write event ${event.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
878
|
+
}
|
|
866
879
|
}
|
|
867
880
|
};
|
|
868
881
|
}
|
|
@@ -933,7 +946,24 @@ function resolveRecorderOptions(options) {
|
|
|
933
946
|
}
|
|
934
947
|
|
|
935
948
|
// packages/opencode-adapter/dist/index.js
|
|
949
|
+
function noopRecorderHooks() {
|
|
950
|
+
return {
|
|
951
|
+
event: async () => {
|
|
952
|
+
},
|
|
953
|
+
"tool.execute.before": async () => {
|
|
954
|
+
},
|
|
955
|
+
"tool.execute.after": async () => {
|
|
956
|
+
},
|
|
957
|
+
"experimental.session.compacting": async () => {
|
|
958
|
+
},
|
|
959
|
+
"experimental.chat.system.transform": async () => {
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
}
|
|
936
963
|
async function createOpenCodeRecorder(context, options = {}) {
|
|
964
|
+
if (process.env.AGENT_BLACKBOX_DISABLE === "1") {
|
|
965
|
+
return noopRecorderHooks();
|
|
966
|
+
}
|
|
937
967
|
const resolved = resolveRecorderOptions(options);
|
|
938
968
|
const directory = context.directory ?? process.cwd();
|
|
939
969
|
const runId = resolved.runId ?? `opencode-${Date.now()}`;
|
|
@@ -1021,7 +1051,7 @@ function createOpenCodeEventFactory(options) {
|
|
|
1021
1051
|
const decision = decideReadServe(readCache.get(key), { hash, content: current }, compactionGen, path);
|
|
1022
1052
|
readCache.set(key, { hash, content: current, gen: compactionGen });
|
|
1023
1053
|
bumpFile(path, "read", hash);
|
|
1024
|
-
if (decision.mode !== "full" && typeof decision.output === "string") {
|
|
1054
|
+
if (decision.mode !== "full" && typeof decision.output === "string" && decision.saved > 0) {
|
|
1025
1055
|
output.output = decision.output;
|
|
1026
1056
|
}
|
|
1027
1057
|
};
|
package/dist/cli.js
CHANGED
|
@@ -61,6 +61,9 @@ function validateTraceEvent(event) {
|
|
|
61
61
|
requireEnum(event, "host", traceHosts, errors);
|
|
62
62
|
requireString(event, "runId", errors);
|
|
63
63
|
requireString(event, "sessionId", errors);
|
|
64
|
+
if (event.cwd !== void 0 && typeof event.cwd !== "string") {
|
|
65
|
+
errors.push("cwd must be a string when present");
|
|
66
|
+
}
|
|
64
67
|
requireEnum(event, "kind", traceEventKinds, errors);
|
|
65
68
|
requireEnum(event, "sensitivity", dataSensitivities, errors);
|
|
66
69
|
if (!isRecord(event.payload)) {
|
|
@@ -134,6 +137,9 @@ function replayWorkflowGraphAtSeq(events, seq) {
|
|
|
134
137
|
}
|
|
135
138
|
function replayWorkflowGraphAtTime(events, at) {
|
|
136
139
|
const atTime = new Date(at).getTime();
|
|
140
|
+
if (Number.isNaN(atTime)) {
|
|
141
|
+
throw new Error("replayWorkflowGraphAtTime: invalid time");
|
|
142
|
+
}
|
|
137
143
|
return materializeWorkflowGraph(events.filter((event) => new Date(event.ts).getTime() <= atTime));
|
|
138
144
|
}
|
|
139
145
|
function applyTraceEvent(graph, event) {
|
|
@@ -248,7 +254,8 @@ function ensureSession(graph, event) {
|
|
|
248
254
|
status: "ACTIVE",
|
|
249
255
|
at: event.ts,
|
|
250
256
|
eventId: event.id,
|
|
251
|
-
data: { sessionId: event.sessionId, parentSessionId: event.parentSessionId ?? null }
|
|
257
|
+
data: { sessionId: event.sessionId, parentSessionId: event.parentSessionId ?? null },
|
|
258
|
+
keepStatusIfExists: true
|
|
252
259
|
});
|
|
253
260
|
ensureEdge(graph, {
|
|
254
261
|
from: runNodeId(event.runId),
|
|
@@ -270,7 +277,8 @@ function ensureAgent(graph, event) {
|
|
|
270
277
|
status: "ACTIVE",
|
|
271
278
|
at: event.ts,
|
|
272
279
|
eventId: event.id,
|
|
273
|
-
data: { agentId: event.agentId, agentRole: event.agentRole ?? "unknown" }
|
|
280
|
+
data: { agentId: event.agentId, agentRole: event.agentRole ?? "unknown" },
|
|
281
|
+
keepStatusIfExists: true
|
|
274
282
|
});
|
|
275
283
|
ensureEdge(graph, {
|
|
276
284
|
from: sessionNodeId(event.sessionId),
|
|
@@ -292,7 +300,8 @@ function ensureTurn(graph, event) {
|
|
|
292
300
|
status: "ACTIVE",
|
|
293
301
|
at: event.ts,
|
|
294
302
|
eventId: event.id,
|
|
295
|
-
data: { turnId: event.turnId }
|
|
303
|
+
data: { turnId: event.turnId },
|
|
304
|
+
keepStatusIfExists: true
|
|
296
305
|
});
|
|
297
306
|
ensureEdge(graph, {
|
|
298
307
|
from: event.agentId ? agentNodeId(event.agentId) : sessionNodeId(event.sessionId),
|
|
@@ -360,7 +369,9 @@ function ensureNode(graph, input) {
|
|
|
360
369
|
const existing = graph.nodes.get(input.id);
|
|
361
370
|
if (existing) {
|
|
362
371
|
existing.updatedAt = input.at;
|
|
363
|
-
|
|
372
|
+
if (!input.keepStatusIfExists) {
|
|
373
|
+
existing.status = input.status;
|
|
374
|
+
}
|
|
364
375
|
existing.data = { ...existing.data, ...input.data ?? {} };
|
|
365
376
|
if (input.eventId && !existing.eventIds.includes(input.eventId)) {
|
|
366
377
|
existing.eventIds.push(input.eventId);
|
|
@@ -514,7 +525,8 @@ function eventNodeIdFromEventId(eventId) {
|
|
|
514
525
|
return `event:${stablePart(eventId)}`;
|
|
515
526
|
}
|
|
516
527
|
function workflowEdgeId(from, to, type) {
|
|
517
|
-
|
|
528
|
+
const enc = (s) => s.replace(/:/g, "__");
|
|
529
|
+
return `edge:${type}:${enc(stablePart(from))}:${enc(stablePart(to))}`;
|
|
518
530
|
}
|
|
519
531
|
function stablePart(value) {
|
|
520
532
|
return value.replace(/[^a-zA-Z0-9_.:-]/g, "_");
|
|
@@ -842,7 +854,7 @@ function computeEfficiencyReport(events) {
|
|
|
842
854
|
label: "Large context injections",
|
|
843
855
|
value: biggest,
|
|
844
856
|
unit: "tokens",
|
|
845
|
-
display: biggest >=
|
|
857
|
+
display: biggest >= 5e3 ? formatTokens(biggest) : "none",
|
|
846
858
|
score,
|
|
847
859
|
status,
|
|
848
860
|
detail: over5k.length === 0 ? "No single tool output flooded the context." : `${over5k.length} output(s) added 5k+ tokens (largest ${formatTokens(biggest)}) \u2014 scope greps/reads or summarise.`,
|
|
@@ -979,7 +991,7 @@ function stringAt(event, key) {
|
|
|
979
991
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
980
992
|
}
|
|
981
993
|
function readTokenSnapshot(event) {
|
|
982
|
-
const present =
|
|
994
|
+
const present = isRecordAtPath(event.payload, "properties.info.tokens") || isRecordAtPath(event.payload, "properties.tokens") || isRecordAtPath(event.payload, "tokens");
|
|
983
995
|
if (!present) return void 0;
|
|
984
996
|
return {
|
|
985
997
|
input: deepNumber(event.payload, ["properties.info.tokens.input", "properties.tokens.input", "tokens.input"]) ?? 0,
|
|
@@ -1082,13 +1094,13 @@ function buildEfficiencyMemory(report, options = {}) {
|
|
|
1082
1094
|
].join("\n");
|
|
1083
1095
|
}
|
|
1084
1096
|
var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1085
|
-
var managedBlockRegExp = () => new RegExp(`${escapeRegExp(EFFICIENCY_MEMORY_START)}[\\s\\S]*?${escapeRegExp(EFFICIENCY_MEMORY_END)}
|
|
1097
|
+
var managedBlockRegExp = () => new RegExp(`${escapeRegExp(EFFICIENCY_MEMORY_START)}[\\s\\S]*?${escapeRegExp(EFFICIENCY_MEMORY_END)}`, "g");
|
|
1086
1098
|
function hasManagedBlock(content) {
|
|
1087
1099
|
return managedBlockRegExp().test(content);
|
|
1088
1100
|
}
|
|
1089
1101
|
function upsertManagedBlock(content, block) {
|
|
1090
1102
|
if (hasManagedBlock(content)) {
|
|
1091
|
-
return content.replace(managedBlockRegExp(), block);
|
|
1103
|
+
return content.replace(managedBlockRegExp(), () => block);
|
|
1092
1104
|
}
|
|
1093
1105
|
const base = content.trimEnd();
|
|
1094
1106
|
return base.length === 0 ? `${block}
|
|
@@ -1139,7 +1151,14 @@ async function startDashboardServer(options) {
|
|
|
1139
1151
|
const injected = indexHtml.replace("</head>", ` <script>window.AGENT_BLACKBOX_DAEMON_URL=${JSON.stringify(options.daemonUrl)};</script>
|
|
1140
1152
|
</head>`);
|
|
1141
1153
|
const server = createServer((request, response) => {
|
|
1142
|
-
|
|
1154
|
+
let rawPath;
|
|
1155
|
+
try {
|
|
1156
|
+
rawPath = decodeURIComponent((request.url ?? "/").split("?")[0] ?? "/");
|
|
1157
|
+
} catch {
|
|
1158
|
+
response.writeHead(400);
|
|
1159
|
+
response.end("Bad Request");
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1143
1162
|
if (rawPath === "/" || rawPath === "/index.html") {
|
|
1144
1163
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1145
1164
|
response.end(injected);
|
|
@@ -1157,15 +1176,21 @@ async function startDashboardServer(options) {
|
|
|
1157
1176
|
throw new Error("not a file");
|
|
1158
1177
|
}
|
|
1159
1178
|
response.writeHead(200, { "content-type": mimeTypes[extname(filePath)] ?? "application/octet-stream" });
|
|
1160
|
-
createReadStream(filePath)
|
|
1179
|
+
const rs = createReadStream(filePath);
|
|
1180
|
+
rs.on("error", () => response.destroy());
|
|
1181
|
+
rs.pipe(response);
|
|
1161
1182
|
}).catch(() => {
|
|
1162
1183
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1163
1184
|
response.end(injected);
|
|
1164
1185
|
});
|
|
1165
1186
|
});
|
|
1166
1187
|
const port = options.port ?? 5173;
|
|
1167
|
-
await new Promise((resolve2) => {
|
|
1168
|
-
server.
|
|
1188
|
+
await new Promise((resolve2, reject) => {
|
|
1189
|
+
server.once("error", reject);
|
|
1190
|
+
server.listen(port, "127.0.0.1", () => {
|
|
1191
|
+
server.off("error", reject);
|
|
1192
|
+
resolve2();
|
|
1193
|
+
});
|
|
1169
1194
|
});
|
|
1170
1195
|
const address = server.address();
|
|
1171
1196
|
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
@@ -1197,7 +1222,19 @@ function parseTraceEventLine(line) {
|
|
|
1197
1222
|
return parsed;
|
|
1198
1223
|
}
|
|
1199
1224
|
function parseTraceEvents(input) {
|
|
1200
|
-
|
|
1225
|
+
const lines = input.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
1226
|
+
const lastIsPossiblyTorn = lines.length > 0 && !/\r?\n$/.test(input);
|
|
1227
|
+
const completeCount = lastIsPossiblyTorn ? lines.length - 1 : lines.length;
|
|
1228
|
+
const events = lines.slice(0, completeCount).map((line) => parseTraceEventLine(line));
|
|
1229
|
+
if (lastIsPossiblyTorn) {
|
|
1230
|
+
const lastLine = lines[lines.length - 1];
|
|
1231
|
+
try {
|
|
1232
|
+
events.push(parseTraceEventLine(lastLine));
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return events;
|
|
1201
1238
|
}
|
|
1202
1239
|
async function appendTraceEvent(filePath, event) {
|
|
1203
1240
|
await mkdir(dirname(filePath), { recursive: true });
|
|
@@ -1226,10 +1263,11 @@ async function runOptimize(options) {
|
|
|
1226
1263
|
}
|
|
1227
1264
|
async function computeOptimize(options) {
|
|
1228
1265
|
const eventsFile = options.eventsFile ?? join2(options.projectDir, ".agent-blackbox", "events.ndjson");
|
|
1229
|
-
const agentsMdPath = join2(options.projectDir, "AGENTS.md");
|
|
1230
|
-
const statePath = join2(options.projectDir, ".agent-blackbox", "optimization.json");
|
|
1231
1266
|
const events = await loadTraceEvents(eventsFile);
|
|
1232
1267
|
const { runId, events: runEvents } = latestRun(events);
|
|
1268
|
+
const targetDir = runEvents.find((e) => typeof e.cwd === "string" && e.cwd.length > 0)?.cwd ?? options.projectDir;
|
|
1269
|
+
const agentsMdPath = join2(targetDir, "AGENTS.md");
|
|
1270
|
+
const statePath = join2(targetDir, ".agent-blackbox", "optimization.json");
|
|
1233
1271
|
const latestTs = runEvents.reduce((max, e) => e.ts > max ? e.ts : max, "");
|
|
1234
1272
|
const report = runEvents.length > 0 ? computeEfficiencyReport(runEvents) : null;
|
|
1235
1273
|
const score = report ? report.overallScore : null;
|
|
@@ -1255,15 +1293,17 @@ async function computeOptimize(options) {
|
|
|
1255
1293
|
}
|
|
1256
1294
|
const prior = await readMaybe(agentsMdPath);
|
|
1257
1295
|
const next = upsertManagedBlock(prior ?? "", block);
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1296
|
+
if (prior === null || next !== prior) {
|
|
1297
|
+
await writeFile(agentsMdPath, next, "utf8");
|
|
1298
|
+
await writeState(statePath, {
|
|
1299
|
+
runId: runId ?? "",
|
|
1300
|
+
baselineScore: score,
|
|
1301
|
+
baselineLatestTs: latestTs,
|
|
1302
|
+
baselineFlagged: flaggedIds(report),
|
|
1303
|
+
fileExisted: prior !== null,
|
|
1304
|
+
appliedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1267
1307
|
return {
|
|
1268
1308
|
mode: "apply",
|
|
1269
1309
|
action: `Wrote efficiency memory to AGENTS.md \u2014 targets ~${report.reclaimableTokens} reclaimable tokens on similar future runs (no re-run needed). Optional: re-run the same task + \`optimize --check\` to benchmark the gain.`,
|
|
@@ -1470,7 +1510,7 @@ function buildDigest(report) {
|
|
|
1470
1510
|
id: m.id,
|
|
1471
1511
|
label: m.label,
|
|
1472
1512
|
status: m.status,
|
|
1473
|
-
value: Number(m.value.toFixed(3)),
|
|
1513
|
+
value: Number((typeof m.value === "number" && Number.isFinite(m.value) ? m.value : 0).toFixed(3)),
|
|
1474
1514
|
display: m.display,
|
|
1475
1515
|
detail: m.detail,
|
|
1476
1516
|
...m.reclaimableTokens ? { reclaimableTokens: m.reclaimableTokens } : {},
|
|
@@ -1492,6 +1532,8 @@ var freeCooldownUntil = /* @__PURE__ */ new Map();
|
|
|
1492
1532
|
function orderFreePool(pool, cooldownUntil, cursor, now) {
|
|
1493
1533
|
const fresh = pool.filter((entry) => (cooldownUntil.get(entry.model) ?? 0) <= now);
|
|
1494
1534
|
const list = fresh.length > 0 ? fresh : pool;
|
|
1535
|
+
if (list.length === 0)
|
|
1536
|
+
return [];
|
|
1495
1537
|
const start = (cursor % list.length + list.length) % list.length;
|
|
1496
1538
|
return [...list.slice(start), ...list.slice(0, start)];
|
|
1497
1539
|
}
|
|
@@ -1636,7 +1678,10 @@ async function fetchJson(url, body, extraHeaders = {}) {
|
|
|
1636
1678
|
}
|
|
1637
1679
|
function runCommand(command, args2) {
|
|
1638
1680
|
return new Promise((resolve2, reject) => {
|
|
1639
|
-
const child = spawn(command, args2, {
|
|
1681
|
+
const child = spawn(command, args2, {
|
|
1682
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1683
|
+
env: { ...process.env, AGENT_BLACKBOX_DISABLE: "1" }
|
|
1684
|
+
});
|
|
1640
1685
|
let out = "";
|
|
1641
1686
|
const timer = setTimeout(() => {
|
|
1642
1687
|
child.kill("SIGKILL");
|
|
@@ -1663,10 +1708,24 @@ function extractJsonObject(text) {
|
|
|
1663
1708
|
if (start === -1)
|
|
1664
1709
|
return void 0;
|
|
1665
1710
|
let depth = 0;
|
|
1711
|
+
let inString = false;
|
|
1712
|
+
let escaped = false;
|
|
1666
1713
|
for (let i = start; i < text.length; i += 1) {
|
|
1667
|
-
|
|
1714
|
+
const ch = text[i];
|
|
1715
|
+
if (inString) {
|
|
1716
|
+
if (escaped)
|
|
1717
|
+
escaped = false;
|
|
1718
|
+
else if (ch === "\\")
|
|
1719
|
+
escaped = true;
|
|
1720
|
+
else if (ch === '"')
|
|
1721
|
+
inString = false;
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
if (ch === '"')
|
|
1725
|
+
inString = true;
|
|
1726
|
+
else if (ch === "{")
|
|
1668
1727
|
depth += 1;
|
|
1669
|
-
else if (
|
|
1728
|
+
else if (ch === "}") {
|
|
1670
1729
|
depth -= 1;
|
|
1671
1730
|
if (depth === 0) {
|
|
1672
1731
|
try {
|
|
@@ -1704,8 +1763,12 @@ async function startTraceDaemon(options) {
|
|
|
1704
1763
|
});
|
|
1705
1764
|
});
|
|
1706
1765
|
const port = options.port ?? 47831;
|
|
1707
|
-
await new Promise((resolve2) => {
|
|
1708
|
-
server.
|
|
1766
|
+
await new Promise((resolve2, reject) => {
|
|
1767
|
+
server.once("error", reject);
|
|
1768
|
+
server.listen(port, "127.0.0.1", () => {
|
|
1769
|
+
server.off("error", reject);
|
|
1770
|
+
resolve2();
|
|
1771
|
+
});
|
|
1709
1772
|
});
|
|
1710
1773
|
const address = server.address();
|
|
1711
1774
|
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
@@ -1714,8 +1777,12 @@ async function startTraceDaemon(options) {
|
|
|
1714
1777
|
port: actualPort,
|
|
1715
1778
|
eventsFile,
|
|
1716
1779
|
close: () => new Promise((resolve2, reject) => {
|
|
1780
|
+
for (const client of clients) {
|
|
1781
|
+
client.terminate();
|
|
1782
|
+
}
|
|
1783
|
+
clients.clear();
|
|
1784
|
+
streamServer.close();
|
|
1717
1785
|
server.close((error) => {
|
|
1718
|
-
streamServer.close();
|
|
1719
1786
|
if (error) {
|
|
1720
1787
|
reject(error);
|
|
1721
1788
|
} else {
|
|
@@ -1769,11 +1836,11 @@ async function buildTraceSnapshot(eventsFile, replay = {}) {
|
|
|
1769
1836
|
async function handleRequest(request, response, eventsFile, clients, suggestConfig, projectDir) {
|
|
1770
1837
|
try {
|
|
1771
1838
|
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
1772
|
-
const replay = parseReplayQuery(url);
|
|
1773
1839
|
if (request.method === "OPTIONS") {
|
|
1774
1840
|
sendEmpty(response, 204);
|
|
1775
1841
|
return;
|
|
1776
1842
|
}
|
|
1843
|
+
const replay = parseReplayQuery(url);
|
|
1777
1844
|
if (request.method === "GET" && url.pathname === "/health") {
|
|
1778
1845
|
sendJson(response, 200, { ok: true, data: { status: "ok", eventsFile } });
|
|
1779
1846
|
return;
|
|
@@ -1868,13 +1935,27 @@ async function sendSnapshot(client, eventsFile) {
|
|
|
1868
1935
|
}
|
|
1869
1936
|
}
|
|
1870
1937
|
}
|
|
1938
|
+
var MAX_BODY_BYTES = 5e7;
|
|
1871
1939
|
async function readJsonBody(request) {
|
|
1872
1940
|
const chunks = [];
|
|
1941
|
+
let total = 0;
|
|
1873
1942
|
for await (const chunk of request) {
|
|
1874
|
-
|
|
1943
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1944
|
+
total += buffer.length;
|
|
1945
|
+
if (total > MAX_BODY_BYTES) {
|
|
1946
|
+
throw new BadRequestError("Request body too large");
|
|
1947
|
+
}
|
|
1948
|
+
chunks.push(buffer);
|
|
1875
1949
|
}
|
|
1876
1950
|
const raw = Buffer.concat(chunks).toString("utf8");
|
|
1877
|
-
|
|
1951
|
+
if (raw.length === 0) {
|
|
1952
|
+
return {};
|
|
1953
|
+
}
|
|
1954
|
+
try {
|
|
1955
|
+
return JSON.parse(raw);
|
|
1956
|
+
} catch {
|
|
1957
|
+
throw new BadRequestError("Invalid JSON body");
|
|
1958
|
+
}
|
|
1878
1959
|
}
|
|
1879
1960
|
function sendJson(response, statusCode, payload) {
|
|
1880
1961
|
response.writeHead(statusCode, {
|
|
@@ -2059,7 +2140,10 @@ function globalDataDir() {
|
|
|
2059
2140
|
const xdg = process.env.XDG_DATA_HOME;
|
|
2060
2141
|
return xdg && xdg.length > 0 ? join6(xdg, "agent-blackbox") : join6(homedir2(), ".local", "share", "agent-blackbox");
|
|
2061
2142
|
}
|
|
2062
|
-
void main(args)
|
|
2143
|
+
void main(args).catch((error) => {
|
|
2144
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2145
|
+
process.exitCode = 1;
|
|
2146
|
+
});
|
|
2063
2147
|
async function main(argv) {
|
|
2064
2148
|
if (argv.includes("--version") || argv.includes("-v")) {
|
|
2065
2149
|
console.log(AGENT_BLACKBOX_DAEMON_VERSION);
|
|
@@ -2068,7 +2152,7 @@ async function main(argv) {
|
|
|
2068
2152
|
const command = argv[0] ?? "help";
|
|
2069
2153
|
if (command === "daemon") {
|
|
2070
2154
|
const projectDir = readFlag(argv, "--project") ?? process.cwd();
|
|
2071
|
-
const port =
|
|
2155
|
+
const port = portArg(readFlag(argv, "--port"), 47831);
|
|
2072
2156
|
const daemon = await startTraceDaemon({ projectDir, port });
|
|
2073
2157
|
console.log(`Agent-Blackbox daemon listening on http://127.0.0.1:${daemon.port}`);
|
|
2074
2158
|
console.log(`Trace file: ${daemon.eventsFile}`);
|
|
@@ -2077,8 +2161,8 @@ async function main(argv) {
|
|
|
2077
2161
|
if (command === "up") {
|
|
2078
2162
|
const projectFlag = readFlag(argv, "--project");
|
|
2079
2163
|
const global = projectFlag === void 0;
|
|
2080
|
-
const port =
|
|
2081
|
-
const uiPort =
|
|
2164
|
+
const port = portArg(readFlag(argv, "--port"), 47831);
|
|
2165
|
+
const uiPort = portArg(readFlag(argv, "--ui-port"), 5173);
|
|
2082
2166
|
const daemonUrl = `http://127.0.0.1:${port}`;
|
|
2083
2167
|
const suggest = readSuggestConfig(argv);
|
|
2084
2168
|
let daemon;
|
|
@@ -2145,7 +2229,7 @@ async function main(argv) {
|
|
|
2145
2229
|
return;
|
|
2146
2230
|
}
|
|
2147
2231
|
if (command === "install") {
|
|
2148
|
-
const port =
|
|
2232
|
+
const port = portArg(readFlag(argv, "--port"), 47831);
|
|
2149
2233
|
const daemonUrl = `http://127.0.0.1:${port}`;
|
|
2150
2234
|
if (!pluginBundlePath) {
|
|
2151
2235
|
throw new Error("Global install needs the self-contained recorder bundle. Use the published npx package, or `npm run build:cli` first.");
|
|
@@ -2247,6 +2331,10 @@ function readFlag(argv, flag) {
|
|
|
2247
2331
|
}
|
|
2248
2332
|
return argv[index + 1];
|
|
2249
2333
|
}
|
|
2334
|
+
function portArg(raw, fallback) {
|
|
2335
|
+
const n = Number(raw);
|
|
2336
|
+
return Number.isInteger(n) && n >= 0 && n <= 65535 ? n : fallback;
|
|
2337
|
+
}
|
|
2250
2338
|
function readSuggestConfig(argv) {
|
|
2251
2339
|
const modes = ["auto", "off", "free", "ollama", "opencode", "openai-compat"];
|
|
2252
2340
|
const raw = readFlag(argv, "--suggest") ?? process.env.AGENT_BLACKBOX_SUGGEST ?? "auto";
|