@agent-native/core 0.35.2 → 0.36.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.
Files changed (151) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/context-xray-local.d.ts +2 -2
  3. package/dist/cli/context-xray-local.d.ts.map +1 -1
  4. package/dist/cli/context-xray-local.js +1449 -53
  5. package/dist/cli/context-xray-local.js.map +1 -1
  6. package/dist/cli/index.js +1 -1
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/skills.d.ts.map +1 -1
  9. package/dist/cli/skills.js +381 -73
  10. package/dist/cli/skills.js.map +1 -1
  11. package/dist/cli/templates-meta.d.ts.map +1 -1
  12. package/dist/cli/templates-meta.js +8 -4
  13. package/dist/cli/templates-meta.js.map +1 -1
  14. package/dist/client/AgentPanel.d.ts.map +1 -1
  15. package/dist/client/AgentPanel.js +5 -11
  16. package/dist/client/AgentPanel.js.map +1 -1
  17. package/dist/client/AssistantChat.d.ts +6 -0
  18. package/dist/client/AssistantChat.d.ts.map +1 -1
  19. package/dist/client/AssistantChat.js +50 -26
  20. package/dist/client/AssistantChat.js.map +1 -1
  21. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  22. package/dist/client/MultiTabAssistantChat.js +81 -8
  23. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  24. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  25. package/dist/client/agent-chat-adapter.js +68 -24
  26. package/dist/client/agent-chat-adapter.js.map +1 -1
  27. package/dist/client/agent-chat.d.ts +39 -3
  28. package/dist/client/agent-chat.d.ts.map +1 -1
  29. package/dist/client/agent-chat.js +168 -33
  30. package/dist/client/agent-chat.js.map +1 -1
  31. package/dist/client/application-state.d.ts +13 -0
  32. package/dist/client/application-state.d.ts.map +1 -0
  33. package/dist/client/application-state.js +99 -0
  34. package/dist/client/application-state.js.map +1 -0
  35. package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
  36. package/dist/client/composer/ComposerPlusMenu.js +174 -8
  37. package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
  38. package/dist/client/composer/PromptComposer.d.ts +2 -0
  39. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  40. package/dist/client/composer/PromptComposer.js +2 -2
  41. package/dist/client/composer/PromptComposer.js.map +1 -1
  42. package/dist/client/composer/TiptapComposer.js +1 -1
  43. package/dist/client/composer/TiptapComposer.js.map +1 -1
  44. package/dist/client/context-xray/ContextMeter.d.ts +2 -1
  45. package/dist/client/context-xray/ContextMeter.d.ts.map +1 -1
  46. package/dist/client/context-xray/ContextMeter.js +19 -25
  47. package/dist/client/context-xray/ContextMeter.js.map +1 -1
  48. package/dist/client/context-xray/ContextXRayPanel.d.ts +1 -3
  49. package/dist/client/context-xray/ContextXRayPanel.d.ts.map +1 -1
  50. package/dist/client/context-xray/ContextXRayPanel.js +27 -24
  51. package/dist/client/context-xray/ContextXRayPanel.js.map +1 -1
  52. package/dist/client/conversation/AgentConversation.d.ts.map +1 -1
  53. package/dist/client/conversation/AgentConversation.js +2 -1
  54. package/dist/client/conversation/AgentConversation.js.map +1 -1
  55. package/dist/client/frame-protocol.d.ts +11 -3
  56. package/dist/client/frame-protocol.d.ts.map +1 -1
  57. package/dist/client/frame-protocol.js.map +1 -1
  58. package/dist/client/index.d.ts +4 -2
  59. package/dist/client/index.d.ts.map +1 -1
  60. package/dist/client/index.js +4 -2
  61. package/dist/client/index.js.map +1 -1
  62. package/dist/client/progress/RunsTray.d.ts +1 -0
  63. package/dist/client/progress/RunsTray.d.ts.map +1 -1
  64. package/dist/client/progress/RunsTray.js +50 -16
  65. package/dist/client/progress/RunsTray.js.map +1 -1
  66. package/dist/client/sse-event-processor.d.ts +1 -0
  67. package/dist/client/sse-event-processor.d.ts.map +1 -1
  68. package/dist/client/sse-event-processor.js +62 -15
  69. package/dist/client/sse-event-processor.js.map +1 -1
  70. package/dist/client/tool-display.d.ts +4 -0
  71. package/dist/client/tool-display.d.ts.map +1 -0
  72. package/dist/client/tool-display.js +28 -0
  73. package/dist/client/tool-display.js.map +1 -0
  74. package/dist/client/use-action.d.ts +12 -0
  75. package/dist/client/use-action.d.ts.map +1 -1
  76. package/dist/client/use-action.js +14 -2
  77. package/dist/client/use-action.js.map +1 -1
  78. package/dist/client/use-agent-chat-context.d.ts +15 -0
  79. package/dist/client/use-agent-chat-context.d.ts.map +1 -0
  80. package/dist/client/use-agent-chat-context.js +32 -0
  81. package/dist/client/use-agent-chat-context.js.map +1 -0
  82. package/dist/client/use-chat-threads.d.ts.map +1 -1
  83. package/dist/client/use-chat-threads.js +40 -31
  84. package/dist/client/use-chat-threads.js.map +1 -1
  85. package/dist/client/use-external-value.d.ts.map +1 -1
  86. package/dist/client/use-external-value.js +14 -7
  87. package/dist/client/use-external-value.js.map +1 -1
  88. package/dist/deploy/build.d.ts.map +1 -1
  89. package/dist/deploy/build.js +1 -2
  90. package/dist/deploy/build.js.map +1 -1
  91. package/dist/extensions/html-shell.d.ts +3 -2
  92. package/dist/extensions/html-shell.d.ts.map +1 -1
  93. package/dist/extensions/html-shell.js +12 -2
  94. package/dist/extensions/html-shell.js.map +1 -1
  95. package/dist/extensions/routes.js +2 -7
  96. package/dist/extensions/routes.js.map +1 -1
  97. package/dist/index.browser.d.ts +1 -1
  98. package/dist/index.browser.d.ts.map +1 -1
  99. package/dist/index.browser.js +1 -1
  100. package/dist/index.browser.js.map +1 -1
  101. package/dist/index.d.ts +1 -1
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +1 -1
  104. package/dist/index.js.map +1 -1
  105. package/dist/mcp/server.d.ts +4 -2
  106. package/dist/mcp/server.d.ts.map +1 -1
  107. package/dist/mcp/server.js +33 -4
  108. package/dist/mcp/server.js.map +1 -1
  109. package/dist/provider-api/index.d.ts.map +1 -1
  110. package/dist/provider-api/index.js +14 -6
  111. package/dist/provider-api/index.js.map +1 -1
  112. package/dist/server/agent-teams.d.ts +4 -1
  113. package/dist/server/agent-teams.d.ts.map +1 -1
  114. package/dist/server/agent-teams.js +104 -28
  115. package/dist/server/agent-teams.js.map +1 -1
  116. package/dist/server/auth.d.ts.map +1 -1
  117. package/dist/server/auth.js +21 -11
  118. package/dist/server/auth.js.map +1 -1
  119. package/dist/server/core-routes-plugin.js +2 -2
  120. package/dist/server/core-routes-plugin.js.map +1 -1
  121. package/dist/server/request-context.d.ts +3 -4
  122. package/dist/server/request-context.d.ts.map +1 -1
  123. package/dist/server/request-context.js.map +1 -1
  124. package/dist/server/security-headers.d.ts +16 -19
  125. package/dist/server/security-headers.d.ts.map +1 -1
  126. package/dist/server/security-headers.js +24 -25
  127. package/dist/server/security-headers.js.map +1 -1
  128. package/dist/server/self-dispatch.d.ts.map +1 -1
  129. package/dist/server/self-dispatch.js +17 -1
  130. package/dist/server/self-dispatch.js.map +1 -1
  131. package/dist/server/ssr-handler.d.ts.map +1 -1
  132. package/dist/server/ssr-handler.js +9 -18
  133. package/dist/server/ssr-handler.js.map +1 -1
  134. package/dist/templates/default/AGENTS.md +1 -1
  135. package/dist/templates/default/DEVELOPING.md +7 -13
  136. package/dist/templates/workspace-core/AGENTS.md +6 -4
  137. package/dist/templates/workspace-root/AGENTS.md +6 -4
  138. package/docs/content/actions.md +5 -7
  139. package/docs/content/client.md +49 -44
  140. package/docs/content/context-awareness.md +20 -33
  141. package/docs/content/creating-templates.md +2 -2
  142. package/docs/content/external-agents.md +1 -1
  143. package/docs/content/key-concepts.md +3 -3
  144. package/docs/content/sharing.md +1 -1
  145. package/docs/content/template-mail.md +1 -1
  146. package/docs/content/voice-input.md +1 -1
  147. package/package.json +5 -1
  148. package/src/templates/default/AGENTS.md +1 -1
  149. package/src/templates/default/DEVELOPING.md +7 -13
  150. package/src/templates/workspace-core/AGENTS.md +6 -4
  151. package/src/templates/workspace-root/AGENTS.md +6 -4
@@ -38,6 +38,10 @@ const COLORS = {
38
38
  metadata: "#9aa3ad",
39
39
  other: "#c3c8ce",
40
40
  };
41
+ const MAX_EVENT_SAMPLES = 80;
42
+ const MAX_STEP_SAMPLES = 900;
43
+ const PRESSURE_WINDOW = 5;
44
+ const PRESSURE_TOKENS = 50000;
41
45
 
42
46
  function parseArgs(argv) {
43
47
  const out = {
@@ -208,6 +212,27 @@ function pathCounts(text) {
208
212
  return out;
209
213
  }
210
214
 
215
+ function eventPreview(text) {
216
+ return cleanTitle(String(text || "")).slice(0, 240);
217
+ }
218
+
219
+ function inferMcpTool(toolName) {
220
+ const value = String(toolName || "");
221
+ if (value.startsWith("mcp__")) {
222
+ const rest = value.slice(5);
223
+ const split = rest.indexOf("__");
224
+ if (split !== -1) return { server: rest.slice(0, split), tool: rest.slice(split + 2) };
225
+ const dot = rest.indexOf(".");
226
+ if (dot !== -1) return { server: rest.slice(0, dot), tool: rest.slice(dot + 1) };
227
+ }
228
+ if (value.startsWith("mcp_")) {
229
+ const rest = value.slice(4);
230
+ const split = rest.indexOf("_");
231
+ if (split !== -1) return { server: rest.slice(0, split), tool: rest.slice(split + 1) };
232
+ }
233
+ return { server: "", tool: "" };
234
+ }
235
+
211
236
  function codexTitle(id) {
212
237
  const index = path.join(CODEX_DIR, "session_index.jsonl");
213
238
  for (const record of readJsonl(index)) {
@@ -218,13 +243,13 @@ function codexTitle(id) {
218
243
 
219
244
  function observedCodexTokens(payload) {
220
245
  if (!payload || payload.type !== "token_count" || !payload.info) return 0;
221
- const last = payload.info.last_token_usage;
222
- if (last && Number(last.total_tokens)) return Number(last.total_tokens);
223
246
  const total = payload.info.total_token_usage;
224
247
  if (total && Number(total.total_tokens)) return Number(total.total_tokens);
225
248
  if (total && typeof total === "object") {
226
249
  return ["input_tokens", "cached_input_tokens", "output_tokens", "reasoning_output_tokens"].reduce((sum, key) => sum + (Number(total[key]) || 0), 0);
227
250
  }
251
+ const last = payload.info.last_token_usage;
252
+ if (last && Number(last.total_tokens)) return Number(last.total_tokens);
228
253
  return 0;
229
254
  }
230
255
 
@@ -233,36 +258,297 @@ function claudeUsageTokens(usage) {
233
258
  return (Number(usage.input_tokens) || 0) + (Number(usage.output_tokens) || 0) + (Number(usage.cache_creation_input_tokens) || 0) + (Number(usage.cache_read_input_tokens) || 0);
234
259
  }
235
260
 
261
+ function emptyUsage() {
262
+ return {
263
+ inputTokens: 0,
264
+ outputTokens: 0,
265
+ cacheCreationInputTokens: 0,
266
+ cacheReadInputTokens: 0,
267
+ cachedInputTokens: 0,
268
+ reasoningOutputTokens: 0,
269
+ totalTokens: 0,
270
+ turnsWithUsage: 0,
271
+ peakTurnTokens: 0,
272
+ peakTurnLabel: "",
273
+ latestTurnTokens: 0,
274
+ latestInputTokens: 0,
275
+ series: [],
276
+ };
277
+ }
278
+
279
+ function usageTotal(usage) {
280
+ if (!usage) return 0;
281
+ if (Number(usage.totalTokens)) return Number(usage.totalTokens) || 0;
282
+ return (Number(usage.inputTokens) || 0) +
283
+ (Number(usage.outputTokens) || 0) +
284
+ (Number(usage.cacheCreationInputTokens) || 0) +
285
+ (Number(usage.cacheReadInputTokens) || 0) +
286
+ (Number(usage.reasoningOutputTokens) || 0);
287
+ }
288
+
289
+ function normalizedUsage(raw) {
290
+ if (!raw || typeof raw !== "object") return null;
291
+ const usage = {
292
+ inputTokens: Number(raw.input_tokens) || Number(raw.inputTokens) || 0,
293
+ outputTokens: Number(raw.output_tokens) || Number(raw.outputTokens) || 0,
294
+ cacheCreationInputTokens: Number(raw.cache_creation_input_tokens) || Number(raw.cacheCreationInputTokens) || 0,
295
+ cacheReadInputTokens: Number(raw.cache_read_input_tokens) || Number(raw.cacheReadInputTokens) || Number(raw.cached_input_tokens) || Number(raw.cachedInputTokens) || 0,
296
+ cachedInputTokens: Number(raw.cached_input_tokens) || Number(raw.cachedInputTokens) || 0,
297
+ reasoningOutputTokens: Number(raw.reasoning_output_tokens) || Number(raw.reasoningOutputTokens) || 0,
298
+ totalTokens: Number(raw.total_tokens) || Number(raw.totalTokens) || 0,
299
+ };
300
+ if (!usage.totalTokens) usage.totalTokens = usageTotal(usage);
301
+ return usage.totalTokens || usage.inputTokens || usage.outputTokens || usage.cacheCreationInputTokens || usage.cacheReadInputTokens ? usage : null;
302
+ }
303
+
304
+ function codexUsageFromPayload(payload) {
305
+ if (!payload || payload.type !== "token_count" || !payload.info) return null;
306
+ return normalizedUsage(payload.info.last_token_usage || payload.info.total_token_usage);
307
+ }
308
+
309
+ function addUsage(summary, usage, timestamp, label) {
310
+ if (!usage) return;
311
+ const total = usageTotal(usage);
312
+ summary.usage.inputTokens += usage.inputTokens || 0;
313
+ summary.usage.outputTokens += usage.outputTokens || 0;
314
+ summary.usage.cacheCreationInputTokens += usage.cacheCreationInputTokens || 0;
315
+ summary.usage.cacheReadInputTokens += usage.cacheReadInputTokens || 0;
316
+ summary.usage.cachedInputTokens += usage.cachedInputTokens || 0;
317
+ summary.usage.reasoningOutputTokens += usage.reasoningOutputTokens || 0;
318
+ summary.usage.totalTokens += total;
319
+ summary.usage.turnsWithUsage += 1;
320
+ summary.usage.latestTurnTokens = total;
321
+ summary.usage.latestInputTokens = (usage.inputTokens || 0) + (usage.cacheCreationInputTokens || 0) + (usage.cacheReadInputTokens || 0);
322
+ if (total > summary.usage.peakTurnTokens) {
323
+ summary.usage.peakTurnTokens = total;
324
+ summary.usage.peakTurnLabel = label || timestamp || "turn " + summary.usage.turnsWithUsage;
325
+ }
326
+ if (summary.usage.series.length < 260) {
327
+ summary.usage.series.push({
328
+ timestamp: String(timestamp || ""),
329
+ label: label || "turn " + summary.usage.turnsWithUsage,
330
+ totalTokens: total,
331
+ inputTokens: usage.inputTokens || 0,
332
+ outputTokens: usage.outputTokens || 0,
333
+ cacheCreationInputTokens: usage.cacheCreationInputTokens || 0,
334
+ cacheReadInputTokens: usage.cacheReadInputTokens || 0,
335
+ cachedInputTokens: usage.cachedInputTokens || 0,
336
+ reasoningOutputTokens: usage.reasoningOutputTokens || 0,
337
+ });
338
+ }
339
+ }
340
+
236
341
  function sessionIdFromPath(file) {
237
342
  const match = path.basename(file).match(SESSION_ID_RE);
238
343
  return match ? match[0] : path.basename(file, ".jsonl");
239
344
  }
240
345
 
346
+ function safeJson(value) {
347
+ if (!value || typeof value !== "string") return null;
348
+ const text = value.trim();
349
+ if (!text || !/^[{[]/.test(text)) return null;
350
+ try {
351
+ return JSON.parse(text);
352
+ } catch {
353
+ return null;
354
+ }
355
+ }
356
+
357
+ function normalizeToolInput(value) {
358
+ if (value == null) return {};
359
+ if (typeof value === "string") {
360
+ const parsed = safeJson(value);
361
+ return parsed && typeof parsed === "object" ? parsed : { text: value };
362
+ }
363
+ if (typeof value === "object") return value;
364
+ return { text: String(value) };
365
+ }
366
+
367
+ function codexToolInput(payload) {
368
+ if (!payload || typeof payload !== "object") return {};
369
+ if (Object.prototype.hasOwnProperty.call(payload, "input")) return normalizeToolInput(payload.input);
370
+ if (Object.prototype.hasOwnProperty.call(payload, "arguments")) return normalizeToolInput(payload.arguments);
371
+ if (Object.prototype.hasOwnProperty.call(payload, "args")) return normalizeToolInput(payload.args);
372
+ if (Object.prototype.hasOwnProperty.call(payload, "parameters")) return normalizeToolInput(payload.parameters);
373
+ return {};
374
+ }
375
+
376
+ function firstStringField(input, keys) {
377
+ if (!input || typeof input !== "object") return "";
378
+ for (const key of keys) {
379
+ const value = input[key];
380
+ if (typeof value === "string" && value.trim()) return value.trim();
381
+ }
382
+ return "";
383
+ }
384
+
385
+ function shortCommand(command) {
386
+ return cleanTitle(command).slice(0, 260);
387
+ }
388
+
389
+ function stripAnsi(value) {
390
+ return String(value || "").replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
391
+ }
392
+
393
+ function normalizeCommand(command) {
394
+ return stripAnsi(command)
395
+ .replace(/\/Users\/[^\s"']+/g, "<abs-path>")
396
+ .replace(/\/tmp\/[^\s"']+/g, "<tmp-path>")
397
+ .replace(/:\d+(:\d+)?/g, ":<line>")
398
+ .replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,36}\b/gi, "<id>")
399
+ .replace(/\s+/g, " ")
400
+ .trim()
401
+ .slice(0, 260)
402
+ .toLowerCase();
403
+ }
404
+
405
+ function toolFamily(toolName, input, inputText) {
406
+ const name = String(toolName || "").toLowerCase();
407
+ if (inferMcpTool(toolName).server) return "mcp";
408
+ if (/(^|_|\.)(task|agent|subagent|spawn_agent|delegate)/.test(name)) return "agent";
409
+ if (/(read|view|open_file|get_file|cat_file)/.test(name)) return "read";
410
+ if (/(grep|glob|search|find|ripgrep|rg|list|ls)/.test(name)) return "search";
411
+ if (/(edit|write|patch|apply_patch|replace|update_file|create_file|delete|rm)/.test(name)) return "write";
412
+ if (/(bash|shell|exec|terminal|command|run)/.test(name)) return "execute";
413
+ const text = String(inputText || "").toLowerCase();
414
+ if (/^\s*(rg|grep|find|ls)\b/.test(text)) return "search";
415
+ if (/^\s*(cat|sed|nl|head|tail)\b/.test(text)) return "read";
416
+ if (/^\s*(python|node|pnpm|npm|yarn|bun|cargo|go|git|make|pytest|vitest)\b/.test(text)) return "execute";
417
+ return "tool";
418
+ }
419
+
420
+ function toolTarget(toolName, input, inputText) {
421
+ const direct = firstStringField(input, ["file_path", "filePath", "path", "filename", "relative_path", "target", "cwd"]);
422
+ if (direct) return direct;
423
+ const command = firstStringField(input, ["command", "cmd", "shell", "script"]);
424
+ if (command) return shortCommand(command);
425
+ const paths = pathCounts(inputText || "");
426
+ const firstPath = Object.keys(paths)[0];
427
+ if (firstPath) return firstPath;
428
+ return shortCommand(inputText || toolName || "");
429
+ }
430
+
431
+ function toolCommand(toolName, input, inputText) {
432
+ const command = firstStringField(input, ["command", "cmd", "shell", "script"]);
433
+ if (command) return shortCommand(command);
434
+ const name = String(toolName || "").toLowerCase();
435
+ if (/(bash|shell|exec|terminal|command|run)/.test(name)) return shortCommand(inputText || "");
436
+ return "";
437
+ }
438
+
439
+ function outputLooksError(value, text) {
440
+ if (!value || typeof value !== "object") {
441
+ return /\b(exit code|status|error)\s*[:=]?\s*[1-9]\b/i.test(text || "") || /\bfailed\b|\btraceback\b|\bexception\b/i.test(text || "");
442
+ }
443
+ if (value.is_error === true || value.error === true || value.success === false) return true;
444
+ const code = Number(value.exit_code ?? value.exitCode ?? value.status ?? value.code);
445
+ if (Number.isFinite(code) && code !== 0) return true;
446
+ return /\b(exit code|status)\s*[:=]?\s*[1-9]\b/i.test(text || "") || /\btraceback\b|\bexception\b/i.test(text || "");
447
+ }
448
+
449
+ function initTraceFields(summary) {
450
+ summary.usage = emptyUsage();
451
+ summary.steps = [];
452
+ summary.stepCount = 0;
453
+ summary._callMap = {};
454
+ summary._lastToolStep = null;
455
+ }
456
+
457
+ function recordToolStep(summary, options) {
458
+ const input = normalizeToolInput(options.input);
459
+ const inputText = textFrom(input, 0) || compact(input);
460
+ const preview = eventPreview(inputText || options.preview || options.tool || "");
461
+ const family = toolFamily(options.tool, input, inputText || preview);
462
+ const command = toolCommand(options.tool, input, inputText || preview);
463
+ const mcp = inferMcpTool(options.tool);
464
+ const step = {
465
+ index: summary.stepCount++,
466
+ source: summary.source,
467
+ sessionId: summary.sessionId,
468
+ timestamp: String(options.timestamp || ""),
469
+ type: "tool_call",
470
+ tool: String(options.tool || "tool_call"),
471
+ family,
472
+ target: toolTarget(options.tool, input, inputText || preview),
473
+ command,
474
+ normalizedCommand: command ? normalizeCommand(command) : "",
475
+ mcpServer: mcp.server,
476
+ mcpTool: mcp.tool,
477
+ tokens: estimateTokens((inputText || preview).length),
478
+ preview,
479
+ isError: false,
480
+ errorPreview: "",
481
+ };
482
+ if (summary.steps.length < MAX_STEP_SAMPLES) summary.steps.push(step);
483
+ for (const id of options.ids || []) {
484
+ if (id) summary._callMap[String(id)] = step;
485
+ }
486
+ summary._lastToolStep = step;
487
+ return step;
488
+ }
489
+
490
+ function markToolResult(summary, ids, isError, preview) {
491
+ let step = null;
492
+ for (const id of ids || []) {
493
+ if (id && summary._callMap[String(id)]) {
494
+ step = summary._callMap[String(id)];
495
+ break;
496
+ }
497
+ }
498
+ if (!step) step = summary._lastToolStep;
499
+ if (!step) return;
500
+ if (isError) {
501
+ step.isError = true;
502
+ step.errorPreview = eventPreview(preview || step.errorPreview || "");
503
+ }
504
+ }
505
+
506
+ function contentBlocks(content) {
507
+ if (Array.isArray(content)) return content;
508
+ if (content == null) return [];
509
+ return [content];
510
+ }
511
+
241
512
  function classifyCodex(record) {
242
513
  const top = String(record.type || "");
243
514
  const payload = record.payload && typeof record.payload === "object" ? record.payload : {};
244
515
  const ptype = String(payload.type || "");
245
516
  let category = "other";
246
517
  const tools = {};
518
+ let toolName = "";
247
519
  if (top === "session_meta") category = "metadata";
248
520
  else if (top === "turn_context") category = "instructions";
249
521
  else if (top === "event_msg") category = ptype === "user_message" ? "user" : "metadata";
250
522
  else if (top === "response_item") {
251
523
  if (["function_call", "custom_tool_call", "web_search_call", "tool_search_call", "tool_call"].includes(ptype) || payload.name && payload.call_id) {
252
524
  category = "tool_call";
525
+ toolName = payload.name ? String(payload.name) : ptype || "tool_call";
253
526
  if (payload.name) addCounter(tools, String(payload.name), 1);
254
- } else if (["function_call_output", "custom_tool_call_output", "tool_search_output", "tool_result"].includes(ptype) || Object.prototype.hasOwnProperty.call(payload, "output")) category = "tool_output";
527
+ } else if (["function_call_output", "custom_tool_call_output", "tool_search_output", "tool_result"].includes(ptype) || Object.prototype.hasOwnProperty.call(payload, "output")) {
528
+ category = "tool_output";
529
+ toolName = payload.name ? String(payload.name) : "tool output";
530
+ }
255
531
  else if (ptype === "reasoning" || payload.summary) category = "reasoning";
256
532
  else if (payload.role === "assistant") category = "assistant";
257
533
  else if (payload.role === "user" || payload.role === "developer") category = "user";
258
534
  }
259
535
  const text = textFrom(record, 0) || (category === "metadata" ? compact(record) : "");
260
- return { category, chars: text.length, tools, paths: pathCounts(text) };
536
+ return {
537
+ category,
538
+ chars: text.length,
539
+ tools,
540
+ paths: pathCounts(text),
541
+ toolName,
542
+ mcp: inferMcpTool(toolName),
543
+ metadataType: top + (ptype ? ":" + ptype : ""),
544
+ preview: eventPreview(text),
545
+ };
261
546
  }
262
547
 
263
548
  function classifyClaude(record) {
264
549
  let category = "other";
265
550
  const tools = {};
551
+ let toolName = "";
266
552
  const message = record.message && typeof record.message === "object" ? record.message : null;
267
553
  let text = "";
268
554
  if (message) {
@@ -274,8 +560,12 @@ function classifyClaude(record) {
274
560
  if (part && typeof part === "object") {
275
561
  if (part.type === "tool_use") {
276
562
  category = "tool_call";
563
+ toolName = part.name ? String(part.name) : "tool_use";
277
564
  if (part.name) addCounter(tools, String(part.name), 1);
278
- } else if (part.type === "tool_result") category = "tool_output";
565
+ } else if (part.type === "tool_result") {
566
+ category = "tool_output";
567
+ toolName = "tool_result";
568
+ }
279
569
  else if (part.type === "thinking") category = "reasoning";
280
570
  }
281
571
  parts.push(textFrom(part, 0));
@@ -283,6 +573,7 @@ function classifyClaude(record) {
283
573
  text = parts.join("\n");
284
574
  } else if (record.toolUseResult) {
285
575
  category = "tool_output";
576
+ toolName = "toolUseResult";
286
577
  text = textFrom(record.toolUseResult, 0);
287
578
  } else if (record.attachment) {
288
579
  category = "attachment";
@@ -290,7 +581,16 @@ function classifyClaude(record) {
290
581
  } else {
291
582
  text = textFrom(record, 0);
292
583
  }
293
- return { category, chars: text.length, tools, paths: pathCounts(text) };
584
+ return {
585
+ category,
586
+ chars: text.length,
587
+ tools,
588
+ paths: pathCounts(text),
589
+ toolName,
590
+ mcp: inferMcpTool(toolName),
591
+ metadataType: String(record.type || message && message.role || "record"),
592
+ preview: eventPreview(text),
593
+ };
294
594
  }
295
595
 
296
596
  function summarizeCodex(file) {
@@ -305,11 +605,18 @@ function summarizeCodex(file) {
305
605
  updatedAt: "",
306
606
  categories: {},
307
607
  tools: {},
608
+ toolTokens: {},
308
609
  paths: {},
610
+ metadata: {},
611
+ mcpUsage: {},
612
+ mcpTools: {},
613
+ toolEvents: [],
614
+ metadataEvents: [],
309
615
  observedTokens: 0,
310
616
  bytes: stat.size,
311
617
  mtime: stat.mtimeMs,
312
618
  };
619
+ initTraceFields(summary);
313
620
  const records = readJsonl(file);
314
621
  for (const record of records) {
315
622
  const payload = record.payload && typeof record.payload === "object" ? record.payload : {};
@@ -321,9 +628,54 @@ function summarizeCodex(file) {
321
628
  if (payload.cwd && !summary.cwd) summary.cwd = String(payload.cwd);
322
629
  if (record.timestamp) summary.updatedAt = String(record.timestamp);
323
630
  summary.observedTokens = Math.max(summary.observedTokens, observedCodexTokens(payload));
631
+ addUsage(summary, codexUsageFromPayload(payload), String(record.timestamp || payload.timestamp || ""), "turn " + (summary.usage.turnsWithUsage + 1));
324
632
  const stats = classifyCodex(record);
325
633
  addCounter(summary.categories, stats.category, stats.chars);
326
634
  mergeCounter(summary.tools, stats.tools);
635
+ if (stats.toolName && stats.category === "tool_call") {
636
+ recordToolStep(summary, {
637
+ tool: stats.toolName,
638
+ input: codexToolInput(payload),
639
+ ids: [payload.call_id, payload.id],
640
+ timestamp: record.timestamp || payload.timestamp,
641
+ preview: stats.preview,
642
+ });
643
+ addCounter(summary.toolTokens, stats.toolName, estimateTokens(stats.chars));
644
+ if (stats.mcp.server) {
645
+ addCounter(summary.mcpUsage, stats.mcp.server, 1);
646
+ addCounter(summary.mcpTools, stats.mcp.server + " / " + (stats.mcp.tool || stats.toolName), 1);
647
+ }
648
+ if (summary.toolEvents.length < MAX_EVENT_SAMPLES) {
649
+ summary.toolEvents.push({
650
+ source: "codex",
651
+ sessionId: summary.sessionId,
652
+ timestamp: String(record.timestamp || payload.timestamp || ""),
653
+ category: stats.category,
654
+ tool: stats.toolName,
655
+ mcpServer: stats.mcp.server,
656
+ mcpTool: stats.mcp.tool,
657
+ tokens: estimateTokens(stats.chars),
658
+ preview: stats.preview,
659
+ });
660
+ }
661
+ }
662
+ if (stats.category === "tool_output") {
663
+ const text = textFrom(payload, 0);
664
+ markToolResult(summary, [payload.call_id, payload.id], outputLooksError(payload, text), text);
665
+ }
666
+ if (stats.category === "metadata") {
667
+ addCounter(summary.metadata, stats.metadataType, stats.chars);
668
+ if (summary.metadataEvents.length < MAX_EVENT_SAMPLES) {
669
+ summary.metadataEvents.push({
670
+ source: "codex",
671
+ sessionId: summary.sessionId,
672
+ timestamp: String(record.timestamp || payload.timestamp || ""),
673
+ type: stats.metadataType,
674
+ tokens: estimateTokens(stats.chars),
675
+ preview: stats.preview,
676
+ });
677
+ }
678
+ }
327
679
  mergeCounter(summary.paths, stats.paths);
328
680
  }
329
681
  summary.title = codexTitle(summary.sessionId) || summary.sessionId;
@@ -342,11 +694,18 @@ function summarizeClaude(file) {
342
694
  updatedAt: "",
343
695
  categories: {},
344
696
  tools: {},
697
+ toolTokens: {},
345
698
  paths: {},
699
+ metadata: {},
700
+ mcpUsage: {},
701
+ mcpTools: {},
702
+ toolEvents: [],
703
+ metadataEvents: [],
346
704
  observedTokens: 0,
347
705
  bytes: stat.size,
348
706
  mtime: stat.mtimeMs,
349
707
  };
708
+ initTraceFields(summary);
350
709
  const records = readJsonl(file);
351
710
  for (const record of records) {
352
711
  summary.sessionId = String(record.sessionId || summary.sessionId);
@@ -357,11 +716,64 @@ function summarizeClaude(file) {
357
716
  }
358
717
  if (record.message && typeof record.message === "object") {
359
718
  summary.observedTokens = Math.max(summary.observedTokens, claudeUsageTokens(record.message.usage));
719
+ addUsage(summary, normalizedUsage(record.message.usage), String(record.timestamp || ""), "turn " + (summary.usage.turnsWithUsage + 1));
360
720
  if (!summary.title && record.message.role === "user") summary.title = cleanTitle(textFrom(record.message, 0)).slice(0, 90);
721
+ for (const part of contentBlocks(record.message.content)) {
722
+ if (!part || typeof part !== "object") continue;
723
+ if (part.type === "tool_use") {
724
+ recordToolStep(summary, {
725
+ tool: part.name || "tool_use",
726
+ input: part.input || {},
727
+ ids: [part.id, record.uuid],
728
+ timestamp: record.timestamp,
729
+ preview: textFrom(part.input || part, 0),
730
+ });
731
+ } else if (part.type === "tool_result") {
732
+ const text = textFrom(part, 0);
733
+ markToolResult(summary, [part.tool_use_id, record.sourceToolAssistantUUID], part.is_error === true || outputLooksError(part, text), text);
734
+ }
735
+ }
736
+ }
737
+ if (record.toolUseResult) {
738
+ const text = textFrom(record.toolUseResult, 0);
739
+ markToolResult(summary, [record.toolUseID, record.toolUseId, record.sourceToolAssistantUUID], outputLooksError(record.toolUseResult, text), text);
361
740
  }
362
741
  const stats = classifyClaude(record);
363
742
  addCounter(summary.categories, stats.category, stats.chars);
364
743
  mergeCounter(summary.tools, stats.tools);
744
+ if (stats.toolName && stats.category === "tool_call") {
745
+ addCounter(summary.toolTokens, stats.toolName, estimateTokens(stats.chars));
746
+ if (stats.mcp.server) {
747
+ addCounter(summary.mcpUsage, stats.mcp.server, 1);
748
+ addCounter(summary.mcpTools, stats.mcp.server + " / " + (stats.mcp.tool || stats.toolName), 1);
749
+ }
750
+ if (summary.toolEvents.length < MAX_EVENT_SAMPLES) {
751
+ summary.toolEvents.push({
752
+ source: "claude",
753
+ sessionId: summary.sessionId,
754
+ timestamp: String(record.timestamp || ""),
755
+ category: stats.category,
756
+ tool: stats.toolName,
757
+ mcpServer: stats.mcp.server,
758
+ mcpTool: stats.mcp.tool,
759
+ tokens: estimateTokens(stats.chars),
760
+ preview: stats.preview,
761
+ });
762
+ }
763
+ }
764
+ if (stats.category === "metadata") {
765
+ addCounter(summary.metadata, stats.metadataType, stats.chars);
766
+ if (summary.metadataEvents.length < MAX_EVENT_SAMPLES) {
767
+ summary.metadataEvents.push({
768
+ source: "claude",
769
+ sessionId: summary.sessionId,
770
+ timestamp: String(record.timestamp || ""),
771
+ type: stats.metadataType,
772
+ tokens: estimateTokens(stats.chars),
773
+ preview: stats.preview,
774
+ });
775
+ }
776
+ }
365
777
  mergeCounter(summary.paths, stats.paths);
366
778
  }
367
779
  if (!summary.title) summary.title = summary.sessionId;
@@ -373,6 +785,9 @@ function finalizeSummary(summary) {
373
785
  summary.totalChars = totalChars;
374
786
  summary.tokens = summary.observedTokens || estimateTokens(totalChars);
375
787
  summary.tokenMethod = summary.observedTokens ? "observed" : "estimated";
788
+ summary.totalSteps = summary.stepCount || (summary.steps || []).length;
789
+ delete summary._callMap;
790
+ delete summary._lastToolStep;
376
791
  return summary;
377
792
  }
378
793
 
@@ -451,11 +866,36 @@ function collectSessions(args) {
451
866
  }
452
867
 
453
868
  function aggregate(sessions) {
454
- const out = { categories: {}, tools: {}, paths: {} };
869
+ const out = { categories: {}, tools: {}, toolTokens: {}, paths: {}, metadata: {}, mcpUsage: {}, mcpTools: {}, toolEvents: [], metadataEvents: [], steps: [], usage: emptyUsage() };
455
870
  for (const session of sessions) {
456
871
  mergeCounter(out.categories, session.categories);
457
872
  mergeCounter(out.tools, session.tools);
873
+ mergeCounter(out.toolTokens, session.toolTokens);
458
874
  mergeCounter(out.paths, session.paths);
875
+ mergeCounter(out.metadata, session.metadata);
876
+ mergeCounter(out.mcpUsage, session.mcpUsage);
877
+ mergeCounter(out.mcpTools, session.mcpTools);
878
+ out.usage.inputTokens += (session.usage && session.usage.inputTokens) || 0;
879
+ out.usage.outputTokens += (session.usage && session.usage.outputTokens) || 0;
880
+ out.usage.cacheCreationInputTokens += (session.usage && session.usage.cacheCreationInputTokens) || 0;
881
+ out.usage.cacheReadInputTokens += (session.usage && session.usage.cacheReadInputTokens) || 0;
882
+ out.usage.cachedInputTokens += (session.usage && session.usage.cachedInputTokens) || 0;
883
+ out.usage.reasoningOutputTokens += (session.usage && session.usage.reasoningOutputTokens) || 0;
884
+ out.usage.totalTokens += (session.usage && session.usage.totalTokens) || 0;
885
+ out.usage.turnsWithUsage += (session.usage && session.usage.turnsWithUsage) || 0;
886
+ if (session.usage && session.usage.peakTurnTokens > out.usage.peakTurnTokens) {
887
+ out.usage.peakTurnTokens = session.usage.peakTurnTokens;
888
+ out.usage.peakTurnLabel = (session.title || session.sessionId) + " · " + session.usage.peakTurnLabel;
889
+ }
890
+ for (const step of session.steps || []) {
891
+ if (out.steps.length < 2000) out.steps.push(Object.assign({ sessionTitle: session.title, cwd: session.cwd, sessionPath: session.path }, step));
892
+ }
893
+ for (const event of session.toolEvents || []) {
894
+ if (out.toolEvents.length < 500) out.toolEvents.push(Object.assign({ sessionTitle: session.title, cwd: session.cwd, sessionPath: session.path }, event));
895
+ }
896
+ for (const event of session.metadataEvents || []) {
897
+ if (out.metadataEvents.length < 500) out.metadataEvents.push(Object.assign({ sessionTitle: session.title, cwd: session.cwd, sessionPath: session.path }, event));
898
+ }
459
899
  }
460
900
  return out;
461
901
  }
@@ -464,8 +904,358 @@ function sortedEntries(counter, limit) {
464
904
  return Object.entries(counter || {}).sort((a, b) => b[1] - a[1]).slice(0, limit);
465
905
  }
466
906
 
467
- function recommendations(sessions) {
907
+ function normalizedTarget(value) {
908
+ return String(value || "").replace(/^file:\/\//, "").replace(/:\d+(:\d+)?$/g, "").replace(/\/+/g, "/").trim();
909
+ }
910
+
911
+ function severity(score) {
912
+ if (score >= 90) return "critical";
913
+ if (score >= 70) return "high";
914
+ if (score >= 40) return "medium";
915
+ if (score >= 15) return "low";
916
+ return "info";
917
+ }
918
+
919
+ function addFinding(findings, rule, score, category, title, summary, evidence, recommendation, action) {
920
+ findings.push({
921
+ rule,
922
+ severity: severity(score),
923
+ score: Math.round(score),
924
+ category,
925
+ title,
926
+ summary,
927
+ evidence: (evidence || []).filter(Boolean).slice(0, 5),
928
+ recommendation,
929
+ action: action || "prompt",
930
+ confidence: score >= 70 ? "high" : score >= 40 ? "medium" : "low",
931
+ });
932
+ }
933
+
934
+ function cacheStability(session) {
935
+ const series = session.usage && session.usage.series || [];
936
+ if (series.length < 5) {
937
+ return { classification: "stable", turnsAboveThreshold: 0, totalTurns: series.length, avgCacheCreationPct: 0, perTurnRatios: [] };
938
+ }
939
+ const ratios = series.map((turn) => {
940
+ const total = (turn.inputTokens || 0) + (turn.cacheCreationInputTokens || 0) + (turn.cacheReadInputTokens || 0);
941
+ return total ? (turn.cacheCreationInputTokens || 0) / total : 0;
942
+ });
943
+ const turnsAboveThreshold = ratios.filter((ratio) => ratio > 0.3).length;
944
+ const mid = Math.floor(ratios.length / 2);
945
+ const first = ratios.slice(0, mid);
946
+ const second = ratios.slice(mid);
947
+ const avg = (items) => items.length ? items.reduce((sum, item) => sum + item, 0) / items.length : 0;
948
+ const firstAvg = avg(first);
949
+ const secondAvg = avg(second);
950
+ const classification = secondAvg > firstAvg && secondAvg > 0.15 ? "degrading" : turnsAboveThreshold > 5 ? "churning" : "stable";
951
+ return {
952
+ classification,
953
+ turnsAboveThreshold,
954
+ totalTurns: series.length,
955
+ avgCacheCreationPct: avg(ratios) * 100,
956
+ perTurnRatios: ratios.map((ratio) => Math.round(ratio * 1000) / 10),
957
+ };
958
+ }
959
+
960
+ function contextGrowth(session) {
961
+ const series = session.usage && session.usage.series || [];
962
+ const perTurnInput = series.map((turn) => (turn.inputTokens || 0) + (turn.cacheCreationInputTokens || 0) + (turn.cacheReadInputTokens || 0));
963
+ let growthFactor = 0;
964
+ let flagged = false;
965
+ if (perTurnInput.length > 5 && perTurnInput[4] > 0) {
966
+ growthFactor = perTurnInput[perTurnInput.length - 1] / perTurnInput[4];
967
+ flagged = growthFactor > 2;
968
+ }
969
+ let pressureWindows = 0;
970
+ let peakWindowAvg = 0;
971
+ if (perTurnInput.length >= PRESSURE_WINDOW) {
972
+ for (let i = 0; i <= perTurnInput.length - PRESSURE_WINDOW; i++) {
973
+ const window = perTurnInput.slice(i, i + PRESSURE_WINDOW);
974
+ const avg = window.reduce((sum, value) => sum + value, 0) / PRESSURE_WINDOW;
975
+ if (avg > PRESSURE_TOKENS) pressureWindows += 1;
976
+ if (avg > peakWindowAvg) peakWindowAvg = avg;
977
+ }
978
+ }
979
+ return {
980
+ flagged,
981
+ growthFactor: Math.round(growthFactor * 10) / 10,
982
+ perTurnInput: perTurnInput.slice(0, 260),
983
+ pressureWindows,
984
+ peakWindowAvg: Math.round(peakWindowAvg),
985
+ };
986
+ }
987
+
988
+ function duplicateReadGroups(sessions) {
989
+ const groups = {};
990
+ let duplicateCount = 0;
991
+ for (const session of sessions) {
992
+ const seen = {};
993
+ const steps = (session.steps || []).slice().sort((a, b) => a.index - b.index);
994
+ for (const step of steps) {
995
+ const target = normalizedTarget(step.target);
996
+ if (!target || !target.includes("/")) continue;
997
+ if (step.family === "write") {
998
+ delete seen[target];
999
+ continue;
1000
+ }
1001
+ if (step.family !== "read") continue;
1002
+ if (!seen[target]) {
1003
+ seen[target] = step;
1004
+ continue;
1005
+ }
1006
+ duplicateCount += 1;
1007
+ const key = session.sessionId + "|" + target;
1008
+ if (!groups[key]) {
1009
+ groups[key] = {
1010
+ path: target,
1011
+ sessionId: session.sessionId,
1012
+ sessionTitle: session.title,
1013
+ count: 1,
1014
+ firstIndex: seen[target].index,
1015
+ repeated: [],
1016
+ };
1017
+ }
1018
+ groups[key].count += 1;
1019
+ groups[key].repeated.push(step.index);
1020
+ }
1021
+ }
1022
+ return { duplicateCount, groups: Object.values(groups).sort((a, b) => b.count - a.count) };
1023
+ }
1024
+
1025
+ function commandRetryGroups(sessions) {
1026
+ const retries = [];
1027
+ for (const session of sessions) {
1028
+ const steps = (session.steps || []).slice().sort((a, b) => a.index - b.index);
1029
+ let current = null;
1030
+ let streak = [];
1031
+ const flush = () => {
1032
+ if (current && streak.length >= 3) {
1033
+ retries.push({
1034
+ command: current,
1035
+ sessionId: session.sessionId,
1036
+ sessionTitle: session.title,
1037
+ count: streak.length,
1038
+ steps: streak.map((step) => step.index),
1039
+ });
1040
+ }
1041
+ };
1042
+ for (const step of steps) {
1043
+ const command = step.family === "execute" ? step.normalizedCommand : "";
1044
+ if (command && command === current) {
1045
+ streak.push(step);
1046
+ } else {
1047
+ flush();
1048
+ current = command || null;
1049
+ streak = command ? [step] : [];
1050
+ }
1051
+ }
1052
+ flush();
1053
+ }
1054
+ return retries.sort((a, b) => b.count - a.count);
1055
+ }
1056
+
1057
+ function failedToolLoops(sessions) {
1058
+ const loops = [];
1059
+ for (const session of sessions) {
1060
+ const steps = (session.steps || []).slice().sort((a, b) => a.index - b.index);
1061
+ let currentTool = "";
1062
+ let streak = [];
1063
+ const flush = () => {
1064
+ if (currentTool && streak.length >= 3) {
1065
+ loops.push({
1066
+ tool: currentTool,
1067
+ sessionId: session.sessionId,
1068
+ sessionTitle: session.title,
1069
+ count: streak.length,
1070
+ steps: streak.map((step) => step.index),
1071
+ sample: streak[0] && (streak[0].errorPreview || streak[0].preview),
1072
+ });
1073
+ }
1074
+ };
1075
+ for (const step of steps) {
1076
+ if (step.isError && step.tool === currentTool) {
1077
+ streak.push(step);
1078
+ } else {
1079
+ flush();
1080
+ currentTool = step.isError ? step.tool : "";
1081
+ streak = step.isError ? [step] : [];
1082
+ }
1083
+ }
1084
+ flush();
1085
+ }
1086
+ return loops.sort((a, b) => b.count - a.count);
1087
+ }
1088
+
1089
+ function dayKey(value) {
1090
+ const date = new Date(value || Date.now());
1091
+ if (!Number.isFinite(date.getTime())) return "unknown";
1092
+ return date.toISOString().slice(0, 10);
1093
+ }
1094
+
1095
+ function analyzeContext(sessions, args, mcpServers) {
1096
+ const agg = aggregate(sessions);
1097
+ const steps = agg.steps || [];
1098
+ const familyCounts = {};
1099
+ let failedTools = 0;
1100
+ for (const step of steps) {
1101
+ addCounter(familyCounts, step.family || "tool", 1);
1102
+ if (step.isError) failedTools += 1;
1103
+ }
1104
+ const reads = familyCounts.read || 0;
1105
+ const searches = familyCounts.search || 0;
1106
+ const writes = familyCounts.write || 0;
1107
+ const executes = familyCounts.execute || 0;
1108
+ const agents = familyCounts.agent || 0;
1109
+ const mcpCalls = familyCounts.mcp || 0;
1110
+ const readSearch = reads + searches;
1111
+ const explorationRatio = writes ? readSearch / writes : readSearch;
1112
+ const usage = agg.usage || emptyUsage();
1113
+ const cacheDenom = (usage.inputTokens || 0) + (usage.cacheCreationInputTokens || 0) + (usage.cacheReadInputTokens || 0);
1114
+ const cacheHitRatio = cacheDenom ? (usage.cacheReadInputTokens || 0) / cacheDenom : 0;
1115
+ const cacheCreationPct = cacheDenom ? (usage.cacheCreationInputTokens || 0) / cacheDenom : 0;
1116
+ const duplicateReads = duplicateReadGroups(sessions);
1117
+ const retries = commandRetryGroups(sessions);
1118
+ const failureLoops = failedToolLoops(sessions);
1119
+ const cacheSessions = sessions.map((session) => Object.assign({ sessionId: session.sessionId, sessionTitle: session.title }, cacheStability(session)));
1120
+ const growthSessions = sessions.map((session) => Object.assign({ sessionId: session.sessionId, sessionTitle: session.title }, contextGrowth(session)));
1121
+ const churning = cacheSessions.filter((item) => item.classification !== "stable");
1122
+ const growing = growthSessions.filter((item) => item.flagged);
1123
+ const pressure = growthSessions.filter((item) => item.pressureWindows > 0);
1124
+ const byDay = {};
1125
+ for (const session of sessions) {
1126
+ const key = dayKey(session.updatedAt || session.startedAt || session.mtime);
1127
+ if (!byDay[key]) byDay[key] = { day: key, sessions: 0, tokens: 0, observed: 0, codex: 0, claude: 0 };
1128
+ byDay[key].sessions += 1;
1129
+ byDay[key].tokens += session.tokens || 0;
1130
+ if (session.tokenMethod === "observed") byDay[key].observed += 1;
1131
+ byDay[key][session.source] = (byDay[key][session.source] || 0) + 1;
1132
+ }
1133
+ const categoryTotal = Object.values(agg.categories).reduce((sum, value) => sum + value, 0);
1134
+ const toolOutputPct = pct(agg.categories.tool_output || 0, categoryTotal);
1135
+ const metadataPct = pct(agg.categories.metadata || 0, categoryTotal);
1136
+ const assistantPct = pct(agg.categories.assistant || 0, categoryTotal);
1137
+ const maxSession = sessions.length ? sessions.reduce((a, b) => a.tokens > b.tokens ? a : b) : null;
1138
+ const failureRate = steps.length ? failedTools / steps.length : 0;
1139
+ const findings = [];
1140
+ if (duplicateReads.duplicateCount > 0) {
1141
+ addFinding(findings, "duplicate_read", Math.min(86, 35 + duplicateReads.duplicateCount * 6), "context", "Repeated file reads", duplicateReads.duplicateCount + " file reads repeated without an intervening edit/write.", duplicateReads.groups.slice(0, 4).map((group) => group.path + " read " + group.count + "x in " + group.sessionTitle), "Ask the agent to keep a short file-role note after the first read and reopen the file only when it needs exact line numbers.", "prompt");
1142
+ }
1143
+ if (retries.length) {
1144
+ addFinding(findings, "command_retry_loop", 78, "loop", "Repeated command loop", "One or more shell commands ran 3+ times in a row.", retries.slice(0, 4).map((retry) => retry.command + " · " + retry.count + "x in " + retry.sessionTitle), "Ask the agent to stop after two identical failures, summarize the error, and change strategy before rerunning the command.", "prompt");
1145
+ }
1146
+ if (failureLoops.length || (failedTools >= 3 && failureRate > 0.12)) {
1147
+ addFinding(findings, "failed_tool_loop", failureLoops.length ? 82 : 55, "loop", "Tool failures need a recovery plan", failedTools + " failed tool calls detected across " + steps.length + " normalized tool steps.", (failureLoops.length ? failureLoops : steps.filter((step) => step.isError).slice(0, 4)).map((item) => (item.tool || item.title || "tool") + " · " + (item.count || "failed") + " · " + (item.sessionTitle || item.sessionId || "")), "Tell the agent to diagnose the first failure, propose the next attempt, and avoid repeating the same tool call unchanged.", "prompt");
1148
+ }
1149
+ if (explorationRatio > 5 && readSearch >= 10) {
1150
+ addFinding(findings, "exploration_ratio", Math.min(80, 35 + explorationRatio * 5), "workflow", "Exploration outweighs edits", "Read/search calls are " + explorationRatio.toFixed(1) + "x edit/write calls.", ["Reads/searches: " + readSearch, "Writes/edits: " + writes, "Execute calls: " + executes], "Give the agent a concrete inspection budget, then ask for a short implementation plan before more reading.", "prompt");
1151
+ }
1152
+ if (agents > 3) {
1153
+ addFinding(findings, "subagent_sprawl", Math.min(72, 35 + agents * 6), "workflow", "Subagent usage is broad", agents + " agent/delegation tool calls were detected.", ["Agent-family calls: " + agents], "Ask for a coordination summary after subagents finish: decisions, files touched, and what should stay in the main thread.", "prompt");
1154
+ }
1155
+ if (churning.length) {
1156
+ addFinding(findings, "cache_churn", churning.some((item) => item.classification === "churning") ? 78 : 62, "cache", "Cache is churning or degrading", churning.length + " session(s) had sustained or rising cache creation.", churning.slice(0, 4).map((item) => item.sessionTitle + " · " + item.classification + " · avg create " + item.avgCacheCreationPct.toFixed(0) + "%"), "Move stable instructions into a skill/repo doc and start long follow-up work from a compact handoff so cache writes settle earlier.", "workflow");
1157
+ }
1158
+ if (growing.length) {
1159
+ addFinding(findings, "context_growth", 74, "context", "Context keeps growing late in the session", growing.length + " session(s) grew >2x from turn 5 to the final observed turn.", growing.slice(0, 4).map((item) => item.sessionTitle + " · " + item.growthFactor + "x growth"), "After a milestone, ask for a handoff summary and continue in a fresh thread rather than carrying the full transcript forward.", "workflow");
1160
+ }
1161
+ if (pressure.length) {
1162
+ addFinding(findings, "context_pressure", 70, "context", "High context pressure windows", pressure.length + " session(s) crossed a " + fmtTokens(PRESSURE_TOKENS) + " five-turn average input window.", pressure.slice(0, 4).map((item) => item.sessionTitle + " · peak avg " + fmtTokens(item.peakWindowAvg)), "Use a context reset before the next implementation phase and preserve only decisions, current files, and known failing commands.", "workflow");
1163
+ }
1164
+ if (toolOutputPct > 45) {
1165
+ addFinding(findings, "tool_output_heavy", Math.min(76, 35 + toolOutputPct), "context", "Tool output dominates the window", toolOutputPct.toFixed(0) + "% of transcript text came from tool output.", sortedEntries(agg.tools, 4).map((entry) => entry[0] + " x" + entry[1]), "Ask the agent to cap logs, request targeted excerpts, and summarize failing output unless exact lines are needed.", "prompt");
1166
+ }
1167
+ if (metadataPct > 18) {
1168
+ addFinding(findings, "metadata_pressure", Math.min(64, 25 + metadataPct), "context", "Metadata is a visible share", metadataPct.toFixed(0) + "% of local transcript text was metadata or protocol state.", sortedEntries(agg.metadata, 4).map((entry) => entry[0] + " · about " + fmtTokens(estimateTokens(entry[1]))), "Keep the report in drilldown mode: inspect metadata when diagnosing protocol overhead, but optimize prompts around user/tool/output buckets first.", "workflow");
1169
+ }
1170
+ if (assistantPct > 45) {
1171
+ addFinding(findings, "assistant_prose", Math.min(68, 25 + assistantPct), "workflow", "Assistant narration is heavy", assistantPct.toFixed(0) + "% of transcript text came from assistant prose.", ["Assistant bucket: about " + fmtTokens(estimateTokens(agg.categories.assistant || 0))], "Ask for concise progress notes and a final decision log, with detailed rationale moved into docs only when useful.", "prompt");
1172
+ }
1173
+ if (maxSession && maxSession.tokens > 80000) {
1174
+ addFinding(findings, "large_session", Math.min(82, 42 + maxSession.tokens / 5000), "context", "One session is carrying a lot", "Largest session is about " + fmtTokens(maxSession.tokens) + " " + maxSession.tokenMethod + " tokens.", [maxSession.title + " · " + maxSession.source + " · " + (maxSession.cwd || maxSession.path)], "Start the next large change with a compact handoff summary instead of continuing the whole thread.", "workflow");
1175
+ }
1176
+ if ((mcpServers || []).length > 12 && mcpCalls < 3) {
1177
+ addFinding(findings, "mcp_surface", 28, "mcp", "Configured MCP surface is broad", (mcpServers || []).length + " MCP servers are configured, but only " + mcpCalls + " MCP-style calls were detected.", (mcpServers || []).slice(0, 5).map((server) => server.source + " · " + server.name), "For focused CLI work, keep rarely used MCP servers disabled or project-scoped so tool lists stay easier to scan.", "configuration");
1178
+ }
1179
+ let score = 100;
1180
+ score -= Math.min(18, duplicateReads.duplicateCount * 2);
1181
+ score -= retries.length ? 14 : 0;
1182
+ score -= failureLoops.length ? 16 : Math.min(12, Math.round(failureRate * 60));
1183
+ score -= explorationRatio > 5 ? Math.min(15, Math.round((explorationRatio - 5) * 2)) : 0;
1184
+ score -= churning.length ? 14 : 0;
1185
+ score -= growing.length ? 12 : 0;
1186
+ score -= pressure.length ? 10 : 0;
1187
+ score -= toolOutputPct > 45 ? 8 : 0;
1188
+ score = Math.max(0, Math.min(100, Math.round(score)));
1189
+ findings.sort((a, b) => b.score - a.score);
1190
+ return {
1191
+ score,
1192
+ scoreLabel: score >= 85 ? "healthy" : score >= 70 ? "watch" : score >= 50 ? "strained" : "critical",
1193
+ metrics: {
1194
+ normalizedSteps: steps.length,
1195
+ failedTools,
1196
+ failureRate,
1197
+ successRate: steps.length ? 1 - failureRate : 1,
1198
+ familyCounts,
1199
+ reads,
1200
+ searches,
1201
+ writes,
1202
+ executes,
1203
+ agents,
1204
+ mcpCalls,
1205
+ explorationRatio: Math.round(explorationRatio * 10) / 10,
1206
+ duplicateReadCount: duplicateReads.duplicateCount,
1207
+ retryLoopCount: retries.length,
1208
+ failureLoopCount: failureLoops.length,
1209
+ cacheHitRatio,
1210
+ cacheCreationPct,
1211
+ tokenUsage: usage,
1212
+ observedSessionCoverage: sessions.length ? sessions.filter((session) => session.tokenMethod === "observed").length / sessions.length : 0,
1213
+ toolOutputPct,
1214
+ metadataPct,
1215
+ assistantPct,
1216
+ },
1217
+ findings,
1218
+ evidence: {
1219
+ duplicateReads: duplicateReads.groups.slice(0, 20),
1220
+ commandRetries: retries.slice(0, 20),
1221
+ failureLoops: failureLoops.slice(0, 20),
1222
+ cacheStability: cacheSessions,
1223
+ contextGrowth: growthSessions,
1224
+ },
1225
+ trends: {
1226
+ byDay: Object.values(byDay).sort((a, b) => a.day.localeCompare(b.day)),
1227
+ topTools: sortedEntries(agg.tools, 12),
1228
+ topPaths: sortedEntries(agg.paths, 12),
1229
+ topMetadata: sortedEntries(agg.metadata, 12),
1230
+ },
1231
+ sourceModel: {
1232
+ sourceProjectSession: sessions.map((session) => ({
1233
+ source: session.source,
1234
+ project: session.cwd || args.project,
1235
+ sessionId: session.sessionId,
1236
+ title: session.title,
1237
+ tokens: session.tokens,
1238
+ updatedAt: session.updatedAt,
1239
+ tokenMethod: session.tokenMethod,
1240
+ steps: session.totalSteps || 0,
1241
+ })),
1242
+ privacy: "Metadata-first: the report stores counters, tool names, paths, short previews, and line offsets where available; transcript bodies stay local.",
1243
+ },
1244
+ };
1245
+ }
1246
+
1247
+ function recommendations(sessions, analysis) {
468
1248
  if (!sessions.length) return ["No matching sessions found. Try context-xray threads --all-projects --since 2w --open."];
1249
+ if (analysis && analysis.findings && analysis.findings.length) {
1250
+ const seen = {};
1251
+ const fromFindings = [];
1252
+ for (const finding of analysis.findings) {
1253
+ if (!finding.recommendation || seen[finding.recommendation]) continue;
1254
+ seen[finding.recommendation] = true;
1255
+ fromFindings.push(finding.recommendation);
1256
+ }
1257
+ if (fromFindings.length) return fromFindings.slice(0, 6);
1258
+ }
469
1259
  const agg = aggregate(sessions);
470
1260
  const total = Object.values(agg.categories).reduce((sum, value) => sum + value, 0);
471
1261
  const tips = [];
@@ -473,15 +1263,15 @@ function recommendations(sessions) {
473
1263
  const instructions = pct(agg.categories.instructions || 0, total);
474
1264
  const assistant = pct(agg.categories.assistant || 0, total);
475
1265
  const maxSession = sessions.reduce((a, b) => a.tokens > b.tokens ? a : b);
476
- if (toolOutput > 45) tips.push("Tool output dominates context. Prefer targeted rg/sed ranges, cap logs, and ask agents to summarize failing blocks instead of pasting full output.");
477
- if (instructions > 25) tips.push("Instructions are a large share. Move stable workflow rules into skills or AGENTS/CLAUDE files and keep per-turn prompts short.");
478
- if (assistant > 45) tips.push("Assistant prose is heavy. Ask for terse progress updates during long runs and save rationale only when it changes decisions.");
479
- if (maxSession.tokens > 80000) tips.push("The largest session is about " + fmtTokens(maxSession.tokens) + " " + maxSession.tokenMethod + " tokens. Compact or start a fresh handoff before another big implementation pass.");
1266
+ if (toolOutput > 45) tips.push("Tool output dominates context. In your prompt, ask the agent to keep command output capped, summarize failures, and only expand logs when the exact lines matter.");
1267
+ if (instructions > 25) tips.push("Instructions are a large share. Move durable workflow preferences into a skill, AGENTS.md, or CLAUDE.md so each thread can start with a shorter task prompt.");
1268
+ if (assistant > 45) tips.push("Assistant prose is heavy. Ask for brief progress updates and a final decision log, instead of detailed narration during every loop.");
1269
+ if (maxSession.tokens > 80000) tips.push("The largest session is about " + fmtTokens(maxSession.tokens) + " " + maxSession.tokenMethod + " tokens. Ask for a handoff summary, then continue in a fresh thread before the next large implementation pass.");
480
1270
  const topTool = sortedEntries(agg.tools, 1)[0];
481
- if (topTool && topTool[1] > 20) tips.push(topTool[0] + " appears " + topTool[1] + " times. Batch independent inspection and use parallel reads/searches.");
1271
+ if (topTool && topTool[1] > 20) tips.push(topTool[0] + " appears " + topTool[1] + " times. Tell the agent to batch independent inspection and avoid rerunning diagnostics unless state changed.");
482
1272
  const topPath = sortedEntries(agg.paths, 1)[0];
483
- if (topPath && topPath[1] > 12) tips.push(topPath[0] + " appears repeatedly (" + topPath[1] + " mentions). Pin a short role summary instead of rereading it.");
484
- return tips.length ? tips.slice(0, 6) : ["Recent sessions look balanced. Keep using focused reads, compact after milestones, and preserve decisions in a skill or repo doc."];
1273
+ if (topPath && topPath[1] > 12) tips.push(topPath[0] + " appears repeatedly (" + topPath[1] + " mentions). Ask the agent to keep a short file-role summary and reopen the file only when it needs exact lines.");
1274
+ return tips.length ? tips.slice(0, 6) : ["Recent sessions look balanced. Keep giving scoped tasks, ask for compaction after milestones, and preserve reusable decisions in a skill or repo doc."];
485
1275
  }
486
1276
 
487
1277
  function escapeHtml(value) {
@@ -503,43 +1293,639 @@ function categoryBar(categories) {
503
1293
  }).join("") + "</div>";
504
1294
  }
505
1295
 
506
- function renderHtml(sessions, args) {
507
- const agg = aggregate(sessions);
508
- const totalTokens = sessions.reduce((sum, s) => sum + s.tokens, 0);
509
- const tips = recommendations(sessions);
510
- const categoryRows = CATEGORIES.map((cat) => {
511
- const chars = agg.categories[cat] || 0;
512
- if (!chars) return "";
513
- return "<tr><td>" + escapeHtml(LABELS[cat]) + "</td><td>" + fmtTokens(estimateTokens(chars)) + "</td><td>" + pct(chars, Object.values(agg.categories).reduce((sum, value) => sum + value, 0)).toFixed(0) + "%</td></tr>";
514
- }).join("");
515
- const sessionCards = sessions.map((session) => {
516
- const cats = Object.entries(session.categories).sort((a, b) => b[1] - a[1]).map((entry) => "<tr><td>" + escapeHtml(LABELS[entry[0]] || entry[0]) + "</td><td>" + fmtTokens(estimateTokens(entry[1])) + "</td><td>" + pct(entry[1], session.totalChars).toFixed(0) + "%</td></tr>").join("");
517
- const tools = sortedEntries(session.tools, 6).map((entry) => "<span class=\"badge\">" + escapeHtml(entry[0]) + " x" + entry[1] + "</span>").join("");
518
- const paths = sortedEntries(session.paths, 5).map((entry) => "<span class=\"badge muted\">" + escapeHtml(entry[0]) + " x" + entry[1] + "</span>").join("");
519
- return "<article class=\"card session\"><div class=\"session-head\"><div><div class=\"eyebrow\">" + escapeHtml(session.source) + " - " + escapeHtml(session.updatedAt || "unknown time") + "</div><h3>" + escapeHtml(session.title || session.sessionId) + "</h3><p class=\"path\">" + escapeHtml(session.cwd || session.path) + "</p></div><div class=\"token-big\">" + fmtTokens(session.tokens) + "</div></div>" + categoryBar(session.categories) + "<div class=\"session-grid\"><table><tbody>" + cats + "</tbody></table><div><div class=\"mini-label\">Frequent tools</div><div class=\"badges\">" + (tools || "<span class=\"muted-text\">none detected</span>") + "</div><div class=\"mini-label\">Repeated paths</div><div class=\"badges\">" + (paths || "<span class=\"muted-text\">none detected</span>") + "</div></div></div></article>";
520
- }).join("");
521
- const topTools = sortedEntries(agg.tools, 10).map((entry) => "<tr><td>" + escapeHtml(entry[0]) + "</td><td>" + entry[1] + "</td></tr>").join("");
522
- const topPaths = sortedEntries(agg.paths, 10).map((entry) => "<tr><td>" + escapeHtml(entry[0]) + "</td><td>" + entry[1] + "</td></tr>").join("");
523
- const sourceCounts = sessions.reduce((counts, s) => (counts[s.source] = (counts[s.source] || 0) + 1, counts), {});
524
- return "<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Context X-Ray</title><style>" +
525
- "body{margin:0;background:#0e1116;color:#f3f5f8;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;line-height:1.45}main{max-width:1180px;margin:0 auto;padding:32px 20px 56px}header{display:flex;justify-content:space-between;gap:24px;align-items:flex-end;margin-bottom:24px}h1{margin:0;font-size:clamp(28px,5vw,52px);letter-spacing:0}h2{margin:0 0 14px;font-size:18px}h3{margin:3px 0 4px;font-size:17px}p{margin:0}.muted,.path,.eyebrow,.muted-text{color:#9aa3ad}.eyebrow{text-transform:uppercase;letter-spacing:.08em;font-size:11px}.summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:18px 0}.card{background:linear-gradient(180deg,#151922,#1c2230);border:1px solid #2a3140;border-radius:8px;padding:16px;box-shadow:0 12px 40px rgba(0,0,0,.22)}.stat strong{display:block;font-size:28px;line-height:1.1}.grid{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(280px,.8fr);gap:16px;align-items:start}.bar{display:flex;overflow:hidden;height:14px;border-radius:999px;background:#252b37;margin:12px 0}.bar span{display:block;min-width:2px}.tips li{margin:0 0 9px}.session{margin-top:14px}.session-head{display:flex;justify-content:space-between;gap:16px}.token-big{font-weight:700;font-size:28px;color:#8ba8ff;white-space:nowrap}.session-grid{display:grid;grid-template-columns:260px minmax(0,1fr);gap:14px;margin-top:12px}table{width:100%;border-collapse:collapse;font-size:13px}td{border-top:1px solid #2a3140;padding:7px 0;vertical-align:top}td:last-child{text-align:right;color:#9aa3ad}.mini-label{margin:7px 0 6px;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#9aa3ad}.badges{display:flex;flex-wrap:wrap;gap:6px}.badge{display:inline-flex;border:1px solid #2a3140;border-radius:999px;padding:3px 8px;font-size:12px;background:rgba(255,255,255,.04)}.badge.muted{color:#9aa3ad}footer{color:#9aa3ad;margin-top:24px;font-size:12px}@media(max-width:840px){header,.grid,.session-grid{grid-template-columns:1fr;display:grid}.summary{grid-template-columns:repeat(2,minmax(0,1fr))}}" +
526
- "</style></head><body><main><header><div><div class=\"eyebrow\">Local coding context profile</div><h1>Context X-Ray</h1><p class=\"muted\">Generated " + escapeHtml(new Date().toLocaleString()) + " - mode=" + escapeHtml(args.mode) + " - source=" + escapeHtml(args.source) + " - since=" + escapeHtml(args.since) + "</p></div></header><section class=\"summary\"><div class=\"card stat\"><span class=\"muted\">Sessions</span><strong>" + sessions.length + "</strong></div><div class=\"card stat\"><span class=\"muted\">Observed/estimated tokens</span><strong>" + fmtTokens(totalTokens) + "</strong></div><div class=\"card stat\"><span class=\"muted\">Codex</span><strong>" + (sourceCounts.codex || 0) + "</strong></div><div class=\"card stat\"><span class=\"muted\">Claude</span><strong>" + (sourceCounts.claude || 0) + "</strong></div></section><section class=\"card\"><h2>Where The Context Is Going</h2>" + categoryBar(agg.categories) + "<table><tbody>" + categoryRows + "</tbody></table></section><div class=\"grid\" style=\"margin-top:16px\"><section class=\"card tips\"><h2>Warnings And Optimizations</h2><ol>" + tips.map((tip) => "<li>" + escapeHtml(tip) + "</li>").join("") + "</ol></section><section class=\"card\"><h2>Hotspots</h2><div class=\"mini-label\">Top tools</div><table><tbody>" + (topTools || "<tr><td>None detected</td><td></td></tr>") + "</tbody></table><div class=\"mini-label\">Top paths</div><table><tbody>" + (topPaths || "<tr><td>None detected</td><td></td></tr>") + "</tbody></table></section></div><section style=\"margin-top:16px\"><h2>Sessions</h2>" + sessionCards + "</section><footer>Reads local transcript files only. No transcript content is uploaded.</footer></main></body></html>";
1296
+ function readJsonFile(file) {
1297
+ try {
1298
+ return JSON.parse(fs.readFileSync(file, "utf8"));
1299
+ } catch {
1300
+ return null;
1301
+ }
527
1302
  }
528
1303
 
529
- function writeJson(sessions, args, file) {
1304
+ function safeUrl(value) {
1305
+ try {
1306
+ const url = new URL(String(value || ""));
1307
+ return url.origin + url.pathname;
1308
+ } catch {
1309
+ return String(value || "").split(/[?#]/)[0].slice(0, 120);
1310
+ }
1311
+ }
1312
+
1313
+ function mcpTarget(definition) {
1314
+ if (!definition || typeof definition !== "object") return "configured";
1315
+ if (definition.url) return safeUrl(definition.url);
1316
+ if (definition.command) return String(definition.command);
1317
+ if (definition.transport) return String(definition.transport);
1318
+ return "configured";
1319
+ }
1320
+
1321
+ function addMcpServer(out, name, source, definition) {
1322
+ if (!name) return;
1323
+ const key = source + ":" + name;
1324
+ if (out.some((server) => server.key === key)) return;
1325
+ out.push({
1326
+ key,
1327
+ name: String(name),
1328
+ source,
1329
+ target: mcpTarget(definition),
1330
+ });
1331
+ }
1332
+
1333
+ function readMcpJson(file, source, out) {
1334
+ const json = readJsonFile(file);
1335
+ if (!json || typeof json !== "object") return;
1336
+ const servers = json.mcpServers || json.mcp_servers || json.servers;
1337
+ if (!servers || typeof servers !== "object") return;
1338
+ for (const name of Object.keys(servers)) addMcpServer(out, name, source, servers[name]);
1339
+ }
1340
+
1341
+ function readCodexTomlServers(file, out) {
1342
+ let text = "";
1343
+ try {
1344
+ text = fs.readFileSync(file, "utf8");
1345
+ } catch {
1346
+ return;
1347
+ }
1348
+ const lines = text.split(/\r?\n/);
1349
+ for (let index = 0; index < lines.length; index++) {
1350
+ const match = lines[index].match(/^\s*\[mcp_servers\.(?:"([^"]+)"|([^\]]+))\]\s*$/);
1351
+ if (!match) continue;
1352
+ const name = match[1] || match[2] || "";
1353
+ const section = {};
1354
+ for (let cursor = index + 1; cursor < lines.length; cursor++) {
1355
+ if (/^\s*\[/.test(lines[cursor])) break;
1356
+ const pair = lines[cursor].match(/^\s*([A-Za-z0-9_-]+)\s*=\s*"?([^"]+)"?\s*$/);
1357
+ if (pair) section[pair[1]] = pair[2];
1358
+ }
1359
+ addMcpServer(out, name, "Codex config", section);
1360
+ }
1361
+ }
1362
+
1363
+ function readMcpServers(project) {
1364
+ const out = [];
1365
+ readCodexTomlServers(path.join(CODEX_DIR, "config.toml"), out);
1366
+ readMcpJson(path.join(HOME, ".claude.json"), "Claude user config", out);
1367
+ readMcpJson(path.join(CLAUDE_DIR, "settings.json"), "Claude settings", out);
1368
+ readMcpJson(path.join(path.resolve(project), ".mcp.json"), "Project .mcp.json", out);
1369
+ readMcpJson(path.join(path.resolve(project), ".cursor", "mcp.json"), "Project Cursor MCP", out);
1370
+ readMcpJson(path.join(HOME, ".cowork", "mcp.json"), "Cowork MCP", out);
1371
+ return out.sort((a, b) => a.name.localeCompare(b.name) || a.source.localeCompare(b.source));
1372
+ }
1373
+
1374
+ function sourceCounts(sessions) {
1375
+ return sessions.reduce((counts, session) => {
1376
+ counts[session.source] = (counts[session.source] || 0) + 1;
1377
+ return counts;
1378
+ }, {});
1379
+ }
1380
+
1381
+ function buildReport(sessions, args) {
530
1382
  const agg = aggregate(sessions);
531
- fs.writeFileSync(file, JSON.stringify({
1383
+ const mcpServers = readMcpServers(args.project);
1384
+ const analysis = analyzeContext(sessions, args, mcpServers);
1385
+ return {
532
1386
  generatedAt: new Date().toISOString(),
1387
+ generatedLabel: new Date().toLocaleString(),
533
1388
  mode: args.mode,
534
1389
  source: args.source,
535
1390
  since: args.since,
1391
+ project: path.resolve(args.project),
1392
+ sessionCount: sessions.length,
1393
+ sourceCounts: sourceCounts(sessions),
536
1394
  totalTokens: sessions.reduce((sum, s) => sum + s.tokens, 0),
537
1395
  categories: agg.categories,
538
- tools: sortedEntries(agg.tools, 25),
539
- paths: sortedEntries(agg.paths, 25),
540
- recommendations: recommendations(sessions),
1396
+ tools: sortedEntries(agg.tools, 50),
1397
+ toolTokens: sortedEntries(agg.toolTokens, 50),
1398
+ paths: sortedEntries(agg.paths, 50),
1399
+ metadata: sortedEntries(agg.metadata, 50),
1400
+ mcpUsage: sortedEntries(agg.mcpUsage, 50),
1401
+ mcpTools: sortedEntries(agg.mcpTools, 50),
1402
+ mcpServers,
1403
+ analysis,
1404
+ recommendations: recommendations(sessions, analysis),
1405
+ toolEvents: agg.toolEvents,
1406
+ metadataEvents: agg.metadataEvents,
1407
+ steps: agg.steps,
541
1408
  sessions,
542
- }, null, 2));
1409
+ };
1410
+ }
1411
+
1412
+ function jsonScript(value) {
1413
+ return JSON.stringify(value).replace(/</g, "\\u003c");
1414
+ }
1415
+
1416
+ function overviewEnhancementScript() {
1417
+ return "(" + function () {
1418
+ const dataEl = document.getElementById("xray-data");
1419
+ if (!dataEl) return;
1420
+ const report = JSON.parse(dataEl.textContent || "{}");
1421
+ const cats = ["user", "assistant", "tool_call", "tool_output", "reasoning", "instructions", "attachment", "metadata", "other"];
1422
+ const labels = {
1423
+ user: "User asks",
1424
+ assistant: "Assistant text",
1425
+ tool_call: "Tool calls",
1426
+ tool_output: "Tool output",
1427
+ reasoning: "Reasoning",
1428
+ instructions: "Instructions/context",
1429
+ attachment: "Attachments",
1430
+ metadata: "Metadata",
1431
+ other: "Other",
1432
+ };
1433
+ const colors = {
1434
+ user: "#8ba8ff",
1435
+ assistant: "#55b982",
1436
+ tool_call: "#f0a85b",
1437
+ tool_output: "#e06b73",
1438
+ reasoning: "#a77be8",
1439
+ instructions: "#6ac3d5",
1440
+ attachment: "#d6a85a",
1441
+ metadata: "#9aa3ad",
1442
+ other: "#c3c8ce",
1443
+ };
1444
+
1445
+ function esc(value) {
1446
+ return String(value || "").replace(/[&<>"']/g, function (ch) {
1447
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[ch];
1448
+ });
1449
+ }
1450
+
1451
+ function fmt(value) {
1452
+ const number = Number(value) || 0;
1453
+ if (number >= 1000000) return (number / 1000000).toFixed(1) + "m";
1454
+ if (number >= 1000) return (number / 1000).toFixed(1) + "k";
1455
+ return String(number);
1456
+ }
1457
+
1458
+ function tok(chars) {
1459
+ chars = Number(chars) || 0;
1460
+ return chars > 0 ? Math.max(1, Math.ceil(chars / 4)) : 0;
1461
+ }
1462
+
1463
+ function totalCounter(counter) {
1464
+ return Object.keys(counter || {}).reduce(function (sum, key) {
1465
+ return sum + (Number(counter[key]) || 0);
1466
+ }, 0);
1467
+ }
1468
+
1469
+ function pct(part, total) {
1470
+ return total > 0 ? Math.max(0, Math.min(100, (part / total) * 100)) : 0;
1471
+ }
1472
+
1473
+ function actionRow(kind, value, title, subtitle, right, farRight, color) {
1474
+ return "<button class=\"row-button\" data-overview-kind=\"" + esc(kind) + "\" data-overview-value=\"" + esc(value) + "\"><span><span class=\"row-title\">" + (color ? "<span class=\"badge\"><span class=\"dot\" style=\"background:" + esc(color) + "\"></span>" + esc(title) + "</span>" : esc(title)) + "</span><span class=\"meta\">" + esc(subtitle) + "</span></span><span class=\"row-meta\">" + esc(right) + "</span><span class=\"row-meta\">" + esc(farRight || "") + "</span></button>";
1475
+ }
1476
+
1477
+ function bar(counter) {
1478
+ const total = totalCounter(counter);
1479
+ if (!total) return "<div class=\"bar\"></div>";
1480
+ return "<div class=\"bar\">" + cats.map(function (cat) {
1481
+ const value = (counter || {})[cat] || 0;
1482
+ if (!value) return "";
1483
+ return "<button type=\"button\" data-overview-kind=\"category\" data-overview-value=\"" + esc(cat) + "\" title=\"" + esc(labels[cat]) + "\" style=\"width:" + Math.max(1, pct(value, total)).toFixed(2) + "%;background:" + colors[cat] + ";border:0;padding:0;display:block;min-width:2px;cursor:pointer\"></button>";
1484
+ }).join("") + "</div>";
1485
+ }
1486
+
1487
+ function topTools(limit) {
1488
+ return (report.tools || []).slice(0, limit || 8);
1489
+ }
1490
+
1491
+ function toolTokens(name) {
1492
+ const found = (report.toolTokens || []).filter(function (entry) {
1493
+ return entry[0] === name;
1494
+ })[0];
1495
+ return found ? found[1] : 0;
1496
+ }
1497
+
1498
+ function sessionMatches(kind, value) {
1499
+ return (report.sessions || []).filter(function (session) {
1500
+ if (kind === "category") return !!((session.categories || {})[value]);
1501
+ if (kind === "path") return !!((session.paths || {})[value]);
1502
+ return false;
1503
+ }).sort(function (a, b) {
1504
+ const left = kind === "path" ? (a.paths || {})[value] || 0 : (a.categories || {})[value] || 0;
1505
+ const right = kind === "path" ? (b.paths || {})[value] || 0 : (b.categories || {})[value] || 0;
1506
+ return right - left;
1507
+ });
1508
+ }
1509
+
1510
+ function sampleCards(events) {
1511
+ events = (events || []).slice(0, 8);
1512
+ if (!events.length) return "<p class=\"empty\">No sampled records for this item.</p>";
1513
+ return "<div class=\"detail-list\">" + events.map(function (event) {
1514
+ return "<article class=\"sample\"><div class=\"sample-top\"><span>" + esc(event.source) + " · " + esc(event.sessionTitle || event.sessionId) + "</span><span>" + fmt(event.tokens || 0) + " tok</span></div><p>" + esc(event.preview || "No preview captured.") + "</p></article>";
1515
+ }).join("") + "</div>";
1516
+ }
1517
+
1518
+ function sessionCards(sessions, kind, value) {
1519
+ sessions = (sessions || []).slice(0, 8);
1520
+ if (!sessions.length) return "<p class=\"empty\">No matching sessions for this item.</p>";
1521
+ return "<div class=\"detail-list\">" + sessions.map(function (session) {
1522
+ const amount = kind === "path" ? (session.paths || {})[value] || 0 : tok((session.categories || {})[value] || 0);
1523
+ const unit = kind === "path" ? "mentions" : "tok";
1524
+ return "<article class=\"sample\"><div class=\"sample-top\"><span>" + esc(session.source) + " · " + esc(session.updatedAt || "unknown time") + "</span><span>" + esc(fmt(amount) + " " + unit) + "</span></div><p><strong>" + esc(session.title || session.sessionId) + "</strong></p><p class=\"meta\">" + esc(session.cwd || session.path) + "</p></article>";
1525
+ }).join("") + "</div>";
1526
+ }
1527
+
1528
+ function openDetailTab(tab, selector, value) {
1529
+ const tabButton = document.querySelector("[data-tab=\"" + tab + "\"]");
1530
+ if (tabButton) tabButton.click();
1531
+ if (!selector) return;
1532
+ window.setTimeout(function () {
1533
+ const rows = Array.prototype.slice.call(document.querySelectorAll(selector));
1534
+ const row = rows.filter(function (item) {
1535
+ return item.getAttribute(selector.slice(1, -1)) === value;
1536
+ })[0];
1537
+ if (row) row.click();
1538
+ }, 0);
1539
+ }
1540
+
1541
+ function jumpButton(tab, label, selector, value) {
1542
+ return "<button class=\"row-button\" data-overview-jump=\"" + esc(tab) + "\" data-overview-selector=\"" + esc(selector) + "\" data-overview-value=\"" + esc(value || "") + "\"><span><span class=\"row-title\">" + esc(label) + "</span><span class=\"meta\">Open the full detail tab</span></span><span class=\"row-meta\">open</span><span class=\"row-meta\"></span></button>";
1543
+ }
1544
+
1545
+ function setDetail(kind, value) {
1546
+ const el = document.getElementById("overview-drilldown");
1547
+ if (!el) return;
1548
+ if (kind === "tool") {
1549
+ const events = (report.toolEvents || []).filter(function (event) {
1550
+ return event.tool === value;
1551
+ });
1552
+ el.innerHTML = "<div class=\"section-head\"><div><h2>" + esc(value) + "</h2><p class=\"meta\">Sampled calls from the selected sessions.</p></div></div>" + sampleCards(events) + "<div class=\"mini-label\">More</div>" + jumpButton("tools", "Open Tool Calls", "[data-tool]", value);
1553
+ } else if (kind === "metadata") {
1554
+ const events = (report.metadataEvents || []).filter(function (event) {
1555
+ return event.type === value;
1556
+ });
1557
+ el.innerHTML = "<div class=\"section-head\"><div><h2>" + esc(value) + "</h2><p class=\"meta\">Sampled metadata records from transcripts.</p></div></div>" + sampleCards(events) + "<div class=\"mini-label\">More</div>" + jumpButton("metadata", "Open Metadata", "[data-meta]", value);
1558
+ } else if (kind === "path") {
1559
+ el.innerHTML = "<div class=\"section-head\"><div><h2>" + esc(value) + "</h2><p class=\"meta\">Sessions where this path repeats.</p></div></div>" + sessionCards(sessionMatches("path", value), "path", value) + "<div class=\"mini-label\">More</div>" + jumpButton("sessions", "Open Sessions", "", "");
1560
+ } else {
1561
+ const chars = (report.categories || {})[value] || 0;
1562
+ const sessions = sessionMatches("category", value);
1563
+ let extra = "";
1564
+ if (value === "tool_call") {
1565
+ extra = "<div class=\"mini-label\">Top tools</div>" + topTools(6).map(function (entry) {
1566
+ return actionRow("tool", entry[0], entry[0], "Click for sampled calls", "x" + entry[1], fmt(toolTokens(entry[0])) + " tok");
1567
+ }).join("");
1568
+ } else if (value === "metadata") {
1569
+ extra = "<div class=\"mini-label\">Metadata types</div>" + (report.metadata || []).slice(0, 6).map(function (entry) {
1570
+ return actionRow("metadata", entry[0], entry[0], "Click for sampled records", fmt(tok(entry[1])) + " tok", fmt(entry[1]) + " ch");
1571
+ }).join("");
1572
+ }
1573
+ el.innerHTML = "<div class=\"section-head\"><div><h2>" + esc(labels[value] || value) + "</h2><p class=\"meta\">" + esc(fmt(tok(chars)) + " estimated tokens across " + sessions.length + " session(s).") + "</p></div></div>" + (extra || sessionCards(sessions, "category", value));
1574
+ }
1575
+ bindOverview();
1576
+ }
1577
+
1578
+ function bindOverview() {
1579
+ Array.prototype.forEach.call(document.querySelectorAll("[data-overview-kind]"), function (button) {
1580
+ button.onclick = function () {
1581
+ setDetail(button.getAttribute("data-overview-kind"), button.getAttribute("data-overview-value"));
1582
+ };
1583
+ });
1584
+ Array.prototype.forEach.call(document.querySelectorAll("[data-overview-jump]"), function (button) {
1585
+ button.onclick = function () {
1586
+ const selector = button.getAttribute("data-overview-selector");
1587
+ const value = button.getAttribute("data-overview-value");
1588
+ openDetailTab(button.getAttribute("data-overview-jump"), selector, value);
1589
+ };
1590
+ });
1591
+ }
1592
+
1593
+ function renderOverview() {
1594
+ const panel = document.getElementById("panel-overview");
1595
+ if (!panel) return;
1596
+ const total = totalCounter(report.categories);
1597
+ const categoryRows = cats.map(function (cat) {
1598
+ const chars = (report.categories || {})[cat] || 0;
1599
+ if (!chars) return "";
1600
+ return actionRow("category", cat, labels[cat], "Click for sessions and hotspots", fmt(tok(chars)) + " tok", pct(chars, total).toFixed(0) + "%", colors[cat]);
1601
+ }).join("");
1602
+ const toolRows = topTools(8).map(function (entry) {
1603
+ return actionRow("tool", entry[0], entry[0], "Click for sampled calls", "x" + entry[1], fmt(toolTokens(entry[0])) + " tok");
1604
+ }).join("") || "<p class=\"empty\">No tool calls detected.</p>";
1605
+ const pathRows = (report.paths || []).slice(0, 8).map(function (entry) {
1606
+ return actionRow("path", entry[0], entry[0], "Click for matching sessions", "x" + entry[1], "");
1607
+ }).join("") || "<p class=\"empty\">No repeated paths detected.</p>";
1608
+ const metadataRows = (report.metadata || []).slice(0, 8).map(function (entry) {
1609
+ return actionRow("metadata", entry[0], entry[0], "Click for sampled records", fmt(tok(entry[1])) + " tok", fmt(entry[1]) + " ch");
1610
+ }).join("") || "<p class=\"empty\">No metadata-heavy records detected.</p>";
1611
+ panel.innerHTML = "<div class=\"grid\"><section class=\"card\"><div class=\"section-head\"><div><h2>Where The Context Is Going</h2><p class=\"meta\">Click a segment or row to inspect it.</p></div></div>" + bar(report.categories) + "<div>" + categoryRows + "</div></section><section class=\"card\" id=\"overview-drilldown\"><div class=\"section-head\"><div><h2>Warnings And Optimizations</h2><p class=\"meta\">Promptable changes you control.</p></div></div><ol class=\"tips\">" + (report.recommendations || []).map(function (tip) { return "<li>" + esc(tip) + "</li>"; }).join("") + "</ol></section></div><div class=\"grid equal\" style=\"margin-top:14px\"><section class=\"card\"><h2>Tool Hotspots</h2>" + toolRows + "</section><section class=\"card\"><h2>Top Paths</h2>" + pathRows + "</section></div><section class=\"card\" style=\"margin-top:14px\"><h2>Metadata Types</h2>" + metadataRows + "</section>";
1612
+ bindOverview();
1613
+ }
1614
+
1615
+ renderOverview();
1616
+ }.toString() + ")();";
1617
+ }
1618
+
1619
+ function insightsEnhancementScript() {
1620
+ return "(" + function () {
1621
+ const dataEl = document.getElementById("xray-data");
1622
+ if (!dataEl) return;
1623
+ const report = JSON.parse(dataEl.textContent || "{}");
1624
+ const analysis = report.analysis || {};
1625
+ const metrics = analysis.metrics || {};
1626
+ const findings = analysis.findings || [];
1627
+ const cats = ["user", "assistant", "tool_call", "tool_output", "reasoning", "instructions", "attachment", "metadata", "other"];
1628
+ const labels = {
1629
+ user: "User asks",
1630
+ assistant: "Assistant text",
1631
+ tool_call: "Tool calls",
1632
+ tool_output: "Tool output",
1633
+ reasoning: "Reasoning",
1634
+ instructions: "Instructions/context",
1635
+ attachment: "Attachments",
1636
+ metadata: "Metadata",
1637
+ other: "Other",
1638
+ };
1639
+ const colors = {
1640
+ user: "#2563eb",
1641
+ assistant: "#16a34a",
1642
+ tool_call: "#d97706",
1643
+ tool_output: "#dc2626",
1644
+ reasoning: "#7c3aed",
1645
+ instructions: "#0891b2",
1646
+ attachment: "#b45309",
1647
+ metadata: "#64748b",
1648
+ other: "#94a3b8",
1649
+ };
1650
+
1651
+ function esc(value) {
1652
+ return String(value || "").replace(/[&<>"']/g, function (ch) {
1653
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;", "'": "&#39;" }[ch];
1654
+ });
1655
+ }
1656
+
1657
+ function fmt(value) {
1658
+ const number = Number(value) || 0;
1659
+ if (Math.abs(number) >= 1000000) return (number / 1000000).toFixed(1) + "m";
1660
+ if (Math.abs(number) >= 1000) return (number / 1000).toFixed(1) + "k";
1661
+ return String(Math.round(number * 10) / 10);
1662
+ }
1663
+
1664
+ function pctText(value) {
1665
+ return Math.round((Number(value) || 0) * 100) + "%";
1666
+ }
1667
+
1668
+ function totalCounter(counter) {
1669
+ return Object.keys(counter || {}).reduce(function (sum, key) {
1670
+ return sum + (Number(counter[key]) || 0);
1671
+ }, 0);
1672
+ }
1673
+
1674
+ function tok(chars) {
1675
+ chars = Number(chars) || 0;
1676
+ return chars > 0 ? Math.max(1, Math.ceil(chars / 4)) : 0;
1677
+ }
1678
+
1679
+ function addStyle() {
1680
+ if (document.getElementById("xray-insights-style")) return;
1681
+ const style = document.createElement("style");
1682
+ style.id = "xray-insights-style";
1683
+ style.textContent = [
1684
+ ".health-shell{display:grid;grid-template-columns:270px minmax(0,1fr);gap:14px;align-items:stretch;margin-bottom:14px}",
1685
+ ".health-card{display:flex;gap:16px;align-items:center;background:hsl(var(--card));border:1px solid hsl(var(--border));border-radius:8px;padding:16px;box-shadow:var(--shadow)}",
1686
+ ".health-dial{width:112px;height:112px;border-radius:999px;display:grid;place-items:center;background:conic-gradient(#16a34a calc(var(--score)*1%),hsl(var(--muted)) 0);position:relative;flex:0 0 auto}",
1687
+ ".health-dial:after{content:\"\";position:absolute;inset:9px;border-radius:inherit;background:hsl(var(--card))}",
1688
+ ".health-score{position:relative;z-index:1;font-size:30px;font-weight:750;letter-spacing:0}",
1689
+ ".health-copy{min-width:0}.health-copy h2{font-size:18px}.health-copy p{margin-top:6px;color:hsl(var(--muted-foreground));font-size:13px}",
1690
+ ".metric-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}",
1691
+ ".metric-tile{border:1px solid hsl(var(--border));border-radius:8px;padding:12px;background:hsl(var(--card));min-height:84px}",
1692
+ ".metric-tile strong{display:block;font-size:22px;line-height:1.1;margin-top:7px}",
1693
+ ".metric-tile span{font-size:11px;text-transform:uppercase;letter-spacing:.08em;font-weight:650;color:hsl(var(--muted-foreground))}",
1694
+ ".finding-button{width:100%;border:1px solid hsl(var(--border));border-radius:8px;background:hsl(var(--card));padding:12px;text-align:left;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;cursor:pointer;margin-bottom:8px}",
1695
+ ".finding-button:hover,.finding-button.active{background:hsl(var(--accent)/.45);border-color:hsl(var(--ring)/.45)}",
1696
+ ".finding-title{font-size:14px;font-weight:650}.finding-summary{font-size:12px;color:hsl(var(--muted-foreground));margin-top:4px;overflow-wrap:anywhere}",
1697
+ ".severity{display:inline-flex;align-items:center;border-radius:999px;border:1px solid hsl(var(--border));padding:3px 7px;font-size:11px;text-transform:uppercase;letter-spacing:.06em;font-weight:700;background:hsl(var(--background))}",
1698
+ ".severity.critical,.severity.high{color:#dc2626;border-color:rgba(220,38,38,.28);background:rgba(220,38,38,.08)}",
1699
+ ".severity.medium{color:#b45309;border-color:rgba(180,83,9,.28);background:rgba(180,83,9,.08)}",
1700
+ ".severity.low,.severity.info{color:#0369a1;border-color:rgba(3,105,161,.26);background:rgba(3,105,161,.08)}",
1701
+ ".insight-detail{position:sticky;top:82px}.evidence-list{display:grid;gap:7px;margin-top:10px}.evidence-item{border-left:3px solid hsl(var(--border));padding:7px 0 7px 10px;font-size:12px;overflow-wrap:anywhere}",
1702
+ ".context-split{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(300px,.9fr);gap:14px;align-items:start}.category-button{width:100%;display:grid;grid-template-columns:minmax(0,1fr) 74px 54px;gap:10px;border:0;background:transparent;border-radius:6px;padding:8px;text-align:left;cursor:pointer}.category-button:hover{background:hsl(var(--accent)/.4)}",
1703
+ ".inline-bar{height:8px;border-radius:999px;background:hsl(var(--muted));overflow:hidden;margin-top:8px}.inline-bar span{display:block;height:100%}",
1704
+ ".trend-bars{display:grid;gap:8px}.trend-row{display:grid;grid-template-columns:86px minmax(0,1fr) 80px;gap:10px;align-items:center;font-size:12px}.trend-track{height:9px;border-radius:999px;background:hsl(var(--muted));overflow:hidden}.trend-track span{display:block;height:100%;background:#2563eb}",
1705
+ ".timeline-list{display:grid;gap:10px}.timeline-session{border:1px solid hsl(var(--border));border-radius:8px;background:hsl(var(--card));padding:12px}.timeline-top{display:flex;justify-content:space-between;gap:12px;margin-bottom:8px}.turn-strip{display:flex;gap:2px;align-items:end;height:42px}.turn-strip button{flex:1;min-width:3px;border:0;border-radius:3px 3px 0 0;background:#2563eb;cursor:pointer}.turn-strip button:hover{filter:brightness(1.15)}",
1706
+ ".source-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.source-row{border:1px solid hsl(var(--border));border-radius:8px;background:hsl(var(--card));padding:11px;min-width:0}.source-row strong{display:block;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-row p{font-size:12px;color:hsl(var(--muted-foreground));overflow-wrap:anywhere}",
1707
+ ".raw-note{border:1px dashed hsl(var(--border));border-radius:8px;padding:12px;background:hsl(var(--muted)/.2);font-size:12px;color:hsl(var(--muted-foreground))}",
1708
+ "@media(max-width:920px){.health-shell,.context-split{grid-template-columns:1fr}.metric-grid,.source-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.insight-detail{position:static}}",
1709
+ "@media(max-width:620px){.metric-grid,.source-grid{grid-template-columns:1fr}.trend-row{grid-template-columns:74px minmax(0,1fr) 54px}.health-card{align-items:flex-start}.health-dial{width:88px;height:88px}.health-score{font-size:24px}}",
1710
+ ].join("");
1711
+ document.head.appendChild(style);
1712
+ }
1713
+
1714
+ function ensureTab(name, label) {
1715
+ const nav = document.querySelector(".tabs");
1716
+ if (!nav || document.querySelector("[data-tab=\"" + name + "\"]")) return;
1717
+ const button = document.createElement("button");
1718
+ button.className = "tab";
1719
+ button.setAttribute("data-tab", name);
1720
+ button.type = "button";
1721
+ button.textContent = label;
1722
+ nav.appendChild(button);
1723
+ }
1724
+
1725
+ function ensurePanel(name) {
1726
+ if (document.getElementById("panel-" + name)) return;
1727
+ const footer = document.querySelector(".footer");
1728
+ const panel = document.createElement("section");
1729
+ panel.className = "panel";
1730
+ panel.id = "panel-" + name;
1731
+ if (footer && footer.parentNode) footer.parentNode.insertBefore(panel, footer);
1732
+ }
1733
+
1734
+ function activate(name) {
1735
+ Array.prototype.forEach.call(document.querySelectorAll(".tab"), function (tab) {
1736
+ tab.classList.toggle("active", tab.getAttribute("data-tab") === name);
1737
+ });
1738
+ Array.prototype.forEach.call(document.querySelectorAll(".panel"), function (panel) {
1739
+ panel.classList.toggle("active", panel.id === "panel-" + name);
1740
+ });
1741
+ if (location.hash !== "#" + name) history.replaceState(null, "", "#" + name);
1742
+ }
1743
+
1744
+ function wireTabs() {
1745
+ Array.prototype.forEach.call(document.querySelectorAll(".tab"), function (tab) {
1746
+ tab.onclick = function () {
1747
+ activate(tab.getAttribute("data-tab"));
1748
+ };
1749
+ });
1750
+ }
1751
+
1752
+ function metric(label, value, detail) {
1753
+ return "<div class=\"metric-tile\"><span>" + esc(label) + "</span><strong>" + esc(value) + "</strong><p class=\"meta\">" + esc(detail || "") + "</p></div>";
1754
+ }
1755
+
1756
+ function findingButton(finding, index) {
1757
+ return "<button class=\"finding-button\" data-finding=\"" + index + "\"><span><span class=\"finding-title\">" + esc(finding.title) + "</span><span class=\"finding-summary\">" + esc(finding.summary) + "</span></span><span class=\"severity " + esc(finding.severity) + "\">" + esc(finding.severity) + "</span></button>";
1758
+ }
1759
+
1760
+ function renderFindingDetail(index, targetId) {
1761
+ const finding = findings[index] || findings[0];
1762
+ const el = document.getElementById(targetId || "finding-detail");
1763
+ if (!el) return;
1764
+ if (!finding) {
1765
+ el.innerHTML = "<h2>No Findings</h2><p class=\"empty\">No deterministic warning crossed the reporting threshold.</p>";
1766
+ return;
1767
+ }
1768
+ const evidence = (finding.evidence || []).map(function (item) {
1769
+ return "<div class=\"evidence-item\">" + esc(item) + "</div>";
1770
+ }).join("") || "<p class=\"empty\">No evidence sample captured.</p>";
1771
+ el.innerHTML = "<div class=\"section-head\"><div><span class=\"severity " + esc(finding.severity) + "\">" + esc(finding.severity) + "</span><h2 style=\"margin-top:8px\">" + esc(finding.title) + "</h2><p class=\"meta\">" + esc(finding.rule) + " · confidence " + esc(finding.confidence) + " · " + esc(finding.action) + "</p></div><strong>" + esc(finding.score) + "</strong></div><p>" + esc(finding.summary) + "</p><div class=\"mini-label\">Evidence</div><div class=\"evidence-list\">" + evidence + "</div><div class=\"mini-label\">Recommended Move</div><p class=\"raw-note\">" + esc(finding.recommendation || "No recommendation captured.") + "</p>";
1772
+ }
1773
+
1774
+ function categoryRows() {
1775
+ const total = totalCounter(report.categories);
1776
+ return cats.map(function (cat) {
1777
+ const chars = (report.categories || {})[cat] || 0;
1778
+ if (!chars) return "";
1779
+ const percentage = total ? chars / total : 0;
1780
+ return "<button class=\"category-button\" data-category=\"" + esc(cat) + "\"><span><span class=\"badge\"><span class=\"dot\" style=\"background:" + colors[cat] + "\"></span>" + esc(labels[cat]) + "</span><span class=\"inline-bar\"><span style=\"width:" + Math.max(2, percentage * 100).toFixed(1) + "%;background:" + colors[cat] + "\"></span></span></span><span class=\"row-meta\">" + fmt(tok(chars)) + " tok</span><span class=\"row-meta\">" + Math.round(percentage * 100) + "%</span></button>";
1781
+ }).join("");
1782
+ }
1783
+
1784
+ function renderStats() {
1785
+ const stats = document.getElementById("stats");
1786
+ if (!stats) return;
1787
+ stats.innerHTML = metric("Health", (analysis.score || 0) + "/100", analysis.scoreLabel || "unknown") +
1788
+ metric("Findings", findings.length, findings.length ? findings[0].title : "no threshold crossed") +
1789
+ metric("Cache hit", pctText(metrics.cacheHitRatio || 0), "create " + pctText(metrics.cacheCreationPct || 0)) +
1790
+ metric("Tool success", pctText(metrics.successRate == null ? 1 : metrics.successRate), fmt(metrics.normalizedSteps || 0) + " normalized steps");
1791
+ }
1792
+
1793
+ function renderOverview() {
1794
+ const panel = document.getElementById("panel-overview");
1795
+ if (!panel) return;
1796
+ const topFindings = findings.slice(0, 4).map(findingButton).join("") || "<p class=\"empty\">No major issues found. Recent sessions look balanced.</p>";
1797
+ panel.innerHTML = "<div class=\"health-shell\"><section class=\"health-card\"><div class=\"health-dial\" style=\"--score:" + esc(analysis.score || 0) + "\"><span class=\"health-score\">" + esc(analysis.score || 0) + "</span></div><div class=\"health-copy\"><div class=\"eyebrow\">Context Health</div><h2>" + esc(analysis.scoreLabel || "unknown") + "</h2><p>Score combines cache behavior, context pressure, duplicate reads, retry loops, tool failures, and exploration-to-edit ratio.</p></div></section><section class=\"card\" id=\"overview-finding-detail\"></section></div><div class=\"context-split\"><section class=\"card\"><div class=\"section-head\"><div><h2>Top Findings</h2><p class=\"meta\">Click a finding to inspect evidence and the suggested move.</p></div></div>" + topFindings + "</section><section class=\"card\"><div class=\"section-head\"><div><h2>Where Context Is Going</h2><p class=\"meta\">Bucket rows are clickable and stay local to this report.</p></div></div>" + categoryRows() + "</section></div><div class=\"metric-grid\" style=\"margin-top:14px\">" + metric("Exploration ratio", (metrics.explorationRatio || 0) + "x", "read/search to edit/write") + metric("Duplicate reads", metrics.duplicateReadCount || 0, "suppressed after edits") + metric("Retry loops", metrics.retryLoopCount || 0, "3+ identical commands") + metric("MCP calls", metrics.mcpCalls || 0, (report.mcpServers || []).length + " configured") + "</div>";
1798
+ Array.prototype.forEach.call(panel.querySelectorAll("[data-finding]"), function (button) {
1799
+ button.onclick = function () {
1800
+ Array.prototype.forEach.call(panel.querySelectorAll("[data-finding]"), function (item) { item.classList.remove("active"); });
1801
+ button.classList.add("active");
1802
+ renderFindingDetail(Number(button.getAttribute("data-finding")), "overview-finding-detail");
1803
+ };
1804
+ });
1805
+ Array.prototype.forEach.call(panel.querySelectorAll("[data-category]"), function (button) {
1806
+ button.onclick = function () {
1807
+ const category = button.getAttribute("data-category");
1808
+ const sessions = (report.sessions || []).filter(function (session) { return (session.categories || {})[category]; }).slice(0, 4);
1809
+ const detail = document.getElementById("overview-finding-detail");
1810
+ if (!detail) return;
1811
+ detail.innerHTML = "<div class=\"section-head\"><div><h2>" + esc(labels[category] || category) + "</h2><p class=\"meta\">Sessions contributing to this bucket.</p></div></div><div class=\"evidence-list\">" + (sessions.map(function (session) { return "<div class=\"evidence-item\"><strong>" + esc(session.title || session.sessionId) + "</strong><br>" + esc(session.source + " · " + fmt(tok((session.categories || {})[category] || 0)) + " tok · " + (session.cwd || session.path)) + "</div>"; }).join("") || "<p class=\"empty\">No sessions matched.</p>") + "</div>";
1812
+ };
1813
+ });
1814
+ renderFindingDetail(0, "overview-finding-detail");
1815
+ const first = panel.querySelector("[data-finding]");
1816
+ if (first) first.classList.add("active");
1817
+ }
1818
+
1819
+ function renderFindings() {
1820
+ const panel = document.getElementById("panel-findings");
1821
+ if (!panel) return;
1822
+ panel.innerHTML = "<div class=\"grid\"><section class=\"card\"><div class=\"section-head\"><div><h2>Findings</h2><p class=\"meta\">Deterministic checks adapted from AgentSight, Argus, Cogpit, and usage dashboards.</p></div></div>" + (findings.map(findingButton).join("") || "<p class=\"empty\">No findings crossed threshold.</p>") + "</section><aside class=\"card detail insight-detail\" id=\"finding-detail\"></aside></div>";
1823
+ Array.prototype.forEach.call(panel.querySelectorAll("[data-finding]"), function (button) {
1824
+ button.onclick = function () {
1825
+ Array.prototype.forEach.call(panel.querySelectorAll("[data-finding]"), function (item) { item.classList.remove("active"); });
1826
+ button.classList.add("active");
1827
+ renderFindingDetail(Number(button.getAttribute("data-finding")), "finding-detail");
1828
+ };
1829
+ });
1830
+ renderFindingDetail(0, "finding-detail");
1831
+ const first = panel.querySelector("[data-finding]");
1832
+ if (first) first.classList.add("active");
1833
+ }
1834
+
1835
+ function renderTrends() {
1836
+ const panel = document.getElementById("panel-trends");
1837
+ if (!panel) return;
1838
+ const days = (analysis.trends && analysis.trends.byDay || []).slice(-14);
1839
+ const maxTokens = days.reduce(function (max, day) { return Math.max(max, day.tokens || 0); }, 1);
1840
+ const dayRows = days.map(function (day) {
1841
+ return "<div class=\"trend-row\"><span>" + esc(day.day) + "</span><span class=\"trend-track\"><span style=\"width:" + Math.max(2, ((day.tokens || 0) / maxTokens) * 100).toFixed(1) + "%\"></span></span><span class=\"row-meta\">" + fmt(day.tokens || 0) + " tok</span></div>";
1842
+ }).join("") || "<p class=\"empty\">No trend window found.</p>";
1843
+ const toolRows = (analysis.trends && analysis.trends.topTools || []).slice(0, 10).map(function (entry) {
1844
+ return "<tr><td>" + esc(entry[0]) + "</td><td>" + esc(entry[1]) + "</td></tr>";
1845
+ }).join("") || "<tr><td>No tools detected</td><td></td></tr>";
1846
+ const pathRows = (analysis.trends && analysis.trends.topPaths || []).slice(0, 10).map(function (entry) {
1847
+ return "<tr><td>" + esc(entry[0]) + "</td><td>" + esc(entry[1]) + "</td></tr>";
1848
+ }).join("") || "<tr><td>No paths detected</td><td></td></tr>";
1849
+ panel.innerHTML = "<div class=\"grid\"><section class=\"card\"><div class=\"section-head\"><div><h2>Daily Trend</h2><p class=\"meta\">Sessions and observed/estimated token load by day.</p></div></div><div class=\"trend-bars\">" + dayRows + "</div></section><section class=\"card\"><h2>Hotspots</h2><div class=\"grid equal\" style=\"margin-top:10px\"><div><div class=\"mini-label\">Tools</div><table class=\"table\"><tbody>" + toolRows + "</tbody></table></div><div><div class=\"mini-label\">Paths</div><table class=\"table\"><tbody>" + pathRows + "</tbody></table></div></div></section></div>";
1850
+ }
1851
+
1852
+ function turnHeight(turn, max) {
1853
+ const value = Number(turn.totalTokens || turn.inputTokens || 0);
1854
+ return Math.max(4, Math.round((value / Math.max(1, max)) * 40));
1855
+ }
1856
+
1857
+ function renderTimeline() {
1858
+ const panel = document.getElementById("panel-timeline");
1859
+ if (!panel) return;
1860
+ const sessions = (report.sessions || []).slice(0, 30);
1861
+ panel.innerHTML = "<div class=\"timeline-list\">" + (sessions.map(function (session) {
1862
+ const series = session.usage && session.usage.series || [];
1863
+ const maxTurn = series.reduce(function (max, turn) { return Math.max(max, turn.totalTokens || turn.inputTokens || 0); }, 1);
1864
+ const strip = series.length ? series.map(function (turn, index) {
1865
+ const cache = (turn.cacheReadInputTokens || 0) + (turn.cachedInputTokens || 0);
1866
+ const color = turn.cacheCreationInputTokens > turn.inputTokens ? "#b45309" : cache > turn.inputTokens ? "#16a34a" : "#2563eb";
1867
+ return "<button title=\"" + esc((turn.label || "turn " + (index + 1)) + " · " + fmt(turn.totalTokens || 0) + " tokens") + "\" style=\"height:" + turnHeight(turn, maxTurn) + "px;background:" + color + "\"></button>";
1868
+ }).join("") : "<p class=\"empty\">No observed per-turn token series in this session.</p>";
1869
+ const cache = cacheStabilityClient(session);
1870
+ const growth = contextGrowthClient(session);
1871
+ return "<article class=\"timeline-session\"><div class=\"timeline-top\"><div><div class=\"eyebrow\">" + esc(session.source + " · " + (session.updatedAt || "unknown time")) + "</div><h2>" + esc(session.title || session.sessionId) + "</h2><p class=\"meta\">" + esc(session.cwd || session.path) + "</p></div><div style=\"text-align:right\"><strong>" + fmt(session.tokens || 0) + "</strong><p class=\"meta\">" + esc(session.tokenMethod || "") + "</p></div></div><div class=\"turn-strip\">" + strip + "</div><div class=\"badge-list\" style=\"margin-top:10px\"><span class=\"badge\">cache " + esc(cache.classification) + "</span><span class=\"badge\">growth " + esc(growth.growthFactor || 0) + "x</span><span class=\"badge\">steps " + esc(session.totalSteps || 0) + "</span></div></article>";
1872
+ }).join("") || "<section class=\"card\"><p class=\"empty\">No sessions found.</p></section>") + "</div>";
1873
+ }
1874
+
1875
+ function cacheStabilityClient(session) {
1876
+ const match = (analysis.evidence && analysis.evidence.cacheStability || []).filter(function (item) { return item.sessionId === session.sessionId; })[0];
1877
+ return match || { classification: "unknown" };
1878
+ }
1879
+
1880
+ function contextGrowthClient(session) {
1881
+ const match = (analysis.evidence && analysis.evidence.contextGrowth || []).filter(function (item) { return item.sessionId === session.sessionId; })[0];
1882
+ return match || { growthFactor: 0 };
1883
+ }
1884
+
1885
+ function renderSources() {
1886
+ const panel = document.getElementById("panel-sources");
1887
+ if (!panel) return;
1888
+ const rows = (analysis.sourceModel && analysis.sourceModel.sourceProjectSession || []).slice(0, 60).map(function (item) {
1889
+ return "<article class=\"source-row\"><strong>" + esc(item.title || item.sessionId) + "</strong><p>" + esc(item.source + " · " + fmt(item.tokens || 0) + " tok · " + (item.project || "")) + "</p><p>" + esc(item.updatedAt || "") + "</p></article>";
1890
+ }).join("") || "<p class=\"empty\">No source records found.</p>";
1891
+ panel.innerHTML = "<section class=\"card\"><div class=\"section-head\"><div><h2>Source / Project / Session</h2><p class=\"meta\">Metadata-first browser model for local Codex and Claude Code sessions.</p></div></div><p class=\"raw-note\">" + esc(analysis.sourceModel && analysis.sourceModel.privacy || "Transcript content stays local.") + "</p><div class=\"source-grid\" style=\"margin-top:12px\">" + rows + "</div></section>";
1892
+ }
1893
+
1894
+ addStyle();
1895
+ ensureTab("findings", "Findings");
1896
+ ensureTab("timeline", "Timeline");
1897
+ ensureTab("trends", "Trends");
1898
+ ensureTab("sources", "Sources");
1899
+ ensurePanel("findings");
1900
+ ensurePanel("timeline");
1901
+ ensurePanel("trends");
1902
+ ensurePanel("sources");
1903
+ wireTabs();
1904
+ renderStats();
1905
+ renderOverview();
1906
+ renderFindings();
1907
+ renderTimeline();
1908
+ renderTrends();
1909
+ renderSources();
1910
+ const initial = (location.hash || "#overview").slice(1);
1911
+ if (document.getElementById("panel-" + initial)) activate(initial);
1912
+ }.toString() + ")();";
1913
+ }
1914
+
1915
+ function renderHtml(sessions, args) {
1916
+ const report = buildReport(sessions, args);
1917
+ return "<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Context X-Ray</title><style>" +
1918
+ "@import url(\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\");" +
1919
+ ":root{--background:0 0% 100%;--foreground:220 10% 10%;--card:0 0% 100%;--muted:220 10% 95%;--muted-foreground:220 5% 45%;--accent:220 10% 92%;--border:220 10% 90%;--ring:220 10% 55%;--destructive:0 84% 60%;--radius:.5rem;--shadow:0 1px 2px rgba(16,24,40,.06);}" +
1920
+ "@media(prefers-color-scheme:dark){:root{--background:220 10% 7%;--foreground:220 8% 92%;--card:220 9% 9%;--muted:220 8% 15%;--muted-foreground:220 6% 64%;--accent:220 8% 17%;--border:220 8% 18%;--ring:220 8% 54%;--destructive:0 72% 51%;--shadow:none;}}" +
1921
+ "*{box-sizing:border-box}html{background:hsl(var(--background))}body{margin:0;background:hsl(var(--background));color:hsl(var(--foreground));font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif;font-feature-settings:\"cv02\",\"cv03\",\"cv04\",\"cv11\";line-height:1.45}button{font:inherit;color:inherit}main{max-width:1180px;margin:0 auto;padding:20px 18px 44px}.topbar{border-bottom:1px solid hsl(var(--border));background:hsl(var(--background));position:sticky;top:0;z-index:10}.topbar-inner{max-width:1180px;margin:0 auto;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;gap:16px}.title-row{display:flex;align-items:center;gap:9px}.logo-dot{width:10px;height:10px;border-radius:999px;background:#38bdf8;box-shadow:0 0 0 4px rgba(56,189,248,.12)}h1{font-size:20px;line-height:1.1;margin:0;font-weight:650;letter-spacing:0}h2{font-size:15px;line-height:1.2;margin:0;font-weight:650}h3{font-size:14px;line-height:1.3;margin:0;font-weight:600}p{margin:0}.muted,.eyebrow,.meta,.empty{color:hsl(var(--muted-foreground))}.eyebrow{text-transform:uppercase;letter-spacing:.08em;font-size:11px;font-weight:650}.meta{font-size:12px}.tabs{display:inline-flex;align-items:center;gap:2px;border:1px solid hsl(var(--border));background:hsl(var(--muted)/.35);border-radius:8px;padding:2px}.tab{border:0;background:transparent;border-radius:6px;padding:6px 10px;font-size:12px;line-height:1.1;cursor:pointer}.tab:hover{background:hsl(var(--accent)/.5)}.tab.active{background:hsl(var(--background));box-shadow:var(--shadow)}.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:18px 0}.card{background:hsl(var(--card));border:1px solid hsl(var(--border));border-radius:8px;padding:14px;box-shadow:var(--shadow)}.stat-value{display:block;font-size:28px;line-height:1.1;font-weight:700;margin-top:6px}.panel{display:none}.panel.active{display:block}.grid{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(280px,.8fr);gap:14px;align-items:start}.grid.equal{grid-template-columns:repeat(2,minmax(0,1fr))}.section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-bottom:12px}.bar{display:flex;overflow:hidden;height:10px;border-radius:999px;background:hsl(var(--muted));margin:10px 0 12px}.bar span{display:block;min-width:2px}.table{width:100%;border-collapse:collapse;font-size:12px}.table th{text-align:left;color:hsl(var(--muted-foreground));font-weight:600;border-bottom:1px solid hsl(var(--border));padding:7px 8px}.table td{border-bottom:1px solid hsl(var(--border));padding:8px;vertical-align:top}.table th:last-child,.table td:last-child{text-align:right}.row-button{width:100%;display:grid;grid-template-columns:minmax(0,1fr) 74px 70px;gap:10px;align-items:center;text-align:left;border:1px solid transparent;background:transparent;border-radius:6px;padding:8px;cursor:pointer}.row-button:hover,.row-button.active{border-color:hsl(var(--border));background:hsl(var(--accent)/.35)}.row-title{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.row-meta{font-size:12px;color:hsl(var(--muted-foreground));text-align:right}.badge-list{display:flex;flex-wrap:wrap;gap:6px}.badge{display:inline-flex;align-items:center;gap:5px;border:1px solid hsl(var(--border));border-radius:999px;padding:3px 7px;font-size:11px;background:hsl(var(--background));max-width:100%}.badge .dot{width:7px;height:7px;border-radius:999px;flex:0 0 auto}.tips{margin:0;padding-left:20px}.tips li{margin:0 0 8px}.detail{min-height:228px}.detail-list{display:grid;gap:8px;margin-top:10px}.sample{border:1px solid hsl(var(--border));border-radius:8px;padding:10px;background:hsl(var(--muted)/.22)}.sample-top{display:flex;justify-content:space-between;gap:10px;margin-bottom:5px;font-size:11px;color:hsl(var(--muted-foreground))}.sample p{font-size:12px;color:hsl(var(--foreground));overflow-wrap:anywhere}.session-card{border:1px solid hsl(var(--border));border-radius:8px;background:hsl(var(--card));margin-bottom:10px;overflow:hidden}.session-card summary{list-style:none;display:flex;justify-content:space-between;gap:14px;cursor:pointer;padding:13px 14px}.session-card summary::-webkit-details-marker{display:none}.session-body{border-top:1px solid hsl(var(--border));padding:12px 14px}.session-title{font-size:13px;font-weight:650;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.session-path{font-size:12px;color:hsl(var(--muted-foreground));margin-top:3px;overflow-wrap:anywhere}.token-big{font-size:20px;font-weight:700;white-space:nowrap}.mini-label{margin:10px 0 6px;font-size:11px;text-transform:uppercase;letter-spacing:.08em;font-weight:650;color:hsl(var(--muted-foreground))}.footer{margin-top:20px;color:hsl(var(--muted-foreground));font-size:12px}.hidden{display:none!important}@media(max-width:840px){.topbar-inner{align-items:flex-start;flex-direction:column}.stats,.grid,.grid.equal{grid-template-columns:1fr}.tabs{width:100%;overflow:auto}.tab{white-space:nowrap}.row-button{grid-template-columns:minmax(0,1fr) 64px 58px}.session-card summary{align-items:flex-start;flex-direction:column}}" +
1922
+ "</style></head><body><header class=\"topbar\"><div class=\"topbar-inner\"><div><div class=\"title-row\"><span class=\"logo-dot\"></span><h1>Context X-Ray</h1></div><p class=\"meta\">Generated " + escapeHtml(report.generatedLabel) + " · mode=" + escapeHtml(report.mode) + " · source=" + escapeHtml(report.source) + " · since=" + escapeHtml(report.since) + "</p></div><nav class=\"tabs\" aria-label=\"Report views\"><button class=\"tab active\" data-tab=\"overview\">Overview</button><button class=\"tab\" data-tab=\"tools\">Tool Calls</button><button class=\"tab\" data-tab=\"mcp\">MCP</button><button class=\"tab\" data-tab=\"metadata\">Metadata</button><button class=\"tab\" data-tab=\"sessions\">Sessions</button></nav></div></header><main><section class=\"stats\" id=\"stats\"></section><section class=\"panel active\" id=\"panel-overview\"></section><section class=\"panel\" id=\"panel-tools\"></section><section class=\"panel\" id=\"panel-mcp\"></section><section class=\"panel\" id=\"panel-metadata\"></section><section class=\"panel\" id=\"panel-sessions\"></section><p class=\"footer\">Reads local transcript and MCP config files only. No transcript content is uploaded.</p></main><script type=\"application/json\" id=\"xray-data\">" + jsonScript(report) + "</script><script>" +
1923
+ "(function(){var report=JSON.parse(document.getElementById('xray-data').textContent);var cats=['user','assistant','tool_call','tool_output','reasoning','instructions','attachment','metadata','other'];var labels={user:'User asks',assistant:'Assistant text',tool_call:'Tool calls',tool_output:'Tool output',reasoning:'Reasoning',instructions:'Instructions/context',attachment:'Attachments',metadata:'Metadata',other:'Other'};var colors={user:'#8ba8ff',assistant:'#55b982',tool_call:'#f0a85b',tool_output:'#e06b73',reasoning:'#a77be8',instructions:'#6ac3d5',attachment:'#d6a85a',metadata:'#9aa3ad',other:'#c3c8ce'};function esc(v){return String(v||'').replace(/[&<>\"']/g,function(ch){return {'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[ch];});}function fmt(n){n=Number(n)||0;if(n>=1000000)return(n/1000000).toFixed(1)+'m';if(n>=1000)return(n/1000).toFixed(1)+'k';return String(n);}function tok(chars){chars=Number(chars)||0;return chars>0?Math.max(1,Math.ceil(chars/4)):0;}function pc(part,total){return total>0?Math.max(0,Math.min(100,(part/total)*100)):0;}function totalCounter(counter){return Object.keys(counter||{}).reduce(function(sum,key){return sum+(Number(counter[key])||0);},0);}function bar(counter){var total=totalCounter(counter);if(!total)return'<div class=\"bar\"></div>';return'<div class=\"bar\">'+cats.map(function(cat){var value=counter[cat]||0;if(!value)return'';return'<span title=\"'+esc(labels[cat])+'\" style=\"width:'+Math.max(1,pc(value,total)).toFixed(2)+'%;background:'+colors[cat]+'\"></span>';}).join('')+'</div>';}function metric(label,value){return'<article class=\"card\"><div class=\"eyebrow\">'+esc(label)+'</div><span class=\"stat-value\">'+esc(value)+'</span></article>';}function tableRows(entries,kind){if(!entries||!entries.length)return'<tr><td>None detected</td><td></td></tr>';return entries.map(function(entry){var value=entry[1];var shown=kind==='chars'?fmt(tok(value)):(kind==='tokens'?fmt(value):String(value));return'<tr><td>'+esc(entry[0])+'</td><td>'+esc(shown)+'</td></tr>';}).join('');}function toolToken(name){var found=(report.toolTokens||[]).filter(function(entry){return entry[0]===name;})[0];return found?found[1]:0;}function renderStats(){var counts=report.sourceCounts||{};document.getElementById('stats').innerHTML=metric('Sessions',report.sessionCount)+metric('Observed/estimated tokens',fmt(report.totalTokens))+metric('Codex',counts.codex||0)+metric('Claude',counts.claude||0);}function renderOverview(){var categoryTotal=totalCounter(report.categories);var categoryRows=cats.map(function(cat){var chars=(report.categories||{})[cat]||0;if(!chars)return'';return'<tr><td><span class=\"badge\"><span class=\"dot\" style=\"background:'+colors[cat]+'\"></span>'+esc(labels[cat])+'</span></td><td>'+fmt(tok(chars))+'</td><td>'+pc(chars,categoryTotal).toFixed(0)+'%</td></tr>';}).join('');document.getElementById('panel-overview').innerHTML='<div class=\"grid\"><section class=\"card\"><div class=\"section-head\"><div><h2>Where The Context Is Going</h2><p class=\"meta\">Approximate contribution by transcript bucket.</p></div></div>'+bar(report.categories)+'<table class=\"table\"><tbody>'+categoryRows+'</tbody></table></section><section class=\"card\"><div class=\"section-head\"><div><h2>Warnings And Optimizations</h2><p class=\"meta\">Promptable changes you control.</p></div></div><ol class=\"tips\">'+(report.recommendations||[]).map(function(tip){return'<li>'+esc(tip)+'</li>';}).join('')+'</ol></section></div><div class=\"grid equal\" style=\"margin-top:14px\"><section class=\"card\"><h2>Top Paths</h2><table class=\"table\"><tbody>'+tableRows(report.paths,'count')+'</tbody></table></section><section class=\"card\"><h2>Metadata Types</h2><table class=\"table\"><tbody>'+tableRows(report.metadata,'chars')+'</tbody></table></section></div>';}function renderTools(){var rows=(report.tools||[]).map(function(entry,index){var name=entry[0];var count=entry[1];return'<button class=\"row-button'+(index===0?' active':'')+'\" data-tool=\"'+esc(name)+'\"><span><span class=\"row-title\">'+esc(name)+'</span><span class=\"meta\">Click to inspect sampled calls</span></span><span class=\"row-meta\">x'+count+'</span><span class=\"row-meta\">'+fmt(toolToken(name))+' tok</span></button>';}).join('')||'<div class=\"empty\">No tool calls detected in these sessions.</div>';document.getElementById('panel-tools').innerHTML='<div class=\"grid\"><section class=\"card\"><div class=\"section-head\"><div><h2>Tool Calls</h2><p class=\"meta\">Calls are clickable; samples are capped so the report stays light.</p></div></div><div>'+rows+'</div></section><aside class=\"card detail\" id=\"tool-detail\"></aside></div>';Array.prototype.forEach.call(document.querySelectorAll('[data-tool]'),function(btn){btn.addEventListener('click',function(){Array.prototype.forEach.call(document.querySelectorAll('[data-tool]'),function(item){item.classList.remove('active');});btn.classList.add('active');renderToolDetail(btn.getAttribute('data-tool'));});});if((report.tools||[]).length)renderToolDetail(report.tools[0][0]);else document.getElementById('tool-detail').innerHTML='<h2>Tool Detail</h2><p class=\"empty\">Nothing to inspect yet.</p>';}function renderToolDetail(name){var events=(report.toolEvents||[]).filter(function(event){return event.tool===name;}).slice(0,30);document.getElementById('tool-detail').innerHTML='<div class=\"section-head\"><div><h2>'+esc(name)+'</h2><p class=\"meta\">'+events.length+' sampled call'+(events.length===1?'':'s')+'</p></div></div><div class=\"detail-list\">'+(events.map(function(event){return'<article class=\"sample\"><div class=\"sample-top\"><span>'+esc(event.source)+' · '+esc(event.sessionTitle||event.sessionId)+'</span><span>'+fmt(event.tokens||0)+' tok</span></div><p>'+esc(event.preview||'No preview captured.')+'</p></article>';}).join('')||'<p class=\"empty\">No sampled call payloads for this tool.</p>')+'</div>';}function renderMcp(){var servers=report.mcpServers||[];var usage=report.mcpUsage||[];var configured=servers.map(function(server){return'<button class=\"row-button\" data-mcp=\"'+esc(server.name)+'\"><span><span class=\"row-title\">'+esc(server.name)+'</span><span class=\"meta\">'+esc(server.source)+' · '+esc(server.target)+'</span></span><span class=\"row-meta\">config</span><span class=\"row-meta\"></span></button>';}).join('')||'<div class=\"empty\">No MCP server config found in the common local config files.</div>';var detected=usage.map(function(entry,index){return'<button class=\"row-button'+(!servers.length&&index===0?' active':'')+'\" data-mcp=\"'+esc(entry[0])+'\"><span><span class=\"row-title\">'+esc(entry[0])+'</span><span class=\"meta\">Detected from mcp__server__tool style names</span></span><span class=\"row-meta\">x'+entry[1]+'</span><span class=\"row-meta\"></span></button>';}).join('')||'<div class=\"empty\">No MCP-prefixed tool calls detected in the selected sessions.</div>';document.getElementById('panel-mcp').innerHTML='<div class=\"grid\"><section class=\"card\"><div class=\"section-head\"><div><h2>MCP Servers</h2><p class=\"meta\">Configured locally plus inferred calls from transcripts.</p></div></div><div class=\"mini-label\">Configured</div>'+configured+'<div class=\"mini-label\">Detected usage</div>'+detected+'</section><aside class=\"card detail\" id=\"mcp-detail\"></aside></div>';Array.prototype.forEach.call(document.querySelectorAll('[data-mcp]'),function(btn){btn.addEventListener('click',function(){Array.prototype.forEach.call(document.querySelectorAll('[data-mcp]'),function(item){item.classList.remove('active');});btn.classList.add('active');renderMcpDetail(btn.getAttribute('data-mcp'));});});if(usage.length)renderMcpDetail(usage[0][0]);else if(servers.length)renderMcpDetail(servers[0].name);else document.getElementById('mcp-detail').innerHTML='<h2>MCP Detail</h2><p class=\"empty\">Install or run sessions with MCP tools to see usage here.</p>';}function renderMcpDetail(name){var server=(report.mcpServers||[]).filter(function(item){return item.name===name;})[0];var tools=(report.mcpTools||[]).filter(function(entry){return entry[0].indexOf(name+' / ')===0;});var events=(report.toolEvents||[]).filter(function(event){return event.mcpServer===name;}).slice(0,25);document.getElementById('mcp-detail').innerHTML='<div class=\"section-head\"><div><h2>'+esc(name)+'</h2><p class=\"meta\">'+(server?esc(server.source+' · '+server.target):'Detected from tool call names')+'</p></div></div><div class=\"mini-label\">Tools</div><table class=\"table\"><tbody>'+tableRows(tools,'count')+'</tbody></table><div class=\"mini-label\">Sample calls</div><div class=\"detail-list\">'+(events.map(function(event){return'<article class=\"sample\"><div class=\"sample-top\"><span>'+esc(event.mcpTool||event.tool)+'</span><span>'+fmt(event.tokens||0)+' tok</span></div><p>'+esc(event.preview||'No preview captured.')+'</p></article>';}).join('')||'<p class=\"empty\">No sampled MCP calls for this server in the selected sessions.</p>')+'</div>';}function renderMetadata(){var rows=(report.metadata||[]).map(function(entry,index){return'<button class=\"row-button'+(index===0?' active':'')+'\" data-meta=\"'+esc(entry[0])+'\"><span><span class=\"row-title\">'+esc(entry[0])+'</span><span class=\"meta\">Click to inspect sampled records</span></span><span class=\"row-meta\">'+fmt(tok(entry[1]))+' tok</span><span class=\"row-meta\">'+fmt(entry[1])+' ch</span></button>';}).join('')||'<div class=\"empty\">No metadata-heavy records detected.</div>';document.getElementById('panel-metadata').innerHTML='<div class=\"grid\"><section class=\"card\"><div class=\"section-head\"><div><h2>Metadata</h2><p class=\"meta\">Breakdown by transcript record type.</p></div></div>'+rows+'</section><aside class=\"card detail\" id=\"metadata-detail\"></aside></div>';Array.prototype.forEach.call(document.querySelectorAll('[data-meta]'),function(btn){btn.addEventListener('click',function(){Array.prototype.forEach.call(document.querySelectorAll('[data-meta]'),function(item){item.classList.remove('active');});btn.classList.add('active');renderMetadataDetail(btn.getAttribute('data-meta'));});});if((report.metadata||[]).length)renderMetadataDetail(report.metadata[0][0]);else document.getElementById('metadata-detail').innerHTML='<h2>Metadata Detail</h2><p class=\"empty\">Nothing to inspect yet.</p>';}function renderMetadataDetail(type){var events=(report.metadataEvents||[]).filter(function(event){return event.type===type;}).slice(0,30);document.getElementById('metadata-detail').innerHTML='<div class=\"section-head\"><div><h2>'+esc(type)+'</h2><p class=\"meta\">'+events.length+' sampled record'+(events.length===1?'':'s')+'</p></div></div><div class=\"detail-list\">'+(events.map(function(event){return'<article class=\"sample\"><div class=\"sample-top\"><span>'+esc(event.source)+' · '+esc(event.sessionTitle||event.sessionId)+'</span><span>'+fmt(event.tokens||0)+' tok</span></div><p>'+esc(event.preview||'No preview captured.')+'</p></article>';}).join('')||'<p class=\"empty\">No sampled records for this metadata type.</p>')+'</div>';}function renderSessions(){document.getElementById('panel-sessions').innerHTML=(report.sessions||[]).map(function(session,index){var catRows=Object.keys(session.categories||{}).sort(function(a,b){return session.categories[b]-session.categories[a];}).map(function(cat){var chars=session.categories[cat];return'<tr><td>'+esc(labels[cat]||cat)+'</td><td>'+fmt(tok(chars))+'</td><td>'+pc(chars,session.totalChars||0).toFixed(0)+'%</td></tr>';}).join('');var tools=(session.tools?Object.keys(session.tools):[]).sort(function(a,b){return session.tools[b]-session.tools[a];}).slice(0,8).map(function(name){return'<span class=\"badge\">'+esc(name)+' x'+session.tools[name]+'</span>';}).join('')||'<span class=\"empty\">none detected</span>';var paths=(session.paths?Object.keys(session.paths):[]).sort(function(a,b){return session.paths[b]-session.paths[a];}).slice(0,8).map(function(name){return'<span class=\"badge\">'+esc(name)+' x'+session.paths[name]+'</span>';}).join('')||'<span class=\"empty\">none detected</span>';return'<details class=\"session-card\"'+(index===0?' open':'')+'><summary><span><span class=\"eyebrow\">'+esc(session.source)+' · '+esc(session.updatedAt||'unknown time')+'</span><span class=\"session-title\">'+esc(session.title||session.sessionId)+'</span><span class=\"session-path\">'+esc(session.cwd||session.path)+'</span></span><span class=\"token-big\">'+fmt(session.tokens)+'</span></summary><div class=\"session-body\">'+bar(session.categories)+'<div class=\"grid equal\"><div><div class=\"mini-label\">Buckets</div><table class=\"table\"><tbody>'+catRows+'</tbody></table></div><div><div class=\"mini-label\">Frequent tools</div><div class=\"badge-list\">'+tools+'</div><div class=\"mini-label\">Repeated paths</div><div class=\"badge-list\">'+paths+'</div></div></div></div></details>';}).join('')||'<section class=\"card\"><p class=\"empty\">No matching sessions found.</p></section>';}function activate(name){Array.prototype.forEach.call(document.querySelectorAll('.tab'),function(tab){tab.classList.toggle('active',tab.getAttribute('data-tab')===name);});Array.prototype.forEach.call(document.querySelectorAll('.panel'),function(panel){panel.classList.toggle('active',panel.id==='panel-'+name);});if(location.hash!=='#'+name)history.replaceState(null,'','#'+name);}Array.prototype.forEach.call(document.querySelectorAll('.tab'),function(tab){tab.addEventListener('click',function(){activate(tab.getAttribute('data-tab'));});});renderStats();renderOverview();renderTools();renderMcp();renderMetadata();renderSessions();var initial=(location.hash||'#overview').slice(1);if(document.getElementById('panel-'+initial))activate(initial);})();" +
1924
+ "</script><script>" + overviewEnhancementScript() + "</script><script>" + insightsEnhancementScript() + "</script></body></html>";
1925
+ }
1926
+
1927
+ function writeJson(sessions, args, file) {
1928
+ fs.writeFileSync(file, JSON.stringify(buildReport(sessions, args), null, 2));
543
1929
  }
544
1930
 
545
1931
  function openUrl(url) {
@@ -552,11 +1938,15 @@ function openUrl(url) {
552
1938
 
553
1939
  function printSummary(sessions, args, file, url) {
554
1940
  const total = sessions.reduce((sum, s) => sum + s.tokens, 0);
1941
+ const analysis = analyzeContext(sessions, args, readMcpServers(args.project));
1942
+ const topFinding = analysis.findings && analysis.findings[0];
555
1943
  console.log("Context X-Ray: analyzed " + sessions.length + " session(s), about " + fmtTokens(total) + " observed/estimated tokens.");
556
1944
  if (url) console.log("Open: " + url);
557
1945
  else console.log("Report: " + file);
1946
+ console.log("Health: " + analysis.score + "/100 (" + analysis.scoreLabel + ")" + (topFinding ? " · " + topFinding.title : ""));
1947
+ if (topFinding && topFinding.evidence && topFinding.evidence.length) console.log("Evidence: " + topFinding.evidence[0]);
558
1948
  console.log("");
559
- for (const tip of recommendations(sessions).slice(0, 4)) console.log("- " + tip);
1949
+ for (const tip of recommendations(sessions, analysis).slice(0, 4)) console.log("- " + tip);
560
1950
  const tools = sortedEntries(aggregate(sessions).tools, 5);
561
1951
  if (tools.length) console.log("- Frequent tools: " + tools.map((entry) => entry[0] + " x" + entry[1]).join(", "));
562
1952
  }
@@ -631,13 +2021,19 @@ background report server running.
631
2021
 
632
2022
  ## Interpret
633
2023
 
634
- - Tool output heavy: use narrower commands, smaller file ranges, and summarized
635
- logs.
636
- - Instructions heavy: move stable behavior into skills or AGENTS/CLAUDE files.
637
- - Assistant prose heavy: ask for shorter status updates during long runs.
638
- - One huge session: compact or start a follow-up thread with a handoff summary.
639
- - Repeated path: pin a short file-role summary instead of rereading the file.
640
- - Repeated tool: batch independent searches or delegate parallel inspection.
2024
+ - Treat the Overview score as a triage signal, then open Findings for evidence.
2025
+ - Repeated file reads: ask the agent to keep a short file-role note and reopen
2026
+ only when exact line numbers are needed.
2027
+ - Retry loops or failed tool loops: ask the agent to stop after two identical
2028
+ failures, summarize the error, and change strategy before rerunning.
2029
+ - Exploration heavy: give an inspection budget, then ask for a short
2030
+ implementation plan before more reading.
2031
+ - Cache churn or context growth: move stable instructions into skills or repo
2032
+ docs and continue large work from a compact handoff summary.
2033
+ - Tool output heavy: ask the agent to cap command output, summarize failures,
2034
+ and only expand logs when exact lines matter.
2035
+ - Metadata heavy: use Metadata/Sources drilldowns for protocol overhead, but
2036
+ prioritize prompt changes around user/tool/output buckets first.
641
2037
  `;
642
2038
  export const CONTEXT_XRAY_COMMAND_MD = `---
643
2039
  description: Visualize local Codex/Claude context usage and get optimization tips.
@@ -671,9 +2067,9 @@ After the command finishes, summarize:
671
2067
 
672
2068
  - the report link
673
2069
  - sessions analyzed
674
- - the largest context bucket
675
- - the most important warning
676
- - two or three concrete ways to improve this thread
2070
+ - the health score and most important finding
2071
+ - one concrete evidence point
2072
+ - two or three promptable ways to improve this thread
677
2073
  `;
678
2074
  function codexHome() {
679
2075
  return process.env.CODEX_HOME?.trim() || path.join(os.homedir(), ".codex");