@taewooopark/agent-blackbox 0.46.3 → 0.47.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
@@ -1,5 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // packages/core/src/json.ts
4
+ function isJsonObject(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+
3
8
  // packages/core/src/events.ts
4
9
  var traceHosts = [
5
10
  "opencode",
@@ -9,6 +14,7 @@ var traceHosts = [
9
14
  "hermes",
10
15
  "custom"
11
16
  ];
17
+ var agentRoles = ["primary", "subagent", "system", "unknown"];
12
18
  var traceEventKinds = [
13
19
  "session_created",
14
20
  "session_updated",
@@ -50,6 +56,55 @@ var dataSensitivities = [
50
56
  "secret",
51
57
  "student_sensitive"
52
58
  ];
59
+ var observedKinds = /* @__PURE__ */ new Set([
60
+ "tool_call",
61
+ "tool_result",
62
+ "file_read",
63
+ "file_edit",
64
+ "file_created",
65
+ "file_deleted",
66
+ "search",
67
+ "bash",
68
+ "permission_asked",
69
+ "permission_replied",
70
+ "todo_updated",
71
+ "git_status",
72
+ "git_commit",
73
+ "git_push"
74
+ ]);
75
+ var claimKinds = /* @__PURE__ */ new Set(["message", "decision_extracted", "handoff_generated"]);
76
+ function createTraceEvent(seq, input) {
77
+ const id = makeTraceEventId(input.runId, seq);
78
+ const evidence = inferEvidence(input.kind, input.evidence);
79
+ return {
80
+ id,
81
+ ts: input.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
82
+ seq,
83
+ host: input.host,
84
+ runId: input.runId,
85
+ sessionId: input.sessionId,
86
+ ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
87
+ ...input.cwd ? { cwd: input.cwd } : {},
88
+ ...input.agentId ? { agentId: input.agentId } : {},
89
+ ...input.agentRole ? { agentRole: input.agentRole } : {},
90
+ ...input.agentLabel ? { agentLabel: input.agentLabel } : {},
91
+ ...input.turnId ? { turnId: input.turnId } : {},
92
+ kind: input.kind,
93
+ ...input.summary ? { summary: input.summary } : {},
94
+ payload: input.payload ?? {},
95
+ sensitivity: input.sensitivity ?? "private",
96
+ redaction: {
97
+ rawStored: input.redaction?.rawStored ?? false,
98
+ rulesApplied: input.redaction?.rulesApplied ?? [],
99
+ truncated: input.redaction?.truncated ?? false
100
+ },
101
+ evidence
102
+ };
103
+ }
104
+ function makeTraceEventId(runId, seq) {
105
+ const safeRunId = runId.replace(/[^a-zA-Z0-9_-]/g, "_");
106
+ return `evt_${safeRunId}_${String(seq).padStart(6, "0")}`;
107
+ }
53
108
  function validateTraceEvent(event) {
54
109
  const errors = [];
55
110
  if (!isRecord(event)) {
@@ -64,6 +119,12 @@ function validateTraceEvent(event) {
64
119
  if (event.cwd !== void 0 && typeof event.cwd !== "string") {
65
120
  errors.push("cwd must be a string when present");
66
121
  }
122
+ optionalEnum(event, "agentRole", agentRoles, errors);
123
+ optionalString(event, "agentId", errors);
124
+ optionalString(event, "agentLabel", errors);
125
+ optionalString(event, "parentSessionId", errors);
126
+ optionalString(event, "turnId", errors);
127
+ optionalString(event, "summary", errors);
67
128
  requireEnum(event, "kind", traceEventKinds, errors);
68
129
  requireEnum(event, "sensitivity", dataSensitivities, errors);
69
130
  if (!isRecord(event.payload)) {
@@ -89,6 +150,12 @@ function assertTraceEvent(event) {
89
150
  throw new Error(`Invalid trace event: ${result.errors.join("; ")}`);
90
151
  }
91
152
  }
153
+ function inferEvidence(kind, override) {
154
+ return {
155
+ observed: override?.observed ?? observedKinds.has(kind),
156
+ claimedByModel: override?.claimedByModel ?? claimKinds.has(kind)
157
+ };
158
+ }
92
159
  function isRecord(value) {
93
160
  return typeof value === "object" && value !== null && !Array.isArray(value);
94
161
  }
@@ -107,6 +174,99 @@ function requireEnum(value, key, allowed, errors) {
107
174
  errors.push(`${key} must be one of ${allowed.join(", ")}`);
108
175
  }
109
176
  }
177
+ function optionalString(value, key, errors) {
178
+ if (value[key] !== void 0 && typeof value[key] !== "string") {
179
+ errors.push(`${key} must be a string when present`);
180
+ }
181
+ }
182
+ function optionalEnum(value, key, allowed, errors) {
183
+ if (value[key] !== void 0 && (typeof value[key] !== "string" || !allowed.includes(value[key]))) {
184
+ errors.push(`${key} must be one of ${allowed.join(", ")} when present`);
185
+ }
186
+ }
187
+
188
+ // packages/core/src/redaction.ts
189
+ var defaultRedactionRules = [
190
+ {
191
+ name: "github-token",
192
+ pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/g,
193
+ replacement: "[REDACTED_GITHUB_TOKEN]"
194
+ },
195
+ {
196
+ name: "anthropic-key",
197
+ pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g,
198
+ replacement: "[REDACTED_ANTHROPIC_KEY]"
199
+ },
200
+ {
201
+ name: "openai-key",
202
+ pattern: /\bsk-[A-Za-z0-9_-]{20,}\b/g,
203
+ replacement: "[REDACTED_OPENAI_KEY]"
204
+ },
205
+ {
206
+ name: "private-key",
207
+ // Tempered quantifier: the body cannot cross another BEGIN marker. Without it, a
208
+ // lone BEGIN with no END forces a scan to end-of-string from every BEGIN — O(n²)
209
+ // backtracking on untrusted tool output peppered with BEGIN markers (slow-path DoS).
210
+ pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----(?:(?!-----BEGIN )[\s\S])*?-----END [A-Z ]*PRIVATE KEY-----/g,
211
+ replacement: "[REDACTED_PRIVATE_KEY]"
212
+ }
213
+ ];
214
+ function redactJsonObject(payload, options = {}) {
215
+ return redactJsonValue(payload, options);
216
+ }
217
+ function redactJsonValue(value, options = {}) {
218
+ const rules = [...defaultRedactionRules, ...options.extraRules ?? []];
219
+ const applied = /* @__PURE__ */ new Set();
220
+ let truncated = false;
221
+ const maxStringLength = options.maxStringLength ?? 4e3;
222
+ const visit = (current) => {
223
+ if (typeof current === "string") {
224
+ let next = current;
225
+ if (options.projectDir) {
226
+ next = replaceLiteral(next, options.projectDir, "$PROJECT", "project-dir", applied);
227
+ }
228
+ if (options.homeDir) {
229
+ next = replaceLiteral(next, options.homeDir, "~", "home-dir", applied);
230
+ }
231
+ for (const rule of rules) {
232
+ if (rule.pattern.test(next)) {
233
+ applied.add(rule.name);
234
+ next = next.replace(rule.pattern, rule.replacement);
235
+ }
236
+ rule.pattern.lastIndex = 0;
237
+ }
238
+ if (next.length > maxStringLength) {
239
+ truncated = true;
240
+ applied.add("truncate-string");
241
+ return `${next.slice(0, maxStringLength)}...[TRUNCATED ${next.length - maxStringLength} chars]`;
242
+ }
243
+ return next;
244
+ }
245
+ if (Array.isArray(current)) {
246
+ return current.map((item) => visit(item));
247
+ }
248
+ if (isJsonObject(current)) {
249
+ const next = {};
250
+ for (const [key, nested] of Object.entries(current)) {
251
+ next[key] = visit(nested);
252
+ }
253
+ return next;
254
+ }
255
+ return current;
256
+ };
257
+ return {
258
+ value: visit(value),
259
+ rulesApplied: [...applied].sort(),
260
+ truncated
261
+ };
262
+ }
263
+ function replaceLiteral(value, search, replacement, ruleName, applied) {
264
+ if (!search || !value.includes(search)) {
265
+ return value;
266
+ }
267
+ applied.add(ruleName);
268
+ return value.split(search).join(replacement);
269
+ }
110
270
 
111
271
  // packages/core/src/graph.ts
112
272
  function materializeWorkflowGraph(events) {
@@ -273,13 +433,23 @@ function ensureAgent(graph, event) {
273
433
  ensureNode(graph, {
274
434
  id,
275
435
  type: "AGENT",
436
+ // Identity stays the agentId (the dashboard matches lanes by it). A readable
437
+ // display name rides in data.agentName, kept separate so matching never breaks.
276
438
  label: event.agentId,
277
439
  status: "ACTIVE",
278
440
  at: event.ts,
279
441
  eventId: event.id,
280
- data: { agentId: event.agentId, agentRole: event.agentRole ?? "unknown" },
442
+ data: {
443
+ agentId: event.agentId,
444
+ agentRole: event.agentRole ?? "unknown",
445
+ ...event.agentLabel ? { agentName: event.agentLabel } : {}
446
+ },
281
447
  keepStatusIfExists: true
282
448
  });
449
+ if (event.agentLabel) {
450
+ const node = graph.nodes.get(id);
451
+ if (node && !node.data.agentName) node.data.agentName = event.agentLabel;
452
+ }
283
453
  ensureEdge(graph, {
284
454
  from: sessionNodeId(event.sessionId),
285
455
  to: id,
@@ -714,7 +884,8 @@ function computeEfficiencyReport(events) {
714
884
  const totalEditTokens = edits.reduce((sum, e) => sum + e.tokens, 0);
715
885
  const editedPaths = new Set(edits.map((e) => e.path));
716
886
  const okCommands = bashRuns.filter((b) => b.exitCode === 0).length;
717
- const totalInputTokens = hasRealTokens ? finalSnapshot.input : estimateTokens(reads.reduce((s, r) => s + r.tokens * CHARS_PER_TOKEN, 0) + injections.reduce((s, i) => s + i.tokens * CHARS_PER_TOKEN, 0));
887
+ const injectionTokens = injections.reduce((s, i) => s + i.tokens, 0);
888
+ const totalInputTokens = hasRealTokens ? finalSnapshot.input : totalReadTokens + totalEditTokens + injectionTokens;
718
889
  const peak = hasRealTokens ? peakInput : totalInputTokens;
719
890
  const metrics = [];
720
891
  {
@@ -729,7 +900,11 @@ function computeEfficiencyReport(events) {
729
900
  display: formatTokens(peak),
730
901
  score,
731
902
  status,
732
- detail: status === "good" ? "The context window stayed comfortably sized." : `Peak input reached ${formatTokens(peak)} \u2014 large prompts cost latency and money on every turn.`,
903
+ detail: status === "good" ? "The context window stayed comfortably sized." : hasRealTokens ? `Peak input reached ${formatTokens(peak)} \u2014 large prompts cost latency and money on every turn.` : (
904
+ // No real token telemetry: this is total input pulled in over the run (we can't
905
+ // measure peak window occupancy), so don't claim a measured peak.
906
+ `About ${formatTokens(peak)} of input flowed through the context \u2014 large prompts cost latency and money on every turn.`
907
+ ),
733
908
  evidenceEventIds: []
734
909
  }
735
910
  });
@@ -879,7 +1054,7 @@ function computeEfficiencyReport(events) {
879
1054
  if (list.length <= 1) continue;
880
1055
  retries += list.length - 1;
881
1056
  for (const attempt of list) {
882
- if (attempt.exitCode !== 0) {
1057
+ if (attempt.exitCode !== void 0 && attempt.exitCode !== 0) {
883
1058
  wasted += attempt.tokens;
884
1059
  evidence.push(attempt.id);
885
1060
  }
@@ -1109,25 +1284,622 @@ function upsertManagedBlock(content, block) {
1109
1284
  ${block}
1110
1285
  `;
1111
1286
  }
1287
+ var managedBlockSeamRegExp = () => new RegExp(`\\n*${escapeRegExp(EFFICIENCY_MEMORY_START)}[\\s\\S]*?${escapeRegExp(EFFICIENCY_MEMORY_END)}\\n*`, "g");
1112
1288
  function removeManagedBlock(content) {
1113
1289
  if (!hasManagedBlock(content)) return content;
1114
- const stripped = content.replace(managedBlockRegExp(), "").replace(/\n{3,}/g, "\n\n").trimEnd();
1290
+ const stripped = content.replace(managedBlockSeamRegExp(), "\n\n").trimEnd();
1115
1291
  return stripped.length === 0 ? "" : `${stripped}
1116
1292
  `;
1117
1293
  }
1118
1294
 
1295
+ // packages/claude-code-adapter/dist/normalize.js
1296
+ var READ_TOOLS = /* @__PURE__ */ new Set(["read"]);
1297
+ var EDIT_TOOLS = /* @__PURE__ */ new Set(["edit", "multiedit", "applypatch", "apply_patch", "notebookedit"]);
1298
+ var WRITE_TOOLS = /* @__PURE__ */ new Set(["write"]);
1299
+ var BASH_TOOLS = /* @__PURE__ */ new Set(["bash", "shell"]);
1300
+ var SEARCH_TOOLS = /* @__PURE__ */ new Set(["grep", "glob", "ls", "websearch"]);
1301
+ var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["task", "agent"]);
1302
+ var TODO_TOOLS = /* @__PURE__ */ new Set(["todowrite", "taskcreate", "taskupdate", "taskstop"]);
1303
+ var WORKFLOW_TOOLS = /* @__PURE__ */ new Set(["workflow"]);
1304
+ var COMMAND_TOOLS = /* @__PURE__ */ new Set(["skill"]);
1305
+ var TEAM_TOOLS = /* @__PURE__ */ new Set(["sendmessage", "teamcreate", "teamdelete", "remotetrigger", "pushnotification"]);
1306
+ function createClaudeNormalizer(ctx) {
1307
+ const toolUses = /* @__PURE__ */ new Map();
1308
+ let lastModel;
1309
+ const consume = (rawLine) => {
1310
+ const line = asRecord(rawLine);
1311
+ switch (readString(line, ["type"])) {
1312
+ case "assistant":
1313
+ return consumeAssistant(line, ctx, toolUses, (m) => lastModel = m, () => lastModel);
1314
+ case "user":
1315
+ return consumeUser(line, ctx, toolUses);
1316
+ case "system":
1317
+ return consumeSystem(line, ctx);
1318
+ default:
1319
+ return [];
1320
+ }
1321
+ };
1322
+ return { consume };
1323
+ }
1324
+ function consumeAssistant(line, ctx, toolUses, setModel, getModel) {
1325
+ const events = [];
1326
+ const msg = asRecord(line.message);
1327
+ const model = readString(msg, ["model"]);
1328
+ const usage = asRecord(msg.usage);
1329
+ if (Object.keys(usage).length > 0) {
1330
+ const inputTokens = num(usage.input_tokens) + num(usage.cache_read_input_tokens) + num(usage.cache_creation_input_tokens);
1331
+ events.push(mkInput(line, ctx, {
1332
+ kind: "message",
1333
+ summary: "assistant turn",
1334
+ payload: {
1335
+ role: "assistant",
1336
+ ...model ? { model } : {},
1337
+ tokens: {
1338
+ input: inputTokens,
1339
+ output: num(usage.output_tokens),
1340
+ cache: { read: num(usage.cache_read_input_tokens), write: num(usage.cache_creation_input_tokens) }
1341
+ }
1342
+ }
1343
+ }));
1344
+ }
1345
+ if (model && model !== "<synthetic>" && model !== getModel()) {
1346
+ if (getModel())
1347
+ events.push(mkInput(line, ctx, { kind: "model_switched", summary: `Model \u2192 ${model}`, payload: { model } }));
1348
+ setModel(model);
1349
+ }
1350
+ for (const block of asArray(msg.content)) {
1351
+ const b = asRecord(block);
1352
+ if (readString(b, ["type"]) !== "tool_use")
1353
+ continue;
1354
+ const id = readString(b, ["id"]);
1355
+ const name = readString(b, ["name"]) ?? "unknown-tool";
1356
+ const input = asJsonObject(b.input);
1357
+ if (id)
1358
+ toolUses.set(id, { name, input });
1359
+ events.push(mkInput(line, ctx, {
1360
+ kind: "tool_call",
1361
+ summary: `tool.call:${prettyTool(name)}`,
1362
+ payload: { tool: name, ...id ? { callID: id } : {} }
1363
+ }));
1364
+ }
1365
+ return events;
1366
+ }
1367
+ function consumeUser(line, ctx, toolUses) {
1368
+ const events = [];
1369
+ const msg = asRecord(line.message);
1370
+ if (ctx.agent && !ctx.agent.label) {
1371
+ const task = extractText(msg.content);
1372
+ if (task)
1373
+ ctx.agent.label = shortLabel(task);
1374
+ }
1375
+ const promptSource = readString(line, ["promptSource"]);
1376
+ const isMeta = line.isMeta === true;
1377
+ if (!isMeta && (promptSource === "typed" || promptSource === "queued")) {
1378
+ const text = extractText(msg.content);
1379
+ if (text)
1380
+ events.push(mkInput(line, ctx, { kind: "message", summary: "user prompt", payload: { role: "user", text } }));
1381
+ }
1382
+ const toolUseResult = line.toolUseResult;
1383
+ for (const block of asArray(msg.content)) {
1384
+ const b = asRecord(block);
1385
+ if (readString(b, ["type"]) !== "tool_result")
1386
+ continue;
1387
+ const id = readString(b, ["tool_use_id"]);
1388
+ const call = id ? toolUses.get(id) : void 0;
1389
+ if (!call)
1390
+ continue;
1391
+ const observed = deriveObserved(call.name, call.input, b, toolUseResult, line, ctx);
1392
+ if (observed)
1393
+ events.push(observed);
1394
+ }
1395
+ return events;
1396
+ }
1397
+ function consumeSystem(line, ctx) {
1398
+ const subtype = readString(line, ["subtype"]);
1399
+ if (subtype === "compact_boundary") {
1400
+ return [mkInput(line, ctx, { kind: "context_compacted", summary: "Context compacted", payload: {} })];
1401
+ }
1402
+ if (subtype === "api_error") {
1403
+ return [mkInput(line, ctx, { kind: "host_event", summary: "API error", payload: { event: "api_error", ...readString(line, ["level"]) ? { level: readString(line, ["level"]) } : {} } })];
1404
+ }
1405
+ if (subtype === "local_command") {
1406
+ return [mkInput(line, ctx, { kind: "command_run", summary: "Local command", payload: { event: "local_command" } })];
1407
+ }
1408
+ if (subtype === "stop_hook_summary") {
1409
+ const prevented = line.preventedContinuation === true;
1410
+ const errors = asArray(line.hookErrors);
1411
+ if (prevented || errors.length > 0) {
1412
+ return [
1413
+ mkInput(line, ctx, {
1414
+ kind: "host_event",
1415
+ summary: prevented ? "Hook blocked continuation" : "Hook error",
1416
+ payload: { event: "hook", preventedContinuation: prevented, hookErrors: errors.length }
1417
+ })
1418
+ ];
1419
+ }
1420
+ }
1421
+ return [];
1422
+ }
1423
+ function deriveObserved(name, input, resultBlock, toolUseResult, line, ctx) {
1424
+ const lname = name.toLowerCase();
1425
+ const tur = asRecord(toolUseResult);
1426
+ const isError = resultBlock.is_error === true;
1427
+ if (READ_TOOLS.has(lname)) {
1428
+ const fileRec = asRecord(tur.file);
1429
+ const path = readString(fileRec, ["filePath"]) ?? readString(input, ["file_path", "path"]);
1430
+ const image = imageReadChars(tur, fileRec, resultBlock);
1431
+ const chars = strlen(fileRec.content) ?? measureResultText(resultBlock) ?? image;
1432
+ return mkInput(line, ctx, {
1433
+ kind: "file_read",
1434
+ summary: path ? `Read ${path}` : "Read file",
1435
+ payload: {
1436
+ ...path ? { path } : {},
1437
+ ...chars !== void 0 ? { chars } : {},
1438
+ ...image !== void 0 ? { image: true } : {}
1439
+ }
1440
+ });
1441
+ }
1442
+ if (EDIT_TOOLS.has(lname)) {
1443
+ const path = readString(tur, ["filePath"]) ?? readString(input, ["file_path", "path"]);
1444
+ const chars = strlen(tur.newString) ?? strlen(input.new_string) ?? measureResultText(resultBlock);
1445
+ return mkInput(line, ctx, {
1446
+ kind: "file_edit",
1447
+ summary: path ? `Edited ${path}` : "Edited file",
1448
+ payload: { ...path ? { path } : {}, ...chars !== void 0 ? { chars } : {} }
1449
+ });
1450
+ }
1451
+ if (WRITE_TOOLS.has(lname)) {
1452
+ const path = readString(tur, ["filePath"]) ?? readString(input, ["file_path", "path"]);
1453
+ const chars = strlen(tur.content) ?? strlen(input.content) ?? measureResultText(resultBlock);
1454
+ return mkInput(line, ctx, {
1455
+ kind: "file_created",
1456
+ summary: path ? `Created ${path}` : "Created file",
1457
+ payload: { ...path ? { path } : {}, ...chars !== void 0 ? { chars } : {} }
1458
+ });
1459
+ }
1460
+ if (BASH_TOOLS.has(lname)) {
1461
+ const command = readString(input, ["command"]);
1462
+ const outputChars2 = (strlen(tur.stdout) ?? 0) + (strlen(tur.stderr) ?? 0) || measureResultText(resultBlock) || 0;
1463
+ const git = command ? gitKind(command) : void 0;
1464
+ if (git) {
1465
+ return mkInput(line, ctx, {
1466
+ kind: git.kind,
1467
+ summary: isError ? `${git.label} (failed)` : git.label,
1468
+ payload: { ...command ? { command } : {}, exitCode: isError ? 1 : 0, outputChars: outputChars2 }
1469
+ });
1470
+ }
1471
+ return mkInput(line, ctx, {
1472
+ kind: "bash",
1473
+ summary: command ? `Ran ${command}` : "Ran shell command",
1474
+ payload: {
1475
+ ...command ? { command } : {},
1476
+ exitCode: isError ? 1 : 0,
1477
+ outputChars: outputChars2,
1478
+ ...readString(input, ["description"]) ? { description: readString(input, ["description"]) } : {}
1479
+ }
1480
+ });
1481
+ }
1482
+ if (SEARCH_TOOLS.has(lname)) {
1483
+ const query = readString(input, ["pattern", "query", "glob", "path"]);
1484
+ return mkInput(line, ctx, {
1485
+ kind: "search",
1486
+ summary: query ? `Searched ${query}` : "Searched",
1487
+ payload: { ...query ? { query } : {} }
1488
+ });
1489
+ }
1490
+ if (SUBAGENT_TOOLS.has(lname)) {
1491
+ const label = readString(input, ["subagent_type", "description"]) ?? "subagent";
1492
+ const agentId = readString(tur, ["agentId"]) ?? label;
1493
+ const ev = mkInput(line, ctx, {
1494
+ kind: "subagent_spawned",
1495
+ summary: `Delegated to ${label}`,
1496
+ payload: {
1497
+ agent: label,
1498
+ agentId,
1499
+ ...readString(input, ["description"]) ? { description: readString(input, ["description"]) } : {},
1500
+ ...shorten2(readString(input, ["prompt"]), 600) ? { prompt: shorten2(readString(input, ["prompt"]), 600) } : {}
1501
+ }
1502
+ });
1503
+ ev.agentId = agentId;
1504
+ ev.agentRole = "subagent";
1505
+ ev.agentLabel = label;
1506
+ return ev;
1507
+ }
1508
+ if (WORKFLOW_TOOLS.has(lname)) {
1509
+ const wfName = readString(tur, ["workflowName"]) ?? "workflow";
1510
+ const wfRun = readString(tur, ["runId"]);
1511
+ const ev = mkInput(line, ctx, {
1512
+ kind: "subagent_spawned",
1513
+ summary: `Ran workflow ${wfName}`,
1514
+ payload: {
1515
+ agent: `workflow:${wfName}`,
1516
+ ...wfRun ? { agentId: wfRun } : {},
1517
+ ...readString(tur, ["summary"]) ? { description: readString(tur, ["summary"]) } : {}
1518
+ }
1519
+ });
1520
+ ev.agentId = wfRun ?? `workflow:${wfName}`;
1521
+ ev.agentRole = "subagent";
1522
+ ev.agentLabel = `workflow:${wfName}`;
1523
+ return ev;
1524
+ }
1525
+ if (COMMAND_TOOLS.has(lname)) {
1526
+ const cmd = readString(input, ["skill", "command"]) ?? readString(tur, ["commandName"]) ?? "command";
1527
+ return mkInput(line, ctx, { kind: "command_run", summary: `/${cmd}`, payload: { command: cmd } });
1528
+ }
1529
+ if (TEAM_TOOLS.has(lname)) {
1530
+ return mkInput(line, ctx, { kind: "host_event", summary: `Team: ${name}`, payload: { tool: name, event: "team" } });
1531
+ }
1532
+ if (TODO_TOOLS.has(lname)) {
1533
+ return mkInput(line, ctx, { kind: "todo_updated", summary: "Updated todos", payload: {} });
1534
+ }
1535
+ const outputChars = measureResultText(resultBlock) ?? strlen(tur.result) ?? strlen(tur.text);
1536
+ return mkInput(line, ctx, {
1537
+ kind: "tool_result",
1538
+ summary: `Used ${prettyTool(name)}`,
1539
+ payload: { tool: name, ...outputChars !== void 0 ? { outputChars } : {} }
1540
+ });
1541
+ }
1542
+ function mkInput(line, ctx, partial) {
1543
+ const redactOpts = {
1544
+ ...ctx.homeDir ? { homeDir: ctx.homeDir } : {},
1545
+ ...ctx.projectDir ? { projectDir: ctx.projectDir } : {},
1546
+ maxStringLength: 4e3
1547
+ };
1548
+ const redacted = redactJsonObject(sanitize(partial.payload), redactOpts);
1549
+ const summary = redactJsonValue(partial.summary, redactOpts).value;
1550
+ const sessionId = readString(line, ["sessionId"]) ?? ctx.defaultSessionId;
1551
+ const ts = readString(line, ["timestamp"]);
1552
+ const cwd = readString(line, ["cwd"]);
1553
+ const input = {
1554
+ host: "claude-code",
1555
+ runId: sessionId,
1556
+ sessionId,
1557
+ kind: partial.kind,
1558
+ summary,
1559
+ payload: redacted.value,
1560
+ redaction: { rawStored: ctx.rawStored ?? false, rulesApplied: redacted.rulesApplied, truncated: redacted.truncated }
1561
+ };
1562
+ if (ts)
1563
+ input.ts = ts;
1564
+ if (cwd)
1565
+ input.cwd = cwd;
1566
+ if (ctx.agent) {
1567
+ input.agentId = ctx.agent.agentId;
1568
+ input.agentRole = "subagent";
1569
+ if (ctx.agent.label)
1570
+ input.agentLabel = redactJsonValue(ctx.agent.label, redactOpts).value;
1571
+ }
1572
+ return input;
1573
+ }
1574
+ function extractText(content) {
1575
+ if (typeof content === "string")
1576
+ return content.trim() || void 0;
1577
+ if (Array.isArray(content)) {
1578
+ const text = content.map((b) => isRecord3(b) && readString(b, ["type"]) === "text" ? readString(b, ["text"]) : void 0).filter((t) => Boolean(t)).join("\n").trim();
1579
+ return text || void 0;
1580
+ }
1581
+ return void 0;
1582
+ }
1583
+ function measureResultText(resultBlock) {
1584
+ const content = resultBlock.content;
1585
+ if (typeof content === "string")
1586
+ return content.length;
1587
+ if (Array.isArray(content)) {
1588
+ let total = 0;
1589
+ for (const b of content) {
1590
+ const t = isRecord3(b) ? readString(b, ["text"]) : void 0;
1591
+ if (t)
1592
+ total += t.length;
1593
+ }
1594
+ return total || void 0;
1595
+ }
1596
+ return void 0;
1597
+ }
1598
+ function strlen(v) {
1599
+ return typeof v === "string" ? v.length : void 0;
1600
+ }
1601
+ function imageReadChars(tur, fileRec, resultBlock) {
1602
+ const isImage = readString(tur, ["type"]) === "image" || typeof fileRec.base64 === "string" || blockHasImage(resultBlock);
1603
+ if (!isImage)
1604
+ return void 0;
1605
+ const dims = asRecord(fileRec.dimensions);
1606
+ const w = readNumber(dims, ["width"]);
1607
+ const h = readNumber(dims, ["height"]);
1608
+ if (w !== void 0 && h !== void 0 && w > 0 && h > 0)
1609
+ return Math.round(w * h / 750) * 4;
1610
+ return 6e3;
1611
+ }
1612
+ function blockHasImage(resultBlock) {
1613
+ const content = resultBlock.content;
1614
+ return Array.isArray(content) && content.some((b) => isRecord3(b) && readString(b, ["type"]) === "image");
1615
+ }
1616
+ function readNumber(record, keys) {
1617
+ for (const key of keys) {
1618
+ const value = readPath(record, key);
1619
+ if (typeof value === "number" && Number.isFinite(value))
1620
+ return value;
1621
+ }
1622
+ return void 0;
1623
+ }
1624
+ function num(v) {
1625
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
1626
+ }
1627
+ function shorten2(v, max) {
1628
+ if (v === void 0)
1629
+ return void 0;
1630
+ return v.length <= max ? v : `${v.slice(0, max)}...`;
1631
+ }
1632
+ function shortLabel(text) {
1633
+ const firstLine = (text.split("\n").find((l) => l.trim().length > 0) ?? text).trim();
1634
+ return firstLine.length > 48 ? `${firstLine.slice(0, 47)}\u2026` : firstLine;
1635
+ }
1636
+ function gitKind(command) {
1637
+ if (/\bgit\s+push\b/.test(command))
1638
+ return { kind: "git_push", label: "Pushed changes" };
1639
+ if (/\bgit\s+commit\b/.test(command))
1640
+ return { kind: "git_commit", label: "Recorded a commit" };
1641
+ return void 0;
1642
+ }
1643
+ function prettyTool(name) {
1644
+ if (!name.startsWith("mcp__"))
1645
+ return name;
1646
+ const rest = name.slice("mcp__".length);
1647
+ const sep = rest.indexOf("__");
1648
+ return sep < 0 ? rest : `${rest.slice(0, sep)}: ${rest.slice(sep + 2)}`;
1649
+ }
1650
+ function readString(record, keys) {
1651
+ for (const key of keys) {
1652
+ const value = readPath(record, key);
1653
+ if (typeof value === "string" && value.length > 0)
1654
+ return value;
1655
+ }
1656
+ return void 0;
1657
+ }
1658
+ function readPath(record, path) {
1659
+ const parts = path.split(".");
1660
+ let current = record;
1661
+ for (const part of parts) {
1662
+ if (!isRecord3(current))
1663
+ return void 0;
1664
+ current = current[part];
1665
+ }
1666
+ return current;
1667
+ }
1668
+ function asArray(value) {
1669
+ return Array.isArray(value) ? value : [];
1670
+ }
1671
+ function asRecord(value) {
1672
+ return isRecord3(value) ? value : {};
1673
+ }
1674
+ function asJsonObject(value) {
1675
+ return isRecord3(value) ? sanitize(value) : {};
1676
+ }
1677
+ function isRecord3(value) {
1678
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1679
+ }
1680
+ function sanitize(value, seen = /* @__PURE__ */ new WeakSet()) {
1681
+ return sanitizeValue(value, seen);
1682
+ }
1683
+ function sanitizeValue(value, seen) {
1684
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1685
+ return value;
1686
+ }
1687
+ if (typeof value !== "object")
1688
+ return String(value);
1689
+ if (seen.has(value))
1690
+ return "[Circular]";
1691
+ seen.add(value);
1692
+ if (Array.isArray(value)) {
1693
+ const out2 = value.map((v) => sanitizeValue(v, seen));
1694
+ seen.delete(value);
1695
+ return out2;
1696
+ }
1697
+ const out = {};
1698
+ for (const [k, v] of Object.entries(value)) {
1699
+ if (typeof v === "undefined" || typeof v === "function" || typeof v === "symbol")
1700
+ continue;
1701
+ out[k] = sanitizeValue(v, seen);
1702
+ }
1703
+ seen.delete(value);
1704
+ return out;
1705
+ }
1706
+
1707
+ // packages/claude-code-adapter/dist/tailer.js
1708
+ import { open, readdir, stat } from "node:fs/promises";
1709
+ import { homedir } from "node:os";
1710
+ import { basename, join } from "node:path";
1711
+ function defaultProjectsDir(homeDir = homedir()) {
1712
+ const override = process.env.CLAUDE_CONFIG_DIR;
1713
+ return override && override.length > 0 ? join(override, "projects") : join(homeDir, ".claude", "projects");
1714
+ }
1715
+ async function startClaudeCodeTailer(sink, options = {}) {
1716
+ const homeDir = options.homeDir ?? homedir();
1717
+ const projectsDir = options.projectsDir ?? defaultProjectsDir(homeDir);
1718
+ const pollMs = options.pollMs ?? 700;
1719
+ const backfillCutoff = Date.now() - (options.backfillDays ?? 2) * 24 * 60 * 60 * 1e3;
1720
+ const files = /* @__PURE__ */ new Map();
1721
+ const seqByRun = /* @__PURE__ */ new Map();
1722
+ const nextSeq = (runId) => {
1723
+ const n = (seqByRun.get(runId) ?? 0) + 1;
1724
+ seqByRun.set(runId, n);
1725
+ return n;
1726
+ };
1727
+ const contextForFile = (filePath) => {
1728
+ const base = basename(filePath).replace(/\.jsonl$/, "");
1729
+ const common = {
1730
+ defaultSessionId: base,
1731
+ homeDir,
1732
+ ...options.rawStored !== void 0 ? { rawStored: options.rawStored } : {}
1733
+ };
1734
+ return base.startsWith("agent-") ? { ...common, agent: { agentId: base.slice("agent-".length) } } : common;
1735
+ };
1736
+ const ensureFile = (filePath) => {
1737
+ let state = files.get(filePath);
1738
+ if (!state) {
1739
+ state = { offset: 0, buffer: "", normalizer: createClaudeNormalizer(contextForFile(filePath)) };
1740
+ files.set(filePath, state);
1741
+ }
1742
+ return state;
1743
+ };
1744
+ const drainFile = async (filePath) => {
1745
+ let size;
1746
+ try {
1747
+ size = (await stat(filePath)).size;
1748
+ } catch {
1749
+ return;
1750
+ }
1751
+ const state = ensureFile(filePath);
1752
+ if (size < state.offset) {
1753
+ state.offset = 0;
1754
+ state.buffer = "";
1755
+ }
1756
+ if (size <= state.offset)
1757
+ return;
1758
+ const fh = await open(filePath, "r");
1759
+ try {
1760
+ const length = size - state.offset;
1761
+ const buf = Buffer.alloc(length);
1762
+ await fh.read(buf, 0, length, state.offset);
1763
+ state.offset = size;
1764
+ state.buffer += buf.toString("utf8");
1765
+ } finally {
1766
+ await fh.close();
1767
+ }
1768
+ const lines = state.buffer.split("\n");
1769
+ state.buffer = lines.pop() ?? "";
1770
+ for (const raw of lines) {
1771
+ const line = raw.trim();
1772
+ if (!line)
1773
+ continue;
1774
+ let parsed;
1775
+ try {
1776
+ parsed = JSON.parse(line);
1777
+ } catch {
1778
+ continue;
1779
+ }
1780
+ const events = state.normalizer.consume(parsed);
1781
+ for (const input of events)
1782
+ await emit(sink, input, nextSeq);
1783
+ }
1784
+ };
1785
+ const listTranscripts = async () => {
1786
+ let entries;
1787
+ try {
1788
+ entries = await readdir(projectsDir, { recursive: true });
1789
+ } catch {
1790
+ return [];
1791
+ }
1792
+ return entries.filter((e) => e.endsWith(".jsonl")).map((e) => join(projectsDir, e));
1793
+ };
1794
+ const initial = await listTranscripts();
1795
+ const recent = [];
1796
+ for (const f of initial) {
1797
+ try {
1798
+ if ((await stat(f)).mtimeMs >= backfillCutoff)
1799
+ recent.push(f);
1800
+ } catch {
1801
+ }
1802
+ }
1803
+ recent.sort((a, b) => Number(basename(a).startsWith("agent-")) - Number(basename(b).startsWith("agent-")));
1804
+ for (const f of recent)
1805
+ await drainFile(f);
1806
+ let running = true;
1807
+ const tick = async () => {
1808
+ if (!running)
1809
+ return;
1810
+ const all = await listTranscripts();
1811
+ all.sort((a, b) => Number(basename(a).startsWith("agent-")) - Number(basename(b).startsWith("agent-")));
1812
+ for (const f of all) {
1813
+ try {
1814
+ await drainFile(f);
1815
+ } catch {
1816
+ }
1817
+ }
1818
+ };
1819
+ const timer = setInterval(() => void tick(), pollMs);
1820
+ if (typeof timer.unref === "function")
1821
+ timer.unref();
1822
+ return {
1823
+ projectsDir,
1824
+ stop: () => {
1825
+ running = false;
1826
+ clearInterval(timer);
1827
+ }
1828
+ };
1829
+ }
1830
+ async function emit(sink, input, nextSeq) {
1831
+ const event = createTraceEvent(nextSeq(input.runId), input);
1832
+ await sink.write(event);
1833
+ }
1834
+
1835
+ // packages/claude-code-adapter/dist/hooks.js
1836
+ var ABB_HOOK_MARKER = "agent-blackbox-hook";
1837
+ function abbHookSpecs() {
1838
+ return [
1839
+ { event: "PreToolUse", matcher: "Read|Edit|MultiEdit|Write|Bash" },
1840
+ { event: "PostToolUse", matcher: "Read|Edit|MultiEdit|Write|Bash" },
1841
+ { event: "UserPromptSubmit" },
1842
+ { event: "PreCompact" },
1843
+ { event: "SessionEnd" }
1844
+ ];
1845
+ }
1846
+ function isAbbGroup(group) {
1847
+ return isRecord4(group) && Array.isArray(group.hooks) && group.hooks.some((h) => isRecord4(h) && typeof h.command === "string" && h.command.includes(ABB_HOOK_MARKER));
1848
+ }
1849
+ function mergeAbbHooks(settings, invocation) {
1850
+ const next = { ...settings };
1851
+ const hooks = isRecord4(settings.hooks) ? { ...settings.hooks } : {};
1852
+ for (const spec of abbHookSpecs()) {
1853
+ const current = hooks[spec.event];
1854
+ const kept = (Array.isArray(current) ? current : []).filter((g) => !isAbbGroup(g));
1855
+ kept.push({
1856
+ ...spec.matcher ? { matcher: spec.matcher } : {},
1857
+ hooks: [{ type: "command", command: `${invocation} ${spec.event} ${ABB_HOOK_MARKER}` }]
1858
+ });
1859
+ hooks[spec.event] = kept;
1860
+ }
1861
+ next.hooks = hooks;
1862
+ return next;
1863
+ }
1864
+ function removeAbbHooks(settings) {
1865
+ if (!isRecord4(settings.hooks))
1866
+ return settings;
1867
+ const next = { ...settings };
1868
+ const hooks = {};
1869
+ for (const [event, groups] of Object.entries(settings.hooks)) {
1870
+ if (!Array.isArray(groups))
1871
+ continue;
1872
+ const kept = groups.filter((g) => !isAbbGroup(g));
1873
+ if (kept.length > 0)
1874
+ hooks[event] = kept;
1875
+ }
1876
+ if (Object.keys(hooks).length > 0)
1877
+ next.hooks = hooks;
1878
+ else
1879
+ delete next.hooks;
1880
+ return next;
1881
+ }
1882
+ function hasAbbHooks(settings) {
1883
+ if (!isRecord4(settings.hooks))
1884
+ return false;
1885
+ return Object.values(settings.hooks).some((groups) => Array.isArray(groups) && groups.some(isAbbGroup));
1886
+ }
1887
+ function isRecord4(value) {
1888
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1889
+ }
1890
+
1119
1891
  // apps/daemon/dist/cli.js
1120
1892
  import { spawn as spawn2 } from "node:child_process";
1121
1893
  import { existsSync } from "node:fs";
1122
- import { homedir as homedir2 } from "node:os";
1894
+ import { homedir as homedir4 } from "node:os";
1123
1895
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1124
- import { dirname as dirname5, join as join6, resolve } from "node:path";
1896
+ import { dirname as dirname6, join as join8, resolve } from "node:path";
1125
1897
 
1126
1898
  // apps/daemon/dist/dashboardServer.js
1127
1899
  import { createReadStream } from "node:fs";
1128
- import { readFile, stat } from "node:fs/promises";
1900
+ import { readFile, stat as stat2 } from "node:fs/promises";
1129
1901
  import { createServer } from "node:http";
1130
- import { extname, join, normalize } from "node:path";
1902
+ import { extname, join as join2, normalize } from "node:path";
1131
1903
  var mimeTypes = {
1132
1904
  ".css": "text/css; charset=utf-8",
1133
1905
  ".html": "text/html; charset=utf-8",
@@ -1141,7 +1913,7 @@ var mimeTypes = {
1141
1913
  ".woff2": "font/woff2"
1142
1914
  };
1143
1915
  async function startDashboardServer(options) {
1144
- const indexPath = join(options.distDir, "index.html");
1916
+ const indexPath = join2(options.distDir, "index.html");
1145
1917
  let indexHtml;
1146
1918
  try {
1147
1919
  indexHtml = await readFile(indexPath, "utf8");
@@ -1165,13 +1937,13 @@ async function startDashboardServer(options) {
1165
1937
  return;
1166
1938
  }
1167
1939
  const safePath = normalize(rawPath).replace(/^(\.\.[/\\])+/, "");
1168
- const filePath = join(options.distDir, safePath);
1940
+ const filePath = join2(options.distDir, safePath);
1169
1941
  if (!filePath.startsWith(options.distDir)) {
1170
1942
  response.writeHead(403);
1171
1943
  response.end("Forbidden");
1172
1944
  return;
1173
1945
  }
1174
- void stat(filePath).then((stats) => {
1946
+ void stat2(filePath).then((stats) => {
1175
1947
  if (!stats.isFile()) {
1176
1948
  throw new Error("not a file");
1177
1949
  }
@@ -1205,7 +1977,7 @@ async function startDashboardServer(options) {
1205
1977
 
1206
1978
  // apps/daemon/dist/index.js
1207
1979
  import { readFileSync } from "node:fs";
1208
- import { dirname as dirname4, join as join5 } from "node:path";
1980
+ import { dirname as dirname4, join as join6 } from "node:path";
1209
1981
  import { fileURLToPath } from "node:url";
1210
1982
 
1211
1983
  // packages/storage/src/ndjson.ts
@@ -1262,13 +2034,17 @@ async function readTraceEvents(filePath) {
1262
2034
  }
1263
2035
 
1264
2036
  // apps/daemon/dist/server.js
2037
+ import { readFile as readFile4 } from "node:fs/promises";
1265
2038
  import { createServer as createServer2 } from "node:http";
1266
- import { join as join3 } from "node:path";
2039
+ import { join as join4 } from "node:path";
1267
2040
  import { WebSocket, WebSocketServer } from "ws";
1268
2041
 
1269
2042
  // apps/daemon/dist/optimize.js
1270
- import { mkdir as mkdir2, readFile as readFile3, rm, writeFile } from "node:fs/promises";
1271
- import { dirname as dirname2, isAbsolute, join as join2 } from "node:path";
2043
+ import { mkdir as mkdir2, readFile as readFile3, rename, rm, writeFile } from "node:fs/promises";
2044
+ import { basename as basename2, dirname as dirname2, isAbsolute, join as join3 } from "node:path";
2045
+ function memoryFileFor(host) {
2046
+ return host === "claude-code" ? "CLAUDE.md" : "AGENTS.md";
2047
+ }
1272
2048
  var flaggedIds = (report) => report.metrics.filter((m) => m.status !== "good").map((m) => m.id);
1273
2049
  var joinIds = (ids) => ids.join(", ");
1274
2050
  var REVERT_MARGIN = 3;
@@ -1278,13 +2054,15 @@ async function runOptimize(options) {
1278
2054
  return { ...result, applied: content !== null && hasManagedBlock(content) };
1279
2055
  }
1280
2056
  async function computeOptimize(options) {
1281
- const eventsFile = options.eventsFile ?? join2(options.projectDir, ".agent-blackbox", "events.ndjson");
2057
+ const eventsFile = options.eventsFile ?? join3(options.projectDir, ".agent-blackbox", "events.ndjson");
1282
2058
  const events = await loadTraceEvents(eventsFile);
1283
2059
  const { runId, events: runEvents } = latestRun(events);
1284
2060
  const runCwd = runEvents.find((e) => typeof e.cwd === "string" && e.cwd.length > 0)?.cwd;
1285
2061
  const targetDir = runCwd && isAbsolute(runCwd) ? runCwd : options.projectDir;
1286
- const agentsMdPath = join2(targetDir, "AGENTS.md");
1287
- const statePath = join2(targetDir, ".agent-blackbox", "optimization.json");
2062
+ const runHost = runEvents.find((e) => typeof e.host === "string")?.host;
2063
+ const memoryFileName = memoryFileFor(runHost);
2064
+ const agentsMdPath = join3(targetDir, memoryFileName);
2065
+ const statePath = join3(targetDir, ".agent-blackbox", "optimization.json");
1288
2066
  const latestTs = runEvents.reduce((max, e) => e.ts > max ? e.ts : max, "");
1289
2067
  const report = runEvents.length > 0 ? computeEfficiencyReport(runEvents) : null;
1290
2068
  const score = report ? report.overallScore : null;
@@ -1295,7 +2073,7 @@ async function computeOptimize(options) {
1295
2073
  if (options.mode === "preview") {
1296
2074
  return {
1297
2075
  mode: "preview",
1298
- action: block ? "Preview only \u2014 re-run with --apply to write this to AGENTS.md." : "This run is clean \u2014 nothing worth pinning.",
2076
+ action: block ? `Preview only \u2014 re-run with --apply to write this to ${memoryFileName}.` : "This run is clean \u2014 nothing worth pinning.",
1299
2077
  score,
1300
2078
  baselineScore: null,
1301
2079
  reclaimableTokens: report?.reclaimableTokens,
@@ -1308,22 +2086,26 @@ async function computeOptimize(options) {
1308
2086
  if (!block || !report || score === null || runId === null) {
1309
2087
  return { mode: "apply", action: "This run is clean \u2014 nothing to apply.", score, baselineScore: null, block: null, agentsMdPath, changed: false };
1310
2088
  }
1311
- const prior = await readMaybe(agentsMdPath);
1312
- const next = upsertManagedBlock(prior ?? "", block);
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
- }
2089
+ const { prior, next } = await serializeWrite(agentsMdPath, async () => {
2090
+ const prior2 = await readMaybe(agentsMdPath);
2091
+ const next2 = upsertManagedBlock(prior2 ?? "", block);
2092
+ if (prior2 === null || next2 !== prior2) {
2093
+ await writeFileAtomic(agentsMdPath, next2);
2094
+ await writeState(statePath, {
2095
+ runId: runId ?? "",
2096
+ baselineScore: score,
2097
+ baselineLatestTs: latestTs,
2098
+ baselineFlagged: flaggedIds(report),
2099
+ fileExisted: prior2 !== null,
2100
+ appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
2101
+ memoryFile: memoryFileName
2102
+ });
2103
+ }
2104
+ return { prior: prior2, next: next2 };
2105
+ });
1324
2106
  return {
1325
2107
  mode: "apply",
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.`,
2108
+ action: `Wrote efficiency memory to ${memoryFileName} \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.`,
1327
2109
  score,
1328
2110
  baselineScore: score,
1329
2111
  reclaimableTokens: report.reclaimableTokens,
@@ -1358,7 +2140,8 @@ async function computeOptimize(options) {
1358
2140
  const metricDiff = [cleared.length ? `cleared ${joinIds(cleared)}` : "", appeared.length ? `new ${joinIds(appeared)}` : ""].filter(Boolean).join("; ");
1359
2141
  const diffSuffix = metricDiff ? ` [${metricDiff}]` : "";
1360
2142
  if (delta < -REVERT_MARGIN) {
1361
- const changed = await restore(agentsMdPath, state.fileExisted);
2143
+ const appliedPath = state.memoryFile ? join3(targetDir, state.memoryFile) : agentsMdPath;
2144
+ const changed = await restore(appliedPath, state.fileExisted);
1362
2145
  await rm(statePath, { force: true });
1363
2146
  return {
1364
2147
  mode: "check",
@@ -1382,32 +2165,35 @@ async function computeOptimize(options) {
1382
2165
  }
1383
2166
  async function revert(agentsMdPath, statePath, score) {
1384
2167
  const state = await readState(statePath);
1385
- const changed = await restore(agentsMdPath, state ? state.fileExisted : true);
2168
+ const path = state?.memoryFile ? join3(dirname2(agentsMdPath), state.memoryFile) : agentsMdPath;
2169
+ const changed = await restore(path, state ? state.fileExisted : true);
1386
2170
  if (state)
1387
2171
  await rm(statePath, { force: true });
1388
2172
  return {
1389
2173
  mode: "revert",
1390
- action: changed ? "Removed the managed efficiency block from AGENTS.md." : "Nothing to revert.",
2174
+ action: changed ? `Removed the managed efficiency block from ${basename2(path)}.` : "Nothing to revert.",
1391
2175
  score,
1392
2176
  baselineScore: state ? state.baselineScore : null,
1393
2177
  block: null,
1394
- agentsMdPath,
2178
+ agentsMdPath: path,
1395
2179
  changed
1396
2180
  };
1397
2181
  }
1398
2182
  async function restore(agentsMdPath, fileExisted) {
1399
- const current = await readMaybe(agentsMdPath);
1400
- if (current === null)
1401
- return false;
1402
- const next = removeManagedBlock(current);
1403
- if (next === current)
1404
- return false;
1405
- if (next.trim() === "" && !fileExisted) {
1406
- await rm(agentsMdPath, { force: true });
2183
+ return serializeWrite(agentsMdPath, async () => {
2184
+ const current = await readMaybe(agentsMdPath);
2185
+ if (current === null)
2186
+ return false;
2187
+ const next = removeManagedBlock(current);
2188
+ if (next === current)
2189
+ return false;
2190
+ if (next.trim() === "" && !fileExisted) {
2191
+ await rm(agentsMdPath, { force: true });
2192
+ return true;
2193
+ }
2194
+ await writeFileAtomic(agentsMdPath, next);
1407
2195
  return true;
1408
- }
1409
- await writeFile(agentsMdPath, next, "utf8");
1410
- return true;
2196
+ });
1411
2197
  }
1412
2198
  function latestRun(events) {
1413
2199
  let latest;
@@ -1483,9 +2269,21 @@ async function readState(path) {
1483
2269
  }
1484
2270
  }
1485
2271
  async function writeState(path, state) {
2272
+ await writeFileAtomic(path, `${JSON.stringify(state, null, 2)}
2273
+ `);
2274
+ }
2275
+ async function writeFileAtomic(path, content) {
1486
2276
  await mkdir2(dirname2(path), { recursive: true });
1487
- await writeFile(path, `${JSON.stringify(state, null, 2)}
1488
- `, "utf8");
2277
+ const tmp = `${path}.${process.pid}.tmp`;
2278
+ await writeFile(tmp, content, "utf8");
2279
+ await rename(tmp, path);
2280
+ }
2281
+ var writeChains2 = /* @__PURE__ */ new Map();
2282
+ function serializeWrite(key, run) {
2283
+ const prev = writeChains2.get(key) ?? Promise.resolve();
2284
+ const result = prev.then(run, run);
2285
+ writeChains2.set(key, result.then(() => void 0, () => void 0));
2286
+ return result;
1489
2287
  }
1490
2288
 
1491
2289
  // apps/daemon/dist/suggestionProvider.js
@@ -1758,11 +2556,12 @@ function extractJsonObject(text) {
1758
2556
 
1759
2557
  // apps/daemon/dist/server.js
1760
2558
  async function startTraceDaemon(options) {
1761
- const eventsFile = options.eventsFile ?? join3(options.projectDir, ".agent-blackbox", "events.ndjson");
2559
+ const eventsFile = options.eventsFile ?? join4(options.projectDir, ".agent-blackbox", "events.ndjson");
1762
2560
  const suggestConfig = options.suggest ?? { mode: "auto" };
1763
2561
  const clients = /* @__PURE__ */ new Set();
2562
+ const scheduleBroadcast = makeBroadcastScheduler(clients, eventsFile);
1764
2563
  const server = createServer2((request, response) => {
1765
- void handleRequest(request, response, eventsFile, clients, suggestConfig, options.projectDir);
2564
+ void handleRequest(request, response, eventsFile, clients, suggestConfig, options.projectDir, scheduleBroadcast);
1766
2565
  });
1767
2566
  const streamServer = new WebSocketServer({ noServer: true });
1768
2567
  server.on("upgrade", (request, socket, head) => {
@@ -1773,8 +2572,11 @@ async function startTraceDaemon(options) {
1773
2572
  }
1774
2573
  streamServer.handleUpgrade(request, socket, head, (client) => {
1775
2574
  clients.add(client);
1776
- client.on("close", () => {
1777
- clients.delete(client);
2575
+ const drop = () => clients.delete(client);
2576
+ client.on("close", drop);
2577
+ client.on("error", () => {
2578
+ drop();
2579
+ client.terminate();
1778
2580
  });
1779
2581
  void sendSnapshot(client, eventsFile);
1780
2582
  });
@@ -1793,6 +2595,12 @@ async function startTraceDaemon(options) {
1793
2595
  server,
1794
2596
  port: actualPort,
1795
2597
  eventsFile,
2598
+ ingest: async (event) => {
2599
+ if (!validateTraceEvent(event).ok)
2600
+ return;
2601
+ await appendTraceEvent(eventsFile, event);
2602
+ scheduleBroadcast();
2603
+ },
1796
2604
  close: () => new Promise((resolve2, reject) => {
1797
2605
  for (const client of clients) {
1798
2606
  client.terminate();
@@ -1819,6 +2627,26 @@ async function loadTraceEvents(eventsFile) {
1819
2627
  throw error;
1820
2628
  }
1821
2629
  }
2630
+ var SNAPSHOT_EVENT_CAP = 3e4;
2631
+ async function loadRecentTraceEvents(eventsFile, cap = SNAPSHOT_EVENT_CAP) {
2632
+ let text;
2633
+ try {
2634
+ text = await readFile4(eventsFile, "utf8");
2635
+ } catch (error) {
2636
+ if (isNodeError(error) && error.code === "ENOENT")
2637
+ return [];
2638
+ throw error;
2639
+ }
2640
+ const lines = text.split("\n");
2641
+ let kept = 0;
2642
+ let start = lines.length;
2643
+ while (start > 0 && kept < cap) {
2644
+ start -= 1;
2645
+ if (lines[start].trim().length > 0)
2646
+ kept += 1;
2647
+ }
2648
+ return parseTraceEvents(lines.slice(start).join("\n"));
2649
+ }
1822
2650
  async function buildReplaySummary(eventsFile) {
1823
2651
  const events = await loadTraceEvents(eventsFile);
1824
2652
  const graph = materializeWorkflowGraph(events);
@@ -1830,7 +2658,7 @@ async function buildReplaySummary(eventsFile) {
1830
2658
  };
1831
2659
  }
1832
2660
  async function buildTraceSnapshot(eventsFile, replay = {}) {
1833
- const events = await loadTraceEvents(eventsFile);
2661
+ const events = await loadRecentTraceEvents(eventsFile);
1834
2662
  const graph = replay.seq !== void 0 ? replayWorkflowGraphAtSeq(events, replay.seq) : replay.at !== void 0 ? replayWorkflowGraphAtTime(events, replay.at) : materializeWorkflowGraph(events);
1835
2663
  const replayedEvents = new Set(graph.appliedEventIds);
1836
2664
  const visibleEvents = events.filter((event) => replayedEvents.has(event.id));
@@ -1850,7 +2678,7 @@ async function buildTraceSnapshot(eventsFile, replay = {}) {
1850
2678
  }
1851
2679
  };
1852
2680
  }
1853
- async function handleRequest(request, response, eventsFile, clients, suggestConfig, projectDir) {
2681
+ async function handleRequest(request, response, eventsFile, clients, suggestConfig, projectDir, scheduleBroadcast) {
1854
2682
  try {
1855
2683
  applyCors(request, response);
1856
2684
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
@@ -1927,7 +2755,7 @@ async function handleRequest(request, response, eventsFile, clients, suggestConf
1927
2755
  return;
1928
2756
  }
1929
2757
  await appendTraceEvent(eventsFile, body);
1930
- void broadcastSnapshot(clients, eventsFile);
2758
+ scheduleBroadcast();
1931
2759
  sendJson(response, 202, { ok: true, data: { accepted: true, id: body.id } });
1932
2760
  return;
1933
2761
  }
@@ -1945,6 +2773,19 @@ async function broadcastSnapshot(clients, eventsFile) {
1945
2773
  }
1946
2774
  await Promise.allSettled([...clients].map((client) => sendSnapshot(client, eventsFile)));
1947
2775
  }
2776
+ function makeBroadcastScheduler(clients, eventsFile, delayMs = 150) {
2777
+ let timer = null;
2778
+ return () => {
2779
+ if (timer)
2780
+ return;
2781
+ timer = setTimeout(() => {
2782
+ timer = null;
2783
+ void broadcastSnapshot(clients, eventsFile);
2784
+ }, delayMs);
2785
+ if (typeof timer.unref === "function")
2786
+ timer.unref();
2787
+ };
2788
+ }
1948
2789
  async function sendSnapshot(client, eventsFile) {
1949
2790
  if (client.readyState !== WebSocket.OPEN) {
1950
2791
  return;
@@ -2037,24 +2878,24 @@ function isNodeError(error) {
2037
2878
  }
2038
2879
 
2039
2880
  // apps/daemon/dist/initOpenCode.js
2040
- import { mkdir as mkdir3, readFile as readFile4, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
2041
- import { homedir } from "node:os";
2042
- import { dirname as dirname3, join as join4 } from "node:path";
2881
+ import { mkdir as mkdir3, readFile as readFile5, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
2882
+ import { homedir as homedir2 } from "node:os";
2883
+ import { dirname as dirname3, join as join5 } from "node:path";
2043
2884
  var defaultAdapterPackage = "@agent-blackbox/opencode-adapter";
2044
2885
  var defaultDaemonUrl = "http://127.0.0.1:47831";
2045
2886
  function globalOpenCodeDir() {
2046
2887
  const xdg = process.env.XDG_CONFIG_HOME;
2047
- return xdg && xdg.length > 0 ? join4(xdg, "opencode") : join4(homedir(), ".config", "opencode");
2888
+ return xdg && xdg.length > 0 ? join5(xdg, "opencode") : join5(homedir2(), ".config", "opencode");
2048
2889
  }
2049
2890
  function globalRecorderPath() {
2050
- return join4(globalOpenCodeDir(), "plugins", "agent-blackbox.js");
2891
+ return join5(globalOpenCodeDir(), "plugins", "agent-blackbox.js");
2051
2892
  }
2052
2893
  async function installGlobalRecorder(options) {
2053
2894
  if (!await pathExists(options.pluginBundlePath)) {
2054
2895
  throw new Error("Self-contained recorder bundle not found. Use the published npx package, or build it from source with `npm run build:cli`.");
2055
2896
  }
2056
2897
  const pluginPath = globalRecorderPath();
2057
- const bundle = (await readFile4(options.pluginBundlePath, "utf8")).replaceAll("__ABB_DAEMON_URL__", options.daemonUrl);
2898
+ const bundle = (await readFile5(options.pluginBundlePath, "utf8")).replaceAll("__ABB_DAEMON_URL__", options.daemonUrl);
2058
2899
  await mkdir3(dirname3(pluginPath), { recursive: true });
2059
2900
  await writeFile2(pluginPath, bundle, "utf8");
2060
2901
  return { pluginPath };
@@ -2074,16 +2915,16 @@ async function initOpenCodeProject(options) {
2074
2915
  const adapterPackage = options.adapterPackage ?? defaultAdapterPackage;
2075
2916
  const adapterImport = inferAdapterImport(adapterPackage);
2076
2917
  const daemonUrl = options.daemonUrl ?? defaultDaemonUrl;
2077
- const opencodeDir = join4(options.projectDir, ".opencode");
2078
- const pluginsDir = join4(opencodeDir, "plugins");
2079
- const pluginPath = join4(pluginsDir, "agent-blackbox.ts");
2080
- const packageJsonPath = join4(opencodeDir, "package.json");
2918
+ const opencodeDir = join5(options.projectDir, ".opencode");
2919
+ const pluginsDir = join5(opencodeDir, "plugins");
2920
+ const pluginPath = join5(pluginsDir, "agent-blackbox.ts");
2921
+ const packageJsonPath = join5(opencodeDir, "package.json");
2081
2922
  await mkdir3(pluginsDir, { recursive: true });
2082
2923
  if (!options.force && await pathExists(pluginPath)) {
2083
2924
  throw new Error(`${pluginPath} already exists. Re-run with --force to overwrite it.`);
2084
2925
  }
2085
2926
  if (options.pluginBundlePath && await pathExists(options.pluginBundlePath)) {
2086
- const bundle = await readFile4(options.pluginBundlePath, "utf8");
2927
+ const bundle = await readFile5(options.pluginBundlePath, "utf8");
2087
2928
  const inlined = bundle.replaceAll("__ABB_DAEMON_URL__", daemonUrl);
2088
2929
  await writeFile2(pluginPath, inlined, "utf8");
2089
2930
  return { pluginPath, packageJsonPath, adapterPackage, adapterImport };
@@ -2122,7 +2963,7 @@ function inferAdapterImport(adapterPackage) {
2122
2963
  }
2123
2964
  async function readPackageJson(packageJsonPath) {
2124
2965
  try {
2125
- return JSON.parse(await readFile4(packageJsonPath, "utf8"));
2966
+ return JSON.parse(await readFile5(packageJsonPath, "utf8"));
2126
2967
  } catch (error) {
2127
2968
  if (isNodeError2(error) && error.code === "ENOENT") {
2128
2969
  return {};
@@ -2132,7 +2973,7 @@ async function readPackageJson(packageJsonPath) {
2132
2973
  }
2133
2974
  async function pathExists(path) {
2134
2975
  try {
2135
- await readFile4(path, "utf8");
2976
+ await readFile5(path, "utf8");
2136
2977
  return true;
2137
2978
  } catch (error) {
2138
2979
  if (isNodeError2(error) && error.code === "ENOENT") {
@@ -2150,7 +2991,7 @@ function resolvePackageVersion() {
2150
2991
  let dir = dirname4(fileURLToPath(import.meta.url));
2151
2992
  for (let i = 0; i < 6; i += 1) {
2152
2993
  try {
2153
- const pkg = JSON.parse(readFileSync(join5(dir, "package.json"), "utf8"));
2994
+ const pkg = JSON.parse(readFileSync(join6(dir, "package.json"), "utf8"));
2154
2995
  if (typeof pkg.version === "string" && pkg.version.length > 0)
2155
2996
  return pkg.version;
2156
2997
  } catch {
@@ -2167,16 +3008,69 @@ function describeDaemon() {
2167
3008
  return "Agent-Blackbox daemon: local ingest, replay, and dashboard bridge.";
2168
3009
  }
2169
3010
 
3011
+ // apps/daemon/dist/initClaudeHooks.js
3012
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
3013
+ import { homedir as homedir3 } from "node:os";
3014
+ import { dirname as dirname5, join as join7 } from "node:path";
3015
+ function globalClaudeDir() {
3016
+ const override = process.env.CLAUDE_CONFIG_DIR;
3017
+ return override && override.length > 0 ? override : join7(homedir3(), ".claude");
3018
+ }
3019
+ function claudeSettingsPath() {
3020
+ return join7(globalClaudeDir(), "settings.json");
3021
+ }
3022
+ async function installClaudeCodeHooks(options) {
3023
+ const settingsPath = claudeSettingsPath();
3024
+ const settings = await readSettings(settingsPath);
3025
+ const next = mergeAbbHooks(settings, `node ${options.hookEntryPath}`);
3026
+ await mkdir4(dirname5(settingsPath), { recursive: true });
3027
+ await writeFile3(settingsPath, `${JSON.stringify(next, null, 2)}
3028
+ `, "utf8");
3029
+ return { settingsPath };
3030
+ }
3031
+ async function uninstallClaudeCodeHooks() {
3032
+ const settingsPath = claudeSettingsPath();
3033
+ let settings;
3034
+ try {
3035
+ settings = JSON.parse(await readFile6(settingsPath, "utf8"));
3036
+ } catch (error) {
3037
+ if (isNotFound(error))
3038
+ return { settingsPath, removed: false };
3039
+ throw error;
3040
+ }
3041
+ if (!hasAbbHooks(settings))
3042
+ return { settingsPath, removed: false };
3043
+ await writeFile3(settingsPath, `${JSON.stringify(removeAbbHooks(settings), null, 2)}
3044
+ `, "utf8");
3045
+ return { settingsPath, removed: true };
3046
+ }
3047
+ async function readSettings(path) {
3048
+ try {
3049
+ return JSON.parse(await readFile6(path, "utf8"));
3050
+ } catch (error) {
3051
+ if (isNotFound(error))
3052
+ return {};
3053
+ throw new Error(`Refusing to edit ${path}: it isn't valid JSON (${error instanceof Error ? error.message : String(error)}).`);
3054
+ }
3055
+ }
3056
+ function isNotFound(error) {
3057
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
3058
+ }
3059
+
2170
3060
  // apps/daemon/dist/cli.js
2171
3061
  var args = process.argv.slice(2);
2172
- var cliDir = dirname5(fileURLToPath2(import.meta.url));
3062
+ var cliDir = dirname6(fileURLToPath2(import.meta.url));
2173
3063
  var repoRoot = resolve(cliDir, "../../..");
2174
3064
  var firstExisting = (paths) => paths.find((p) => existsSync(p));
2175
3065
  var dashboardDistDir = firstExisting([resolve(cliDir, "dashboard"), resolve(repoRoot, "apps/dashboard/dist")]) ?? resolve(repoRoot, "apps/dashboard/dist");
2176
3066
  var pluginBundlePath = firstExisting([resolve(cliDir, "agent-blackbox.plugin.mjs")]);
3067
+ var hookEntryPath = firstExisting([
3068
+ resolve(cliDir, "agent-blackbox-hook.mjs"),
3069
+ resolve(repoRoot, "packages/claude-code-adapter/dist/hook-entry.js")
3070
+ ]);
2177
3071
  function globalDataDir() {
2178
3072
  const xdg = process.env.XDG_DATA_HOME;
2179
- return xdg && xdg.length > 0 ? join6(xdg, "agent-blackbox") : join6(homedir2(), ".local", "share", "agent-blackbox");
3073
+ return xdg && xdg.length > 0 ? join8(xdg, "agent-blackbox") : join8(homedir4(), ".local", "share", "agent-blackbox");
2180
3074
  }
2181
3075
  void main(args).catch((error) => {
2182
3076
  console.error(error instanceof Error ? error.message : String(error));
@@ -2205,26 +3099,50 @@ async function main(argv) {
2205
3099
  const suggest = readSuggestConfig(argv);
2206
3100
  let daemon;
2207
3101
  if (global) {
2208
- if (!pluginBundlePath) {
2209
- 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>.)");
2210
- }
2211
- const { pluginPath } = await installGlobalRecorder({ daemonUrl, pluginBundlePath });
3102
+ const host = readHost(argv);
2212
3103
  const dataDir = globalDataDir();
2213
- const eventsFile = join6(dataDir, "events.ndjson");
3104
+ const eventsFile = join8(dataDir, "events.ndjson");
2214
3105
  daemon = await startTraceDaemon({ projectDir: dataDir, port, eventsFile, suggest });
3106
+ const recorders = [];
3107
+ if (host === "opencode" || host === "all") {
3108
+ if (!pluginBundlePath) {
3109
+ throw new Error("The OpenCode recorder needs the self-contained bundle. Use the published npx package, or `npm run build:cli` then `node packages/cli/dist/cli.js up`.\n(Claude Code needs no bundle \u2014 try: agent-blackbox up --host claude-code.)");
3110
+ }
3111
+ const { pluginPath } = await installGlobalRecorder({ daemonUrl, pluginBundlePath });
3112
+ recorders.push(`OpenCode recorder installed \u2192 ${pluginPath}`);
3113
+ }
3114
+ if (host === "claude-code" || host === "all") {
3115
+ const tailer = await startClaudeCodeTailer({ write: (event) => daemon.ingest(event) });
3116
+ recorders.push(`Claude Code transcripts tailed \u2190 ${tailer.projectsDir} (no install)`);
3117
+ if (argv.includes("--optimize")) {
3118
+ if (hookEntryPath) {
3119
+ const { settingsPath } = await installClaudeCodeHooks({ hookEntryPath });
3120
+ recorders.push(`Claude Code in-run actuator installed \u2192 ${settingsPath} (read-dedup + working-set)`);
3121
+ } else {
3122
+ recorders.push("Claude Code actuator needs the built hook \u2014 run `npm run build` first (recording only for now).");
3123
+ }
3124
+ }
3125
+ }
3126
+ if (host === "codex") {
3127
+ recorders.push("Codex recorder isn't built yet (see local-planning/). Use --host opencode|claude-code|all.");
3128
+ }
2215
3129
  const ui2 = await startDashboardServer({ distDir: dashboardDistDir, port: uiPort, daemonUrl });
2216
3130
  const dashboardUrl2 = `http://127.0.0.1:${ui2.port}`;
2217
- console.log(`\u2713 Global OpenCode recorder installed: ${pluginPath}`);
2218
- console.log(`\u2713 Agent-Blackbox is up (recording all OpenCode sessions)`);
3131
+ for (const line of recorders)
3132
+ console.log(`\u2713 ${line}`);
3133
+ console.log(`\u2713 Agent-Blackbox is up (host: ${host})`);
2219
3134
  console.log(` Dashboard: ${dashboardUrl2}`);
2220
3135
  console.log(` Daemon API: ${daemonUrl} (trace: ${daemon.eventsFile})`);
2221
3136
  console.log(` Suggestions: ${suggest.mode}${suggest.model ? ` (${suggest.model})` : ""}`);
2222
3137
  console.log("");
2223
3138
  if (!argv.includes("--no-open"))
2224
3139
  openInBrowser(dashboardUrl2);
2225
- console.log("Now use OpenCode however you already do \u2014 the dashboard fills in live:");
2226
- console.log(" opencode # in any folder (terminal), or");
2227
- console.log(" the OpenCode desktop app # open any project");
3140
+ if (host === "claude-code" || host === "all") {
3141
+ console.log("Now use Claude Code however you already do \u2014 the map fills in live as it writes transcripts.");
3142
+ }
3143
+ if (host === "opencode" || host === "all") {
3144
+ console.log("Now use OpenCode however you already do (terminal or the desktop app) \u2014 the map fills in live.");
3145
+ }
2228
3146
  console.log("");
2229
3147
  console.log("Stop recording any time with: agent-blackbox uninstall");
2230
3148
  console.log("Press Ctrl+C to stop the daemon + dashboard.");
@@ -2282,6 +3200,24 @@ async function main(argv) {
2282
3200
  if (command === "uninstall") {
2283
3201
  const { pluginPath, removed } = await uninstallGlobalRecorder();
2284
3202
  console.log(removed ? `\u2713 Removed global OpenCode recorder: ${pluginPath}` : `Nothing to remove \u2014 ${pluginPath} is not present.`);
3203
+ const hooks = await uninstallClaudeCodeHooks();
3204
+ if (hooks.removed)
3205
+ console.log(`\u2713 Removed Claude Code actuator hooks: ${hooks.settingsPath}`);
3206
+ return;
3207
+ }
3208
+ if (command === "install-hooks") {
3209
+ if (!hookEntryPath) {
3210
+ throw new Error("Built hook not found. Run `npm run build` first, or use the published npx package.");
3211
+ }
3212
+ const { settingsPath } = await installClaudeCodeHooks({ hookEntryPath });
3213
+ console.log(`\u2713 Claude Code in-run actuator installed: ${settingsPath}`);
3214
+ console.log(" New sessions get PreToolUse read-dedup (skip re-reading unchanged files) + a UserPromptSubmit working-set reminder.");
3215
+ console.log(" Remove with: agent-blackbox uninstall-hooks");
3216
+ return;
3217
+ }
3218
+ if (command === "uninstall-hooks") {
3219
+ const { settingsPath, removed } = await uninstallClaudeCodeHooks();
3220
+ console.log(removed ? `\u2713 Removed Claude Code actuator hooks: ${settingsPath}` : `Nothing to remove \u2014 no Agent-Blackbox hooks in ${settingsPath}.`);
2285
3221
  return;
2286
3222
  }
2287
3223
  if (command === "replay") {
@@ -2342,10 +3278,14 @@ function printHelp() {
2342
3278
  console.log("");
2343
3279
  console.log("Usage:");
2344
3280
  console.log(" agent-blackbox up # GLOBAL: record every OpenCode session (any folder / the app) + daemon + dashboard");
3281
+ console.log(" agent-blackbox up --host claude-code # record Claude Code instead \u2014 no install, tails transcripts (also: opencode | codex | all)");
2345
3282
  console.log(" agent-blackbox up --project <dir> # scope the recorder to one project instead");
2346
3283
  console.log(" [--port <port>] [--ui-port <port>] [--suggest auto|free|off|ollama|opencode|openai-compat] [--suggest-model <id>] [--optimize] [--no-open]");
3284
+ console.log(" [--optimize] with --host claude-code: also install the in-run actuator (read-dedup + working-set hooks)");
2347
3285
  console.log(" agent-blackbox install [--port <port>] # install the global recorder only (no daemon)");
2348
- console.log(" agent-blackbox uninstall # remove the global recorder");
3286
+ console.log(" agent-blackbox install-hooks # install the Claude Code in-run actuator hooks (opt-in)");
3287
+ console.log(" agent-blackbox uninstall-hooks # remove the Claude Code actuator hooks");
3288
+ console.log(" agent-blackbox uninstall # remove the global recorder (+ Claude Code hooks)");
2349
3289
  console.log(" agent-blackbox daemon [--project <dir>] [--port <port>]");
2350
3290
  console.log(" agent-blackbox init-opencode [--project <dir>] [--daemon-url <url>] [--adapter-package <specifier>] [--force] [--optimize]");
2351
3291
  console.log(" agent-blackbox optimize [--project <dir>] [--apply | --check | --revert] # write/measure/rollback AGENTS.md efficiency memory");
@@ -2376,6 +3316,11 @@ function portArg(raw, fallback) {
2376
3316
  const n = Number(raw);
2377
3317
  return Number.isInteger(n) && n >= 0 && n <= 65535 ? n : fallback;
2378
3318
  }
3319
+ function readHost(argv) {
3320
+ const allowed = ["opencode", "claude-code", "codex", "all"];
3321
+ const raw = readFlag(argv, "--host") ?? process.env.AGENT_BLACKBOX_HOST ?? "opencode";
3322
+ return allowed.includes(raw) ? raw : "opencode";
3323
+ }
2379
3324
  function readSuggestConfig(argv) {
2380
3325
  const modes = ["auto", "off", "free", "ollama", "opencode", "openai-compat"];
2381
3326
  const raw = readFlag(argv, "--suggest") ?? process.env.AGENT_BLACKBOX_SUGGEST ?? "auto";