@taewooopark/agent-blackbox 0.44.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.
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}
@@ -1107,8 +1119,9 @@ function removeManagedBlock(content) {
1107
1119
  // apps/daemon/dist/cli.js
1108
1120
  import { spawn as spawn2 } from "node:child_process";
1109
1121
  import { existsSync } from "node:fs";
1122
+ import { homedir as homedir2 } from "node:os";
1110
1123
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1111
- import { dirname as dirname4, resolve } from "node:path";
1124
+ import { dirname as dirname5, join as join6, resolve } from "node:path";
1112
1125
 
1113
1126
  // apps/daemon/dist/dashboardServer.js
1114
1127
  import { createReadStream } from "node:fs";
@@ -1138,7 +1151,14 @@ async function startDashboardServer(options) {
1138
1151
  const injected = indexHtml.replace("</head>", ` <script>window.AGENT_BLACKBOX_DAEMON_URL=${JSON.stringify(options.daemonUrl)};</script>
1139
1152
  </head>`);
1140
1153
  const server = createServer((request, response) => {
1141
- 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
+ }
1142
1162
  if (rawPath === "/" || rawPath === "/index.html") {
1143
1163
  response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
1144
1164
  response.end(injected);
@@ -1156,15 +1176,21 @@ async function startDashboardServer(options) {
1156
1176
  throw new Error("not a file");
1157
1177
  }
1158
1178
  response.writeHead(200, { "content-type": mimeTypes[extname(filePath)] ?? "application/octet-stream" });
1159
- createReadStream(filePath).pipe(response);
1179
+ const rs = createReadStream(filePath);
1180
+ rs.on("error", () => response.destroy());
1181
+ rs.pipe(response);
1160
1182
  }).catch(() => {
1161
1183
  response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
1162
1184
  response.end(injected);
1163
1185
  });
1164
1186
  });
1165
1187
  const port = options.port ?? 5173;
1166
- await new Promise((resolve2) => {
1167
- 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
+ });
1168
1194
  });
1169
1195
  const address = server.address();
1170
1196
  const actualPort = typeof address === "object" && address ? address.port : port;
@@ -1179,7 +1205,7 @@ async function startDashboardServer(options) {
1179
1205
 
1180
1206
  // apps/daemon/dist/index.js
1181
1207
  import { readFileSync } from "node:fs";
1182
- import { dirname as dirname3, join as join5 } from "node:path";
1208
+ import { dirname as dirname4, join as join5 } from "node:path";
1183
1209
  import { fileURLToPath } from "node:url";
1184
1210
 
1185
1211
  // packages/storage/src/ndjson.ts
@@ -1196,7 +1222,19 @@ function parseTraceEventLine(line) {
1196
1222
  return parsed;
1197
1223
  }
1198
1224
  function parseTraceEvents(input) {
1199
- 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;
1200
1238
  }
1201
1239
  async function appendTraceEvent(filePath, event) {
1202
1240
  await mkdir(dirname(filePath), { recursive: true });
@@ -1225,10 +1263,11 @@ async function runOptimize(options) {
1225
1263
  }
1226
1264
  async function computeOptimize(options) {
1227
1265
  const eventsFile = options.eventsFile ?? join2(options.projectDir, ".agent-blackbox", "events.ndjson");
1228
- const agentsMdPath = join2(options.projectDir, "AGENTS.md");
1229
- const statePath = join2(options.projectDir, ".agent-blackbox", "optimization.json");
1230
1266
  const events = await loadTraceEvents(eventsFile);
1231
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");
1232
1271
  const latestTs = runEvents.reduce((max, e) => e.ts > max ? e.ts : max, "");
1233
1272
  const report = runEvents.length > 0 ? computeEfficiencyReport(runEvents) : null;
1234
1273
  const score = report ? report.overallScore : null;
@@ -1254,15 +1293,17 @@ async function computeOptimize(options) {
1254
1293
  }
1255
1294
  const prior = await readMaybe(agentsMdPath);
1256
1295
  const next = upsertManagedBlock(prior ?? "", block);
1257
- await writeFile(agentsMdPath, next, "utf8");
1258
- await writeState(statePath, {
1259
- runId: runId ?? "",
1260
- baselineScore: score,
1261
- baselineLatestTs: latestTs,
1262
- baselineFlagged: flaggedIds(report),
1263
- fileExisted: prior !== null,
1264
- appliedAt: (/* @__PURE__ */ new Date()).toISOString()
1265
- });
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
+ }
1266
1307
  return {
1267
1308
  mode: "apply",
1268
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.`,
@@ -1469,7 +1510,7 @@ function buildDigest(report) {
1469
1510
  id: m.id,
1470
1511
  label: m.label,
1471
1512
  status: m.status,
1472
- value: Number(m.value.toFixed(3)),
1513
+ value: Number((typeof m.value === "number" && Number.isFinite(m.value) ? m.value : 0).toFixed(3)),
1473
1514
  display: m.display,
1474
1515
  detail: m.detail,
1475
1516
  ...m.reclaimableTokens ? { reclaimableTokens: m.reclaimableTokens } : {},
@@ -1491,6 +1532,8 @@ var freeCooldownUntil = /* @__PURE__ */ new Map();
1491
1532
  function orderFreePool(pool, cooldownUntil, cursor, now) {
1492
1533
  const fresh = pool.filter((entry) => (cooldownUntil.get(entry.model) ?? 0) <= now);
1493
1534
  const list = fresh.length > 0 ? fresh : pool;
1535
+ if (list.length === 0)
1536
+ return [];
1494
1537
  const start = (cursor % list.length + list.length) % list.length;
1495
1538
  return [...list.slice(start), ...list.slice(0, start)];
1496
1539
  }
@@ -1635,7 +1678,10 @@ async function fetchJson(url, body, extraHeaders = {}) {
1635
1678
  }
1636
1679
  function runCommand(command, args2) {
1637
1680
  return new Promise((resolve2, reject) => {
1638
- 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
+ });
1639
1685
  let out = "";
1640
1686
  const timer = setTimeout(() => {
1641
1687
  child.kill("SIGKILL");
@@ -1662,10 +1708,24 @@ function extractJsonObject(text) {
1662
1708
  if (start === -1)
1663
1709
  return void 0;
1664
1710
  let depth = 0;
1711
+ let inString = false;
1712
+ let escaped = false;
1665
1713
  for (let i = start; i < text.length; i += 1) {
1666
- 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 === "{")
1667
1727
  depth += 1;
1668
- else if (text[i] === "}") {
1728
+ else if (ch === "}") {
1669
1729
  depth -= 1;
1670
1730
  if (depth === 0) {
1671
1731
  try {
@@ -1703,8 +1763,12 @@ async function startTraceDaemon(options) {
1703
1763
  });
1704
1764
  });
1705
1765
  const port = options.port ?? 47831;
1706
- await new Promise((resolve2) => {
1707
- 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
+ });
1708
1772
  });
1709
1773
  const address = server.address();
1710
1774
  const actualPort = typeof address === "object" && address ? address.port : port;
@@ -1713,8 +1777,12 @@ async function startTraceDaemon(options) {
1713
1777
  port: actualPort,
1714
1778
  eventsFile,
1715
1779
  close: () => new Promise((resolve2, reject) => {
1780
+ for (const client of clients) {
1781
+ client.terminate();
1782
+ }
1783
+ clients.clear();
1784
+ streamServer.close();
1716
1785
  server.close((error) => {
1717
- streamServer.close();
1718
1786
  if (error) {
1719
1787
  reject(error);
1720
1788
  } else {
@@ -1768,11 +1836,11 @@ async function buildTraceSnapshot(eventsFile, replay = {}) {
1768
1836
  async function handleRequest(request, response, eventsFile, clients, suggestConfig, projectDir) {
1769
1837
  try {
1770
1838
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
1771
- const replay = parseReplayQuery(url);
1772
1839
  if (request.method === "OPTIONS") {
1773
1840
  sendEmpty(response, 204);
1774
1841
  return;
1775
1842
  }
1843
+ const replay = parseReplayQuery(url);
1776
1844
  if (request.method === "GET" && url.pathname === "/health") {
1777
1845
  sendJson(response, 200, { ok: true, data: { status: "ok", eventsFile } });
1778
1846
  return;
@@ -1867,13 +1935,27 @@ async function sendSnapshot(client, eventsFile) {
1867
1935
  }
1868
1936
  }
1869
1937
  }
1938
+ var MAX_BODY_BYTES = 5e7;
1870
1939
  async function readJsonBody(request) {
1871
1940
  const chunks = [];
1941
+ let total = 0;
1872
1942
  for await (const chunk of request) {
1873
- 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);
1874
1949
  }
1875
1950
  const raw = Buffer.concat(chunks).toString("utf8");
1876
- 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
+ }
1877
1959
  }
1878
1960
  function sendJson(response, statusCode, payload) {
1879
1961
  response.writeHead(statusCode, {
@@ -1917,10 +1999,39 @@ function isNodeError(error) {
1917
1999
  }
1918
2000
 
1919
2001
  // apps/daemon/dist/initOpenCode.js
1920
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
1921
- import { join as join4 } from "node:path";
2002
+ import { mkdir as mkdir3, readFile as readFile4, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
2003
+ import { homedir } from "node:os";
2004
+ import { dirname as dirname3, join as join4 } from "node:path";
1922
2005
  var defaultAdapterPackage = "@agent-blackbox/opencode-adapter";
1923
2006
  var defaultDaemonUrl = "http://127.0.0.1:47831";
2007
+ function globalOpenCodeDir() {
2008
+ const xdg = process.env.XDG_CONFIG_HOME;
2009
+ return xdg && xdg.length > 0 ? join4(xdg, "opencode") : join4(homedir(), ".config", "opencode");
2010
+ }
2011
+ function globalRecorderPath() {
2012
+ return join4(globalOpenCodeDir(), "plugins", "agent-blackbox.js");
2013
+ }
2014
+ async function installGlobalRecorder(options) {
2015
+ if (!await pathExists(options.pluginBundlePath)) {
2016
+ throw new Error("Self-contained recorder bundle not found. Use the published npx package, or build it from source with `npm run build:cli`.");
2017
+ }
2018
+ const pluginPath = globalRecorderPath();
2019
+ const bundle = (await readFile4(options.pluginBundlePath, "utf8")).replaceAll("__ABB_DAEMON_URL__", options.daemonUrl);
2020
+ await mkdir3(dirname3(pluginPath), { recursive: true });
2021
+ await writeFile2(pluginPath, bundle, "utf8");
2022
+ return { pluginPath };
2023
+ }
2024
+ async function uninstallGlobalRecorder() {
2025
+ const pluginPath = globalRecorderPath();
2026
+ try {
2027
+ await rm2(pluginPath);
2028
+ return { pluginPath, removed: true };
2029
+ } catch (error) {
2030
+ if (isNodeError2(error) && error.code === "ENOENT")
2031
+ return { pluginPath, removed: false };
2032
+ throw error;
2033
+ }
2034
+ }
1924
2035
  async function initOpenCodeProject(options) {
1925
2036
  const adapterPackage = options.adapterPackage ?? defaultAdapterPackage;
1926
2037
  const adapterImport = inferAdapterImport(adapterPackage);
@@ -1998,7 +2109,7 @@ function isNodeError2(error) {
1998
2109
 
1999
2110
  // apps/daemon/dist/index.js
2000
2111
  function resolvePackageVersion() {
2001
- let dir = dirname3(fileURLToPath(import.meta.url));
2112
+ let dir = dirname4(fileURLToPath(import.meta.url));
2002
2113
  for (let i = 0; i < 6; i += 1) {
2003
2114
  try {
2004
2115
  const pkg = JSON.parse(readFileSync(join5(dir, "package.json"), "utf8"));
@@ -2006,7 +2117,7 @@ function resolvePackageVersion() {
2006
2117
  return pkg.version;
2007
2118
  } catch {
2008
2119
  }
2009
- const parent = dirname3(dir);
2120
+ const parent = dirname4(dir);
2010
2121
  if (parent === dir)
2011
2122
  break;
2012
2123
  dir = parent;
@@ -2020,12 +2131,19 @@ function describeDaemon() {
2020
2131
 
2021
2132
  // apps/daemon/dist/cli.js
2022
2133
  var args = process.argv.slice(2);
2023
- var cliDir = dirname4(fileURLToPath2(import.meta.url));
2134
+ var cliDir = dirname5(fileURLToPath2(import.meta.url));
2024
2135
  var repoRoot = resolve(cliDir, "../../..");
2025
2136
  var firstExisting = (paths) => paths.find((p) => existsSync(p));
2026
2137
  var dashboardDistDir = firstExisting([resolve(cliDir, "dashboard"), resolve(repoRoot, "apps/dashboard/dist")]) ?? resolve(repoRoot, "apps/dashboard/dist");
2027
2138
  var pluginBundlePath = firstExisting([resolve(cliDir, "agent-blackbox.plugin.mjs")]);
2028
- void main(args);
2139
+ function globalDataDir() {
2140
+ const xdg = process.env.XDG_DATA_HOME;
2141
+ return xdg && xdg.length > 0 ? join6(xdg, "agent-blackbox") : join6(homedir2(), ".local", "share", "agent-blackbox");
2142
+ }
2143
+ void main(args).catch((error) => {
2144
+ console.error(error instanceof Error ? error.message : String(error));
2145
+ process.exitCode = 1;
2146
+ });
2029
2147
  async function main(argv) {
2030
2148
  if (argv.includes("--version") || argv.includes("-v")) {
2031
2149
  console.log(AGENT_BLACKBOX_DAEMON_VERSION);
@@ -2034,19 +2152,48 @@ async function main(argv) {
2034
2152
  const command = argv[0] ?? "help";
2035
2153
  if (command === "daemon") {
2036
2154
  const projectDir = readFlag(argv, "--project") ?? process.cwd();
2037
- const port = Number(readFlag(argv, "--port") ?? "47831");
2155
+ const port = portArg(readFlag(argv, "--port"), 47831);
2038
2156
  const daemon = await startTraceDaemon({ projectDir, port });
2039
2157
  console.log(`Agent-Blackbox daemon listening on http://127.0.0.1:${daemon.port}`);
2040
2158
  console.log(`Trace file: ${daemon.eventsFile}`);
2041
2159
  return;
2042
2160
  }
2043
2161
  if (command === "up") {
2044
- const projectDir = resolve(readFlag(argv, "--project") ?? process.cwd());
2045
- const port = Number(readFlag(argv, "--port") ?? "47831");
2046
- const uiPort = Number(readFlag(argv, "--ui-port") ?? "5173");
2162
+ const projectFlag = readFlag(argv, "--project");
2163
+ const global = projectFlag === void 0;
2164
+ const port = portArg(readFlag(argv, "--port"), 47831);
2165
+ const uiPort = portArg(readFlag(argv, "--ui-port"), 5173);
2047
2166
  const daemonUrl = `http://127.0.0.1:${port}`;
2048
- const adapterPackage = readFlag(argv, "--adapter-package") ?? `file:${resolve(repoRoot, "packages/opencode-adapter")}`;
2049
2167
  const suggest = readSuggestConfig(argv);
2168
+ let daemon;
2169
+ if (global) {
2170
+ if (!pluginBundlePath) {
2171
+ throw new Error("Global install needs the self-contained recorder bundle. Use the published npx package, or `npm run build:cli` then `node packages/cli/dist/cli.js up`.\n(Or scope to one project with: agent-blackbox up --project <dir>.)");
2172
+ }
2173
+ const { pluginPath } = await installGlobalRecorder({ daemonUrl, pluginBundlePath });
2174
+ const dataDir = globalDataDir();
2175
+ const eventsFile = join6(dataDir, "events.ndjson");
2176
+ daemon = await startTraceDaemon({ projectDir: dataDir, port, eventsFile, suggest });
2177
+ const ui2 = await startDashboardServer({ distDir: dashboardDistDir, port: uiPort, daemonUrl });
2178
+ const dashboardUrl2 = `http://127.0.0.1:${ui2.port}`;
2179
+ console.log(`\u2713 Global OpenCode recorder installed: ${pluginPath}`);
2180
+ console.log(`\u2713 Agent-Blackbox is up (recording all OpenCode sessions)`);
2181
+ console.log(` Dashboard: ${dashboardUrl2}`);
2182
+ console.log(` Daemon API: ${daemonUrl} (trace: ${daemon.eventsFile})`);
2183
+ console.log(` Suggestions: ${suggest.mode}${suggest.model ? ` (${suggest.model})` : ""}`);
2184
+ console.log("");
2185
+ if (!argv.includes("--no-open"))
2186
+ openInBrowser(dashboardUrl2);
2187
+ console.log("Now use OpenCode however you already do \u2014 the dashboard fills in live:");
2188
+ console.log(" opencode # in any folder (terminal), or");
2189
+ console.log(" the OpenCode desktop app # open any project");
2190
+ console.log("");
2191
+ console.log("Stop recording any time with: agent-blackbox uninstall");
2192
+ console.log("Press Ctrl+C to stop the daemon + dashboard.");
2193
+ return;
2194
+ }
2195
+ const projectDir = resolve(projectFlag);
2196
+ const adapterPackage = readFlag(argv, "--adapter-package") ?? `file:${resolve(repoRoot, "packages/opencode-adapter")}`;
2050
2197
  try {
2051
2198
  const result = await initOpenCodeProject({
2052
2199
  projectDir,
@@ -2064,7 +2211,7 @@ async function main(argv) {
2064
2211
  throw error;
2065
2212
  }
2066
2213
  }
2067
- const daemon = await startTraceDaemon({ projectDir, port, suggest });
2214
+ daemon = await startTraceDaemon({ projectDir, port, suggest });
2068
2215
  const ui = await startDashboardServer({ distDir: dashboardDistDir, port: uiPort, daemonUrl });
2069
2216
  const dashboardUrl = `http://127.0.0.1:${ui.port}`;
2070
2217
  console.log("");
@@ -2076,11 +2223,29 @@ async function main(argv) {
2076
2223
  if (!argv.includes("--no-open"))
2077
2224
  openInBrowser(dashboardUrl);
2078
2225
  console.log("Now run your agent in that project, e.g.:");
2079
- console.log(` AGENT_BLACKBOX_DAEMON_URL=${daemonUrl} opencode run --dir ${projectDir} "Read the code, run tests, summarize."`);
2226
+ console.log(` opencode # in ${projectDir} (the project-local recorder streams here)`);
2080
2227
  console.log("");
2081
2228
  console.log("Press Ctrl+C to stop.");
2082
2229
  return;
2083
2230
  }
2231
+ if (command === "install") {
2232
+ const port = portArg(readFlag(argv, "--port"), 47831);
2233
+ const daemonUrl = `http://127.0.0.1:${port}`;
2234
+ if (!pluginBundlePath) {
2235
+ throw new Error("Global install needs the self-contained recorder bundle. Use the published npx package, or `npm run build:cli` first.");
2236
+ }
2237
+ const { pluginPath } = await installGlobalRecorder({ daemonUrl, pluginBundlePath });
2238
+ console.log(`\u2713 Global OpenCode recorder installed: ${pluginPath}`);
2239
+ console.log(` Every OpenCode session (any folder, terminal, or the app) now streams to ${daemonUrl}.`);
2240
+ console.log(` Start the dashboard with: agent-blackbox up`);
2241
+ console.log(` Remove with: agent-blackbox uninstall`);
2242
+ return;
2243
+ }
2244
+ if (command === "uninstall") {
2245
+ const { pluginPath, removed } = await uninstallGlobalRecorder();
2246
+ console.log(removed ? `\u2713 Removed global OpenCode recorder: ${pluginPath}` : `Nothing to remove \u2014 ${pluginPath} is not present.`);
2247
+ return;
2248
+ }
2084
2249
  if (command === "replay") {
2085
2250
  const eventsFile = argv[1];
2086
2251
  if (!eventsFile) {
@@ -2138,8 +2303,11 @@ function printHelp() {
2138
2303
  console.log(describeDaemon());
2139
2304
  console.log("");
2140
2305
  console.log("Usage:");
2141
- console.log(" agent-blackbox up [--project <dir>] [--port <port>] [--ui-port <port>] # plugin + daemon + dashboard, one command");
2142
- console.log(" [--suggest auto|free|off|ollama|opencode|openai-compat] [--suggest-model <id>] [--suggest-base-url <url>] [--optimize] [--no-open]");
2306
+ console.log(" agent-blackbox up # GLOBAL: record every OpenCode session (any folder / the app) + daemon + dashboard");
2307
+ console.log(" agent-blackbox up --project <dir> # scope the recorder to one project instead");
2308
+ console.log(" [--port <port>] [--ui-port <port>] [--suggest auto|free|off|ollama|opencode|openai-compat] [--suggest-model <id>] [--optimize] [--no-open]");
2309
+ console.log(" agent-blackbox install [--port <port>] # install the global recorder only (no daemon)");
2310
+ console.log(" agent-blackbox uninstall # remove the global recorder");
2143
2311
  console.log(" agent-blackbox daemon [--project <dir>] [--port <port>]");
2144
2312
  console.log(" agent-blackbox init-opencode [--project <dir>] [--daemon-url <url>] [--adapter-package <specifier>] [--force] [--optimize]");
2145
2313
  console.log(" agent-blackbox optimize [--project <dir>] [--apply | --check | --revert] # write/measure/rollback AGENTS.md efficiency memory");
@@ -2163,6 +2331,10 @@ function readFlag(argv, flag) {
2163
2331
  }
2164
2332
  return argv[index + 1];
2165
2333
  }
2334
+ function portArg(raw, fallback) {
2335
+ const n = Number(raw);
2336
+ return Number.isInteger(n) && n >= 0 && n <= 65535 ? n : fallback;
2337
+ }
2166
2338
  function readSuggestConfig(argv) {
2167
2339
  const modes = ["auto", "off", "free", "ollama", "opencode", "openai-compat"];
2168
2340
  const raw = readFlag(argv, "--suggest") ?? process.env.AGENT_BLACKBOX_SUGGEST ?? "auto";