@taewooopark/agent-blackbox 0.45.0 → 0.46.1

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);
@@ -429,6 +433,7 @@ function normalizeToolBefore(input, output, context) {
429
433
  const agentId = extractAgentId(input);
430
434
  if (agentId) {
431
435
  traceInput.agentId = agentId;
436
+ traceInput.agentRole = extractAgentRole(input);
432
437
  }
433
438
  return createTraceEvent(context.seq, traceInput);
434
439
  }
@@ -462,6 +467,7 @@ function normalizeToolAfter(input, output, context) {
462
467
  const agentId = extractAgentId(input);
463
468
  if (agentId) {
464
469
  traceInput.agentId = agentId;
470
+ traceInput.agentRole = extractAgentRole(input);
465
471
  }
466
472
  }
467
473
  return createTraceEvent(context.seq, traceInput);
@@ -618,7 +624,9 @@ function sanitizeJson(value, seen) {
618
624
  return "[Circular]";
619
625
  }
620
626
  seen.add(value);
621
- return value.map((item) => sanitizeJson(item, seen));
627
+ const mapped = value.map((item) => sanitizeJson(item, seen));
628
+ seen.delete(value);
629
+ return mapped;
622
630
  }
623
631
  if (typeof value === "object") {
624
632
  if (seen.has(value)) {
@@ -632,6 +640,7 @@ function sanitizeJson(value, seen) {
632
640
  }
633
641
  output[key] = sanitizeJson(nested, seen);
634
642
  }
643
+ seen.delete(value);
635
644
  return output;
636
645
  }
637
646
  return String(value);
@@ -843,26 +852,48 @@ function serializeTraceEvent(event) {
843
852
  return `${JSON.stringify(event)}
844
853
  `;
845
854
  }
855
+ var writeChains = /* @__PURE__ */ new Map();
846
856
  async function appendTraceEvent(filePath, event) {
847
- await mkdir(dirname(filePath), { recursive: true });
848
- await appendFile(filePath, serializeTraceEvent(event), "utf8");
857
+ const line = serializeTraceEvent(event);
858
+ const run = async () => {
859
+ await mkdir(dirname(filePath), { recursive: true });
860
+ await appendFile(filePath, line, "utf8");
861
+ };
862
+ const prev = writeChains.get(filePath) ?? Promise.resolve();
863
+ const next = prev.then(run, run);
864
+ const tail = next.then(
865
+ () => void 0,
866
+ () => void 0
867
+ );
868
+ writeChains.set(filePath, tail);
869
+ try {
870
+ await next;
871
+ } finally {
872
+ if (writeChains.get(filePath) === tail) writeChains.delete(filePath);
873
+ }
849
874
  }
850
875
 
851
876
  // packages/opencode-adapter/dist/sink.js
852
877
  import { join } from "node:path";
853
878
  function createTraceSink(options) {
854
- if (options.sink) {
855
- return options.sink;
856
- }
857
- if (options.daemonUrl) {
858
- return createHttpTraceSink(options.daemonUrl);
859
- }
860
- return createFileTraceSink(options.eventsFile ?? join(options.directory, ".agent-blackbox", "events.ndjson"));
879
+ const base = options.sink ? options.sink : options.daemonUrl ? createHttpTraceSink(options.daemonUrl) : createFileTraceSink(options.eventsFile ?? join(options.directory, ".agent-blackbox", "events.ndjson"));
880
+ return {
881
+ async write(event) {
882
+ if (!event.cwd && options.directory) {
883
+ event.cwd = options.directory;
884
+ }
885
+ await base.write(event);
886
+ }
887
+ };
861
888
  }
862
889
  function createFileTraceSink(eventsFile) {
863
890
  return {
864
891
  async write(event) {
865
- await appendTraceEvent(eventsFile, event);
892
+ try {
893
+ await appendTraceEvent(eventsFile, event);
894
+ } catch (error) {
895
+ console.warn(`[agent-blackbox] could not write event ${event.id}: ${error instanceof Error ? error.message : String(error)}`);
896
+ }
866
897
  }
867
898
  };
868
899
  }
@@ -933,7 +964,24 @@ function resolveRecorderOptions(options) {
933
964
  }
934
965
 
935
966
  // packages/opencode-adapter/dist/index.js
967
+ function noopRecorderHooks() {
968
+ return {
969
+ event: async () => {
970
+ },
971
+ "tool.execute.before": async () => {
972
+ },
973
+ "tool.execute.after": async () => {
974
+ },
975
+ "experimental.session.compacting": async () => {
976
+ },
977
+ "experimental.chat.system.transform": async () => {
978
+ }
979
+ };
980
+ }
936
981
  async function createOpenCodeRecorder(context, options = {}) {
982
+ if (process.env.AGENT_BLACKBOX_DISABLE === "1") {
983
+ return noopRecorderHooks();
984
+ }
937
985
  const resolved = resolveRecorderOptions(options);
938
986
  const directory = context.directory ?? process.cwd();
939
987
  const runId = resolved.runId ?? `opencode-${Date.now()}`;
@@ -1021,7 +1069,7 @@ function createOpenCodeEventFactory(options) {
1021
1069
  const decision = decideReadServe(readCache.get(key), { hash, content: current }, compactionGen, path);
1022
1070
  readCache.set(key, { hash, content: current, gen: compactionGen });
1023
1071
  bumpFile(path, "read", hash);
1024
- if (decision.mode !== "full" && typeof decision.output === "string") {
1072
+ if (decision.mode !== "full" && typeof decision.output === "string" && decision.saved > 0) {
1025
1073
  output.output = decision.output;
1026
1074
  }
1027
1075
  };
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
- existing.status = input.status;
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
- return `edge:${type}:${stablePart(from)}:${stablePart(to)}`;
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 >= 2e3 ? formatTokens(biggest) : "none",
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 = deepNumber(event.payload, "properties.info.tokens") !== void 0 || deepNumber(event.payload, "properties.tokens") !== void 0 || deepNumber(event.payload, "tokens") !== void 0 || isRecordAtPath(event.payload, "properties.info.tokens") || isRecordAtPath(event.payload, "properties.tokens") || isRecordAtPath(event.payload, "tokens");
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
- const rawPath = decodeURIComponent((request.url ?? "/").split("?")[0] ?? "/");
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).pipe(response);
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.listen(port, "127.0.0.1", resolve2);
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,11 +1222,39 @@ function parseTraceEventLine(line) {
1197
1222
  return parsed;
1198
1223
  }
1199
1224
  function parseTraceEvents(input) {
1200
- return input.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => parseTraceEventLine(line));
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
  }
1239
+ var writeChains = /* @__PURE__ */ new Map();
1202
1240
  async function appendTraceEvent(filePath, event) {
1203
- await mkdir(dirname(filePath), { recursive: true });
1204
- await appendFile(filePath, serializeTraceEvent(event), "utf8");
1241
+ const line = serializeTraceEvent(event);
1242
+ const run = async () => {
1243
+ await mkdir(dirname(filePath), { recursive: true });
1244
+ await appendFile(filePath, line, "utf8");
1245
+ };
1246
+ const prev = writeChains.get(filePath) ?? Promise.resolve();
1247
+ const next = prev.then(run, run);
1248
+ const tail = next.then(
1249
+ () => void 0,
1250
+ () => void 0
1251
+ );
1252
+ writeChains.set(filePath, tail);
1253
+ try {
1254
+ await next;
1255
+ } finally {
1256
+ if (writeChains.get(filePath) === tail) writeChains.delete(filePath);
1257
+ }
1205
1258
  }
1206
1259
  async function readTraceEvents(filePath) {
1207
1260
  const input = await readFile2(filePath, "utf8");
@@ -1215,7 +1268,7 @@ import { WebSocket, WebSocketServer } from "ws";
1215
1268
 
1216
1269
  // apps/daemon/dist/optimize.js
1217
1270
  import { mkdir as mkdir2, readFile as readFile3, rm, writeFile } from "node:fs/promises";
1218
- import { dirname as dirname2, join as join2 } from "node:path";
1271
+ import { dirname as dirname2, isAbsolute, join as join2 } from "node:path";
1219
1272
  var flaggedIds = (report) => report.metrics.filter((m) => m.status !== "good").map((m) => m.id);
1220
1273
  var joinIds = (ids) => ids.join(", ");
1221
1274
  var REVERT_MARGIN = 3;
@@ -1226,10 +1279,12 @@ async function runOptimize(options) {
1226
1279
  }
1227
1280
  async function computeOptimize(options) {
1228
1281
  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
1282
  const events = await loadTraceEvents(eventsFile);
1232
1283
  const { runId, events: runEvents } = latestRun(events);
1284
+ const runCwd = runEvents.find((e) => typeof e.cwd === "string" && e.cwd.length > 0)?.cwd;
1285
+ const targetDir = runCwd && isAbsolute(runCwd) ? runCwd : options.projectDir;
1286
+ const agentsMdPath = join2(targetDir, "AGENTS.md");
1287
+ const statePath = join2(targetDir, ".agent-blackbox", "optimization.json");
1233
1288
  const latestTs = runEvents.reduce((max, e) => e.ts > max ? e.ts : max, "");
1234
1289
  const report = runEvents.length > 0 ? computeEfficiencyReport(runEvents) : null;
1235
1290
  const score = report ? report.overallScore : null;
@@ -1255,15 +1310,17 @@ async function computeOptimize(options) {
1255
1310
  }
1256
1311
  const prior = await readMaybe(agentsMdPath);
1257
1312
  const next = upsertManagedBlock(prior ?? "", block);
1258
- await writeFile(agentsMdPath, next, "utf8");
1259
- await writeState(statePath, {
1260
- runId: runId ?? "",
1261
- baselineScore: score,
1262
- baselineLatestTs: latestTs,
1263
- baselineFlagged: flaggedIds(report),
1264
- fileExisted: prior !== null,
1265
- appliedAt: (/* @__PURE__ */ new Date()).toISOString()
1266
- });
1313
+ if (prior === null || next !== prior) {
1314
+ await writeFile(agentsMdPath, next, "utf8");
1315
+ await writeState(statePath, {
1316
+ runId: runId ?? "",
1317
+ baselineScore: score,
1318
+ baselineLatestTs: latestTs,
1319
+ baselineFlagged: flaggedIds(report),
1320
+ fileExisted: prior !== null,
1321
+ appliedAt: (/* @__PURE__ */ new Date()).toISOString()
1322
+ });
1323
+ }
1267
1324
  return {
1268
1325
  mode: "apply",
1269
1326
  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 +1527,7 @@ function buildDigest(report) {
1470
1527
  id: m.id,
1471
1528
  label: m.label,
1472
1529
  status: m.status,
1473
- value: Number(m.value.toFixed(3)),
1530
+ value: Number((typeof m.value === "number" && Number.isFinite(m.value) ? m.value : 0).toFixed(3)),
1474
1531
  display: m.display,
1475
1532
  detail: m.detail,
1476
1533
  ...m.reclaimableTokens ? { reclaimableTokens: m.reclaimableTokens } : {},
@@ -1492,6 +1549,8 @@ var freeCooldownUntil = /* @__PURE__ */ new Map();
1492
1549
  function orderFreePool(pool, cooldownUntil, cursor, now) {
1493
1550
  const fresh = pool.filter((entry) => (cooldownUntil.get(entry.model) ?? 0) <= now);
1494
1551
  const list = fresh.length > 0 ? fresh : pool;
1552
+ if (list.length === 0)
1553
+ return [];
1495
1554
  const start = (cursor % list.length + list.length) % list.length;
1496
1555
  return [...list.slice(start), ...list.slice(0, start)];
1497
1556
  }
@@ -1636,7 +1695,10 @@ async function fetchJson(url, body, extraHeaders = {}) {
1636
1695
  }
1637
1696
  function runCommand(command, args2) {
1638
1697
  return new Promise((resolve2, reject) => {
1639
- const child = spawn(command, args2, { stdio: ["ignore", "pipe", "ignore"] });
1698
+ const child = spawn(command, args2, {
1699
+ stdio: ["ignore", "pipe", "ignore"],
1700
+ env: { ...process.env, AGENT_BLACKBOX_DISABLE: "1" }
1701
+ });
1640
1702
  let out = "";
1641
1703
  const timer = setTimeout(() => {
1642
1704
  child.kill("SIGKILL");
@@ -1663,10 +1725,24 @@ function extractJsonObject(text) {
1663
1725
  if (start === -1)
1664
1726
  return void 0;
1665
1727
  let depth = 0;
1728
+ let inString = false;
1729
+ let escaped = false;
1666
1730
  for (let i = start; i < text.length; i += 1) {
1667
- if (text[i] === "{")
1731
+ const ch = text[i];
1732
+ if (inString) {
1733
+ if (escaped)
1734
+ escaped = false;
1735
+ else if (ch === "\\")
1736
+ escaped = true;
1737
+ else if (ch === '"')
1738
+ inString = false;
1739
+ continue;
1740
+ }
1741
+ if (ch === '"')
1742
+ inString = true;
1743
+ else if (ch === "{")
1668
1744
  depth += 1;
1669
- else if (text[i] === "}") {
1745
+ else if (ch === "}") {
1670
1746
  depth -= 1;
1671
1747
  if (depth === 0) {
1672
1748
  try {
@@ -1704,8 +1780,12 @@ async function startTraceDaemon(options) {
1704
1780
  });
1705
1781
  });
1706
1782
  const port = options.port ?? 47831;
1707
- await new Promise((resolve2) => {
1708
- server.listen(port, "127.0.0.1", resolve2);
1783
+ await new Promise((resolve2, reject) => {
1784
+ server.once("error", reject);
1785
+ server.listen(port, "127.0.0.1", () => {
1786
+ server.off("error", reject);
1787
+ resolve2();
1788
+ });
1709
1789
  });
1710
1790
  const address = server.address();
1711
1791
  const actualPort = typeof address === "object" && address ? address.port : port;
@@ -1714,8 +1794,12 @@ async function startTraceDaemon(options) {
1714
1794
  port: actualPort,
1715
1795
  eventsFile,
1716
1796
  close: () => new Promise((resolve2, reject) => {
1797
+ for (const client of clients) {
1798
+ client.terminate();
1799
+ }
1800
+ clients.clear();
1801
+ streamServer.close();
1717
1802
  server.close((error) => {
1718
- streamServer.close();
1719
1803
  if (error) {
1720
1804
  reject(error);
1721
1805
  } else {
@@ -1768,12 +1852,13 @@ async function buildTraceSnapshot(eventsFile, replay = {}) {
1768
1852
  }
1769
1853
  async function handleRequest(request, response, eventsFile, clients, suggestConfig, projectDir) {
1770
1854
  try {
1855
+ applyCors(request, response);
1771
1856
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
1772
- const replay = parseReplayQuery(url);
1773
1857
  if (request.method === "OPTIONS") {
1774
1858
  sendEmpty(response, 204);
1775
1859
  return;
1776
1860
  }
1861
+ const replay = parseReplayQuery(url);
1777
1862
  if (request.method === "GET" && url.pathname === "/health") {
1778
1863
  sendJson(response, 200, { ok: true, data: { status: "ok", eventsFile } });
1779
1864
  return;
@@ -1868,19 +1953,47 @@ async function sendSnapshot(client, eventsFile) {
1868
1953
  }
1869
1954
  }
1870
1955
  }
1956
+ var MAX_BODY_BYTES = 5e7;
1871
1957
  async function readJsonBody(request) {
1872
1958
  const chunks = [];
1959
+ let total = 0;
1873
1960
  for await (const chunk of request) {
1874
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
1961
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1962
+ total += buffer.length;
1963
+ if (total > MAX_BODY_BYTES) {
1964
+ throw new BadRequestError("Request body too large");
1965
+ }
1966
+ chunks.push(buffer);
1875
1967
  }
1876
1968
  const raw = Buffer.concat(chunks).toString("utf8");
1877
- return raw.length > 0 ? JSON.parse(raw) : {};
1969
+ if (raw.length === 0) {
1970
+ return {};
1971
+ }
1972
+ try {
1973
+ return JSON.parse(raw);
1974
+ } catch {
1975
+ throw new BadRequestError("Invalid JSON body");
1976
+ }
1977
+ }
1978
+ function applyCors(request, response) {
1979
+ const origin = request.headers.origin;
1980
+ if (typeof origin === "string" && isLoopbackOrigin(origin)) {
1981
+ response.setHeader("access-control-allow-origin", origin);
1982
+ response.setHeader("vary", "Origin");
1983
+ }
1984
+ }
1985
+ function isLoopbackOrigin(origin) {
1986
+ try {
1987
+ const { hostname } = new URL(origin);
1988
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "[::1]" || hostname === "::1";
1989
+ } catch {
1990
+ return false;
1991
+ }
1878
1992
  }
1879
1993
  function sendJson(response, statusCode, payload) {
1880
1994
  response.writeHead(statusCode, {
1881
1995
  "access-control-allow-headers": "content-type",
1882
1996
  "access-control-allow-methods": "GET,POST,OPTIONS",
1883
- "access-control-allow-origin": "*",
1884
1997
  "content-type": "application/json; charset=utf-8"
1885
1998
  });
1886
1999
  response.end(JSON.stringify(payload));
@@ -1888,8 +2001,7 @@ function sendJson(response, statusCode, payload) {
1888
2001
  function sendEmpty(response, statusCode) {
1889
2002
  response.writeHead(statusCode, {
1890
2003
  "access-control-allow-headers": "content-type",
1891
- "access-control-allow-methods": "GET,POST,OPTIONS",
1892
- "access-control-allow-origin": "*"
2004
+ "access-control-allow-methods": "GET,POST,OPTIONS"
1893
2005
  });
1894
2006
  response.end();
1895
2007
  }
@@ -2059,7 +2171,10 @@ function globalDataDir() {
2059
2171
  const xdg = process.env.XDG_DATA_HOME;
2060
2172
  return xdg && xdg.length > 0 ? join6(xdg, "agent-blackbox") : join6(homedir2(), ".local", "share", "agent-blackbox");
2061
2173
  }
2062
- void main(args);
2174
+ void main(args).catch((error) => {
2175
+ console.error(error instanceof Error ? error.message : String(error));
2176
+ process.exitCode = 1;
2177
+ });
2063
2178
  async function main(argv) {
2064
2179
  if (argv.includes("--version") || argv.includes("-v")) {
2065
2180
  console.log(AGENT_BLACKBOX_DAEMON_VERSION);
@@ -2068,7 +2183,7 @@ async function main(argv) {
2068
2183
  const command = argv[0] ?? "help";
2069
2184
  if (command === "daemon") {
2070
2185
  const projectDir = readFlag(argv, "--project") ?? process.cwd();
2071
- const port = Number(readFlag(argv, "--port") ?? "47831");
2186
+ const port = portArg(readFlag(argv, "--port"), 47831);
2072
2187
  const daemon = await startTraceDaemon({ projectDir, port });
2073
2188
  console.log(`Agent-Blackbox daemon listening on http://127.0.0.1:${daemon.port}`);
2074
2189
  console.log(`Trace file: ${daemon.eventsFile}`);
@@ -2077,8 +2192,8 @@ async function main(argv) {
2077
2192
  if (command === "up") {
2078
2193
  const projectFlag = readFlag(argv, "--project");
2079
2194
  const global = projectFlag === void 0;
2080
- const port = Number(readFlag(argv, "--port") ?? "47831");
2081
- const uiPort = Number(readFlag(argv, "--ui-port") ?? "5173");
2195
+ const port = portArg(readFlag(argv, "--port"), 47831);
2196
+ const uiPort = portArg(readFlag(argv, "--ui-port"), 5173);
2082
2197
  const daemonUrl = `http://127.0.0.1:${port}`;
2083
2198
  const suggest = readSuggestConfig(argv);
2084
2199
  let daemon;
@@ -2145,7 +2260,7 @@ async function main(argv) {
2145
2260
  return;
2146
2261
  }
2147
2262
  if (command === "install") {
2148
- const port = Number(readFlag(argv, "--port") ?? "47831");
2263
+ const port = portArg(readFlag(argv, "--port"), 47831);
2149
2264
  const daemonUrl = `http://127.0.0.1:${port}`;
2150
2265
  if (!pluginBundlePath) {
2151
2266
  throw new Error("Global install needs the self-contained recorder bundle. Use the published npx package, or `npm run build:cli` first.");
@@ -2247,6 +2362,10 @@ function readFlag(argv, flag) {
2247
2362
  }
2248
2363
  return argv[index + 1];
2249
2364
  }
2365
+ function portArg(raw, fallback) {
2366
+ const n = Number(raw);
2367
+ return Number.isInteger(n) && n >= 0 && n <= 65535 ? n : fallback;
2368
+ }
2250
2369
  function readSuggestConfig(argv) {
2251
2370
  const modes = ["auto", "off", "free", "ollama", "opencode", "openai-compat"];
2252
2371
  const raw = readFlag(argv, "--suggest") ?? process.env.AGENT_BLACKBOX_SUGGEST ?? "auto";