agentwaste-core 0.1.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/src/scanner.js ADDED
@@ -0,0 +1,1217 @@
1
+ import { createReadStream, existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, join, relative } from "node:path";
4
+ import { createInterface } from "node:readline";
5
+
6
+ const DEFAULT_SCAN_DAYS = 365;
7
+ const DEFAULT_MAX_FILES = Number.MAX_SAFE_INTEGER;
8
+ const MAX_FILE_BYTES = 64 * 1024 * 1024;
9
+ const CONTEXT_REVIEW_FRESH_INPUT_PER_TOOL_CALL = 4000;
10
+ const DEFAULT_COST_PER_MILLION_TOKENS = 3.0;
11
+
12
+ const IGNORED_DIRS = new Set([
13
+ ".git",
14
+ ".hg",
15
+ ".svn",
16
+ "node_modules",
17
+ ".next",
18
+ "dist",
19
+ "build",
20
+ "coverage",
21
+ ".turbo",
22
+ ".cache",
23
+ "vendor",
24
+ ]);
25
+
26
+ const GLUE_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".sh", ".bash", ".zsh", ".py", ".rb"]);
27
+ const GLUE_DIR_HINTS = new Set(["scripts", "script", "bin", "tools", "tooling", "automation", ".claude", ".codex", ".cursor", ".agents", ".mcp"]);
28
+ const GLUE_NAME_RE = /(?:agent|mcp|tool|tools|workflow|automation|script|sync|deploy|hook|runner|doctor)/i;
29
+ const STATE_LOSS_RE = /(?:lost|forgot|missing|dropped|reset).{0,40}(?:context|state|history|memory)|(?:context|state|history|memory).{0,40}(?:lost|forgot|missing|dropped|reset)|continue from|previous session|start over|compact(?:ed|ion)?/i;
30
+ const TRACE_RE = /(?:trace[_-]?id|span[_-]?id|run[_-]?id|structured\s+trace|opentelemetry|otel)/i;
31
+ const AGENT_TEXT_EXTENSIONS = new Set([".json", ".jsonl", ".log", ".md", ".txt", ".env", ".toml", ".yaml", ".yml"]);
32
+ const AGENT_DB_EXTENSIONS = new Set([".db", ".sqlite", ".sqlite3", ".vscdb"]);
33
+
34
+ const SOURCE_LABELS = {
35
+ codex: "Codex",
36
+ claude: "Claude Code",
37
+ gemini: "Gemini CLI",
38
+ pi: "pi",
39
+ cursor: "Cursor",
40
+ windsurf: "Windsurf",
41
+ opencode: "OpenCode",
42
+ aider: "Aider",
43
+ cline: "Cline",
44
+ roo: "Roo Code",
45
+ copilot: "GitHub Copilot",
46
+ continue: "Continue",
47
+ cody: "Cody",
48
+ configs: "Agent config",
49
+ };
50
+
51
+ const SECRET_PATTERNS = [
52
+ { name: "openai", re: /\bsk-proj-[A-Za-z0-9_-]{20,}\b|\bsk-[A-Za-z0-9_-]{32,}\b/g },
53
+ { name: "anthropic", re: /\bsk-ant-[A-Za-z0-9_-]{24,}\b/g },
54
+ { name: "github", re: /\bgh[pousr]_[A-Za-z0-9_]{30,255}\b/g },
55
+ { name: "stripe", re: /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{20,}\b/g },
56
+ { name: "slack", re: /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g },
57
+ { name: "aws", re: /\bAKIA[0-9A-Z]{16}\b/g },
58
+ {
59
+ name: "generic",
60
+ re: /\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|secret|password)\b\s*[:=]\s*["']?([A-Za-z0-9_./+=-]{24,})/gi,
61
+ capture: 1,
62
+ },
63
+ ];
64
+
65
+ export async function scanAgentWaste(input = {}) {
66
+ const homeDir = input.homeDir ?? homedir();
67
+ const projectDir = input.projectDir ?? process.cwd();
68
+ const days = normalizeScanDays(input.days ?? DEFAULT_SCAN_DAYS);
69
+ const maxFiles = input.maxFiles ?? DEFAULT_MAX_FILES;
70
+ const now = input.now ?? Date.now();
71
+ const sinceMs = days === "all" ? 0 : now - days * 24 * 60 * 60 * 1000;
72
+ const totals = createTotals({ homeDir, projectDir, days, sinceMs, now });
73
+
74
+ await scanCodex(homeDir, sinceMs, maxFiles, totals);
75
+ await scanClaude(homeDir, sinceMs, maxFiles, totals);
76
+ await scanPi(homeDir, sinceMs, maxFiles, totals);
77
+ await scanGemini(homeDir, sinceMs, maxFiles, totals);
78
+ await scanOpenCode(homeDir, sinceMs, maxFiles, totals);
79
+ await scanCursor(homeDir, sinceMs, maxFiles, totals);
80
+ await scanWindsurf(homeDir, sinceMs, maxFiles, totals);
81
+ await scanEditorExtensionAgents(homeDir, sinceMs, maxFiles, totals);
82
+ await scanCopilot(homeDir, sinceMs, maxFiles, totals);
83
+ scanAider(projectDir, sinceMs, totals);
84
+ scanConfigFiles(homeDir, sinceMs, totals);
85
+ scanGlueCode(projectDir, totals);
86
+ finalize(totals);
87
+ return totals;
88
+ }
89
+
90
+ function normalizeScanDays(value) {
91
+ if (String(value).toLowerCase() === "all") return "all";
92
+ const days = Number.parseInt(value, 10);
93
+ return Number.isFinite(days) && days > 0 ? days : DEFAULT_SCAN_DAYS;
94
+ }
95
+
96
+ function createTotals({ homeDir, projectDir, days, sinceMs, now }) {
97
+ return {
98
+ homeDir,
99
+ projectDir,
100
+ days,
101
+ sinceMs,
102
+ now,
103
+ sources: {
104
+ ...Object.fromEntries(Object.entries(SOURCE_LABELS).map(([key, label]) => [key, createSourceStats(label)])),
105
+ },
106
+ fileSources: new Map(),
107
+ files: 0,
108
+ sessions: 0,
109
+ subagentSessions: 0,
110
+ toolCalls: 0,
111
+ failedToolCalls: 0,
112
+ retryAfterFailure: 0,
113
+ tokens: {
114
+ total: 0,
115
+ active: 0,
116
+ input: 0,
117
+ freshInput: 0,
118
+ output: 0,
119
+ cached: 0,
120
+ reasoning: 0,
121
+ observedModelCalls: 0,
122
+ },
123
+ secrets: {
124
+ rawKeys: 0,
125
+ modelFacingKeys: 0,
126
+ byKind: {},
127
+ fingerprints: new Set(),
128
+ modelFacingFingerprints: new Set(),
129
+ files: new Set(),
130
+ },
131
+ stateLoss: {
132
+ incidents: 0,
133
+ compactSignals: 0,
134
+ abortedTurns: 0,
135
+ phraseSignals: 0,
136
+ files: new Set(),
137
+ },
138
+ traces: {
139
+ signals: 0,
140
+ files: new Set(),
141
+ sessionFiles: new Set(),
142
+ configSignals: 0,
143
+ },
144
+ glue: {
145
+ lines: 0,
146
+ files: 0,
147
+ roots: new Set(),
148
+ scriptFiles: [],
149
+ },
150
+ sessionsById: new Set(),
151
+ diagnostics: [],
152
+ derived: {},
153
+ };
154
+ }
155
+
156
+ function createSourceStats(label) {
157
+ return {
158
+ label,
159
+ files: 0,
160
+ sessions: 0,
161
+ toolCalls: 0,
162
+ failedToolCalls: 0,
163
+ tokens: 0,
164
+ activeTokens: 0,
165
+ freshInputTokens: 0,
166
+ cachedInputTokens: 0,
167
+ secrets: 0,
168
+ modelFacingSecrets: 0,
169
+ stateLoss: 0,
170
+ traces: 0,
171
+ traceFiles: new Set(),
172
+ tracedFiles: 0,
173
+ retryAfterFailure: 0,
174
+ unavailable: false,
175
+ };
176
+ }
177
+
178
+ async function scanCodex(homeDir, sinceMs, maxFiles, totals) {
179
+ const root = join(homeDir, ".codex", "sessions");
180
+ const source = totals.sources.codex;
181
+ if (!existsSync(root)) {
182
+ source.unavailable = true;
183
+ return;
184
+ }
185
+
186
+ const files = recentFiles(root, sinceMs, maxFiles, (path) => path.endsWith(".jsonl"));
187
+ source.files = files.length;
188
+ totals.files += files.length;
189
+ for (const path of files) await scanCodexFile(path, totals);
190
+ }
191
+
192
+ async function scanCodexFile(path, totals) {
193
+ const source = totals.sources.codex;
194
+ registerFileSource(totals, path, source);
195
+ const session = createSessionAccumulator("codex", path);
196
+ const seenTokenSnapshots = new Set();
197
+ const seenCalls = new Set();
198
+ let index = 0;
199
+
200
+ await forEachJsonLine(path, (record, rawLine) => {
201
+ index += 1;
202
+ const timestamp = parseTime(record.timestamp);
203
+ if (timestamp > 0 && timestamp < totals.sinceMs) return;
204
+ if (record.type === "session_meta") {
205
+ const id = record.payload?.id;
206
+ if (typeof id === "string") session.id = id;
207
+ addSession(totals, source, id ?? path);
208
+ }
209
+
210
+ scanTextForSecrets(rawLine, totals, { modelFacing: isCodexModelFacing(record), file: path });
211
+ const payload = record.payload;
212
+ if (!payload || typeof payload !== "object") return;
213
+
214
+ if (record.type === "turn_context" && typeof payload.summary === "string" && payload.summary.trim().length > 0 && !session.compactionCounted) {
215
+ session.compactionCounted = true;
216
+ addCompactionSignal(totals, source, path);
217
+ }
218
+
219
+ if (record.type === "event_msg" && (payload.type === "user_message" || payload.type === "agent_message")) {
220
+ scanTextForStateLoss(String(payload.message ?? ""), totals, path);
221
+ }
222
+
223
+ if (record.type === "response_item" && payload.type === "message" && (payload.role === "user" || payload.role === "assistant")) {
224
+ scanTextForStateLoss(contentText(payload.content), totals, path);
225
+ }
226
+
227
+ if (record.type === "event_msg" && payload.type === "token_count" && payload.info?.last_token_usage) {
228
+ const usage = normalizeCodexUsage(payload.info.last_token_usage);
229
+ const snapshotKey = [
230
+ usage.total,
231
+ payload.info.total_token_usage?.total_tokens ?? "",
232
+ payload.info.total_token_usage?.input_tokens ?? "",
233
+ ].join(":");
234
+ if (!seenTokenSnapshots.has(snapshotKey) && usage.total > 0) {
235
+ seenTokenSnapshots.add(snapshotKey);
236
+ addUsage(totals, source, usage);
237
+ }
238
+ }
239
+
240
+ if (record.type === "response_item" && isCodexToolStart(payload)) {
241
+ const callId = payload.call_id ?? `codex:${path}:${index}`;
242
+ if (!seenCalls.has(callId)) {
243
+ seenCalls.add(callId);
244
+ addToolCall(totals, source);
245
+ noteToolStart(session, callId, toolSignature(payload.name ?? payload.type, payload.arguments ?? payload.input));
246
+ }
247
+ scanTextForTrace(structuredTraceText(payload.name, payload.arguments ?? payload.input), totals, path);
248
+ }
249
+
250
+ if (record.type === "event_msg" && isCodexToolEnd(payload)) {
251
+ const callId = payload.call_id ?? `codex:end:${path}:${index}`;
252
+ const failed = codexEndFailed(payload);
253
+ const signature = session.calls.get(callId) ?? toolSignature(payload.command ?? payload.invocation?.tool ?? payload.type, payload.command ?? payload.result);
254
+ if (!seenCalls.has(callId)) {
255
+ seenCalls.add(callId);
256
+ addToolCall(totals, source);
257
+ }
258
+ if (failed) addFailure(totals, source);
259
+ noteToolEnd(totals, session, signature, failed, timestamp, index);
260
+ scanTextForTrace(structuredTraceText(payload.type, {
261
+ command: payload.command,
262
+ invocation: payload.invocation,
263
+ result: payload.result,
264
+ stdout: payload.stdout,
265
+ }), totals, path);
266
+ }
267
+
268
+ if (record.type === "event_msg" && payload.type === "turn_aborted") {
269
+ totals.stateLoss.abortedTurns += 1;
270
+ addStateLoss(totals, source, path);
271
+ }
272
+ });
273
+
274
+ if (!session.id) addSession(totals, source, path);
275
+ }
276
+
277
+ async function scanClaude(homeDir, sinceMs, maxFiles, totals) {
278
+ const root = join(homeDir, ".claude", "projects");
279
+ const source = totals.sources.claude;
280
+ if (!existsSync(root)) {
281
+ source.unavailable = true;
282
+ return;
283
+ }
284
+
285
+ const files = recentFiles(root, sinceMs, maxFiles, (path) => path.endsWith(".jsonl"));
286
+ source.files = files.length;
287
+ totals.files += files.length;
288
+ for (const path of files) await scanClaudeFile(path, totals);
289
+
290
+ const facetsRoot = join(homeDir, ".claude", "usage-data", "facets");
291
+ if (existsSync(facetsRoot)) {
292
+ for (const path of recentFiles(facetsRoot, sinceMs, 1000, (item) => item.endsWith(".json"))) {
293
+ try {
294
+ const text = readFileSync(path, "utf8");
295
+ scanTextForStateLoss(text, totals, path);
296
+ } catch {
297
+ // Ignore unreadable optional usage facets.
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ async function scanClaudeFile(path, totals) {
304
+ const source = totals.sources.claude;
305
+ registerFileSource(totals, path, source);
306
+ const session = createSessionAccumulator("claude", path);
307
+ let index = 0;
308
+
309
+ if (basename(path).includes("compact") || path.includes("/subagents/agent-acompact")) {
310
+ addCompactionSignal(totals, source, path);
311
+ }
312
+ if (path.includes("/subagents/")) totals.subagentSessions += 1;
313
+
314
+ await forEachJsonLine(path, (record, rawLine) => {
315
+ index += 1;
316
+ const timestamp = parseTime(record.timestamp);
317
+ if (timestamp > 0 && timestamp < totals.sinceMs) return;
318
+ const sessionId = record.sessionId;
319
+ if (typeof sessionId === "string") {
320
+ session.id = sessionId;
321
+ addSession(totals, source, sessionId);
322
+ }
323
+
324
+ scanTextForSecrets(rawLine, totals, { modelFacing: isClaudeModelFacing(record), file: path });
325
+ if (record.type === "assistant" && record.message?.usage) {
326
+ addUsage(totals, source, normalizeClaudeUsage(record.message.usage));
327
+ }
328
+
329
+ if (record.type === "user" || record.type === "assistant") {
330
+ scanTextForStateLoss(claudeMessageText(record.message), totals, path);
331
+ }
332
+
333
+ if (record.type === "assistant") {
334
+ for (const block of array(record.message?.content)) {
335
+ if (block?.type === "tool_use") {
336
+ addToolCall(totals, source);
337
+ noteToolStart(session, block.id ?? `claude:${path}:${index}`, toolSignature(block.name, block.input));
338
+ scanTextForTrace(structuredTraceText(block.name, block.input), totals, path);
339
+ }
340
+ }
341
+ }
342
+
343
+ if (record.type === "user") {
344
+ const failed = claudeToolResultFailed(record);
345
+ if (record.toolUseResult || hasToolResult(record.message?.content)) {
346
+ const signature = toolSignature(record.toolUseResult?.name ?? record.sourceToolUseID ?? "tool_result", record.toolUseResult ?? record.message?.content);
347
+ if (failed) addFailure(totals, source);
348
+ noteToolEnd(totals, session, signature, failed, timestamp, index);
349
+ }
350
+ }
351
+
352
+ if (record.type === "system" && /compact/i.test(String(record.subtype ?? ""))) {
353
+ addCompactionSignal(totals, source, path);
354
+ }
355
+ });
356
+
357
+ if (!session.id) addSession(totals, source, path);
358
+ }
359
+
360
+ async function scanPi(homeDir, sinceMs, maxFiles, totals) {
361
+ const source = totals.sources.pi;
362
+ const sessionRoot = join(homeDir, ".pi", "agent", "sessions");
363
+ const files = recentFilesFromRoots([sessionRoot], sinceMs, maxFiles, (path) => path.endsWith(".jsonl"));
364
+ if (files.length === 0 && !existsSync(sessionRoot)) source.unavailable = true;
365
+ for (const path of files) await scanGenericAgentFile(path, totals, source, { modelFacing: true, sessionPerFile: true });
366
+
367
+ scanKnownFiles([
368
+ join(homeDir, ".pi", "agent", "settings.json"),
369
+ join(homeDir, ".pi", "agent", "auth.json"),
370
+ join(homeDir, ".pi", "agent", "models.json"),
371
+ join(homeDir, ".pi", "agent", "AGENTS.md"),
372
+ ], sinceMs, totals, source, { modelFacing: false, sessionPerFile: false });
373
+ }
374
+
375
+ async function scanGemini(homeDir, sinceMs, maxFiles, totals) {
376
+ const source = totals.sources.gemini;
377
+ const root = join(homeDir, ".gemini");
378
+ const sessionRoots = [join(root, "tmp"), join(root, "sessions"), join(root, "exports")];
379
+ const files = recentFilesFromRoots(sessionRoots, sinceMs, maxFiles, (path) => {
380
+ const name = basename(path);
381
+ return path.endsWith(".jsonl") ||
382
+ /(?:^session-|chats\/session-).+\.json$/i.test(path) ||
383
+ name === "logs.json" ||
384
+ name === "collector.log";
385
+ });
386
+ if (files.length === 0 && !existsSync(root)) source.unavailable = true;
387
+ for (const path of files) await scanGenericAgentFile(path, totals, source, { modelFacing: true, sessionPerFile: /(?:session|chat|\.jsonl)/i.test(path) });
388
+
389
+ scanKnownFiles([
390
+ join(root, "settings.json"),
391
+ join(root, ".env"),
392
+ join(root, "GEMINI.md"),
393
+ ], sinceMs, totals, source, { modelFacing: false, sessionPerFile: false });
394
+ }
395
+
396
+ async function scanOpenCode(homeDir, sinceMs, maxFiles, totals) {
397
+ const source = totals.sources.opencode;
398
+ const root = join(homeDir, ".local", "share", "opencode");
399
+ const files = recentFilesFromRoots([join(root, "log"), join(root, "project")], sinceMs, maxFiles, isLikelyAgentDataFile);
400
+ if (files.length === 0 && !existsSync(root)) source.unavailable = true;
401
+ for (const path of files) await scanGenericAgentFile(path, totals, source, { modelFacing: true, sessionPerFile: path.includes("/project/") });
402
+
403
+ scanKnownFiles([
404
+ join(root, "auth.json"),
405
+ join(homeDir, ".config", "opencode", "opencode.json"),
406
+ join(homeDir, ".config", "opencode", "config.json"),
407
+ ], sinceMs, totals, source, { modelFacing: false, sessionPerFile: false });
408
+ }
409
+
410
+ async function scanCursor(homeDir, sinceMs, maxFiles, totals) {
411
+ const source = totals.sources.cursor;
412
+ const roots = editorUserRoots(homeDir, "Cursor");
413
+ const files = recentFilesFromRoots([
414
+ ...roots.map((root) => join(root, "globalStorage")),
415
+ ...roots.map((root) => join(root, "workspaceStorage")),
416
+ ...roots.map((root) => join(root, "..", "logs")),
417
+ join(homeDir, ".cursor"),
418
+ ], sinceMs, maxFiles, isLikelyAgentDataFile);
419
+ if (files.length === 0 && roots.every((root) => !existsSync(root)) && !existsSync(join(homeDir, ".cursor"))) source.unavailable = true;
420
+ for (const path of files) await scanGenericAgentFile(path, totals, source, { modelFacing: true, sessionPerFile: false });
421
+ }
422
+
423
+ async function scanWindsurf(homeDir, sinceMs, maxFiles, totals) {
424
+ const source = totals.sources.windsurf;
425
+ const roots = editorUserRoots(homeDir, "Windsurf");
426
+ const files = recentFilesFromRoots([
427
+ ...roots.map((root) => join(root, "globalStorage")),
428
+ ...roots.map((root) => join(root, "workspaceStorage")),
429
+ ...roots.map((root) => join(root, "..", "logs")),
430
+ join(homeDir, ".codeium"),
431
+ join(homeDir, ".windsurf"),
432
+ ], sinceMs, maxFiles, isLikelyAgentDataFile);
433
+ if (files.length === 0 && roots.every((root) => !existsSync(root)) && !existsSync(join(homeDir, ".windsurf"))) source.unavailable = true;
434
+ for (const path of files) await scanGenericAgentFile(path, totals, source, { modelFacing: true, sessionPerFile: false });
435
+ }
436
+
437
+ async function scanEditorExtensionAgents(homeDir, sinceMs, maxFiles, totals) {
438
+ const editorRoots = [
439
+ ...editorUserRoots(homeDir, "Code"),
440
+ ...editorUserRoots(homeDir, "Code - Insiders"),
441
+ ...editorUserRoots(homeDir, "Cursor"),
442
+ ...editorUserRoots(homeDir, "Windsurf"),
443
+ ];
444
+ const extensionSources = [
445
+ { source: totals.sources.cline, ids: ["saoudrizwan.claude-dev", "cline.cline"] },
446
+ { source: totals.sources.roo, ids: ["rooveterinaryinc.roo-cline", "roo-cline.roo-cline"] },
447
+ { source: totals.sources.continue, ids: ["continue.continue"] },
448
+ { source: totals.sources.cody, ids: ["sourcegraph.cody-ai"] },
449
+ ];
450
+
451
+ for (const item of extensionSources) {
452
+ const roots = [];
453
+ for (const editorRoot of editorRoots) {
454
+ for (const id of item.ids) roots.push(join(editorRoot, "globalStorage", id));
455
+ }
456
+ const files = recentFilesFromRoots(roots, sinceMs, Math.min(maxFiles, 1000), isLikelyAgentDataFile);
457
+ if (files.length === 0 && roots.every((root) => !existsSync(root))) item.source.unavailable = true;
458
+ for (const path of files) await scanGenericAgentFile(path, totals, item.source, { modelFacing: true, sessionPerFile: /(?:task|history|session|conversation)/i.test(path) });
459
+ }
460
+ }
461
+
462
+ async function scanCopilot(homeDir, sinceMs, maxFiles, totals) {
463
+ const source = totals.sources.copilot;
464
+ const roots = [
465
+ join(homeDir, ".copilot", "session-state"),
466
+ join(homeDir, ".copilot", "history-session-state"),
467
+ ...editorUserRoots(homeDir, "Code").map((root) => join(root, "workspaceStorage")),
468
+ ...editorUserRoots(homeDir, "Code - Insiders").map((root) => join(root, "workspaceStorage")),
469
+ ];
470
+ const files = recentFilesFromRoots(roots, sinceMs, maxFiles, (path) => {
471
+ if (path.includes("/workspaceStorage/")) return basename(path) === "state.vscdb";
472
+ return isLikelyAgentDataFile(path);
473
+ });
474
+ if (files.length === 0 && roots.every((root) => !existsSync(root))) source.unavailable = true;
475
+ for (const path of files) await scanGenericAgentFile(path, totals, source, { modelFacing: true, sessionPerFile: !path.endsWith(".vscdb") });
476
+ }
477
+
478
+ function scanAider(projectDir, sinceMs, totals) {
479
+ const source = totals.sources.aider;
480
+ const candidates = [
481
+ join(projectDir, ".aider.chat.history.md"),
482
+ join(projectDir, ".aider.llm.history"),
483
+ join(projectDir, ".aider.input.history"),
484
+ join(projectDir, ".aider.conf.yml"),
485
+ join(projectDir, ".aider.conf.yaml"),
486
+ ];
487
+ const found = scanKnownFiles(candidates, sinceMs, totals, source, { modelFacing: true, sessionPerFile: true });
488
+ if (found === 0) source.unavailable = true;
489
+ }
490
+
491
+ function scanConfigFiles(homeDir, sinceMs, totals) {
492
+ const candidates = [
493
+ join(homeDir, ".codex", "config.toml"),
494
+ join(homeDir, ".codex", "history.jsonl"),
495
+ join(homeDir, ".codex", "log", "codex-tui.log"),
496
+ join(homeDir, ".claude", "settings.json"),
497
+ join(homeDir, ".claude.json"),
498
+ join(homeDir, ".cursor", "mcp.json"),
499
+ ];
500
+ const source = totals.sources.configs;
501
+ for (const path of candidates) {
502
+ if (!existsSync(path)) continue;
503
+ if (totals.fileSources.has(path)) continue;
504
+ const stat = safeStat(path);
505
+ if (!stat || stat.size > MAX_FILE_BYTES || stat.mtimeMs < sinceMs) continue;
506
+ try {
507
+ registerFileSource(totals, path, source);
508
+ const text = readFileSync(path, "utf8");
509
+ source.files += 1;
510
+ totals.files += 1;
511
+ scanTextForSecrets(text, totals, { modelFacing: false, file: path });
512
+ scanTextForTrace(text, totals, path);
513
+ } catch {
514
+ totals.diagnostics.push(`Skipped unreadable config ${path}`);
515
+ }
516
+ }
517
+ }
518
+
519
+ function scanKnownFiles(paths, sinceMs, totals, source, options) {
520
+ let found = 0;
521
+ for (const path of paths) {
522
+ if (!existsSync(path)) continue;
523
+ const stat = safeStat(path);
524
+ if (!stat || !stat.isFile() || stat.size > MAX_FILE_BYTES || stat.mtimeMs < sinceMs) continue;
525
+ found += 1;
526
+ scanGenericAgentFileSync(path, totals, source, options);
527
+ }
528
+ return found;
529
+ }
530
+
531
+ async function scanGenericAgentFile(path, totals, source, options = {}) {
532
+ registerAgentDataFile(totals, source, path);
533
+ if (path.endsWith(".jsonl")) {
534
+ await scanGenericJsonLines(path, totals, source, options);
535
+ return;
536
+ }
537
+ scanGenericAgentFileSync(path, totals, source, options);
538
+ }
539
+
540
+ function scanGenericAgentFileSync(path, totals, source, options = {}) {
541
+ registerAgentDataFile(totals, source, path);
542
+ let text;
543
+ try {
544
+ text = readFileSync(path, "utf8");
545
+ } catch {
546
+ totals.diagnostics.push(`Skipped unreadable ${source.label} file ${path}`);
547
+ return;
548
+ }
549
+ scanTextForSecrets(text, totals, { modelFacing: options.modelFacing === true, file: path });
550
+ scanTextForStateLoss(text, totals, path);
551
+ scanTextForTrace(text, totals, path);
552
+ if (options.sessionPerFile) addSession(totals, source, path);
553
+ if (path.endsWith(".json")) scanGenericJsonDocument(text, totals, source, path);
554
+ }
555
+
556
+ async function scanGenericJsonLines(path, totals, source, options = {}) {
557
+ const session = createSessionAccumulator(source.label.toLowerCase(), path);
558
+ let sawRecord = false;
559
+ await forEachJsonLine(path, (record, rawLine) => {
560
+ sawRecord = true;
561
+ scanTextForSecrets(rawLine, totals, { modelFacing: options.modelFacing === true || isGenericModelFacing(record), file: path });
562
+ scanTextForStateLoss(genericRecordText(record), totals, path);
563
+ scanTextForTrace(rawLine, totals, path);
564
+ noteGenericRecordSignals(record, totals, source, session);
565
+ });
566
+ if (options.sessionPerFile || sawRecord) addSession(totals, source, session.id ?? path);
567
+ }
568
+
569
+ function scanGenericJsonDocument(text, totals, source, path) {
570
+ try {
571
+ const parsed = JSON.parse(text);
572
+ const session = createSessionAccumulator(source.label.toLowerCase(), path);
573
+ visitGenericJson(parsed, (record) => {
574
+ noteGenericRecordSignals(record, totals, source, session);
575
+ });
576
+ } catch {
577
+ // Non-JSON configs with .json suffix are not useful beyond text scanning.
578
+ }
579
+ }
580
+
581
+ function noteGenericRecordSignals(record, totals, source, session) {
582
+ if (!record || typeof record !== "object") return;
583
+ const id = record.sessionId ?? record.session_id ?? record.conversationId ?? record.conversation_id ?? record.chatId ?? record.chat_id ?? record.id;
584
+ if (typeof id === "string" && /(?:session|conversation|chat|thread|conv)/i.test(String(Object.keys(record).join(" ")))) {
585
+ session.id = id;
586
+ addSession(totals, source, id);
587
+ }
588
+
589
+ const usage = normalizeGenericUsage(record.usage ?? record.tokenUsage ?? record.token_usage ?? record.metadata?.usage);
590
+ if (usage.total > 0) addUsage(totals, source, usage);
591
+
592
+ if (isGenericToolStart(record)) {
593
+ const callId = record.id ?? record.call_id ?? record.tool_call_id ?? stableSnippet(record).slice(0, 64);
594
+ addToolCall(totals, source);
595
+ noteToolStart(session, callId, toolSignature(record.name ?? record.toolName ?? record.tool_name ?? record.function?.name, record.input ?? record.arguments ?? record.args));
596
+ }
597
+
598
+ if (isGenericToolFailure(record)) {
599
+ addFailure(totals, source);
600
+ noteToolEnd(totals, session, toolSignature(record.name ?? record.toolName ?? record.tool_name ?? record.type, record.error ?? record.result ?? record.output), true, 0, 0);
601
+ }
602
+ }
603
+
604
+ function visitGenericJson(value, visit, depth = 0) {
605
+ if (depth > 8 || value == null) return;
606
+ if (Array.isArray(value)) {
607
+ for (const item of value.slice(0, 10000)) visitGenericJson(item, visit, depth + 1);
608
+ return;
609
+ }
610
+ if (typeof value !== "object") return;
611
+ visit(value);
612
+ for (const item of Object.values(value)) visitGenericJson(item, visit, depth + 1);
613
+ }
614
+
615
+ function scanGlueCode(projectDir, totals) {
616
+ if (!existsSync(projectDir)) return;
617
+ const files = [];
618
+ walk(projectDir, (path, dirent) => {
619
+ if (dirent.isDirectory()) return !IGNORED_DIRS.has(dirent.name);
620
+ if (!dirent.isFile()) return false;
621
+ if (!isGlueFile(projectDir, path)) return false;
622
+ files.push(path);
623
+ return false;
624
+ });
625
+
626
+ for (const path of files.slice(0, 500)) {
627
+ try {
628
+ const text = readFileSync(path, "utf8");
629
+ const lines = countLines(text);
630
+ const root = relative(projectDir, path).split(/[\\/]/)[0] || ".";
631
+ totals.glue.lines += lines;
632
+ totals.glue.files += 1;
633
+ totals.glue.roots.add(root);
634
+ totals.glue.scriptFiles.push({ path: relative(projectDir, path), lines });
635
+ } catch {
636
+ totals.diagnostics.push(`Skipped unreadable glue file ${path}`);
637
+ }
638
+ }
639
+ totals.glue.scriptFiles.sort((a, b) => b.lines - a.lines);
640
+ }
641
+
642
+ function recentFiles(root, sinceMs, maxFiles, predicate) {
643
+ const files = [];
644
+ walk(root, (path, dirent) => {
645
+ if (dirent.isDirectory()) {
646
+ if (IGNORED_DIRS.has(dirent.name)) return false;
647
+ return true;
648
+ }
649
+ if (!dirent.isFile() || !predicate(path)) return false;
650
+ const stat = safeStat(path);
651
+ if (!stat || stat.size > MAX_FILE_BYTES || stat.mtimeMs < sinceMs) return false;
652
+ files.push({ path, mtimeMs: stat.mtimeMs });
653
+ return false;
654
+ });
655
+ files.sort((a, b) => b.mtimeMs - a.mtimeMs);
656
+ return files.slice(0, maxFiles).map((item) => item.path);
657
+ }
658
+
659
+ function recentFilesFromRoots(roots, sinceMs, maxFiles, predicate) {
660
+ const seen = new Set();
661
+ const files = [];
662
+ for (const root of roots) {
663
+ for (const path of recentFiles(root, sinceMs, maxFiles, predicate)) {
664
+ if (seen.has(path)) continue;
665
+ seen.add(path);
666
+ const stat = safeStat(path);
667
+ if (stat) files.push({ path, mtimeMs: stat.mtimeMs });
668
+ }
669
+ }
670
+ files.sort((a, b) => b.mtimeMs - a.mtimeMs);
671
+ return files.slice(0, maxFiles).map((item) => item.path);
672
+ }
673
+
674
+ function isLikelyAgentDataFile(path) {
675
+ const ext = extension(path);
676
+ const name = basename(path);
677
+ if (AGENT_TEXT_EXTENSIONS.has(ext) || AGENT_DB_EXTENSIONS.has(ext)) return true;
678
+ return name === "state.vscdb" || name === "storage.json";
679
+ }
680
+
681
+ function editorUserRoots(homeDir, appName) {
682
+ return [
683
+ join(homeDir, "Library", "Application Support", appName, "User"),
684
+ join(homeDir, ".config", appName, "User"),
685
+ ];
686
+ }
687
+
688
+ function registerAgentDataFile(totals, source, path) {
689
+ if (totals.fileSources.has(path)) return false;
690
+ registerFileSource(totals, path, source);
691
+ source.unavailable = false;
692
+ source.files += 1;
693
+ totals.files += 1;
694
+ return true;
695
+ }
696
+
697
+ function registerFileSource(totals, path, source) {
698
+ totals.fileSources.set(path, source);
699
+ }
700
+
701
+ function walk(root, visit) {
702
+ let entries;
703
+ try {
704
+ entries = readdirSync(root, { withFileTypes: true });
705
+ } catch {
706
+ return;
707
+ }
708
+ for (const entry of entries) {
709
+ const path = join(root, entry.name);
710
+ const shouldDescend = visit(path, entry);
711
+ if (entry.isDirectory() && shouldDescend !== false) walk(path, visit);
712
+ }
713
+ }
714
+
715
+ async function forEachJsonLine(path, onRecord) {
716
+ const stream = createReadStream(path, { encoding: "utf8" });
717
+ const lines = createInterface({ input: stream, crlfDelay: Infinity });
718
+ for await (const line of lines) {
719
+ if (!line.trim()) continue;
720
+ try {
721
+ onRecord(JSON.parse(line), line);
722
+ } catch {
723
+ // Some logs can contain partial writes. They are not useful for metrics.
724
+ }
725
+ }
726
+ }
727
+
728
+ function createSessionAccumulator(kind, path) {
729
+ return {
730
+ kind,
731
+ path,
732
+ id: undefined,
733
+ calls: new Map(),
734
+ lastFailureBySignature: new Map(),
735
+ compactionCounted: false,
736
+ };
737
+ }
738
+
739
+ function addSession(totals, source, id) {
740
+ const key = `${source.label}:${id}`;
741
+ if (totals.sessionsById.has(key)) return;
742
+ totals.sessionsById.add(key);
743
+ source.sessions += 1;
744
+ totals.sessions += 1;
745
+ }
746
+
747
+ function addUsage(totals, source, usage) {
748
+ totals.tokens.total += usage.total;
749
+ totals.tokens.active += usage.active;
750
+ totals.tokens.input += usage.input;
751
+ totals.tokens.freshInput += usage.freshInput;
752
+ totals.tokens.output += usage.output;
753
+ totals.tokens.cached += usage.cached;
754
+ totals.tokens.reasoning += usage.reasoning;
755
+ totals.tokens.observedModelCalls += 1;
756
+ source.tokens += usage.total;
757
+ source.activeTokens += usage.active;
758
+ source.freshInputTokens += usage.freshInput;
759
+ source.cachedInputTokens += usage.cached;
760
+ }
761
+
762
+ function addToolCall(totals, source) {
763
+ totals.toolCalls += 1;
764
+ source.toolCalls += 1;
765
+ }
766
+
767
+ function addFailure(totals, source) {
768
+ totals.failedToolCalls += 1;
769
+ source.failedToolCalls += 1;
770
+ }
771
+
772
+ function noteToolStart(session, callId, signature) {
773
+ session.calls.set(callId, signature);
774
+ const failed = session.lastFailureBySignature.get(signature);
775
+ if (failed && failed.retryCounted !== true) {
776
+ failed.retryCounted = true;
777
+ session.lastRetrySignal = true;
778
+ }
779
+ }
780
+
781
+ function noteToolEnd(totals, session, signature, failed, timestamp, index) {
782
+ if (session.lastRetrySignal) {
783
+ totals.retryAfterFailure += 1;
784
+ const source = sourceForPath(totals, session.path);
785
+ source.retryAfterFailure += 1;
786
+ session.lastRetrySignal = false;
787
+ }
788
+ if (failed) {
789
+ session.lastFailureBySignature.set(signature, { timestamp, index, retryCounted: false });
790
+ }
791
+ }
792
+
793
+ function addStateLoss(totals, source, path) {
794
+ totals.stateLoss.incidents += 1;
795
+ totals.stateLoss.files.add(path);
796
+ source.stateLoss += 1;
797
+ }
798
+
799
+ function addCompactionSignal(totals, source, path) {
800
+ totals.stateLoss.compactSignals += 1;
801
+ totals.stateLoss.files.add(path);
802
+ source.stateLoss += 1;
803
+ }
804
+
805
+ function scanTextForStateLoss(text, totals, path) {
806
+ if (!STATE_LOSS_RE.test(text)) return;
807
+ totals.stateLoss.phraseSignals += 1;
808
+ totals.stateLoss.incidents += 1;
809
+ totals.stateLoss.files.add(path);
810
+ const source = sourceForPath(totals, path);
811
+ source.stateLoss += 1;
812
+ }
813
+
814
+ function scanTextForTrace(text, totals, path) {
815
+ if (!TRACE_RE.test(text)) return;
816
+ totals.traces.signals += 1;
817
+ totals.traces.files.add(path);
818
+ const source = sourceForPath(totals, path);
819
+ if (source === totals.sources.configs) {
820
+ totals.traces.configSignals += 1;
821
+ } else {
822
+ totals.traces.sessionFiles.add(path);
823
+ source.traceFiles.add(path);
824
+ }
825
+ source.traces += 1;
826
+ }
827
+
828
+ function scanTextForSecrets(text, totals, { modelFacing, file }) {
829
+ for (const pattern of SECRET_PATTERNS) {
830
+ pattern.re.lastIndex = 0;
831
+ let match;
832
+ while ((match = pattern.re.exec(text)) !== null) {
833
+ const raw = pattern.capture ? match[pattern.capture] : match[0];
834
+ if (!raw || looksLikePlaceholder(raw)) continue;
835
+ const fingerprint = `${pattern.name}:${raw.slice(0, 8)}:${raw.length}:${raw.slice(-4)}`;
836
+ if (!totals.secrets.fingerprints.has(fingerprint)) {
837
+ totals.secrets.fingerprints.add(fingerprint);
838
+ totals.secrets.rawKeys += 1;
839
+ totals.secrets.byKind[pattern.name] = (totals.secrets.byKind[pattern.name] ?? 0) + 1;
840
+ totals.secrets.files.add(file);
841
+ sourceForPath(totals, file).secrets += 1;
842
+ }
843
+ if (modelFacing && !totals.secrets.modelFacingFingerprints.has(fingerprint)) {
844
+ totals.secrets.modelFacingFingerprints.add(fingerprint);
845
+ totals.secrets.modelFacingKeys += 1;
846
+ sourceForPath(totals, file).modelFacingSecrets += 1;
847
+ }
848
+ }
849
+ }
850
+ }
851
+
852
+ function sourceForPath(totals, path) {
853
+ const mapped = totals.fileSources.get(path);
854
+ if (mapped) return mapped;
855
+ if (path.includes("/.codex/")) return totals.sources.codex;
856
+ if (path.includes("/.claude/")) return totals.sources.claude;
857
+ if (path.includes("/.gemini/")) return totals.sources.gemini;
858
+ if (path.includes("/.pi/")) return totals.sources.pi;
859
+ if (path.includes("/Cursor/") || path.includes("/.cursor/")) return totals.sources.cursor;
860
+ if (path.includes("/Windsurf/") || path.includes("/.windsurf/") || path.includes("/.codeium/")) return totals.sources.windsurf;
861
+ if (path.includes("/opencode/")) return totals.sources.opencode;
862
+ return totals.sources.configs;
863
+ }
864
+
865
+ function isCodexModelFacing(record) {
866
+ if (record.type !== "response_item" && record.type !== "event_msg") return false;
867
+ const payload = record.payload;
868
+ if (!payload) return false;
869
+ return payload.type === "message" || payload.type === "user_message" || payload.type === "function_call" || payload.type === "custom_tool_call";
870
+ }
871
+
872
+ function isClaudeModelFacing(record) {
873
+ return record.type === "user" || record.type === "assistant";
874
+ }
875
+
876
+ function isCodexToolStart(payload) {
877
+ return payload.type === "function_call" || payload.type === "custom_tool_call" || payload.type === "web_search_call";
878
+ }
879
+
880
+ function isCodexToolEnd(payload) {
881
+ return /_end$/.test(String(payload.type ?? "")) && (
882
+ payload.type === "exec_command_end" ||
883
+ payload.type === "patch_apply_end" ||
884
+ payload.type === "web_search_end" ||
885
+ payload.type === "mcp_tool_call_end"
886
+ );
887
+ }
888
+
889
+ function codexEndFailed(payload) {
890
+ if (payload.status && !["completed", "ok", "success"].includes(String(payload.status).toLowerCase())) return true;
891
+ if (typeof payload.exit_code === "number" && payload.exit_code !== 0) return true;
892
+ if (payload.success === false) return true;
893
+ const stderr = typeof payload.stderr === "string" ? payload.stderr : "";
894
+ return /\b(error|failed|exception|permission denied|not found)\b/i.test(stderr) && !/\b0 errors\b/i.test(stderr);
895
+ }
896
+
897
+ function claudeToolResultFailed(record) {
898
+ if (record.toolUseResult?.is_error === true || record.toolUseResult?.error) return true;
899
+ const content = array(record.message?.content);
900
+ return content.some((item) => item?.type === "tool_result" && (item.is_error === true || /\b(error|failed|exception|permission denied)\b/i.test(String(item.content ?? ""))));
901
+ }
902
+
903
+ function hasToolResult(content) {
904
+ return array(content).some((item) => item?.type === "tool_result");
905
+ }
906
+
907
+ function isGenericModelFacing(record) {
908
+ const role = String(record?.role ?? record?.message?.role ?? record?.type ?? "").toLowerCase();
909
+ return role === "user" || role === "assistant" || role === "model" || role === "tool_use" || role === "function_call";
910
+ }
911
+
912
+ function genericRecordText(record) {
913
+ if (!record || typeof record !== "object") return "";
914
+ return [
915
+ contentText(record.content),
916
+ contentText(record.message?.content),
917
+ record.text,
918
+ record.prompt,
919
+ record.response,
920
+ record.output,
921
+ record.error,
922
+ ].filter((value) => typeof value === "string" && value.length > 0).join("\n");
923
+ }
924
+
925
+ function isGenericToolStart(record) {
926
+ const type = String(record.type ?? record.kind ?? "").toLowerCase();
927
+ if (type === "tool_use" || type === "tool_call" || type === "function_call") return true;
928
+ if (record.functionCall || record.function_call || record.toolCall || record.tool_call) return true;
929
+ return typeof record.toolName === "string" || typeof record.tool_name === "string";
930
+ }
931
+
932
+ function isGenericToolFailure(record) {
933
+ if (record.is_error === true || record.isError === true || record.success === false) return true;
934
+ const status = String(record.status ?? record.state ?? "").toLowerCase();
935
+ if (["error", "failed", "failure", "rejected"].includes(status)) return true;
936
+ const text = String(record.error ?? record.stderr ?? record.output ?? record.result ?? "");
937
+ return /\b(error|failed|exception|permission denied)\b/i.test(text) && !/\b0 errors\b/i.test(text);
938
+ }
939
+
940
+ function contentText(content) {
941
+ if (typeof content === "string") return content;
942
+ return array(content)
943
+ .map((item) => {
944
+ if (!item || typeof item !== "object") return "";
945
+ return typeof item.text === "string" ? item.text : typeof item.content === "string" ? item.content : "";
946
+ })
947
+ .filter(Boolean)
948
+ .join("\n");
949
+ }
950
+
951
+ function claudeMessageText(message) {
952
+ if (!message) return "";
953
+ return contentText(message.content);
954
+ }
955
+
956
+ function structuredTraceText(name, payload) {
957
+ const tool = String(name ?? "");
958
+ const text = typeof payload === "string" ? payload : stableSnippet(payload);
959
+ if (!/(?:trace[_-]?id|span[_-]?id|run[_-]?id|opentelemetry|otel)/i.test(`${tool}\n${text}`)) return "";
960
+ return `${tool}\n${text}`;
961
+ }
962
+
963
+ function normalizeCodexUsage(usage) {
964
+ const input = number(usage.input_tokens);
965
+ const output = number(usage.output_tokens);
966
+ const cached = number(usage.cached_input_tokens);
967
+ const reasoning = number(usage.reasoning_output_tokens);
968
+ const total = number(usage.total_tokens) || input + output;
969
+ const freshInput = Math.max(0, input - cached);
970
+ const active = freshInput + output;
971
+ return { input, freshInput, output, cached, reasoning, total, active };
972
+ }
973
+
974
+ function normalizeClaudeUsage(usage) {
975
+ const input = number(usage.input_tokens);
976
+ const output = number(usage.output_tokens);
977
+ const cacheCreation = number(usage.cache_creation_input_tokens);
978
+ const cacheRead = number(usage.cache_read_input_tokens);
979
+ const total = input + output + cacheCreation + cacheRead;
980
+ const freshInput = input + cacheCreation;
981
+ const active = freshInput + output;
982
+ return { input: input + cacheCreation + cacheRead, freshInput, output, cached: cacheRead, reasoning: 0, total, active };
983
+ }
984
+
985
+ function normalizeGenericUsage(usage) {
986
+ if (!usage || typeof usage !== "object") return { input: 0, freshInput: 0, output: 0, cached: 0, reasoning: 0, total: 0, active: 0 };
987
+ const input = firstNumber(usage.input_tokens, usage.inputTokens, usage.prompt_tokens, usage.promptTokens, usage.promptTokenCount, usage.inputTokenCount);
988
+ const output = firstNumber(usage.output_tokens, usage.outputTokens, usage.completion_tokens, usage.completionTokens, usage.candidatesTokenCount, usage.outputTokenCount);
989
+ const cached = firstNumber(usage.cached_input_tokens, usage.cache_read_input_tokens, usage.cachedTokens, usage.cachedContentTokenCount);
990
+ const cacheCreation = firstNumber(usage.cache_creation_input_tokens, usage.cacheCreationInputTokens);
991
+ const reasoning = firstNumber(usage.reasoning_output_tokens, usage.reasoningTokens, usage.thoughtsTokenCount);
992
+ const total = firstNumber(usage.total_tokens, usage.totalTokens, usage.totalTokenCount) || input + output + cached + cacheCreation;
993
+ const freshInput = Math.max(0, input + cacheCreation - cached);
994
+ const active = freshInput + output;
995
+ return { input: input + cacheCreation, freshInput, output, cached, reasoning, total, active };
996
+ }
997
+
998
+ function finalize(totals) {
999
+ const avgTokensPerToolCall = totals.toolCalls > 0 ? Math.round(totals.tokens.active / totals.toolCalls) : 0;
1000
+ const avgFreshInputPerToolCall = totals.toolCalls > 0 ? Math.round(totals.tokens.freshInput / totals.toolCalls) : 0;
1001
+ const contextDragPercent = totals.tokens.active > 0 ? Math.round((totals.tokens.freshInput / totals.tokens.active) * 100) : 0;
1002
+ const cacheableInput = totals.tokens.freshInput + totals.tokens.cached;
1003
+ const cacheHitPercent = cacheableInput > 0 ? Math.round((totals.tokens.cached / cacheableInput) * 100) : 0;
1004
+ const excessFreshInputTokens = Math.max(0, totals.tokens.freshInput - CONTEXT_REVIEW_FRESH_INPUT_PER_TOOL_CALL * totals.toolCalls);
1005
+ const excessFreshInputPercent = totals.tokens.freshInput > 0 ? Math.round((excessFreshInputTokens / totals.tokens.freshInput) * 100) : 0;
1006
+ const retryRatePercent = totals.toolCalls > 0 ? round((totals.failedToolCalls / totals.toolCalls) * 100, 1) : 0;
1007
+ const avgRetriesPerFailure = totals.failedToolCalls > 0 ? round(totals.retryAfterFailure / totals.failedToolCalls, 1) : 0;
1008
+ const retryRecoveryPercent = totals.failedToolCalls > 0 ? Math.min(100, Math.round((totals.retryAfterFailure / totals.failedToolCalls) * 100)) : 0;
1009
+ const unresolvedFailures = Math.max(0, totals.failedToolCalls - totals.retryAfterFailure);
1010
+ const sessionLogFiles = Object.entries(totals.sources)
1011
+ .filter(([key]) => key !== "configs")
1012
+ .reduce((sum, [, source]) => sum + source.files, 0);
1013
+ const tracedSessionLogFiles = totals.traces.sessionFiles.size;
1014
+ const traceCoveragePercent = sessionLogFiles > 0 ? Math.min(100, Math.round((tracedSessionLogFiles / sessionLogFiles) * 100)) : 0;
1015
+ const observedDays = totals.days === "all" ? 30 : Math.max(1, totals.days);
1016
+ const monthlyTokens = totals.tokens.active * (30 / observedDays);
1017
+ const excessMonthlyFreshInputTokens = excessFreshInputTokens * (30 / observedDays);
1018
+ const excessSpendMid = excessMonthlyFreshInputTokens / 1_000_000 * DEFAULT_COST_PER_MILLION_TOKENS;
1019
+ const excessSpendLow = Math.round(excessSpendMid * 0.75);
1020
+ const excessSpendHigh = Math.max(excessSpendLow, Math.round(excessSpendMid * 1.35));
1021
+ const failureRecoveryMinutes = totals.failedToolCalls * 3 + totals.retryAfterFailure * 4;
1022
+ const stateRecoveryMinutes = totals.stateLoss.incidents * 20;
1023
+ const automationMaintenanceMinutes = Math.round(totals.glue.lines / 35);
1024
+ const debuggingHours = Math.round((failureRecoveryMinutes + stateRecoveryMinutes + automationMaintenanceMinutes) / 60);
1025
+ const credentialRiskScore = credentialExposureScore(totals.secrets.rawKeys, totals.secrets.modelFacingKeys);
1026
+ const hardBreakRatePerSession = totals.sessions > 0 ? round(totals.stateLoss.incidents / totals.sessions, 2) : 0;
1027
+ const compactionsPerSession = totals.sessions > 0 ? round(totals.stateLoss.compactSignals / totals.sessions, 2) : 0;
1028
+
1029
+ totals.derived = {
1030
+ contextReviewFreshInputPerToolCall: CONTEXT_REVIEW_FRESH_INPUT_PER_TOOL_CALL,
1031
+ avgTokensPerToolCall,
1032
+ avgFreshInputPerToolCall,
1033
+ contextDragPercent,
1034
+ cacheHitPercent,
1035
+ excessFreshInputTokens,
1036
+ excessFreshInputPercent,
1037
+ contextOverheadLevel: contextOverheadLevel(avgFreshInputPerToolCall, contextDragPercent, cacheHitPercent, totals.toolCalls),
1038
+ sampleConfidence: sampleConfidence(totals),
1039
+ retryRatePercent,
1040
+ avgRetriesPerFailure,
1041
+ retryRecoveryPercent,
1042
+ unresolvedFailures,
1043
+ reliabilityLevel: reliabilityLevel(retryRatePercent, unresolvedFailures, totals.toolCalls),
1044
+ traceCoveragePercent,
1045
+ traceCoverageLevel: traceCoverageLevel(traceCoveragePercent, sessionLogFiles),
1046
+ sessionLogFiles,
1047
+ tracedSessionLogFiles,
1048
+ monthlyTokens: Math.round(monthlyTokens),
1049
+ excessMonthlyFreshInputTokens: Math.round(excessMonthlyFreshInputTokens),
1050
+ excessSpendLow,
1051
+ excessSpendHigh,
1052
+ costPerMillionTokens: DEFAULT_COST_PER_MILLION_TOKENS,
1053
+ failureRecoveryMinutes,
1054
+ stateRecoveryMinutes,
1055
+ automationMaintenanceMinutes,
1056
+ debuggingHours,
1057
+ credentialRiskScore,
1058
+ credentialRiskLevel: credentialExposureLevel(totals.secrets.rawKeys, totals.secrets.modelFacingKeys),
1059
+ stateContinuityLevel: stateContinuityLevel(hardBreakRatePerSession, compactionsPerSession),
1060
+ hardBreakRatePerSession,
1061
+ compactionsPerSession,
1062
+ automationFootprintLevel: automationFootprintLevel(totals.glue.lines, totals.glue.files, totals.glue.roots.size),
1063
+ securityExposure: securityExposure(totals.secrets.rawKeys, totals.secrets.modelFacingKeys),
1064
+ };
1065
+
1066
+ totals.secrets.fingerprints = undefined;
1067
+ totals.secrets.modelFacingFingerprints = undefined;
1068
+ for (const source of Object.values(totals.sources)) {
1069
+ if (source.traceFiles instanceof Set) source.tracedFiles = source.traceFiles.size;
1070
+ source.traceFiles = undefined;
1071
+ }
1072
+
1073
+ totals.secrets.files = [...totals.secrets.files].slice(0, 10);
1074
+ totals.stateLoss.files = [...totals.stateLoss.files].slice(0, 10);
1075
+ totals.traces.files = [...totals.traces.files].slice(0, 10);
1076
+ totals.traces.sessionFiles = undefined;
1077
+ totals.glue.roots = [...totals.glue.roots].sort();
1078
+ totals.sessionsById = undefined;
1079
+ totals.fileSources = undefined;
1080
+ }
1081
+
1082
+ function contextOverheadLevel(avgFreshInputPerToolCall, contextDragPercent, cacheHitPercent, toolCalls) {
1083
+ if (toolCalls === 0) return "NO_DATA";
1084
+ if (avgFreshInputPerToolCall >= 25000) return "CRITICAL";
1085
+ if (avgFreshInputPerToolCall >= 12000) return "HIGH";
1086
+ if (avgFreshInputPerToolCall >= CONTEXT_REVIEW_FRESH_INPUT_PER_TOOL_CALL) return "MEDIUM";
1087
+ if (avgFreshInputPerToolCall >= 2000 && contextDragPercent >= 90 && cacheHitPercent < 20) return "MEDIUM";
1088
+ return "LOW";
1089
+ }
1090
+
1091
+ function reliabilityLevel(failureRatePercent, unresolvedFailures, toolCalls) {
1092
+ if (toolCalls === 0) return "NO_DATA";
1093
+ if (failureRatePercent >= 30 || unresolvedFailures >= 5) return "CRITICAL";
1094
+ if (failureRatePercent >= 15 || unresolvedFailures >= 2) return "HIGH";
1095
+ if (failureRatePercent >= 5 || unresolvedFailures >= 1) return "MEDIUM";
1096
+ return "LOW";
1097
+ }
1098
+
1099
+ function credentialExposureScore(rawKeys, modelFacingKeys) {
1100
+ if (modelFacingKeys > 0) return 100;
1101
+ if (rawKeys > 0) return Math.min(95, 70 + (rawKeys - 1) * 5);
1102
+ return 0;
1103
+ }
1104
+
1105
+ function credentialExposureLevel(rawKeys, modelFacingKeys) {
1106
+ if (modelFacingKeys > 0) return "CRITICAL";
1107
+ if (rawKeys > 0) return "HIGH";
1108
+ return "LOW";
1109
+ }
1110
+
1111
+ function stateContinuityLevel(hardBreakRatePerSession, compactionsPerSession) {
1112
+ if (hardBreakRatePerSession >= 1) return "HIGH";
1113
+ if (hardBreakRatePerSession >= 0.25) return "MEDIUM";
1114
+ if (compactionsPerSession >= 1) return "MEDIUM";
1115
+ if (compactionsPerSession > 0) return "LOW";
1116
+ return "LOW";
1117
+ }
1118
+
1119
+ function traceCoverageLevel(percent, sessionLogFiles) {
1120
+ if (sessionLogFiles === 0) return "NO_DATA";
1121
+ if (percent >= 80) return "GOOD";
1122
+ if (percent >= 30) return "PARTIAL";
1123
+ return "MISSING";
1124
+ }
1125
+
1126
+ function automationFootprintLevel(lines, files, roots) {
1127
+ if (files === 0) return "LOW";
1128
+ if (files >= 50 || lines >= 10000 || roots >= 8) return "CRITICAL";
1129
+ if (files >= 16 || lines >= 3000 || roots >= 5) return "HIGH";
1130
+ if (files >= 6 || lines >= 1000 || roots >= 3) return "MEDIUM";
1131
+ return "LOW";
1132
+ }
1133
+
1134
+ function sampleConfidence(totals) {
1135
+ if (totals.tokens.observedModelCalls >= 10 && totals.toolCalls >= 20) return "HIGH";
1136
+ if (totals.tokens.observedModelCalls >= 3 && totals.toolCalls >= 5) return "MEDIUM";
1137
+ if (totals.tokens.observedModelCalls > 0 || totals.toolCalls > 0 || totals.sessions > 0) return "LOW";
1138
+ return "NO_DATA";
1139
+ }
1140
+
1141
+ function securityExposure(rawKeys, modelFacingKeys) {
1142
+ if (modelFacingKeys > 0) return "Critical: secret material reached model-facing context";
1143
+ if (rawKeys > 0) return "High: secret material present in local logs or config";
1144
+ return "Low";
1145
+ }
1146
+
1147
+ function isGlueFile(projectDir, path) {
1148
+ const rel = relative(projectDir, path);
1149
+ const parts = rel.split(/[\\/]/);
1150
+ const ext = extension(path);
1151
+ if (!GLUE_EXTENSIONS.has(ext)) return false;
1152
+ if (parts.some((part) => GLUE_DIR_HINTS.has(part))) return true;
1153
+ return GLUE_NAME_RE.test(basename(path));
1154
+ }
1155
+
1156
+ function extension(path) {
1157
+ const name = basename(path);
1158
+ const index = name.lastIndexOf(".");
1159
+ return index === -1 ? "" : name.slice(index);
1160
+ }
1161
+
1162
+ function toolSignature(name, payload) {
1163
+ const value = typeof payload === "string" ? payload : stableSnippet(payload);
1164
+ return `${String(name ?? "tool").toLowerCase()}:${value.slice(0, 120).replace(/\s+/g, " ")}`;
1165
+ }
1166
+
1167
+ function stableSnippet(value) {
1168
+ try {
1169
+ return JSON.stringify(value);
1170
+ } catch {
1171
+ return String(value ?? "");
1172
+ }
1173
+ }
1174
+
1175
+ function looksLikePlaceholder(value) {
1176
+ return /(?:example|placeholder|your[_-]?|xxx|redacted|token_here|api_key_here|changeme)/i.test(value);
1177
+ }
1178
+
1179
+ function countLines(text) {
1180
+ if (!text) return 0;
1181
+ return text.split(/\r\n|\r|\n/).length;
1182
+ }
1183
+
1184
+ function array(value) {
1185
+ return Array.isArray(value) ? value : [];
1186
+ }
1187
+
1188
+ function number(value) {
1189
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1190
+ }
1191
+
1192
+ function firstNumber(...values) {
1193
+ for (const value of values) {
1194
+ const parsed = number(value);
1195
+ if (parsed > 0) return parsed;
1196
+ }
1197
+ return 0;
1198
+ }
1199
+
1200
+ function parseTime(value) {
1201
+ if (typeof value !== "string") return 0;
1202
+ const parsed = Date.parse(value);
1203
+ return Number.isFinite(parsed) ? parsed : 0;
1204
+ }
1205
+
1206
+ function safeStat(path) {
1207
+ try {
1208
+ return statSync(path);
1209
+ } catch {
1210
+ return null;
1211
+ }
1212
+ }
1213
+
1214
+ function round(value, digits) {
1215
+ const factor = 10 ** digits;
1216
+ return Math.round(value * factor) / factor;
1217
+ }