@agent-native/core 0.35.0 → 0.35.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/context-xray-local.d.ts +2 -2
- package/dist/cli/context-xray-local.d.ts.map +1 -1
- package/dist/cli/context-xray-local.js +1449 -53
- package/dist/cli/context-xray-local.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +1 -0
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +226 -78
- package/dist/cli/skills.js.map +1 -1
- package/dist/cli/templates-meta.d.ts.map +1 -1
- package/dist/cli/templates-meta.js +8 -4
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +5 -11
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +6 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +33 -5
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +63 -3
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +39 -3
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +168 -33
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/application-state.d.ts +13 -0
- package/dist/client/application-state.d.ts.map +1 -0
- package/dist/client/application-state.js +99 -0
- package/dist/client/application-state.js.map +1 -0
- package/dist/client/frame-protocol.d.ts +11 -3
- package/dist/client/frame-protocol.d.ts.map +1 -1
- package/dist/client/frame-protocol.js.map +1 -1
- package/dist/client/index.d.ts +4 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +4 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/progress/RunsTray.d.ts +1 -0
- package/dist/client/progress/RunsTray.d.ts.map +1 -1
- package/dist/client/progress/RunsTray.js +50 -16
- package/dist/client/progress/RunsTray.js.map +1 -1
- package/dist/client/use-action.d.ts +12 -0
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js +14 -2
- package/dist/client/use-action.js.map +1 -1
- package/dist/client/use-agent-chat-context.d.ts +15 -0
- package/dist/client/use-agent-chat-context.d.ts.map +1 -0
- package/dist/client/use-agent-chat-context.js +32 -0
- package/dist/client/use-agent-chat-context.js.map +1 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +1 -2
- package/dist/deploy/build.js.map +1 -1
- package/dist/index.browser.d.ts +1 -1
- package/dist/index.browser.d.ts.map +1 -1
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +4 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +33 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +14 -6
- package/dist/provider-api/index.js.map +1 -1
- package/dist/server/agent-teams.d.ts +4 -1
- package/dist/server/agent-teams.d.ts.map +1 -1
- package/dist/server/agent-teams.js +104 -28
- package/dist/server/agent-teams.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +21 -11
- package/dist/server/auth.js.map +1 -1
- package/dist/server/request-context.d.ts +3 -4
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/self-dispatch.d.ts.map +1 -1
- package/dist/server/self-dispatch.js +17 -1
- package/dist/server/self-dispatch.js.map +1 -1
- package/dist/server/ssr-handler.d.ts.map +1 -1
- package/dist/server/ssr-handler.js +9 -18
- package/dist/server/ssr-handler.js.map +1 -1
- package/dist/templates/default/AGENTS.md +1 -1
- package/dist/templates/default/DEVELOPING.md +7 -13
- package/dist/templates/default/package.json +3 -3
- package/dist/templates/workspace-core/AGENTS.md +6 -4
- package/dist/templates/workspace-root/AGENTS.md +6 -4
- package/docs/content/actions.md +5 -7
- package/docs/content/client.md +49 -44
- package/docs/content/context-awareness.md +20 -33
- package/docs/content/creating-templates.md +2 -2
- package/docs/content/key-concepts.md +3 -3
- package/docs/content/sharing.md +1 -1
- package/docs/content/template-mail.md +1 -1
- package/docs/content/voice-input.md +1 -1
- package/package.json +11 -7
- package/src/templates/default/AGENTS.md +1 -1
- package/src/templates/default/DEVELOPING.md +7 -13
- package/src/templates/default/package.json +3 -3
- package/src/templates/workspace-core/AGENTS.md +6 -4
- 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"))
|
|
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 {
|
|
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")
|
|
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 {
|
|
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
|
|
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.
|
|
477
|
-
if (instructions > 25) tips.push("Instructions are a large share. Move
|
|
478
|
-
if (assistant > 45) tips.push("Assistant prose is heavy. Ask for
|
|
479
|
-
if (maxSession.tokens > 80000) tips.push("The largest session is about " + fmtTokens(maxSession.tokens) + " " + maxSession.tokenMethod + " tokens.
|
|
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.
|
|
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).
|
|
484
|
-
return tips.length ? tips.slice(0, 6) : ["Recent sessions look balanced. Keep
|
|
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
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
}
|
|
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 { "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[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 { "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[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 {'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[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
|
-
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
-
|
|
638
|
-
|
|
639
|
-
-
|
|
640
|
-
|
|
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
|
|
675
|
-
-
|
|
676
|
-
- two or three
|
|
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");
|