@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
- return value.map((item) => sanitizeJson(item, seen));
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
- 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"));
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
- await appendTraceEvent(eventsFile, event);
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
- 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,7 +1222,19 @@ 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
  }
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
- 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
- });
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, { stdio: ["ignore", "pipe", "ignore"] });
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
- if (text[i] === "{")
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 (text[i] === "}") {
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.listen(port, "127.0.0.1", resolve2);
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
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
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
- return raw.length > 0 ? JSON.parse(raw) : {};
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 = Number(readFlag(argv, "--port") ?? "47831");
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 = Number(readFlag(argv, "--port") ?? "47831");
2081
- const uiPort = Number(readFlag(argv, "--ui-port") ?? "5173");
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 = Number(readFlag(argv, "--port") ?? "47831");
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";