@taewooopark/agent-blackbox 0.46.4 → 0.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-blackbox.plugin.mjs +18 -0
- package/dist/cli.js +1042 -84
- package/dist/dashboard/assets/index-BLyfeT35.css +1 -0
- package/dist/dashboard/assets/index-D4LAVDU_.js +12 -0
- package/dist/dashboard/index.html +2 -2
- package/package.json +1 -1
- package/dist/dashboard/assets/index-4DvMVgn8.js +0 -12
- package/dist/dashboard/assets/index-Ck9bKtYh.css +0 -1
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: {
|
|
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
|
|
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
|
}
|
|
@@ -1117,18 +1292,628 @@ function removeManagedBlock(content) {
|
|
|
1117
1292
|
`;
|
|
1118
1293
|
}
|
|
1119
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 ?? 0) * 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
|
+
for (const f of initial) {
|
|
1796
|
+
try {
|
|
1797
|
+
ensureFile(f).offset = (await stat(f)).size;
|
|
1798
|
+
} catch {
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
if ((options.backfillDays ?? 0) > 0) {
|
|
1802
|
+
const recent = [];
|
|
1803
|
+
for (const f of initial) {
|
|
1804
|
+
try {
|
|
1805
|
+
if ((await stat(f)).mtimeMs >= backfillCutoff)
|
|
1806
|
+
recent.push(f);
|
|
1807
|
+
} catch {
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
recent.sort((a, b) => Number(basename(a).startsWith("agent-")) - Number(basename(b).startsWith("agent-")));
|
|
1811
|
+
for (const f of recent) {
|
|
1812
|
+
const state = files.get(f);
|
|
1813
|
+
if (state) {
|
|
1814
|
+
state.offset = 0;
|
|
1815
|
+
state.buffer = "";
|
|
1816
|
+
}
|
|
1817
|
+
await drainFile(f);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
let running = true;
|
|
1821
|
+
const tick = async () => {
|
|
1822
|
+
if (!running)
|
|
1823
|
+
return;
|
|
1824
|
+
const all = await listTranscripts();
|
|
1825
|
+
all.sort((a, b) => Number(basename(a).startsWith("agent-")) - Number(basename(b).startsWith("agent-")));
|
|
1826
|
+
for (const f of all) {
|
|
1827
|
+
try {
|
|
1828
|
+
await drainFile(f);
|
|
1829
|
+
} catch {
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
const timer = setInterval(() => void tick(), pollMs);
|
|
1834
|
+
if (typeof timer.unref === "function")
|
|
1835
|
+
timer.unref();
|
|
1836
|
+
return {
|
|
1837
|
+
projectsDir,
|
|
1838
|
+
stop: () => {
|
|
1839
|
+
running = false;
|
|
1840
|
+
clearInterval(timer);
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
async function emit(sink, input, nextSeq) {
|
|
1845
|
+
const event = createTraceEvent(nextSeq(input.runId), input);
|
|
1846
|
+
await sink.write(event);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// packages/claude-code-adapter/dist/hooks.js
|
|
1850
|
+
var ABB_HOOK_MARKER = "agent-blackbox-hook";
|
|
1851
|
+
function abbHookSpecs() {
|
|
1852
|
+
return [
|
|
1853
|
+
{ event: "PreToolUse", matcher: "Read|Edit|MultiEdit|Write|Bash" },
|
|
1854
|
+
{ event: "PostToolUse", matcher: "Read|Edit|MultiEdit|Write|Bash" },
|
|
1855
|
+
{ event: "UserPromptSubmit" },
|
|
1856
|
+
{ event: "PreCompact" },
|
|
1857
|
+
{ event: "SessionEnd" }
|
|
1858
|
+
];
|
|
1859
|
+
}
|
|
1860
|
+
function isAbbGroup(group) {
|
|
1861
|
+
return isRecord4(group) && Array.isArray(group.hooks) && group.hooks.some((h) => isRecord4(h) && typeof h.command === "string" && h.command.includes(ABB_HOOK_MARKER));
|
|
1862
|
+
}
|
|
1863
|
+
function mergeAbbHooks(settings, invocation) {
|
|
1864
|
+
const next = { ...settings };
|
|
1865
|
+
const hooks = isRecord4(settings.hooks) ? { ...settings.hooks } : {};
|
|
1866
|
+
for (const spec of abbHookSpecs()) {
|
|
1867
|
+
const current = hooks[spec.event];
|
|
1868
|
+
const kept = (Array.isArray(current) ? current : []).filter((g) => !isAbbGroup(g));
|
|
1869
|
+
kept.push({
|
|
1870
|
+
...spec.matcher ? { matcher: spec.matcher } : {},
|
|
1871
|
+
hooks: [{ type: "command", command: `${invocation} ${spec.event} ${ABB_HOOK_MARKER}` }]
|
|
1872
|
+
});
|
|
1873
|
+
hooks[spec.event] = kept;
|
|
1874
|
+
}
|
|
1875
|
+
next.hooks = hooks;
|
|
1876
|
+
return next;
|
|
1877
|
+
}
|
|
1878
|
+
function removeAbbHooks(settings) {
|
|
1879
|
+
if (!isRecord4(settings.hooks))
|
|
1880
|
+
return settings;
|
|
1881
|
+
const next = { ...settings };
|
|
1882
|
+
const hooks = {};
|
|
1883
|
+
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
1884
|
+
if (!Array.isArray(groups))
|
|
1885
|
+
continue;
|
|
1886
|
+
const kept = groups.filter((g) => !isAbbGroup(g));
|
|
1887
|
+
if (kept.length > 0)
|
|
1888
|
+
hooks[event] = kept;
|
|
1889
|
+
}
|
|
1890
|
+
if (Object.keys(hooks).length > 0)
|
|
1891
|
+
next.hooks = hooks;
|
|
1892
|
+
else
|
|
1893
|
+
delete next.hooks;
|
|
1894
|
+
return next;
|
|
1895
|
+
}
|
|
1896
|
+
function hasAbbHooks(settings) {
|
|
1897
|
+
if (!isRecord4(settings.hooks))
|
|
1898
|
+
return false;
|
|
1899
|
+
return Object.values(settings.hooks).some((groups) => Array.isArray(groups) && groups.some(isAbbGroup));
|
|
1900
|
+
}
|
|
1901
|
+
function isRecord4(value) {
|
|
1902
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1120
1905
|
// apps/daemon/dist/cli.js
|
|
1121
1906
|
import { spawn as spawn2 } from "node:child_process";
|
|
1122
1907
|
import { existsSync } from "node:fs";
|
|
1123
|
-
import { homedir as
|
|
1908
|
+
import { homedir as homedir4 } from "node:os";
|
|
1124
1909
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1125
|
-
import { dirname as
|
|
1910
|
+
import { dirname as dirname6, join as join8, resolve } from "node:path";
|
|
1126
1911
|
|
|
1127
1912
|
// apps/daemon/dist/dashboardServer.js
|
|
1128
1913
|
import { createReadStream } from "node:fs";
|
|
1129
|
-
import { readFile, stat } from "node:fs/promises";
|
|
1914
|
+
import { readFile, stat as stat2 } from "node:fs/promises";
|
|
1130
1915
|
import { createServer } from "node:http";
|
|
1131
|
-
import { extname, join, normalize } from "node:path";
|
|
1916
|
+
import { extname, join as join2, normalize } from "node:path";
|
|
1132
1917
|
var mimeTypes = {
|
|
1133
1918
|
".css": "text/css; charset=utf-8",
|
|
1134
1919
|
".html": "text/html; charset=utf-8",
|
|
@@ -1142,7 +1927,7 @@ var mimeTypes = {
|
|
|
1142
1927
|
".woff2": "font/woff2"
|
|
1143
1928
|
};
|
|
1144
1929
|
async function startDashboardServer(options) {
|
|
1145
|
-
const indexPath =
|
|
1930
|
+
const indexPath = join2(options.distDir, "index.html");
|
|
1146
1931
|
let indexHtml;
|
|
1147
1932
|
try {
|
|
1148
1933
|
indexHtml = await readFile(indexPath, "utf8");
|
|
@@ -1166,13 +1951,13 @@ async function startDashboardServer(options) {
|
|
|
1166
1951
|
return;
|
|
1167
1952
|
}
|
|
1168
1953
|
const safePath = normalize(rawPath).replace(/^(\.\.[/\\])+/, "");
|
|
1169
|
-
const filePath =
|
|
1954
|
+
const filePath = join2(options.distDir, safePath);
|
|
1170
1955
|
if (!filePath.startsWith(options.distDir)) {
|
|
1171
1956
|
response.writeHead(403);
|
|
1172
1957
|
response.end("Forbidden");
|
|
1173
1958
|
return;
|
|
1174
1959
|
}
|
|
1175
|
-
void
|
|
1960
|
+
void stat2(filePath).then((stats) => {
|
|
1176
1961
|
if (!stats.isFile()) {
|
|
1177
1962
|
throw new Error("not a file");
|
|
1178
1963
|
}
|
|
@@ -1206,7 +1991,7 @@ async function startDashboardServer(options) {
|
|
|
1206
1991
|
|
|
1207
1992
|
// apps/daemon/dist/index.js
|
|
1208
1993
|
import { readFileSync } from "node:fs";
|
|
1209
|
-
import { dirname as dirname4, join as
|
|
1994
|
+
import { dirname as dirname4, join as join6 } from "node:path";
|
|
1210
1995
|
import { fileURLToPath } from "node:url";
|
|
1211
1996
|
|
|
1212
1997
|
// packages/storage/src/ndjson.ts
|
|
@@ -1263,13 +2048,17 @@ async function readTraceEvents(filePath) {
|
|
|
1263
2048
|
}
|
|
1264
2049
|
|
|
1265
2050
|
// apps/daemon/dist/server.js
|
|
2051
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
1266
2052
|
import { createServer as createServer2 } from "node:http";
|
|
1267
|
-
import { join as
|
|
2053
|
+
import { join as join4 } from "node:path";
|
|
1268
2054
|
import { WebSocket, WebSocketServer } from "ws";
|
|
1269
2055
|
|
|
1270
2056
|
// apps/daemon/dist/optimize.js
|
|
1271
|
-
import { mkdir as mkdir2, readFile as readFile3, rm, writeFile } from "node:fs/promises";
|
|
1272
|
-
import { dirname as dirname2, isAbsolute, join as
|
|
2057
|
+
import { mkdir as mkdir2, readFile as readFile3, rename, rm, writeFile } from "node:fs/promises";
|
|
2058
|
+
import { basename as basename2, dirname as dirname2, isAbsolute, join as join3 } from "node:path";
|
|
2059
|
+
function memoryFileFor(host) {
|
|
2060
|
+
return host === "claude-code" ? "CLAUDE.md" : "AGENTS.md";
|
|
2061
|
+
}
|
|
1273
2062
|
var flaggedIds = (report) => report.metrics.filter((m) => m.status !== "good").map((m) => m.id);
|
|
1274
2063
|
var joinIds = (ids) => ids.join(", ");
|
|
1275
2064
|
var REVERT_MARGIN = 3;
|
|
@@ -1279,13 +2068,15 @@ async function runOptimize(options) {
|
|
|
1279
2068
|
return { ...result, applied: content !== null && hasManagedBlock(content) };
|
|
1280
2069
|
}
|
|
1281
2070
|
async function computeOptimize(options) {
|
|
1282
|
-
const eventsFile = options.eventsFile ??
|
|
2071
|
+
const eventsFile = options.eventsFile ?? join3(options.projectDir, ".agent-blackbox", "events.ndjson");
|
|
1283
2072
|
const events = await loadTraceEvents(eventsFile);
|
|
1284
2073
|
const { runId, events: runEvents } = latestRun(events);
|
|
1285
2074
|
const runCwd = runEvents.find((e) => typeof e.cwd === "string" && e.cwd.length > 0)?.cwd;
|
|
1286
2075
|
const targetDir = runCwd && isAbsolute(runCwd) ? runCwd : options.projectDir;
|
|
1287
|
-
const
|
|
1288
|
-
const
|
|
2076
|
+
const runHost = runEvents.find((e) => typeof e.host === "string")?.host;
|
|
2077
|
+
const memoryFileName = memoryFileFor(runHost);
|
|
2078
|
+
const agentsMdPath = join3(targetDir, memoryFileName);
|
|
2079
|
+
const statePath = join3(targetDir, ".agent-blackbox", "optimization.json");
|
|
1289
2080
|
const latestTs = runEvents.reduce((max, e) => e.ts > max ? e.ts : max, "");
|
|
1290
2081
|
const report = runEvents.length > 0 ? computeEfficiencyReport(runEvents) : null;
|
|
1291
2082
|
const score = report ? report.overallScore : null;
|
|
@@ -1296,7 +2087,7 @@ async function computeOptimize(options) {
|
|
|
1296
2087
|
if (options.mode === "preview") {
|
|
1297
2088
|
return {
|
|
1298
2089
|
mode: "preview",
|
|
1299
|
-
action: block ?
|
|
2090
|
+
action: block ? `Preview only \u2014 re-run with --apply to write this to ${memoryFileName}.` : "This run is clean \u2014 nothing worth pinning.",
|
|
1300
2091
|
score,
|
|
1301
2092
|
baselineScore: null,
|
|
1302
2093
|
reclaimableTokens: report?.reclaimableTokens,
|
|
@@ -1309,22 +2100,26 @@ async function computeOptimize(options) {
|
|
|
1309
2100
|
if (!block || !report || score === null || runId === null) {
|
|
1310
2101
|
return { mode: "apply", action: "This run is clean \u2014 nothing to apply.", score, baselineScore: null, block: null, agentsMdPath, changed: false };
|
|
1311
2102
|
}
|
|
1312
|
-
const prior = await
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
2103
|
+
const { prior, next } = await serializeWrite(agentsMdPath, async () => {
|
|
2104
|
+
const prior2 = await readMaybe(agentsMdPath);
|
|
2105
|
+
const next2 = upsertManagedBlock(prior2 ?? "", block);
|
|
2106
|
+
if (prior2 === null || next2 !== prior2) {
|
|
2107
|
+
await writeFileAtomic(agentsMdPath, next2);
|
|
2108
|
+
await writeState(statePath, {
|
|
2109
|
+
runId: runId ?? "",
|
|
2110
|
+
baselineScore: score,
|
|
2111
|
+
baselineLatestTs: latestTs,
|
|
2112
|
+
baselineFlagged: flaggedIds(report),
|
|
2113
|
+
fileExisted: prior2 !== null,
|
|
2114
|
+
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2115
|
+
memoryFile: memoryFileName
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
return { prior: prior2, next: next2 };
|
|
2119
|
+
});
|
|
1325
2120
|
return {
|
|
1326
2121
|
mode: "apply",
|
|
1327
|
-
action: `Wrote efficiency memory to
|
|
2122
|
+
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.`,
|
|
1328
2123
|
score,
|
|
1329
2124
|
baselineScore: score,
|
|
1330
2125
|
reclaimableTokens: report.reclaimableTokens,
|
|
@@ -1359,7 +2154,8 @@ async function computeOptimize(options) {
|
|
|
1359
2154
|
const metricDiff = [cleared.length ? `cleared ${joinIds(cleared)}` : "", appeared.length ? `new ${joinIds(appeared)}` : ""].filter(Boolean).join("; ");
|
|
1360
2155
|
const diffSuffix = metricDiff ? ` [${metricDiff}]` : "";
|
|
1361
2156
|
if (delta < -REVERT_MARGIN) {
|
|
1362
|
-
const
|
|
2157
|
+
const appliedPath = state.memoryFile ? join3(targetDir, state.memoryFile) : agentsMdPath;
|
|
2158
|
+
const changed = await restore(appliedPath, state.fileExisted);
|
|
1363
2159
|
await rm(statePath, { force: true });
|
|
1364
2160
|
return {
|
|
1365
2161
|
mode: "check",
|
|
@@ -1383,32 +2179,35 @@ async function computeOptimize(options) {
|
|
|
1383
2179
|
}
|
|
1384
2180
|
async function revert(agentsMdPath, statePath, score) {
|
|
1385
2181
|
const state = await readState(statePath);
|
|
1386
|
-
const
|
|
2182
|
+
const path = state?.memoryFile ? join3(dirname2(agentsMdPath), state.memoryFile) : agentsMdPath;
|
|
2183
|
+
const changed = await restore(path, state ? state.fileExisted : true);
|
|
1387
2184
|
if (state)
|
|
1388
2185
|
await rm(statePath, { force: true });
|
|
1389
2186
|
return {
|
|
1390
2187
|
mode: "revert",
|
|
1391
|
-
action: changed ?
|
|
2188
|
+
action: changed ? `Removed the managed efficiency block from ${basename2(path)}.` : "Nothing to revert.",
|
|
1392
2189
|
score,
|
|
1393
2190
|
baselineScore: state ? state.baselineScore : null,
|
|
1394
2191
|
block: null,
|
|
1395
|
-
agentsMdPath,
|
|
2192
|
+
agentsMdPath: path,
|
|
1396
2193
|
changed
|
|
1397
2194
|
};
|
|
1398
2195
|
}
|
|
1399
2196
|
async function restore(agentsMdPath, fileExisted) {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
2197
|
+
return serializeWrite(agentsMdPath, async () => {
|
|
2198
|
+
const current = await readMaybe(agentsMdPath);
|
|
2199
|
+
if (current === null)
|
|
2200
|
+
return false;
|
|
2201
|
+
const next = removeManagedBlock(current);
|
|
2202
|
+
if (next === current)
|
|
2203
|
+
return false;
|
|
2204
|
+
if (next.trim() === "" && !fileExisted) {
|
|
2205
|
+
await rm(agentsMdPath, { force: true });
|
|
2206
|
+
return true;
|
|
2207
|
+
}
|
|
2208
|
+
await writeFileAtomic(agentsMdPath, next);
|
|
1408
2209
|
return true;
|
|
1409
|
-
}
|
|
1410
|
-
await writeFile(agentsMdPath, next, "utf8");
|
|
1411
|
-
return true;
|
|
2210
|
+
});
|
|
1412
2211
|
}
|
|
1413
2212
|
function latestRun(events) {
|
|
1414
2213
|
let latest;
|
|
@@ -1484,9 +2283,21 @@ async function readState(path) {
|
|
|
1484
2283
|
}
|
|
1485
2284
|
}
|
|
1486
2285
|
async function writeState(path, state) {
|
|
2286
|
+
await writeFileAtomic(path, `${JSON.stringify(state, null, 2)}
|
|
2287
|
+
`);
|
|
2288
|
+
}
|
|
2289
|
+
async function writeFileAtomic(path, content) {
|
|
1487
2290
|
await mkdir2(dirname2(path), { recursive: true });
|
|
1488
|
-
|
|
1489
|
-
|
|
2291
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
2292
|
+
await writeFile(tmp, content, "utf8");
|
|
2293
|
+
await rename(tmp, path);
|
|
2294
|
+
}
|
|
2295
|
+
var writeChains2 = /* @__PURE__ */ new Map();
|
|
2296
|
+
function serializeWrite(key, run) {
|
|
2297
|
+
const prev = writeChains2.get(key) ?? Promise.resolve();
|
|
2298
|
+
const result = prev.then(run, run);
|
|
2299
|
+
writeChains2.set(key, result.then(() => void 0, () => void 0));
|
|
2300
|
+
return result;
|
|
1490
2301
|
}
|
|
1491
2302
|
|
|
1492
2303
|
// apps/daemon/dist/suggestionProvider.js
|
|
@@ -1759,11 +2570,12 @@ function extractJsonObject(text) {
|
|
|
1759
2570
|
|
|
1760
2571
|
// apps/daemon/dist/server.js
|
|
1761
2572
|
async function startTraceDaemon(options) {
|
|
1762
|
-
const eventsFile = options.eventsFile ??
|
|
2573
|
+
const eventsFile = options.eventsFile ?? join4(options.projectDir, ".agent-blackbox", "events.ndjson");
|
|
1763
2574
|
const suggestConfig = options.suggest ?? { mode: "auto" };
|
|
1764
2575
|
const clients = /* @__PURE__ */ new Set();
|
|
2576
|
+
const scheduleBroadcast = makeBroadcastScheduler(clients, eventsFile);
|
|
1765
2577
|
const server = createServer2((request, response) => {
|
|
1766
|
-
void handleRequest(request, response, eventsFile, clients, suggestConfig, options.projectDir);
|
|
2578
|
+
void handleRequest(request, response, eventsFile, clients, suggestConfig, options.projectDir, scheduleBroadcast);
|
|
1767
2579
|
});
|
|
1768
2580
|
const streamServer = new WebSocketServer({ noServer: true });
|
|
1769
2581
|
server.on("upgrade", (request, socket, head) => {
|
|
@@ -1774,8 +2586,11 @@ async function startTraceDaemon(options) {
|
|
|
1774
2586
|
}
|
|
1775
2587
|
streamServer.handleUpgrade(request, socket, head, (client) => {
|
|
1776
2588
|
clients.add(client);
|
|
1777
|
-
|
|
1778
|
-
|
|
2589
|
+
const drop = () => clients.delete(client);
|
|
2590
|
+
client.on("close", drop);
|
|
2591
|
+
client.on("error", () => {
|
|
2592
|
+
drop();
|
|
2593
|
+
client.terminate();
|
|
1779
2594
|
});
|
|
1780
2595
|
void sendSnapshot(client, eventsFile);
|
|
1781
2596
|
});
|
|
@@ -1794,6 +2609,12 @@ async function startTraceDaemon(options) {
|
|
|
1794
2609
|
server,
|
|
1795
2610
|
port: actualPort,
|
|
1796
2611
|
eventsFile,
|
|
2612
|
+
ingest: async (event) => {
|
|
2613
|
+
if (!validateTraceEvent(event).ok)
|
|
2614
|
+
return;
|
|
2615
|
+
await appendTraceEvent(eventsFile, event);
|
|
2616
|
+
scheduleBroadcast();
|
|
2617
|
+
},
|
|
1797
2618
|
close: () => new Promise((resolve2, reject) => {
|
|
1798
2619
|
for (const client of clients) {
|
|
1799
2620
|
client.terminate();
|
|
@@ -1820,6 +2641,26 @@ async function loadTraceEvents(eventsFile) {
|
|
|
1820
2641
|
throw error;
|
|
1821
2642
|
}
|
|
1822
2643
|
}
|
|
2644
|
+
var SNAPSHOT_EVENT_CAP = 3e4;
|
|
2645
|
+
async function loadRecentTraceEvents(eventsFile, cap = SNAPSHOT_EVENT_CAP) {
|
|
2646
|
+
let text;
|
|
2647
|
+
try {
|
|
2648
|
+
text = await readFile4(eventsFile, "utf8");
|
|
2649
|
+
} catch (error) {
|
|
2650
|
+
if (isNodeError(error) && error.code === "ENOENT")
|
|
2651
|
+
return [];
|
|
2652
|
+
throw error;
|
|
2653
|
+
}
|
|
2654
|
+
const lines = text.split("\n");
|
|
2655
|
+
let kept = 0;
|
|
2656
|
+
let start = lines.length;
|
|
2657
|
+
while (start > 0 && kept < cap) {
|
|
2658
|
+
start -= 1;
|
|
2659
|
+
if (lines[start].trim().length > 0)
|
|
2660
|
+
kept += 1;
|
|
2661
|
+
}
|
|
2662
|
+
return parseTraceEvents(lines.slice(start).join("\n"));
|
|
2663
|
+
}
|
|
1823
2664
|
async function buildReplaySummary(eventsFile) {
|
|
1824
2665
|
const events = await loadTraceEvents(eventsFile);
|
|
1825
2666
|
const graph = materializeWorkflowGraph(events);
|
|
@@ -1831,7 +2672,7 @@ async function buildReplaySummary(eventsFile) {
|
|
|
1831
2672
|
};
|
|
1832
2673
|
}
|
|
1833
2674
|
async function buildTraceSnapshot(eventsFile, replay = {}) {
|
|
1834
|
-
const events = await
|
|
2675
|
+
const events = await loadRecentTraceEvents(eventsFile);
|
|
1835
2676
|
const graph = replay.seq !== void 0 ? replayWorkflowGraphAtSeq(events, replay.seq) : replay.at !== void 0 ? replayWorkflowGraphAtTime(events, replay.at) : materializeWorkflowGraph(events);
|
|
1836
2677
|
const replayedEvents = new Set(graph.appliedEventIds);
|
|
1837
2678
|
const visibleEvents = events.filter((event) => replayedEvents.has(event.id));
|
|
@@ -1851,7 +2692,7 @@ async function buildTraceSnapshot(eventsFile, replay = {}) {
|
|
|
1851
2692
|
}
|
|
1852
2693
|
};
|
|
1853
2694
|
}
|
|
1854
|
-
async function handleRequest(request, response, eventsFile, clients, suggestConfig, projectDir) {
|
|
2695
|
+
async function handleRequest(request, response, eventsFile, clients, suggestConfig, projectDir, scheduleBroadcast) {
|
|
1855
2696
|
try {
|
|
1856
2697
|
applyCors(request, response);
|
|
1857
2698
|
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
@@ -1928,7 +2769,7 @@ async function handleRequest(request, response, eventsFile, clients, suggestConf
|
|
|
1928
2769
|
return;
|
|
1929
2770
|
}
|
|
1930
2771
|
await appendTraceEvent(eventsFile, body);
|
|
1931
|
-
|
|
2772
|
+
scheduleBroadcast();
|
|
1932
2773
|
sendJson(response, 202, { ok: true, data: { accepted: true, id: body.id } });
|
|
1933
2774
|
return;
|
|
1934
2775
|
}
|
|
@@ -1946,6 +2787,19 @@ async function broadcastSnapshot(clients, eventsFile) {
|
|
|
1946
2787
|
}
|
|
1947
2788
|
await Promise.allSettled([...clients].map((client) => sendSnapshot(client, eventsFile)));
|
|
1948
2789
|
}
|
|
2790
|
+
function makeBroadcastScheduler(clients, eventsFile, delayMs = 150) {
|
|
2791
|
+
let timer = null;
|
|
2792
|
+
return () => {
|
|
2793
|
+
if (timer)
|
|
2794
|
+
return;
|
|
2795
|
+
timer = setTimeout(() => {
|
|
2796
|
+
timer = null;
|
|
2797
|
+
void broadcastSnapshot(clients, eventsFile);
|
|
2798
|
+
}, delayMs);
|
|
2799
|
+
if (typeof timer.unref === "function")
|
|
2800
|
+
timer.unref();
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
1949
2803
|
async function sendSnapshot(client, eventsFile) {
|
|
1950
2804
|
if (client.readyState !== WebSocket.OPEN) {
|
|
1951
2805
|
return;
|
|
@@ -2038,24 +2892,24 @@ function isNodeError(error) {
|
|
|
2038
2892
|
}
|
|
2039
2893
|
|
|
2040
2894
|
// apps/daemon/dist/initOpenCode.js
|
|
2041
|
-
import { mkdir as mkdir3, readFile as
|
|
2042
|
-
import { homedir } from "node:os";
|
|
2043
|
-
import { dirname as dirname3, join as
|
|
2895
|
+
import { mkdir as mkdir3, readFile as readFile5, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2896
|
+
import { homedir as homedir2 } from "node:os";
|
|
2897
|
+
import { dirname as dirname3, join as join5 } from "node:path";
|
|
2044
2898
|
var defaultAdapterPackage = "@agent-blackbox/opencode-adapter";
|
|
2045
2899
|
var defaultDaemonUrl = "http://127.0.0.1:47831";
|
|
2046
2900
|
function globalOpenCodeDir() {
|
|
2047
2901
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
2048
|
-
return xdg && xdg.length > 0 ?
|
|
2902
|
+
return xdg && xdg.length > 0 ? join5(xdg, "opencode") : join5(homedir2(), ".config", "opencode");
|
|
2049
2903
|
}
|
|
2050
2904
|
function globalRecorderPath() {
|
|
2051
|
-
return
|
|
2905
|
+
return join5(globalOpenCodeDir(), "plugins", "agent-blackbox.js");
|
|
2052
2906
|
}
|
|
2053
2907
|
async function installGlobalRecorder(options) {
|
|
2054
2908
|
if (!await pathExists(options.pluginBundlePath)) {
|
|
2055
2909
|
throw new Error("Self-contained recorder bundle not found. Use the published npx package, or build it from source with `npm run build:cli`.");
|
|
2056
2910
|
}
|
|
2057
2911
|
const pluginPath = globalRecorderPath();
|
|
2058
|
-
const bundle = (await
|
|
2912
|
+
const bundle = (await readFile5(options.pluginBundlePath, "utf8")).replaceAll("__ABB_DAEMON_URL__", options.daemonUrl);
|
|
2059
2913
|
await mkdir3(dirname3(pluginPath), { recursive: true });
|
|
2060
2914
|
await writeFile2(pluginPath, bundle, "utf8");
|
|
2061
2915
|
return { pluginPath };
|
|
@@ -2075,16 +2929,16 @@ async function initOpenCodeProject(options) {
|
|
|
2075
2929
|
const adapterPackage = options.adapterPackage ?? defaultAdapterPackage;
|
|
2076
2930
|
const adapterImport = inferAdapterImport(adapterPackage);
|
|
2077
2931
|
const daemonUrl = options.daemonUrl ?? defaultDaemonUrl;
|
|
2078
|
-
const opencodeDir =
|
|
2079
|
-
const pluginsDir =
|
|
2080
|
-
const pluginPath =
|
|
2081
|
-
const packageJsonPath =
|
|
2932
|
+
const opencodeDir = join5(options.projectDir, ".opencode");
|
|
2933
|
+
const pluginsDir = join5(opencodeDir, "plugins");
|
|
2934
|
+
const pluginPath = join5(pluginsDir, "agent-blackbox.ts");
|
|
2935
|
+
const packageJsonPath = join5(opencodeDir, "package.json");
|
|
2082
2936
|
await mkdir3(pluginsDir, { recursive: true });
|
|
2083
2937
|
if (!options.force && await pathExists(pluginPath)) {
|
|
2084
2938
|
throw new Error(`${pluginPath} already exists. Re-run with --force to overwrite it.`);
|
|
2085
2939
|
}
|
|
2086
2940
|
if (options.pluginBundlePath && await pathExists(options.pluginBundlePath)) {
|
|
2087
|
-
const bundle = await
|
|
2941
|
+
const bundle = await readFile5(options.pluginBundlePath, "utf8");
|
|
2088
2942
|
const inlined = bundle.replaceAll("__ABB_DAEMON_URL__", daemonUrl);
|
|
2089
2943
|
await writeFile2(pluginPath, inlined, "utf8");
|
|
2090
2944
|
return { pluginPath, packageJsonPath, adapterPackage, adapterImport };
|
|
@@ -2123,7 +2977,7 @@ function inferAdapterImport(adapterPackage) {
|
|
|
2123
2977
|
}
|
|
2124
2978
|
async function readPackageJson(packageJsonPath) {
|
|
2125
2979
|
try {
|
|
2126
|
-
return JSON.parse(await
|
|
2980
|
+
return JSON.parse(await readFile5(packageJsonPath, "utf8"));
|
|
2127
2981
|
} catch (error) {
|
|
2128
2982
|
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
2129
2983
|
return {};
|
|
@@ -2133,7 +2987,7 @@ async function readPackageJson(packageJsonPath) {
|
|
|
2133
2987
|
}
|
|
2134
2988
|
async function pathExists(path) {
|
|
2135
2989
|
try {
|
|
2136
|
-
await
|
|
2990
|
+
await readFile5(path, "utf8");
|
|
2137
2991
|
return true;
|
|
2138
2992
|
} catch (error) {
|
|
2139
2993
|
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
@@ -2151,7 +3005,7 @@ function resolvePackageVersion() {
|
|
|
2151
3005
|
let dir = dirname4(fileURLToPath(import.meta.url));
|
|
2152
3006
|
for (let i = 0; i < 6; i += 1) {
|
|
2153
3007
|
try {
|
|
2154
|
-
const pkg = JSON.parse(readFileSync(
|
|
3008
|
+
const pkg = JSON.parse(readFileSync(join6(dir, "package.json"), "utf8"));
|
|
2155
3009
|
if (typeof pkg.version === "string" && pkg.version.length > 0)
|
|
2156
3010
|
return pkg.version;
|
|
2157
3011
|
} catch {
|
|
@@ -2168,16 +3022,69 @@ function describeDaemon() {
|
|
|
2168
3022
|
return "Agent-Blackbox daemon: local ingest, replay, and dashboard bridge.";
|
|
2169
3023
|
}
|
|
2170
3024
|
|
|
3025
|
+
// apps/daemon/dist/initClaudeHooks.js
|
|
3026
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
|
|
3027
|
+
import { homedir as homedir3 } from "node:os";
|
|
3028
|
+
import { dirname as dirname5, join as join7 } from "node:path";
|
|
3029
|
+
function globalClaudeDir() {
|
|
3030
|
+
const override = process.env.CLAUDE_CONFIG_DIR;
|
|
3031
|
+
return override && override.length > 0 ? override : join7(homedir3(), ".claude");
|
|
3032
|
+
}
|
|
3033
|
+
function claudeSettingsPath() {
|
|
3034
|
+
return join7(globalClaudeDir(), "settings.json");
|
|
3035
|
+
}
|
|
3036
|
+
async function installClaudeCodeHooks(options) {
|
|
3037
|
+
const settingsPath = claudeSettingsPath();
|
|
3038
|
+
const settings = await readSettings(settingsPath);
|
|
3039
|
+
const next = mergeAbbHooks(settings, `node ${options.hookEntryPath}`);
|
|
3040
|
+
await mkdir4(dirname5(settingsPath), { recursive: true });
|
|
3041
|
+
await writeFile3(settingsPath, `${JSON.stringify(next, null, 2)}
|
|
3042
|
+
`, "utf8");
|
|
3043
|
+
return { settingsPath };
|
|
3044
|
+
}
|
|
3045
|
+
async function uninstallClaudeCodeHooks() {
|
|
3046
|
+
const settingsPath = claudeSettingsPath();
|
|
3047
|
+
let settings;
|
|
3048
|
+
try {
|
|
3049
|
+
settings = JSON.parse(await readFile6(settingsPath, "utf8"));
|
|
3050
|
+
} catch (error) {
|
|
3051
|
+
if (isNotFound(error))
|
|
3052
|
+
return { settingsPath, removed: false };
|
|
3053
|
+
throw error;
|
|
3054
|
+
}
|
|
3055
|
+
if (!hasAbbHooks(settings))
|
|
3056
|
+
return { settingsPath, removed: false };
|
|
3057
|
+
await writeFile3(settingsPath, `${JSON.stringify(removeAbbHooks(settings), null, 2)}
|
|
3058
|
+
`, "utf8");
|
|
3059
|
+
return { settingsPath, removed: true };
|
|
3060
|
+
}
|
|
3061
|
+
async function readSettings(path) {
|
|
3062
|
+
try {
|
|
3063
|
+
return JSON.parse(await readFile6(path, "utf8"));
|
|
3064
|
+
} catch (error) {
|
|
3065
|
+
if (isNotFound(error))
|
|
3066
|
+
return {};
|
|
3067
|
+
throw new Error(`Refusing to edit ${path}: it isn't valid JSON (${error instanceof Error ? error.message : String(error)}).`);
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
function isNotFound(error) {
|
|
3071
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
3072
|
+
}
|
|
3073
|
+
|
|
2171
3074
|
// apps/daemon/dist/cli.js
|
|
2172
3075
|
var args = process.argv.slice(2);
|
|
2173
|
-
var cliDir =
|
|
3076
|
+
var cliDir = dirname6(fileURLToPath2(import.meta.url));
|
|
2174
3077
|
var repoRoot = resolve(cliDir, "../../..");
|
|
2175
3078
|
var firstExisting = (paths) => paths.find((p) => existsSync(p));
|
|
2176
3079
|
var dashboardDistDir = firstExisting([resolve(cliDir, "dashboard"), resolve(repoRoot, "apps/dashboard/dist")]) ?? resolve(repoRoot, "apps/dashboard/dist");
|
|
2177
3080
|
var pluginBundlePath = firstExisting([resolve(cliDir, "agent-blackbox.plugin.mjs")]);
|
|
3081
|
+
var hookEntryPath = firstExisting([
|
|
3082
|
+
resolve(cliDir, "agent-blackbox-hook.mjs"),
|
|
3083
|
+
resolve(repoRoot, "packages/claude-code-adapter/dist/hook-entry.js")
|
|
3084
|
+
]);
|
|
2178
3085
|
function globalDataDir() {
|
|
2179
3086
|
const xdg = process.env.XDG_DATA_HOME;
|
|
2180
|
-
return xdg && xdg.length > 0 ?
|
|
3087
|
+
return xdg && xdg.length > 0 ? join8(xdg, "agent-blackbox") : join8(homedir4(), ".local", "share", "agent-blackbox");
|
|
2181
3088
|
}
|
|
2182
3089
|
void main(args).catch((error) => {
|
|
2183
3090
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -2206,26 +3113,50 @@ async function main(argv) {
|
|
|
2206
3113
|
const suggest = readSuggestConfig(argv);
|
|
2207
3114
|
let daemon;
|
|
2208
3115
|
if (global) {
|
|
2209
|
-
|
|
2210
|
-
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>.)");
|
|
2211
|
-
}
|
|
2212
|
-
const { pluginPath } = await installGlobalRecorder({ daemonUrl, pluginBundlePath });
|
|
3116
|
+
const host = readHost(argv);
|
|
2213
3117
|
const dataDir = globalDataDir();
|
|
2214
|
-
const eventsFile =
|
|
3118
|
+
const eventsFile = join8(dataDir, "events.ndjson");
|
|
2215
3119
|
daemon = await startTraceDaemon({ projectDir: dataDir, port, eventsFile, suggest });
|
|
3120
|
+
const recorders = [];
|
|
3121
|
+
if (host === "opencode" || host === "all") {
|
|
3122
|
+
if (!pluginBundlePath) {
|
|
3123
|
+
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.)");
|
|
3124
|
+
}
|
|
3125
|
+
const { pluginPath } = await installGlobalRecorder({ daemonUrl, pluginBundlePath });
|
|
3126
|
+
recorders.push(`OpenCode recorder installed \u2192 ${pluginPath}`);
|
|
3127
|
+
}
|
|
3128
|
+
if (host === "claude-code" || host === "all") {
|
|
3129
|
+
const tailer = await startClaudeCodeTailer({ write: (event) => daemon.ingest(event) });
|
|
3130
|
+
recorders.push(`Claude Code transcripts tailed \u2190 ${tailer.projectsDir} (no install)`);
|
|
3131
|
+
if (argv.includes("--optimize")) {
|
|
3132
|
+
if (hookEntryPath) {
|
|
3133
|
+
const { settingsPath } = await installClaudeCodeHooks({ hookEntryPath });
|
|
3134
|
+
recorders.push(`Claude Code in-run actuator installed \u2192 ${settingsPath} (read-dedup + working-set)`);
|
|
3135
|
+
} else {
|
|
3136
|
+
recorders.push("Claude Code actuator needs the built hook \u2014 run `npm run build` first (recording only for now).");
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
if (host === "codex") {
|
|
3141
|
+
recorders.push("Codex recorder isn't built yet (see local-planning/). Use --host opencode|claude-code|all.");
|
|
3142
|
+
}
|
|
2216
3143
|
const ui2 = await startDashboardServer({ distDir: dashboardDistDir, port: uiPort, daemonUrl });
|
|
2217
3144
|
const dashboardUrl2 = `http://127.0.0.1:${ui2.port}`;
|
|
2218
|
-
|
|
2219
|
-
|
|
3145
|
+
for (const line of recorders)
|
|
3146
|
+
console.log(`\u2713 ${line}`);
|
|
3147
|
+
console.log(`\u2713 Agent-Blackbox is up (host: ${host})`);
|
|
2220
3148
|
console.log(` Dashboard: ${dashboardUrl2}`);
|
|
2221
3149
|
console.log(` Daemon API: ${daemonUrl} (trace: ${daemon.eventsFile})`);
|
|
2222
3150
|
console.log(` Suggestions: ${suggest.mode}${suggest.model ? ` (${suggest.model})` : ""}`);
|
|
2223
3151
|
console.log("");
|
|
2224
3152
|
if (!argv.includes("--no-open"))
|
|
2225
3153
|
openInBrowser(dashboardUrl2);
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
3154
|
+
if (host === "claude-code" || host === "all") {
|
|
3155
|
+
console.log("Now use Claude Code however you already do \u2014 the map fills in live as it writes transcripts.");
|
|
3156
|
+
}
|
|
3157
|
+
if (host === "opencode" || host === "all") {
|
|
3158
|
+
console.log("Now use OpenCode however you already do (terminal or the desktop app) \u2014 the map fills in live.");
|
|
3159
|
+
}
|
|
2229
3160
|
console.log("");
|
|
2230
3161
|
console.log("Stop recording any time with: agent-blackbox uninstall");
|
|
2231
3162
|
console.log("Press Ctrl+C to stop the daemon + dashboard.");
|
|
@@ -2283,6 +3214,24 @@ async function main(argv) {
|
|
|
2283
3214
|
if (command === "uninstall") {
|
|
2284
3215
|
const { pluginPath, removed } = await uninstallGlobalRecorder();
|
|
2285
3216
|
console.log(removed ? `\u2713 Removed global OpenCode recorder: ${pluginPath}` : `Nothing to remove \u2014 ${pluginPath} is not present.`);
|
|
3217
|
+
const hooks = await uninstallClaudeCodeHooks();
|
|
3218
|
+
if (hooks.removed)
|
|
3219
|
+
console.log(`\u2713 Removed Claude Code actuator hooks: ${hooks.settingsPath}`);
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
3222
|
+
if (command === "install-hooks") {
|
|
3223
|
+
if (!hookEntryPath) {
|
|
3224
|
+
throw new Error("Built hook not found. Run `npm run build` first, or use the published npx package.");
|
|
3225
|
+
}
|
|
3226
|
+
const { settingsPath } = await installClaudeCodeHooks({ hookEntryPath });
|
|
3227
|
+
console.log(`\u2713 Claude Code in-run actuator installed: ${settingsPath}`);
|
|
3228
|
+
console.log(" New sessions get PreToolUse read-dedup (skip re-reading unchanged files) + a UserPromptSubmit working-set reminder.");
|
|
3229
|
+
console.log(" Remove with: agent-blackbox uninstall-hooks");
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
3232
|
+
if (command === "uninstall-hooks") {
|
|
3233
|
+
const { settingsPath, removed } = await uninstallClaudeCodeHooks();
|
|
3234
|
+
console.log(removed ? `\u2713 Removed Claude Code actuator hooks: ${settingsPath}` : `Nothing to remove \u2014 no Agent-Blackbox hooks in ${settingsPath}.`);
|
|
2286
3235
|
return;
|
|
2287
3236
|
}
|
|
2288
3237
|
if (command === "replay") {
|
|
@@ -2343,10 +3292,14 @@ function printHelp() {
|
|
|
2343
3292
|
console.log("");
|
|
2344
3293
|
console.log("Usage:");
|
|
2345
3294
|
console.log(" agent-blackbox up # GLOBAL: record every OpenCode session (any folder / the app) + daemon + dashboard");
|
|
3295
|
+
console.log(" agent-blackbox up --host claude-code # record Claude Code instead \u2014 no install, tails transcripts (also: opencode | codex | all)");
|
|
2346
3296
|
console.log(" agent-blackbox up --project <dir> # scope the recorder to one project instead");
|
|
2347
3297
|
console.log(" [--port <port>] [--ui-port <port>] [--suggest auto|free|off|ollama|opencode|openai-compat] [--suggest-model <id>] [--optimize] [--no-open]");
|
|
3298
|
+
console.log(" [--optimize] with --host claude-code: also install the in-run actuator (read-dedup + working-set hooks)");
|
|
2348
3299
|
console.log(" agent-blackbox install [--port <port>] # install the global recorder only (no daemon)");
|
|
2349
|
-
console.log(" agent-blackbox
|
|
3300
|
+
console.log(" agent-blackbox install-hooks # install the Claude Code in-run actuator hooks (opt-in)");
|
|
3301
|
+
console.log(" agent-blackbox uninstall-hooks # remove the Claude Code actuator hooks");
|
|
3302
|
+
console.log(" agent-blackbox uninstall # remove the global recorder (+ Claude Code hooks)");
|
|
2350
3303
|
console.log(" agent-blackbox daemon [--project <dir>] [--port <port>]");
|
|
2351
3304
|
console.log(" agent-blackbox init-opencode [--project <dir>] [--daemon-url <url>] [--adapter-package <specifier>] [--force] [--optimize]");
|
|
2352
3305
|
console.log(" agent-blackbox optimize [--project <dir>] [--apply | --check | --revert] # write/measure/rollback AGENTS.md efficiency memory");
|
|
@@ -2377,6 +3330,11 @@ function portArg(raw, fallback) {
|
|
|
2377
3330
|
const n = Number(raw);
|
|
2378
3331
|
return Number.isInteger(n) && n >= 0 && n <= 65535 ? n : fallback;
|
|
2379
3332
|
}
|
|
3333
|
+
function readHost(argv) {
|
|
3334
|
+
const allowed = ["opencode", "claude-code", "codex", "all"];
|
|
3335
|
+
const raw = readFlag(argv, "--host") ?? process.env.AGENT_BLACKBOX_HOST ?? "opencode";
|
|
3336
|
+
return allowed.includes(raw) ? raw : "opencode";
|
|
3337
|
+
}
|
|
2380
3338
|
function readSuggestConfig(argv) {
|
|
2381
3339
|
const modes = ["auto", "off", "free", "ollama", "opencode", "openai-compat"];
|
|
2382
3340
|
const raw = readFlag(argv, "--suggest") ?? process.env.AGENT_BLACKBOX_SUGGEST ?? "auto";
|