@agent-native/core 0.35.2 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +381 -73
- 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 +50 -26
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +81 -8
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +68 -24
- package/dist/client/agent-chat-adapter.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/composer/ComposerPlusMenu.d.ts.map +1 -1
- package/dist/client/composer/ComposerPlusMenu.js +174 -8
- package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts +2 -0
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +2 -2
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/composer/TiptapComposer.js +1 -1
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/context-xray/ContextMeter.d.ts +2 -1
- package/dist/client/context-xray/ContextMeter.d.ts.map +1 -1
- package/dist/client/context-xray/ContextMeter.js +19 -25
- package/dist/client/context-xray/ContextMeter.js.map +1 -1
- package/dist/client/context-xray/ContextXRayPanel.d.ts +1 -3
- package/dist/client/context-xray/ContextXRayPanel.d.ts.map +1 -1
- package/dist/client/context-xray/ContextXRayPanel.js +27 -24
- package/dist/client/context-xray/ContextXRayPanel.js.map +1 -1
- package/dist/client/conversation/AgentConversation.d.ts.map +1 -1
- package/dist/client/conversation/AgentConversation.js +2 -1
- package/dist/client/conversation/AgentConversation.js.map +1 -1
- 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/sse-event-processor.d.ts +1 -0
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +62 -15
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/tool-display.d.ts +4 -0
- package/dist/client/tool-display.d.ts.map +1 -0
- package/dist/client/tool-display.js +28 -0
- package/dist/client/tool-display.js.map +1 -0
- 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/client/use-chat-threads.d.ts.map +1 -1
- package/dist/client/use-chat-threads.js +40 -31
- package/dist/client/use-chat-threads.js.map +1 -1
- package/dist/client/use-external-value.d.ts.map +1 -1
- package/dist/client/use-external-value.js +14 -7
- package/dist/client/use-external-value.js.map +1 -1
- 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/extensions/html-shell.d.ts +3 -2
- package/dist/extensions/html-shell.d.ts.map +1 -1
- package/dist/extensions/html-shell.js +12 -2
- package/dist/extensions/html-shell.js.map +1 -1
- package/dist/extensions/routes.js +2 -7
- package/dist/extensions/routes.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/core-routes-plugin.js +2 -2
- package/dist/server/core-routes-plugin.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/security-headers.d.ts +16 -19
- package/dist/server/security-headers.d.ts.map +1 -1
- package/dist/server/security-headers.js +24 -25
- package/dist/server/security-headers.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/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/external-agents.md +1 -1
- 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 +5 -1
- package/src/templates/default/AGENTS.md +1 -1
- package/src/templates/default/DEVELOPING.md +7 -13
- package/src/templates/workspace-core/AGENTS.md +6 -4
- package/src/templates/workspace-root/AGENTS.md +6 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-xray-local.js","sourceRoot":"","sources":["../../src/cli/context-xray-local.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nimport type { ClientId } from \"./mcp-config-writers.js\";\n\nexport interface InstallLocalContextXrayOptions {\n baseDir?: string;\n clients: ClientId[];\n scope: string;\n dryRun?: boolean;\n}\n\nexport interface InstallLocalContextXrayResult {\n commands: string[];\n scriptPath: string;\n written: string[];\n}\n\nconst CONTEXT_XRAY_EXECUTABLE = String.raw`#!/usr/bin/env node\n\"use strict\";\n\nconst childProcess = require(\"node:child_process\");\nconst fs = require(\"node:fs\");\nconst os = require(\"node:os\");\nconst path = require(\"node:path\");\nconst { pathToFileURL } = require(\"node:url\");\n\nconst HOME = os.homedir();\nconst CODEX_DIR = process.env.CODEX_HOME && process.env.CODEX_HOME.trim() ? process.env.CODEX_HOME.trim() : path.join(HOME, \".codex\");\nconst CLAUDE_DIR = path.join(HOME, \".claude\");\nconst OUT_DIR = path.join(CODEX_DIR, \"context-xray\");\nconst SESSION_ID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;\nconst CATEGORIES = [\"user\", \"assistant\", \"tool_call\", \"tool_output\", \"reasoning\", \"instructions\", \"attachment\", \"metadata\", \"other\"];\nconst LABELS = {\n user: \"User asks\",\n assistant: \"Assistant text\",\n tool_call: \"Tool calls\",\n tool_output: \"Tool output\",\n reasoning: \"Reasoning\",\n instructions: \"Instructions/context\",\n attachment: \"Attachments\",\n metadata: \"Metadata\",\n other: \"Other\",\n};\nconst COLORS = {\n user: \"#8ba8ff\",\n assistant: \"#55b982\",\n tool_call: \"#f0a85b\",\n tool_output: \"#e06b73\",\n reasoning: \"#a77be8\",\n instructions: \"#6ac3d5\",\n attachment: \"#d6a85a\",\n metadata: \"#9aa3ad\",\n other: \"#c3c8ce\",\n};\n\nfunction parseArgs(argv) {\n const out = {\n mode: \"current\",\n source: \"both\",\n since: \"7d\",\n last: 12,\n scanLimit: 80,\n project: process.cwd(),\n allProjects: false,\n sessionId: \"\",\n format: \"html\",\n out: \"\",\n open: false,\n port: 0,\n };\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n const eat = (flag) => {\n if (arg === flag) return argv[++i] || \"\";\n if (arg.startsWith(flag + \"=\")) return arg.slice(flag.length + 1);\n return undefined;\n };\n let value;\n if (arg === \"threads\" || arg === \"--threads\") out.mode = \"threads\";\n else if (arg === \"trends\" || arg === \"--trends\") out.mode = \"trends\";\n else if (arg === \"current\" || arg === \"--current\") out.mode = \"current\";\n else if ((value = eat(\"--source\")) !== undefined) out.source = value;\n else if ((value = eat(\"--since\")) !== undefined) out.since = value;\n else if ((value = eat(\"--last\")) !== undefined) out.last = Number(value) || out.last;\n else if ((value = eat(\"--scan-limit\")) !== undefined) out.scanLimit = Number(value) || out.scanLimit;\n else if ((value = eat(\"--project\")) !== undefined) out.project = value;\n else if ((value = eat(\"--session-id\")) !== undefined) out.sessionId = value;\n else if ((value = eat(\"--format\")) !== undefined) out.format = value;\n else if ((value = eat(\"--out\")) !== undefined) out.out = value;\n else if ((value = eat(\"--port\")) !== undefined) out.port = Number(value) || 0;\n else if (arg === \"--all-projects\") out.allProjects = true;\n else if (arg === \"--open\") out.open = true;\n else if (arg === \"--json\") out.format = \"json\";\n else if (arg === \"--help\" || arg === \"-h\") out.help = true;\n }\n if (process.env.CLAUDE_CODE_SESSION_ID && !out.sessionId && out.mode === \"current\") {\n out.sessionId = process.env.CLAUDE_CODE_SESSION_ID;\n }\n if (out.mode === \"threads\") {\n out.allProjects = true;\n out.last = Math.max(out.last, 30);\n }\n if (out.mode === \"trends\") {\n out.allProjects = true;\n out.last = Math.max(out.last, 60);\n }\n if (out.mode === \"current\") {\n out.last = 1;\n }\n return out;\n}\n\nfunction help() {\n console.log([\n \"Context X-Ray\",\n \"\",\n \"Usage:\",\n \" context-xray --open Visualize the current/recent local thread\",\n \" context-xray threads --open Pick from recent Codex/Claude sessions\",\n \" context-xray trends --since 7d --open Show recent usage trends\",\n \" context-xray --session-id <id> --open Analyze one exact session\",\n \"\",\n \"Options:\",\n \" --source codex|claude|both\",\n \" --since 24h|7d|2w|ISO\",\n \" --last <n>\",\n \" --all-projects\",\n \" --format html|json\",\n \" --out <path>\",\n ].join(\"\\n\"));\n}\n\nfunction mkdirp(dir) {\n fs.mkdirSync(dir, { recursive: true });\n}\n\nfunction parseSince(value) {\n const now = Date.now();\n const match = String(value || \"7d\").trim().toLowerCase().match(/^(\\d+)([hdw])$/);\n if (match) {\n const amount = Number(match[1]);\n const unit = match[2];\n const mult = unit === \"h\" ? 3600000 : unit === \"d\" ? 86400000 : 604800000;\n return now - amount * mult;\n }\n const parsed = Date.parse(value);\n return Number.isFinite(parsed) ? parsed : now - 7 * 86400000;\n}\n\nfunction readJsonl(file) {\n try {\n return fs.readFileSync(file, \"utf8\").split(/\\r?\\n/).filter(Boolean).map((line) => {\n try {\n return JSON.parse(line);\n } catch {\n return null;\n }\n }).filter(Boolean);\n } catch {\n return [];\n }\n}\n\nfunction compact(value) {\n try {\n const text = JSON.stringify(value);\n return text.length > 250000 ? text.slice(0, 250000) : text;\n } catch {\n return String(value || \"\");\n }\n}\n\nfunction textFrom(value, depth) {\n if (depth > 8 || value == null) return \"\";\n if (typeof value === \"string\") return value.length > 250000 ? value.slice(0, 250000) : value;\n if (typeof value !== \"object\") return \"\";\n if (Array.isArray(value)) return value.map((item) => textFrom(item, depth + 1)).filter(Boolean).join(\"\\n\");\n const skip = new Set([\"encrypted_content\", \"id\", \"uuid\", \"call_id\", \"sessionId\", \"parentUuid\"]);\n const keys = new Set([\"text\", \"message\", \"output\", \"result\", \"content\", \"summary\", \"arguments\", \"args\", \"input\", \"stdout\", \"stderr\", \"attachment\"]);\n const parts = [];\n for (const key of Object.keys(value)) {\n if (skip.has(key)) continue;\n const item = value[key];\n if (keys.has(key) || typeof item === \"object\") {\n const text = textFrom(item, depth + 1);\n if (text) parts.push(text);\n }\n }\n return parts.join(\"\\n\");\n}\n\nfunction addCounter(counter, key, amount) {\n if (!key || !amount) return;\n counter[key] = (counter[key] || 0) + amount;\n}\n\nfunction mergeCounter(into, from) {\n for (const key of Object.keys(from || {})) addCounter(into, key, from[key]);\n}\n\nfunction estimateTokens(chars) {\n return chars > 0 ? Math.max(1, Math.ceil(chars / 4)) : 0;\n}\n\nfunction fmtTokens(tokens) {\n if (tokens >= 1000000) return (tokens / 1000000).toFixed(1) + \"m\";\n if (tokens >= 1000) return (tokens / 1000).toFixed(1) + \"k\";\n return String(tokens);\n}\n\nfunction pct(part, total) {\n return total > 0 ? Math.max(0, Math.min(100, (part / total) * 100)) : 0;\n}\n\nfunction pathCounts(text) {\n const out = {};\n const matches = String(text || \"\").match(/(?:(?:\\/[\\w@.+,=-]+)+|(?:[\\w.-]+\\/)+[\\w.+,=-]+)(?:\\.[A-Za-z0-9_+-]+)?/g) || [];\n for (const raw of matches) {\n const value = raw.replace(/['\",.)]+$/g, \"\");\n if (value.length > 5 && !value.startsWith(\"http\") && value.includes(\"/\")) addCounter(out, value, 1);\n }\n return out;\n}\n\nfunction codexTitle(id) {\n const index = path.join(CODEX_DIR, \"session_index.jsonl\");\n for (const record of readJsonl(index)) {\n if (record.id === id && record.thread_name) return String(record.thread_name);\n }\n return \"\";\n}\n\nfunction observedCodexTokens(payload) {\n if (!payload || payload.type !== \"token_count\" || !payload.info) return 0;\n const last = payload.info.last_token_usage;\n if (last && Number(last.total_tokens)) return Number(last.total_tokens);\n const total = payload.info.total_token_usage;\n if (total && Number(total.total_tokens)) return Number(total.total_tokens);\n if (total && typeof total === \"object\") {\n return [\"input_tokens\", \"cached_input_tokens\", \"output_tokens\", \"reasoning_output_tokens\"].reduce((sum, key) => sum + (Number(total[key]) || 0), 0);\n }\n return 0;\n}\n\nfunction claudeUsageTokens(usage) {\n if (!usage || typeof usage !== \"object\") return 0;\n 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);\n}\n\nfunction sessionIdFromPath(file) {\n const match = path.basename(file).match(SESSION_ID_RE);\n return match ? match[0] : path.basename(file, \".jsonl\");\n}\n\nfunction classifyCodex(record) {\n const top = String(record.type || \"\");\n const payload = record.payload && typeof record.payload === \"object\" ? record.payload : {};\n const ptype = String(payload.type || \"\");\n let category = \"other\";\n const tools = {};\n if (top === \"session_meta\") category = \"metadata\";\n else if (top === \"turn_context\") category = \"instructions\";\n else if (top === \"event_msg\") category = ptype === \"user_message\" ? \"user\" : \"metadata\";\n else if (top === \"response_item\") {\n if ([\"function_call\", \"custom_tool_call\", \"web_search_call\", \"tool_search_call\", \"tool_call\"].includes(ptype) || payload.name && payload.call_id) {\n category = \"tool_call\";\n if (payload.name) addCounter(tools, String(payload.name), 1);\n } else if ([\"function_call_output\", \"custom_tool_call_output\", \"tool_search_output\", \"tool_result\"].includes(ptype) || Object.prototype.hasOwnProperty.call(payload, \"output\")) category = \"tool_output\";\n else if (ptype === \"reasoning\" || payload.summary) category = \"reasoning\";\n else if (payload.role === \"assistant\") category = \"assistant\";\n else if (payload.role === \"user\" || payload.role === \"developer\") category = \"user\";\n }\n const text = textFrom(record, 0) || (category === \"metadata\" ? compact(record) : \"\");\n return { category, chars: text.length, tools, paths: pathCounts(text) };\n}\n\nfunction classifyClaude(record) {\n let category = \"other\";\n const tools = {};\n const message = record.message && typeof record.message === \"object\" ? record.message : null;\n let text = \"\";\n if (message) {\n if (message.role === \"user\") category = \"user\";\n else if (message.role === \"assistant\") category = \"assistant\";\n const content = Array.isArray(message.content) ? message.content : [message.content];\n const parts = [];\n for (const part of content) {\n if (part && typeof part === \"object\") {\n if (part.type === \"tool_use\") {\n category = \"tool_call\";\n if (part.name) addCounter(tools, String(part.name), 1);\n } else if (part.type === \"tool_result\") category = \"tool_output\";\n else if (part.type === \"thinking\") category = \"reasoning\";\n }\n parts.push(textFrom(part, 0));\n }\n text = parts.join(\"\\n\");\n } else if (record.toolUseResult) {\n category = \"tool_output\";\n text = textFrom(record.toolUseResult, 0);\n } else if (record.attachment) {\n category = \"attachment\";\n text = textFrom(record.attachment, 0);\n } else {\n text = textFrom(record, 0);\n }\n return { category, chars: text.length, tools, paths: pathCounts(text) };\n}\n\nfunction summarizeCodex(file) {\n const stat = fs.statSync(file);\n const summary = {\n source: \"codex\",\n path: file,\n sessionId: sessionIdFromPath(file),\n title: \"\",\n cwd: \"\",\n startedAt: \"\",\n updatedAt: \"\",\n categories: {},\n tools: {},\n paths: {},\n observedTokens: 0,\n bytes: stat.size,\n mtime: stat.mtimeMs,\n };\n const records = readJsonl(file);\n for (const record of records) {\n const payload = record.payload && typeof record.payload === \"object\" ? record.payload : {};\n if (record.type === \"session_meta\") {\n summary.sessionId = String(payload.id || summary.sessionId);\n summary.cwd = String(payload.cwd || summary.cwd);\n summary.startedAt = String(payload.timestamp || summary.startedAt);\n }\n if (payload.cwd && !summary.cwd) summary.cwd = String(payload.cwd);\n if (record.timestamp) summary.updatedAt = String(record.timestamp);\n summary.observedTokens = Math.max(summary.observedTokens, observedCodexTokens(payload));\n const stats = classifyCodex(record);\n addCounter(summary.categories, stats.category, stats.chars);\n mergeCounter(summary.tools, stats.tools);\n mergeCounter(summary.paths, stats.paths);\n }\n summary.title = codexTitle(summary.sessionId) || summary.sessionId;\n return finalizeSummary(summary);\n}\n\nfunction summarizeClaude(file) {\n const stat = fs.statSync(file);\n const summary = {\n source: \"claude\",\n path: file,\n sessionId: sessionIdFromPath(file),\n title: \"\",\n cwd: \"\",\n startedAt: \"\",\n updatedAt: \"\",\n categories: {},\n tools: {},\n paths: {},\n observedTokens: 0,\n bytes: stat.size,\n mtime: stat.mtimeMs,\n };\n const records = readJsonl(file);\n for (const record of records) {\n summary.sessionId = String(record.sessionId || summary.sessionId);\n summary.cwd = String(record.cwd || summary.cwd);\n if (record.timestamp) {\n summary.updatedAt = String(record.timestamp);\n if (!summary.startedAt) summary.startedAt = String(record.timestamp);\n }\n if (record.message && typeof record.message === \"object\") {\n summary.observedTokens = Math.max(summary.observedTokens, claudeUsageTokens(record.message.usage));\n if (!summary.title && record.message.role === \"user\") summary.title = cleanTitle(textFrom(record.message, 0)).slice(0, 90);\n }\n const stats = classifyClaude(record);\n addCounter(summary.categories, stats.category, stats.chars);\n mergeCounter(summary.tools, stats.tools);\n mergeCounter(summary.paths, stats.paths);\n }\n if (!summary.title) summary.title = summary.sessionId;\n return finalizeSummary(summary);\n}\n\nfunction finalizeSummary(summary) {\n const totalChars = Object.values(summary.categories).reduce((sum, value) => sum + value, 0);\n summary.totalChars = totalChars;\n summary.tokens = summary.observedTokens || estimateTokens(totalChars);\n summary.tokenMethod = summary.observedTokens ? \"observed\" : \"estimated\";\n return summary;\n}\n\nfunction walk(root) {\n const out = [];\n if (!fs.existsSync(root)) return out;\n const visit = (dir) => {\n let entries = [];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n const file = path.join(dir, entry.name);\n if (entry.isDirectory()) visit(file);\n else if (entry.isFile() && entry.name.endsWith(\".jsonl\")) out.push(file);\n }\n };\n visit(root);\n return out;\n}\n\nfunction encodedProject(project) {\n return path.resolve(project).replace(/\\//g, \"-\");\n}\n\nfunction pathInsideOrEqual(value, parent) {\n const relative = path.relative(parent, value);\n return relative === \"\" || (!!relative && !relative.startsWith(\"..\") && !path.isAbsolute(relative));\n}\n\nfunction candidateFiles(source, args) {\n if (args.sessionId) {\n const roots = source === \"codex\" ? [path.join(CODEX_DIR, \"sessions\"), path.join(CODEX_DIR, \"archived_sessions\")] : [path.join(CLAUDE_DIR, \"projects\")];\n return roots.flatMap(walk).filter((file) => file.includes(args.sessionId));\n }\n const since = parseSince(args.since);\n const roots = source === \"codex\" ? [path.join(CODEX_DIR, \"sessions\"), path.join(CODEX_DIR, \"archived_sessions\")] : [path.join(CLAUDE_DIR, \"projects\")];\n const projectFragment = encodedProject(args.project);\n const files = roots.flatMap(walk).filter((file) => {\n let stat;\n try {\n stat = fs.statSync(file);\n } catch {\n return false;\n }\n if (stat.mtimeMs < since) return false;\n if (args.allProjects || source === \"codex\") return true;\n return file.includes(projectFragment) || file.includes(path.basename(args.project));\n }).sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);\n return source === \"codex\" && !args.allProjects ? files : files.slice(0, args.scanLimit);\n}\n\nfunction projectMatches(session, args) {\n if (args.allProjects || args.sessionId) return true;\n const project = path.resolve(args.project);\n if (session.cwd && pathInsideOrEqual(path.resolve(session.cwd), project)) return true;\n return session.path.includes(encodedProject(project));\n}\n\nfunction collectSessions(args) {\n const sources = args.source === \"both\" ? [\"codex\", \"claude\"] : [args.source];\n const sessions = [];\n for (const source of sources) {\n for (const file of candidateFiles(source, args)) {\n try {\n const summary = source === \"codex\" ? summarizeCodex(file) : summarizeClaude(file);\n if (projectMatches(summary, args)) sessions.push(summary);\n } catch {}\n }\n }\n if (args.mode === \"current\" || args.sessionId) sessions.sort((a, b) => b.mtime - a.mtime);\n else sessions.sort((a, b) => b.tokens - a.tokens || b.mtime - a.mtime);\n return sessions.slice(0, args.last);\n}\n\nfunction aggregate(sessions) {\n const out = { categories: {}, tools: {}, paths: {} };\n for (const session of sessions) {\n mergeCounter(out.categories, session.categories);\n mergeCounter(out.tools, session.tools);\n mergeCounter(out.paths, session.paths);\n }\n return out;\n}\n\nfunction sortedEntries(counter, limit) {\n return Object.entries(counter || {}).sort((a, b) => b[1] - a[1]).slice(0, limit);\n}\n\nfunction recommendations(sessions) {\n if (!sessions.length) return [\"No matching sessions found. Try context-xray threads --all-projects --since 2w --open.\"];\n const agg = aggregate(sessions);\n const total = Object.values(agg.categories).reduce((sum, value) => sum + value, 0);\n const tips = [];\n const toolOutput = pct(agg.categories.tool_output || 0, total);\n const instructions = pct(agg.categories.instructions || 0, total);\n const assistant = pct(agg.categories.assistant || 0, total);\n const maxSession = sessions.reduce((a, b) => a.tokens > b.tokens ? a : b);\n if (toolOutput > 45) tips.push(\"Tool output dominates context. Prefer targeted rg/sed ranges, cap logs, and ask agents to summarize failing blocks instead of pasting full output.\");\n if (instructions > 25) tips.push(\"Instructions are a large share. Move stable workflow rules into skills or AGENTS/CLAUDE files and keep per-turn prompts short.\");\n if (assistant > 45) tips.push(\"Assistant prose is heavy. Ask for terse progress updates during long runs and save rationale only when it changes decisions.\");\n if (maxSession.tokens > 80000) tips.push(\"The largest session is about \" + fmtTokens(maxSession.tokens) + \" \" + maxSession.tokenMethod + \" tokens. Compact or start a fresh handoff before another big implementation pass.\");\n const topTool = sortedEntries(agg.tools, 1)[0];\n if (topTool && topTool[1] > 20) tips.push(topTool[0] + \" appears \" + topTool[1] + \" times. Batch independent inspection and use parallel reads/searches.\");\n const topPath = sortedEntries(agg.paths, 1)[0];\n if (topPath && topPath[1] > 12) tips.push(topPath[0] + \" appears repeatedly (\" + topPath[1] + \" mentions). Pin a short role summary instead of rereading it.\");\n return tips.length ? tips.slice(0, 6) : [\"Recent sessions look balanced. Keep using focused reads, compact after milestones, and preserve decisions in a skill or repo doc.\"];\n}\n\nfunction escapeHtml(value) {\n return String(value || \"\").replace(/[&<>\"']/g, (ch) => ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", \"\\\"\": \""\", \"'\": \"'\" }[ch]));\n}\n\nfunction cleanTitle(value) {\n return String(value || \"\").replace(/<[^>]+>/g, \" \").replace(/\\s+/g, \" \").trim();\n}\n\nfunction categoryBar(categories) {\n const total = Object.values(categories || {}).reduce((sum, value) => sum + value, 0);\n if (!total) return \"<div class=\\\"bar\\\"></div>\";\n return \"<div class=\\\"bar\\\">\" + CATEGORIES.map((cat) => {\n const value = categories[cat] || 0;\n if (!value) return \"\";\n const width = Math.max(1, pct(value, total));\n return \"<span title=\\\"\" + escapeHtml(LABELS[cat]) + \"\\\" style=\\\"width:\" + width.toFixed(2) + \"%;background:\" + COLORS[cat] + \"\\\"></span>\";\n }).join(\"\") + \"</div>\";\n}\n\nfunction renderHtml(sessions, args) {\n const agg = aggregate(sessions);\n const totalTokens = sessions.reduce((sum, s) => sum + s.tokens, 0);\n const tips = recommendations(sessions);\n const categoryRows = CATEGORIES.map((cat) => {\n const chars = agg.categories[cat] || 0;\n if (!chars) return \"\";\n 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>\";\n }).join(\"\");\n const sessionCards = sessions.map((session) => {\n 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(\"\");\n const tools = sortedEntries(session.tools, 6).map((entry) => \"<span class=\\\"badge\\\">\" + escapeHtml(entry[0]) + \" x\" + entry[1] + \"</span>\").join(\"\");\n const paths = sortedEntries(session.paths, 5).map((entry) => \"<span class=\\\"badge muted\\\">\" + escapeHtml(entry[0]) + \" x\" + entry[1] + \"</span>\").join(\"\");\n 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>\";\n }).join(\"\");\n const topTools = sortedEntries(agg.tools, 10).map((entry) => \"<tr><td>\" + escapeHtml(entry[0]) + \"</td><td>\" + entry[1] + \"</td></tr>\").join(\"\");\n const topPaths = sortedEntries(agg.paths, 10).map((entry) => \"<tr><td>\" + escapeHtml(entry[0]) + \"</td><td>\" + entry[1] + \"</td></tr>\").join(\"\");\n const sourceCounts = sessions.reduce((counts, s) => (counts[s.source] = (counts[s.source] || 0) + 1, counts), {});\n 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>\" +\n \"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))}}\" +\n \"</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>\";\n}\n\nfunction writeJson(sessions, args, file) {\n const agg = aggregate(sessions);\n fs.writeFileSync(file, JSON.stringify({\n generatedAt: new Date().toISOString(),\n mode: args.mode,\n source: args.source,\n since: args.since,\n totalTokens: sessions.reduce((sum, s) => sum + s.tokens, 0),\n categories: agg.categories,\n tools: sortedEntries(agg.tools, 25),\n paths: sortedEntries(agg.paths, 25),\n recommendations: recommendations(sessions),\n sessions,\n }, null, 2));\n}\n\nfunction openUrl(url) {\n const cmd = process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"cmd\" : \"xdg-open\";\n const args = process.platform === \"win32\" ? [\"/c\", \"start\", \"\", url] : [url];\n try {\n childProcess.spawn(cmd, args, { detached: true, stdio: \"ignore\" }).unref();\n } catch {}\n}\n\nfunction printSummary(sessions, args, file, url) {\n const total = sessions.reduce((sum, s) => sum + s.tokens, 0);\n console.log(\"Context X-Ray: analyzed \" + sessions.length + \" session(s), about \" + fmtTokens(total) + \" observed/estimated tokens.\");\n if (url) console.log(\"Open: \" + url);\n else console.log(\"Report: \" + file);\n console.log(\"\");\n for (const tip of recommendations(sessions).slice(0, 4)) console.log(\"- \" + tip);\n const tools = sortedEntries(aggregate(sessions).tools, 5);\n if (tools.length) console.log(\"- Frequent tools: \" + tools.map((entry) => entry[0] + \" x\" + entry[1]).join(\", \"));\n}\n\nfunction main() {\n const args = parseArgs(process.argv.slice(2));\n if (args.help) return help();\n const sessions = collectSessions(args);\n mkdirp(OUT_DIR);\n const suffix = args.format === \"json\" ? \"json\" : \"html\";\n const file = args.out || path.join(OUT_DIR, \"context-xray-\" + new Date().toISOString().replace(/[:.]/g, \"-\") + \".\" + suffix);\n mkdirp(path.dirname(file));\n if (args.format === \"json\") writeJson(sessions, args, file);\n else fs.writeFileSync(file, renderHtml(sessions, args));\n const url = args.open && args.format === \"html\" ? pathToFileURL(file).href : \"\";\n if (url) openUrl(url);\n printSummary(sessions, args, file, url);\n}\n\nmain();\n`;\n\nexport const CONTEXT_XRAY_SKILL_MD = `---\nname: context-xray\ndescription: >-\n Visualize local Codex and Claude Code context usage, open an inline/browser\n report, flag warnings, and suggest prompt/tooling optimizations. Use when the\n user types /context-xray, asks where context is going, wants recent local\n coding-agent trends, or wants to improve context efficiency.\nmetadata:\n visibility: exported\n---\n\n# Context X-Ray\n\nUse the locally installed Context X-Ray command to visualize recent Codex and\nClaude Code context usage. It reads local transcript files only and does not\nupload transcript content.\n\nProject-scoped installs write only project \\`.agents\\` skill and command\nartifacts; user-scoped installs write global Codex/Claude instructions.\n\n## Run\n\nCurrent or most recent local thread:\n\n\\`\\`\\`sh\n~/.agent-native/context-xray/context-xray --open\n\\`\\`\\`\n\nThread picker / recent sessions:\n\n\\`\\`\\`sh\n~/.agent-native/context-xray/context-xray threads --open\n\\`\\`\\`\n\nWeekly trends:\n\n\\`\\`\\`sh\n~/.agent-native/context-xray/context-xray trends --since 7d --open\n\\`\\`\\`\n\nExact session when the host exposes one:\n\n\\`\\`\\`sh\n~/.agent-native/context-xray/context-xray --session-id \"$CLAUDE_CODE_SESSION_ID\" --open\n\\`\\`\\`\n\nAfter running, report the link, the number of sessions analyzed, the largest\ncontext buckets, and 3-5 specific optimizations.\n\\`--open\\` opens the generated local HTML file directly and does not keep a\nbackground report server running.\n\n## Interpret\n\n- Tool output heavy: use narrower commands, smaller file ranges, and summarized\n logs.\n- Instructions heavy: move stable behavior into skills or AGENTS/CLAUDE files.\n- Assistant prose heavy: ask for shorter status updates during long runs.\n- One huge session: compact or start a follow-up thread with a handoff summary.\n- Repeated path: pin a short file-role summary instead of rereading the file.\n- Repeated tool: batch independent searches or delegate parallel inspection.\n`;\n\nexport const CONTEXT_XRAY_COMMAND_MD = `---\ndescription: Visualize local Codex/Claude context usage and get optimization tips.\nargument-hint: [current|threads|trends|--since 7d]\n---\n\nRun Context X-Ray locally and show the user the generated report link plus the\ntop warnings.\n\nChoose the command from the user's arguments:\n\n- No arguments or \\`current\\`:\n \\`~/.agent-native/context-xray/context-xray --open\\`\n- \\`threads\\`:\n \\`~/.agent-native/context-xray/context-xray threads --open\\`\n- \\`trends\\`:\n \\`~/.agent-native/context-xray/context-xray trends --since 7d --open\\`\n\nIf \\`$ARGUMENTS\\` includes flags such as \\`--since 24h\\`, \\`--last 20\\`, or\n\\`--all-projects\\`, pass them through to the command. If the host exposes\n\\`CLAUDE_CODE_SESSION_ID\\`, prefer:\n\n\\`\\`\\`sh\n~/.agent-native/context-xray/context-xray --session-id \"$CLAUDE_CODE_SESSION_ID\" --open\n\\`\\`\\`\n\n\\`--open\\` opens a local HTML report file directly; there should not be a\nlong-running server process to monitor.\n\nAfter the command finishes, summarize:\n\n- the report link\n- sessions analyzed\n- the largest context bucket\n- the most important warning\n- two or three concrete ways to improve this thread\n`;\n\nfunction codexHome(): string {\n return process.env.CODEX_HOME?.trim() || path.join(os.homedir(), \".codex\");\n}\n\nfunction writeExecutable(file: string, content: string): void {\n fs.mkdirSync(path.dirname(file), { recursive: true });\n fs.writeFileSync(file, content, \"utf-8\");\n fs.chmodSync(file, 0o755);\n}\n\nfunction writeFile(file: string, content: string, written: string[]): void {\n fs.mkdirSync(path.dirname(file), { recursive: true });\n fs.writeFileSync(file, content, \"utf-8\");\n written.push(file);\n}\n\nfunction installProjectArtifacts(baseDir: string, written: string[]): void {\n writeFile(\n path.join(baseDir, \".agents\", \"skills\", \"context-xray\", \"SKILL.md\"),\n CONTEXT_XRAY_SKILL_MD,\n written,\n );\n writeFile(\n path.join(baseDir, \".agents\", \"commands\", \"context-xray.md\"),\n CONTEXT_XRAY_COMMAND_MD,\n written,\n );\n}\n\nexport function installLocalContextXray(\n options: InstallLocalContextXrayOptions,\n): InstallLocalContextXrayResult {\n const installDir = path.join(os.homedir(), \".agent-native\", \"context-xray\");\n const scriptPath = path.join(installDir, \"context-xray\");\n const binPath = path.join(os.homedir(), \".local\", \"bin\", \"context-xray\");\n const written: string[] = [];\n\n if (options.dryRun) {\n return {\n commands: [\"context-xray --open\"],\n scriptPath,\n written,\n };\n }\n\n writeExecutable(scriptPath, CONTEXT_XRAY_EXECUTABLE);\n written.push(scriptPath);\n if (process.platform === \"win32\") {\n const cmdPath = `${binPath}.cmd`;\n writeExecutable(\n cmdPath,\n `@echo off\\r\\nnode ${JSON.stringify(scriptPath)} %*\\r\\n`,\n );\n written.push(cmdPath);\n } else {\n writeExecutable(\n binPath,\n `#!/usr/bin/env sh\\nexec ${JSON.stringify(scriptPath)} \"$@\"\\n`,\n );\n written.push(binPath);\n }\n\n const clientSet = new Set(options.clients);\n const wantsCodex = clientSet.has(\"codex\");\n const wantsClaude =\n clientSet.has(\"claude-code\") || clientSet.has(\"claude-code-cli\");\n\n if (options.scope === \"project\" && options.baseDir) {\n installProjectArtifacts(options.baseDir, written);\n } else if (wantsCodex) {\n writeFile(\n path.join(codexHome(), \"skills\", \"context-xray\", \"SKILL.md\"),\n CONTEXT_XRAY_SKILL_MD,\n written,\n );\n writeFile(\n path.join(codexHome(), \"commands\", \"context-xray.md\"),\n CONTEXT_XRAY_COMMAND_MD,\n written,\n );\n }\n\n if (options.scope !== \"project\" && wantsClaude) {\n writeFile(\n path.join(os.homedir(), \".claude\", \"skills\", \"context-xray\", \"SKILL.md\"),\n CONTEXT_XRAY_SKILL_MD,\n written,\n );\n writeFile(\n path.join(os.homedir(), \".claude\", \"commands\", \"context-xray.md\"),\n CONTEXT_XRAY_COMMAND_MD,\n written,\n );\n }\n\n return {\n commands: [\"context-xray --open\"],\n scriptPath,\n written,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"context-xray-local.js","sourceRoot":"","sources":["../../src/cli/context-xray-local.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\n\nimport type { ClientId } from \"./mcp-config-writers.js\";\n\nexport interface InstallLocalContextXrayOptions {\n baseDir?: string;\n clients: ClientId[];\n scope: string;\n dryRun?: boolean;\n}\n\nexport interface InstallLocalContextXrayResult {\n commands: string[];\n scriptPath: string;\n written: string[];\n}\n\nconst CONTEXT_XRAY_EXECUTABLE = String.raw`#!/usr/bin/env node\n\"use strict\";\n\nconst childProcess = require(\"node:child_process\");\nconst fs = require(\"node:fs\");\nconst os = require(\"node:os\");\nconst path = require(\"node:path\");\nconst { pathToFileURL } = require(\"node:url\");\n\nconst HOME = os.homedir();\nconst CODEX_DIR = process.env.CODEX_HOME && process.env.CODEX_HOME.trim() ? process.env.CODEX_HOME.trim() : path.join(HOME, \".codex\");\nconst CLAUDE_DIR = path.join(HOME, \".claude\");\nconst OUT_DIR = path.join(CODEX_DIR, \"context-xray\");\nconst SESSION_ID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;\nconst CATEGORIES = [\"user\", \"assistant\", \"tool_call\", \"tool_output\", \"reasoning\", \"instructions\", \"attachment\", \"metadata\", \"other\"];\nconst LABELS = {\n user: \"User asks\",\n assistant: \"Assistant text\",\n tool_call: \"Tool calls\",\n tool_output: \"Tool output\",\n reasoning: \"Reasoning\",\n instructions: \"Instructions/context\",\n attachment: \"Attachments\",\n metadata: \"Metadata\",\n other: \"Other\",\n};\nconst COLORS = {\n user: \"#8ba8ff\",\n assistant: \"#55b982\",\n tool_call: \"#f0a85b\",\n tool_output: \"#e06b73\",\n reasoning: \"#a77be8\",\n instructions: \"#6ac3d5\",\n attachment: \"#d6a85a\",\n metadata: \"#9aa3ad\",\n other: \"#c3c8ce\",\n};\nconst MAX_EVENT_SAMPLES = 80;\nconst MAX_STEP_SAMPLES = 900;\nconst PRESSURE_WINDOW = 5;\nconst PRESSURE_TOKENS = 50000;\n\nfunction parseArgs(argv) {\n const out = {\n mode: \"current\",\n source: \"both\",\n since: \"7d\",\n last: 12,\n scanLimit: 80,\n project: process.cwd(),\n allProjects: false,\n sessionId: \"\",\n format: \"html\",\n out: \"\",\n open: false,\n port: 0,\n };\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n const eat = (flag) => {\n if (arg === flag) return argv[++i] || \"\";\n if (arg.startsWith(flag + \"=\")) return arg.slice(flag.length + 1);\n return undefined;\n };\n let value;\n if (arg === \"threads\" || arg === \"--threads\") out.mode = \"threads\";\n else if (arg === \"trends\" || arg === \"--trends\") out.mode = \"trends\";\n else if (arg === \"current\" || arg === \"--current\") out.mode = \"current\";\n else if ((value = eat(\"--source\")) !== undefined) out.source = value;\n else if ((value = eat(\"--since\")) !== undefined) out.since = value;\n else if ((value = eat(\"--last\")) !== undefined) out.last = Number(value) || out.last;\n else if ((value = eat(\"--scan-limit\")) !== undefined) out.scanLimit = Number(value) || out.scanLimit;\n else if ((value = eat(\"--project\")) !== undefined) out.project = value;\n else if ((value = eat(\"--session-id\")) !== undefined) out.sessionId = value;\n else if ((value = eat(\"--format\")) !== undefined) out.format = value;\n else if ((value = eat(\"--out\")) !== undefined) out.out = value;\n else if ((value = eat(\"--port\")) !== undefined) out.port = Number(value) || 0;\n else if (arg === \"--all-projects\") out.allProjects = true;\n else if (arg === \"--open\") out.open = true;\n else if (arg === \"--json\") out.format = \"json\";\n else if (arg === \"--help\" || arg === \"-h\") out.help = true;\n }\n if (process.env.CLAUDE_CODE_SESSION_ID && !out.sessionId && out.mode === \"current\") {\n out.sessionId = process.env.CLAUDE_CODE_SESSION_ID;\n }\n if (out.mode === \"threads\") {\n out.allProjects = true;\n out.last = Math.max(out.last, 30);\n }\n if (out.mode === \"trends\") {\n out.allProjects = true;\n out.last = Math.max(out.last, 60);\n }\n if (out.mode === \"current\") {\n out.last = 1;\n }\n return out;\n}\n\nfunction help() {\n console.log([\n \"Context X-Ray\",\n \"\",\n \"Usage:\",\n \" context-xray --open Visualize the current/recent local thread\",\n \" context-xray threads --open Pick from recent Codex/Claude sessions\",\n \" context-xray trends --since 7d --open Show recent usage trends\",\n \" context-xray --session-id <id> --open Analyze one exact session\",\n \"\",\n \"Options:\",\n \" --source codex|claude|both\",\n \" --since 24h|7d|2w|ISO\",\n \" --last <n>\",\n \" --all-projects\",\n \" --format html|json\",\n \" --out <path>\",\n ].join(\"\\n\"));\n}\n\nfunction mkdirp(dir) {\n fs.mkdirSync(dir, { recursive: true });\n}\n\nfunction parseSince(value) {\n const now = Date.now();\n const match = String(value || \"7d\").trim().toLowerCase().match(/^(\\d+)([hdw])$/);\n if (match) {\n const amount = Number(match[1]);\n const unit = match[2];\n const mult = unit === \"h\" ? 3600000 : unit === \"d\" ? 86400000 : 604800000;\n return now - amount * mult;\n }\n const parsed = Date.parse(value);\n return Number.isFinite(parsed) ? parsed : now - 7 * 86400000;\n}\n\nfunction readJsonl(file) {\n try {\n return fs.readFileSync(file, \"utf8\").split(/\\r?\\n/).filter(Boolean).map((line) => {\n try {\n return JSON.parse(line);\n } catch {\n return null;\n }\n }).filter(Boolean);\n } catch {\n return [];\n }\n}\n\nfunction compact(value) {\n try {\n const text = JSON.stringify(value);\n return text.length > 250000 ? text.slice(0, 250000) : text;\n } catch {\n return String(value || \"\");\n }\n}\n\nfunction textFrom(value, depth) {\n if (depth > 8 || value == null) return \"\";\n if (typeof value === \"string\") return value.length > 250000 ? value.slice(0, 250000) : value;\n if (typeof value !== \"object\") return \"\";\n if (Array.isArray(value)) return value.map((item) => textFrom(item, depth + 1)).filter(Boolean).join(\"\\n\");\n const skip = new Set([\"encrypted_content\", \"id\", \"uuid\", \"call_id\", \"sessionId\", \"parentUuid\"]);\n const keys = new Set([\"text\", \"message\", \"output\", \"result\", \"content\", \"summary\", \"arguments\", \"args\", \"input\", \"stdout\", \"stderr\", \"attachment\"]);\n const parts = [];\n for (const key of Object.keys(value)) {\n if (skip.has(key)) continue;\n const item = value[key];\n if (keys.has(key) || typeof item === \"object\") {\n const text = textFrom(item, depth + 1);\n if (text) parts.push(text);\n }\n }\n return parts.join(\"\\n\");\n}\n\nfunction addCounter(counter, key, amount) {\n if (!key || !amount) return;\n counter[key] = (counter[key] || 0) + amount;\n}\n\nfunction mergeCounter(into, from) {\n for (const key of Object.keys(from || {})) addCounter(into, key, from[key]);\n}\n\nfunction estimateTokens(chars) {\n return chars > 0 ? Math.max(1, Math.ceil(chars / 4)) : 0;\n}\n\nfunction fmtTokens(tokens) {\n if (tokens >= 1000000) return (tokens / 1000000).toFixed(1) + \"m\";\n if (tokens >= 1000) return (tokens / 1000).toFixed(1) + \"k\";\n return String(tokens);\n}\n\nfunction pct(part, total) {\n return total > 0 ? Math.max(0, Math.min(100, (part / total) * 100)) : 0;\n}\n\nfunction pathCounts(text) {\n const out = {};\n const matches = String(text || \"\").match(/(?:(?:\\/[\\w@.+,=-]+)+|(?:[\\w.-]+\\/)+[\\w.+,=-]+)(?:\\.[A-Za-z0-9_+-]+)?/g) || [];\n for (const raw of matches) {\n const value = raw.replace(/['\",.)]+$/g, \"\");\n if (value.length > 5 && !value.startsWith(\"http\") && value.includes(\"/\")) addCounter(out, value, 1);\n }\n return out;\n}\n\nfunction eventPreview(text) {\n return cleanTitle(String(text || \"\")).slice(0, 240);\n}\n\nfunction inferMcpTool(toolName) {\n const value = String(toolName || \"\");\n if (value.startsWith(\"mcp__\")) {\n const rest = value.slice(5);\n const split = rest.indexOf(\"__\");\n if (split !== -1) return { server: rest.slice(0, split), tool: rest.slice(split + 2) };\n const dot = rest.indexOf(\".\");\n if (dot !== -1) return { server: rest.slice(0, dot), tool: rest.slice(dot + 1) };\n }\n if (value.startsWith(\"mcp_\")) {\n const rest = value.slice(4);\n const split = rest.indexOf(\"_\");\n if (split !== -1) return { server: rest.slice(0, split), tool: rest.slice(split + 1) };\n }\n return { server: \"\", tool: \"\" };\n}\n\nfunction codexTitle(id) {\n const index = path.join(CODEX_DIR, \"session_index.jsonl\");\n for (const record of readJsonl(index)) {\n if (record.id === id && record.thread_name) return String(record.thread_name);\n }\n return \"\";\n}\n\nfunction observedCodexTokens(payload) {\n if (!payload || payload.type !== \"token_count\" || !payload.info) return 0;\n const total = payload.info.total_token_usage;\n if (total && Number(total.total_tokens)) return Number(total.total_tokens);\n if (total && typeof total === \"object\") {\n return [\"input_tokens\", \"cached_input_tokens\", \"output_tokens\", \"reasoning_output_tokens\"].reduce((sum, key) => sum + (Number(total[key]) || 0), 0);\n }\n const last = payload.info.last_token_usage;\n if (last && Number(last.total_tokens)) return Number(last.total_tokens);\n return 0;\n}\n\nfunction claudeUsageTokens(usage) {\n if (!usage || typeof usage !== \"object\") return 0;\n 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);\n}\n\nfunction emptyUsage() {\n return {\n inputTokens: 0,\n outputTokens: 0,\n cacheCreationInputTokens: 0,\n cacheReadInputTokens: 0,\n cachedInputTokens: 0,\n reasoningOutputTokens: 0,\n totalTokens: 0,\n turnsWithUsage: 0,\n peakTurnTokens: 0,\n peakTurnLabel: \"\",\n latestTurnTokens: 0,\n latestInputTokens: 0,\n series: [],\n };\n}\n\nfunction usageTotal(usage) {\n if (!usage) return 0;\n if (Number(usage.totalTokens)) return Number(usage.totalTokens) || 0;\n return (Number(usage.inputTokens) || 0) +\n (Number(usage.outputTokens) || 0) +\n (Number(usage.cacheCreationInputTokens) || 0) +\n (Number(usage.cacheReadInputTokens) || 0) +\n (Number(usage.reasoningOutputTokens) || 0);\n}\n\nfunction normalizedUsage(raw) {\n if (!raw || typeof raw !== \"object\") return null;\n const usage = {\n inputTokens: Number(raw.input_tokens) || Number(raw.inputTokens) || 0,\n outputTokens: Number(raw.output_tokens) || Number(raw.outputTokens) || 0,\n cacheCreationInputTokens: Number(raw.cache_creation_input_tokens) || Number(raw.cacheCreationInputTokens) || 0,\n cacheReadInputTokens: Number(raw.cache_read_input_tokens) || Number(raw.cacheReadInputTokens) || Number(raw.cached_input_tokens) || Number(raw.cachedInputTokens) || 0,\n cachedInputTokens: Number(raw.cached_input_tokens) || Number(raw.cachedInputTokens) || 0,\n reasoningOutputTokens: Number(raw.reasoning_output_tokens) || Number(raw.reasoningOutputTokens) || 0,\n totalTokens: Number(raw.total_tokens) || Number(raw.totalTokens) || 0,\n };\n if (!usage.totalTokens) usage.totalTokens = usageTotal(usage);\n return usage.totalTokens || usage.inputTokens || usage.outputTokens || usage.cacheCreationInputTokens || usage.cacheReadInputTokens ? usage : null;\n}\n\nfunction codexUsageFromPayload(payload) {\n if (!payload || payload.type !== \"token_count\" || !payload.info) return null;\n return normalizedUsage(payload.info.last_token_usage || payload.info.total_token_usage);\n}\n\nfunction addUsage(summary, usage, timestamp, label) {\n if (!usage) return;\n const total = usageTotal(usage);\n summary.usage.inputTokens += usage.inputTokens || 0;\n summary.usage.outputTokens += usage.outputTokens || 0;\n summary.usage.cacheCreationInputTokens += usage.cacheCreationInputTokens || 0;\n summary.usage.cacheReadInputTokens += usage.cacheReadInputTokens || 0;\n summary.usage.cachedInputTokens += usage.cachedInputTokens || 0;\n summary.usage.reasoningOutputTokens += usage.reasoningOutputTokens || 0;\n summary.usage.totalTokens += total;\n summary.usage.turnsWithUsage += 1;\n summary.usage.latestTurnTokens = total;\n summary.usage.latestInputTokens = (usage.inputTokens || 0) + (usage.cacheCreationInputTokens || 0) + (usage.cacheReadInputTokens || 0);\n if (total > summary.usage.peakTurnTokens) {\n summary.usage.peakTurnTokens = total;\n summary.usage.peakTurnLabel = label || timestamp || \"turn \" + summary.usage.turnsWithUsage;\n }\n if (summary.usage.series.length < 260) {\n summary.usage.series.push({\n timestamp: String(timestamp || \"\"),\n label: label || \"turn \" + summary.usage.turnsWithUsage,\n totalTokens: total,\n inputTokens: usage.inputTokens || 0,\n outputTokens: usage.outputTokens || 0,\n cacheCreationInputTokens: usage.cacheCreationInputTokens || 0,\n cacheReadInputTokens: usage.cacheReadInputTokens || 0,\n cachedInputTokens: usage.cachedInputTokens || 0,\n reasoningOutputTokens: usage.reasoningOutputTokens || 0,\n });\n }\n}\n\nfunction sessionIdFromPath(file) {\n const match = path.basename(file).match(SESSION_ID_RE);\n return match ? match[0] : path.basename(file, \".jsonl\");\n}\n\nfunction safeJson(value) {\n if (!value || typeof value !== \"string\") return null;\n const text = value.trim();\n if (!text || !/^[{[]/.test(text)) return null;\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n\nfunction normalizeToolInput(value) {\n if (value == null) return {};\n if (typeof value === \"string\") {\n const parsed = safeJson(value);\n return parsed && typeof parsed === \"object\" ? parsed : { text: value };\n }\n if (typeof value === \"object\") return value;\n return { text: String(value) };\n}\n\nfunction codexToolInput(payload) {\n if (!payload || typeof payload !== \"object\") return {};\n if (Object.prototype.hasOwnProperty.call(payload, \"input\")) return normalizeToolInput(payload.input);\n if (Object.prototype.hasOwnProperty.call(payload, \"arguments\")) return normalizeToolInput(payload.arguments);\n if (Object.prototype.hasOwnProperty.call(payload, \"args\")) return normalizeToolInput(payload.args);\n if (Object.prototype.hasOwnProperty.call(payload, \"parameters\")) return normalizeToolInput(payload.parameters);\n return {};\n}\n\nfunction firstStringField(input, keys) {\n if (!input || typeof input !== \"object\") return \"\";\n for (const key of keys) {\n const value = input[key];\n if (typeof value === \"string\" && value.trim()) return value.trim();\n }\n return \"\";\n}\n\nfunction shortCommand(command) {\n return cleanTitle(command).slice(0, 260);\n}\n\nfunction stripAnsi(value) {\n return String(value || \"\").replace(/\\x1b\\[[0-9;?]*[ -/]*[@-~]/g, \"\");\n}\n\nfunction normalizeCommand(command) {\n return stripAnsi(command)\n .replace(/\\/Users\\/[^\\s\"']+/g, \"<abs-path>\")\n .replace(/\\/tmp\\/[^\\s\"']+/g, \"<tmp-path>\")\n .replace(/:\\d+(:\\d+)?/g, \":<line>\")\n .replace(/\\b[0-9a-f]{8}-[0-9a-f-]{27,36}\\b/gi, \"<id>\")\n .replace(/\\s+/g, \" \")\n .trim()\n .slice(0, 260)\n .toLowerCase();\n}\n\nfunction toolFamily(toolName, input, inputText) {\n const name = String(toolName || \"\").toLowerCase();\n if (inferMcpTool(toolName).server) return \"mcp\";\n if (/(^|_|\\.)(task|agent|subagent|spawn_agent|delegate)/.test(name)) return \"agent\";\n if (/(read|view|open_file|get_file|cat_file)/.test(name)) return \"read\";\n if (/(grep|glob|search|find|ripgrep|rg|list|ls)/.test(name)) return \"search\";\n if (/(edit|write|patch|apply_patch|replace|update_file|create_file|delete|rm)/.test(name)) return \"write\";\n if (/(bash|shell|exec|terminal|command|run)/.test(name)) return \"execute\";\n const text = String(inputText || \"\").toLowerCase();\n if (/^\\s*(rg|grep|find|ls)\\b/.test(text)) return \"search\";\n if (/^\\s*(cat|sed|nl|head|tail)\\b/.test(text)) return \"read\";\n if (/^\\s*(python|node|pnpm|npm|yarn|bun|cargo|go|git|make|pytest|vitest)\\b/.test(text)) return \"execute\";\n return \"tool\";\n}\n\nfunction toolTarget(toolName, input, inputText) {\n const direct = firstStringField(input, [\"file_path\", \"filePath\", \"path\", \"filename\", \"relative_path\", \"target\", \"cwd\"]);\n if (direct) return direct;\n const command = firstStringField(input, [\"command\", \"cmd\", \"shell\", \"script\"]);\n if (command) return shortCommand(command);\n const paths = pathCounts(inputText || \"\");\n const firstPath = Object.keys(paths)[0];\n if (firstPath) return firstPath;\n return shortCommand(inputText || toolName || \"\");\n}\n\nfunction toolCommand(toolName, input, inputText) {\n const command = firstStringField(input, [\"command\", \"cmd\", \"shell\", \"script\"]);\n if (command) return shortCommand(command);\n const name = String(toolName || \"\").toLowerCase();\n if (/(bash|shell|exec|terminal|command|run)/.test(name)) return shortCommand(inputText || \"\");\n return \"\";\n}\n\nfunction outputLooksError(value, text) {\n if (!value || typeof value !== \"object\") {\n return /\\b(exit code|status|error)\\s*[:=]?\\s*[1-9]\\b/i.test(text || \"\") || /\\bfailed\\b|\\btraceback\\b|\\bexception\\b/i.test(text || \"\");\n }\n if (value.is_error === true || value.error === true || value.success === false) return true;\n const code = Number(value.exit_code ?? value.exitCode ?? value.status ?? value.code);\n if (Number.isFinite(code) && code !== 0) return true;\n return /\\b(exit code|status)\\s*[:=]?\\s*[1-9]\\b/i.test(text || \"\") || /\\btraceback\\b|\\bexception\\b/i.test(text || \"\");\n}\n\nfunction initTraceFields(summary) {\n summary.usage = emptyUsage();\n summary.steps = [];\n summary.stepCount = 0;\n summary._callMap = {};\n summary._lastToolStep = null;\n}\n\nfunction recordToolStep(summary, options) {\n const input = normalizeToolInput(options.input);\n const inputText = textFrom(input, 0) || compact(input);\n const preview = eventPreview(inputText || options.preview || options.tool || \"\");\n const family = toolFamily(options.tool, input, inputText || preview);\n const command = toolCommand(options.tool, input, inputText || preview);\n const mcp = inferMcpTool(options.tool);\n const step = {\n index: summary.stepCount++,\n source: summary.source,\n sessionId: summary.sessionId,\n timestamp: String(options.timestamp || \"\"),\n type: \"tool_call\",\n tool: String(options.tool || \"tool_call\"),\n family,\n target: toolTarget(options.tool, input, inputText || preview),\n command,\n normalizedCommand: command ? normalizeCommand(command) : \"\",\n mcpServer: mcp.server,\n mcpTool: mcp.tool,\n tokens: estimateTokens((inputText || preview).length),\n preview,\n isError: false,\n errorPreview: \"\",\n };\n if (summary.steps.length < MAX_STEP_SAMPLES) summary.steps.push(step);\n for (const id of options.ids || []) {\n if (id) summary._callMap[String(id)] = step;\n }\n summary._lastToolStep = step;\n return step;\n}\n\nfunction markToolResult(summary, ids, isError, preview) {\n let step = null;\n for (const id of ids || []) {\n if (id && summary._callMap[String(id)]) {\n step = summary._callMap[String(id)];\n break;\n }\n }\n if (!step) step = summary._lastToolStep;\n if (!step) return;\n if (isError) {\n step.isError = true;\n step.errorPreview = eventPreview(preview || step.errorPreview || \"\");\n }\n}\n\nfunction contentBlocks(content) {\n if (Array.isArray(content)) return content;\n if (content == null) return [];\n return [content];\n}\n\nfunction classifyCodex(record) {\n const top = String(record.type || \"\");\n const payload = record.payload && typeof record.payload === \"object\" ? record.payload : {};\n const ptype = String(payload.type || \"\");\n let category = \"other\";\n const tools = {};\n let toolName = \"\";\n if (top === \"session_meta\") category = \"metadata\";\n else if (top === \"turn_context\") category = \"instructions\";\n else if (top === \"event_msg\") category = ptype === \"user_message\" ? \"user\" : \"metadata\";\n else if (top === \"response_item\") {\n if ([\"function_call\", \"custom_tool_call\", \"web_search_call\", \"tool_search_call\", \"tool_call\"].includes(ptype) || payload.name && payload.call_id) {\n category = \"tool_call\";\n toolName = payload.name ? String(payload.name) : ptype || \"tool_call\";\n if (payload.name) addCounter(tools, String(payload.name), 1);\n } else if ([\"function_call_output\", \"custom_tool_call_output\", \"tool_search_output\", \"tool_result\"].includes(ptype) || Object.prototype.hasOwnProperty.call(payload, \"output\")) {\n category = \"tool_output\";\n toolName = payload.name ? String(payload.name) : \"tool output\";\n }\n else if (ptype === \"reasoning\" || payload.summary) category = \"reasoning\";\n else if (payload.role === \"assistant\") category = \"assistant\";\n else if (payload.role === \"user\" || payload.role === \"developer\") category = \"user\";\n }\n const text = textFrom(record, 0) || (category === \"metadata\" ? compact(record) : \"\");\n return {\n category,\n chars: text.length,\n tools,\n paths: pathCounts(text),\n toolName,\n mcp: inferMcpTool(toolName),\n metadataType: top + (ptype ? \":\" + ptype : \"\"),\n preview: eventPreview(text),\n };\n}\n\nfunction classifyClaude(record) {\n let category = \"other\";\n const tools = {};\n let toolName = \"\";\n const message = record.message && typeof record.message === \"object\" ? record.message : null;\n let text = \"\";\n if (message) {\n if (message.role === \"user\") category = \"user\";\n else if (message.role === \"assistant\") category = \"assistant\";\n const content = Array.isArray(message.content) ? message.content : [message.content];\n const parts = [];\n for (const part of content) {\n if (part && typeof part === \"object\") {\n if (part.type === \"tool_use\") {\n category = \"tool_call\";\n toolName = part.name ? String(part.name) : \"tool_use\";\n if (part.name) addCounter(tools, String(part.name), 1);\n } else if (part.type === \"tool_result\") {\n category = \"tool_output\";\n toolName = \"tool_result\";\n }\n else if (part.type === \"thinking\") category = \"reasoning\";\n }\n parts.push(textFrom(part, 0));\n }\n text = parts.join(\"\\n\");\n } else if (record.toolUseResult) {\n category = \"tool_output\";\n toolName = \"toolUseResult\";\n text = textFrom(record.toolUseResult, 0);\n } else if (record.attachment) {\n category = \"attachment\";\n text = textFrom(record.attachment, 0);\n } else {\n text = textFrom(record, 0);\n }\n return {\n category,\n chars: text.length,\n tools,\n paths: pathCounts(text),\n toolName,\n mcp: inferMcpTool(toolName),\n metadataType: String(record.type || message && message.role || \"record\"),\n preview: eventPreview(text),\n };\n}\n\nfunction summarizeCodex(file) {\n const stat = fs.statSync(file);\n const summary = {\n source: \"codex\",\n path: file,\n sessionId: sessionIdFromPath(file),\n title: \"\",\n cwd: \"\",\n startedAt: \"\",\n updatedAt: \"\",\n categories: {},\n tools: {},\n toolTokens: {},\n paths: {},\n metadata: {},\n mcpUsage: {},\n mcpTools: {},\n toolEvents: [],\n metadataEvents: [],\n observedTokens: 0,\n bytes: stat.size,\n mtime: stat.mtimeMs,\n };\n initTraceFields(summary);\n const records = readJsonl(file);\n for (const record of records) {\n const payload = record.payload && typeof record.payload === \"object\" ? record.payload : {};\n if (record.type === \"session_meta\") {\n summary.sessionId = String(payload.id || summary.sessionId);\n summary.cwd = String(payload.cwd || summary.cwd);\n summary.startedAt = String(payload.timestamp || summary.startedAt);\n }\n if (payload.cwd && !summary.cwd) summary.cwd = String(payload.cwd);\n if (record.timestamp) summary.updatedAt = String(record.timestamp);\n summary.observedTokens = Math.max(summary.observedTokens, observedCodexTokens(payload));\n addUsage(summary, codexUsageFromPayload(payload), String(record.timestamp || payload.timestamp || \"\"), \"turn \" + (summary.usage.turnsWithUsage + 1));\n const stats = classifyCodex(record);\n addCounter(summary.categories, stats.category, stats.chars);\n mergeCounter(summary.tools, stats.tools);\n if (stats.toolName && stats.category === \"tool_call\") {\n recordToolStep(summary, {\n tool: stats.toolName,\n input: codexToolInput(payload),\n ids: [payload.call_id, payload.id],\n timestamp: record.timestamp || payload.timestamp,\n preview: stats.preview,\n });\n addCounter(summary.toolTokens, stats.toolName, estimateTokens(stats.chars));\n if (stats.mcp.server) {\n addCounter(summary.mcpUsage, stats.mcp.server, 1);\n addCounter(summary.mcpTools, stats.mcp.server + \" / \" + (stats.mcp.tool || stats.toolName), 1);\n }\n if (summary.toolEvents.length < MAX_EVENT_SAMPLES) {\n summary.toolEvents.push({\n source: \"codex\",\n sessionId: summary.sessionId,\n timestamp: String(record.timestamp || payload.timestamp || \"\"),\n category: stats.category,\n tool: stats.toolName,\n mcpServer: stats.mcp.server,\n mcpTool: stats.mcp.tool,\n tokens: estimateTokens(stats.chars),\n preview: stats.preview,\n });\n }\n }\n if (stats.category === \"tool_output\") {\n const text = textFrom(payload, 0);\n markToolResult(summary, [payload.call_id, payload.id], outputLooksError(payload, text), text);\n }\n if (stats.category === \"metadata\") {\n addCounter(summary.metadata, stats.metadataType, stats.chars);\n if (summary.metadataEvents.length < MAX_EVENT_SAMPLES) {\n summary.metadataEvents.push({\n source: \"codex\",\n sessionId: summary.sessionId,\n timestamp: String(record.timestamp || payload.timestamp || \"\"),\n type: stats.metadataType,\n tokens: estimateTokens(stats.chars),\n preview: stats.preview,\n });\n }\n }\n mergeCounter(summary.paths, stats.paths);\n }\n summary.title = codexTitle(summary.sessionId) || summary.sessionId;\n return finalizeSummary(summary);\n}\n\nfunction summarizeClaude(file) {\n const stat = fs.statSync(file);\n const summary = {\n source: \"claude\",\n path: file,\n sessionId: sessionIdFromPath(file),\n title: \"\",\n cwd: \"\",\n startedAt: \"\",\n updatedAt: \"\",\n categories: {},\n tools: {},\n toolTokens: {},\n paths: {},\n metadata: {},\n mcpUsage: {},\n mcpTools: {},\n toolEvents: [],\n metadataEvents: [],\n observedTokens: 0,\n bytes: stat.size,\n mtime: stat.mtimeMs,\n };\n initTraceFields(summary);\n const records = readJsonl(file);\n for (const record of records) {\n summary.sessionId = String(record.sessionId || summary.sessionId);\n summary.cwd = String(record.cwd || summary.cwd);\n if (record.timestamp) {\n summary.updatedAt = String(record.timestamp);\n if (!summary.startedAt) summary.startedAt = String(record.timestamp);\n }\n if (record.message && typeof record.message === \"object\") {\n summary.observedTokens = Math.max(summary.observedTokens, claudeUsageTokens(record.message.usage));\n addUsage(summary, normalizedUsage(record.message.usage), String(record.timestamp || \"\"), \"turn \" + (summary.usage.turnsWithUsage + 1));\n if (!summary.title && record.message.role === \"user\") summary.title = cleanTitle(textFrom(record.message, 0)).slice(0, 90);\n for (const part of contentBlocks(record.message.content)) {\n if (!part || typeof part !== \"object\") continue;\n if (part.type === \"tool_use\") {\n recordToolStep(summary, {\n tool: part.name || \"tool_use\",\n input: part.input || {},\n ids: [part.id, record.uuid],\n timestamp: record.timestamp,\n preview: textFrom(part.input || part, 0),\n });\n } else if (part.type === \"tool_result\") {\n const text = textFrom(part, 0);\n markToolResult(summary, [part.tool_use_id, record.sourceToolAssistantUUID], part.is_error === true || outputLooksError(part, text), text);\n }\n }\n }\n if (record.toolUseResult) {\n const text = textFrom(record.toolUseResult, 0);\n markToolResult(summary, [record.toolUseID, record.toolUseId, record.sourceToolAssistantUUID], outputLooksError(record.toolUseResult, text), text);\n }\n const stats = classifyClaude(record);\n addCounter(summary.categories, stats.category, stats.chars);\n mergeCounter(summary.tools, stats.tools);\n if (stats.toolName && stats.category === \"tool_call\") {\n addCounter(summary.toolTokens, stats.toolName, estimateTokens(stats.chars));\n if (stats.mcp.server) {\n addCounter(summary.mcpUsage, stats.mcp.server, 1);\n addCounter(summary.mcpTools, stats.mcp.server + \" / \" + (stats.mcp.tool || stats.toolName), 1);\n }\n if (summary.toolEvents.length < MAX_EVENT_SAMPLES) {\n summary.toolEvents.push({\n source: \"claude\",\n sessionId: summary.sessionId,\n timestamp: String(record.timestamp || \"\"),\n category: stats.category,\n tool: stats.toolName,\n mcpServer: stats.mcp.server,\n mcpTool: stats.mcp.tool,\n tokens: estimateTokens(stats.chars),\n preview: stats.preview,\n });\n }\n }\n if (stats.category === \"metadata\") {\n addCounter(summary.metadata, stats.metadataType, stats.chars);\n if (summary.metadataEvents.length < MAX_EVENT_SAMPLES) {\n summary.metadataEvents.push({\n source: \"claude\",\n sessionId: summary.sessionId,\n timestamp: String(record.timestamp || \"\"),\n type: stats.metadataType,\n tokens: estimateTokens(stats.chars),\n preview: stats.preview,\n });\n }\n }\n mergeCounter(summary.paths, stats.paths);\n }\n if (!summary.title) summary.title = summary.sessionId;\n return finalizeSummary(summary);\n}\n\nfunction finalizeSummary(summary) {\n const totalChars = Object.values(summary.categories).reduce((sum, value) => sum + value, 0);\n summary.totalChars = totalChars;\n summary.tokens = summary.observedTokens || estimateTokens(totalChars);\n summary.tokenMethod = summary.observedTokens ? \"observed\" : \"estimated\";\n summary.totalSteps = summary.stepCount || (summary.steps || []).length;\n delete summary._callMap;\n delete summary._lastToolStep;\n return summary;\n}\n\nfunction walk(root) {\n const out = [];\n if (!fs.existsSync(root)) return out;\n const visit = (dir) => {\n let entries = [];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n const file = path.join(dir, entry.name);\n if (entry.isDirectory()) visit(file);\n else if (entry.isFile() && entry.name.endsWith(\".jsonl\")) out.push(file);\n }\n };\n visit(root);\n return out;\n}\n\nfunction encodedProject(project) {\n return path.resolve(project).replace(/\\//g, \"-\");\n}\n\nfunction pathInsideOrEqual(value, parent) {\n const relative = path.relative(parent, value);\n return relative === \"\" || (!!relative && !relative.startsWith(\"..\") && !path.isAbsolute(relative));\n}\n\nfunction candidateFiles(source, args) {\n if (args.sessionId) {\n const roots = source === \"codex\" ? [path.join(CODEX_DIR, \"sessions\"), path.join(CODEX_DIR, \"archived_sessions\")] : [path.join(CLAUDE_DIR, \"projects\")];\n return roots.flatMap(walk).filter((file) => file.includes(args.sessionId));\n }\n const since = parseSince(args.since);\n const roots = source === \"codex\" ? [path.join(CODEX_DIR, \"sessions\"), path.join(CODEX_DIR, \"archived_sessions\")] : [path.join(CLAUDE_DIR, \"projects\")];\n const projectFragment = encodedProject(args.project);\n const files = roots.flatMap(walk).filter((file) => {\n let stat;\n try {\n stat = fs.statSync(file);\n } catch {\n return false;\n }\n if (stat.mtimeMs < since) return false;\n if (args.allProjects || source === \"codex\") return true;\n return file.includes(projectFragment) || file.includes(path.basename(args.project));\n }).sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);\n return source === \"codex\" && !args.allProjects ? files : files.slice(0, args.scanLimit);\n}\n\nfunction projectMatches(session, args) {\n if (args.allProjects || args.sessionId) return true;\n const project = path.resolve(args.project);\n if (session.cwd && pathInsideOrEqual(path.resolve(session.cwd), project)) return true;\n return session.path.includes(encodedProject(project));\n}\n\nfunction collectSessions(args) {\n const sources = args.source === \"both\" ? [\"codex\", \"claude\"] : [args.source];\n const sessions = [];\n for (const source of sources) {\n for (const file of candidateFiles(source, args)) {\n try {\n const summary = source === \"codex\" ? summarizeCodex(file) : summarizeClaude(file);\n if (projectMatches(summary, args)) sessions.push(summary);\n } catch {}\n }\n }\n if (args.mode === \"current\" || args.sessionId) sessions.sort((a, b) => b.mtime - a.mtime);\n else sessions.sort((a, b) => b.tokens - a.tokens || b.mtime - a.mtime);\n return sessions.slice(0, args.last);\n}\n\nfunction aggregate(sessions) {\n const out = { categories: {}, tools: {}, toolTokens: {}, paths: {}, metadata: {}, mcpUsage: {}, mcpTools: {}, toolEvents: [], metadataEvents: [], steps: [], usage: emptyUsage() };\n for (const session of sessions) {\n mergeCounter(out.categories, session.categories);\n mergeCounter(out.tools, session.tools);\n mergeCounter(out.toolTokens, session.toolTokens);\n mergeCounter(out.paths, session.paths);\n mergeCounter(out.metadata, session.metadata);\n mergeCounter(out.mcpUsage, session.mcpUsage);\n mergeCounter(out.mcpTools, session.mcpTools);\n out.usage.inputTokens += (session.usage && session.usage.inputTokens) || 0;\n out.usage.outputTokens += (session.usage && session.usage.outputTokens) || 0;\n out.usage.cacheCreationInputTokens += (session.usage && session.usage.cacheCreationInputTokens) || 0;\n out.usage.cacheReadInputTokens += (session.usage && session.usage.cacheReadInputTokens) || 0;\n out.usage.cachedInputTokens += (session.usage && session.usage.cachedInputTokens) || 0;\n out.usage.reasoningOutputTokens += (session.usage && session.usage.reasoningOutputTokens) || 0;\n out.usage.totalTokens += (session.usage && session.usage.totalTokens) || 0;\n out.usage.turnsWithUsage += (session.usage && session.usage.turnsWithUsage) || 0;\n if (session.usage && session.usage.peakTurnTokens > out.usage.peakTurnTokens) {\n out.usage.peakTurnTokens = session.usage.peakTurnTokens;\n out.usage.peakTurnLabel = (session.title || session.sessionId) + \" · \" + session.usage.peakTurnLabel;\n }\n for (const step of session.steps || []) {\n if (out.steps.length < 2000) out.steps.push(Object.assign({ sessionTitle: session.title, cwd: session.cwd, sessionPath: session.path }, step));\n }\n for (const event of session.toolEvents || []) {\n if (out.toolEvents.length < 500) out.toolEvents.push(Object.assign({ sessionTitle: session.title, cwd: session.cwd, sessionPath: session.path }, event));\n }\n for (const event of session.metadataEvents || []) {\n if (out.metadataEvents.length < 500) out.metadataEvents.push(Object.assign({ sessionTitle: session.title, cwd: session.cwd, sessionPath: session.path }, event));\n }\n }\n return out;\n}\n\nfunction sortedEntries(counter, limit) {\n return Object.entries(counter || {}).sort((a, b) => b[1] - a[1]).slice(0, limit);\n}\n\nfunction normalizedTarget(value) {\n return String(value || \"\").replace(/^file:\\/\\//, \"\").replace(/:\\d+(:\\d+)?$/g, \"\").replace(/\\/+/g, \"/\").trim();\n}\n\nfunction severity(score) {\n if (score >= 90) return \"critical\";\n if (score >= 70) return \"high\";\n if (score >= 40) return \"medium\";\n if (score >= 15) return \"low\";\n return \"info\";\n}\n\nfunction addFinding(findings, rule, score, category, title, summary, evidence, recommendation, action) {\n findings.push({\n rule,\n severity: severity(score),\n score: Math.round(score),\n category,\n title,\n summary,\n evidence: (evidence || []).filter(Boolean).slice(0, 5),\n recommendation,\n action: action || \"prompt\",\n confidence: score >= 70 ? \"high\" : score >= 40 ? \"medium\" : \"low\",\n });\n}\n\nfunction cacheStability(session) {\n const series = session.usage && session.usage.series || [];\n if (series.length < 5) {\n return { classification: \"stable\", turnsAboveThreshold: 0, totalTurns: series.length, avgCacheCreationPct: 0, perTurnRatios: [] };\n }\n const ratios = series.map((turn) => {\n const total = (turn.inputTokens || 0) + (turn.cacheCreationInputTokens || 0) + (turn.cacheReadInputTokens || 0);\n return total ? (turn.cacheCreationInputTokens || 0) / total : 0;\n });\n const turnsAboveThreshold = ratios.filter((ratio) => ratio > 0.3).length;\n const mid = Math.floor(ratios.length / 2);\n const first = ratios.slice(0, mid);\n const second = ratios.slice(mid);\n const avg = (items) => items.length ? items.reduce((sum, item) => sum + item, 0) / items.length : 0;\n const firstAvg = avg(first);\n const secondAvg = avg(second);\n const classification = secondAvg > firstAvg && secondAvg > 0.15 ? \"degrading\" : turnsAboveThreshold > 5 ? \"churning\" : \"stable\";\n return {\n classification,\n turnsAboveThreshold,\n totalTurns: series.length,\n avgCacheCreationPct: avg(ratios) * 100,\n perTurnRatios: ratios.map((ratio) => Math.round(ratio * 1000) / 10),\n };\n}\n\nfunction contextGrowth(session) {\n const series = session.usage && session.usage.series || [];\n const perTurnInput = series.map((turn) => (turn.inputTokens || 0) + (turn.cacheCreationInputTokens || 0) + (turn.cacheReadInputTokens || 0));\n let growthFactor = 0;\n let flagged = false;\n if (perTurnInput.length > 5 && perTurnInput[4] > 0) {\n growthFactor = perTurnInput[perTurnInput.length - 1] / perTurnInput[4];\n flagged = growthFactor > 2;\n }\n let pressureWindows = 0;\n let peakWindowAvg = 0;\n if (perTurnInput.length >= PRESSURE_WINDOW) {\n for (let i = 0; i <= perTurnInput.length - PRESSURE_WINDOW; i++) {\n const window = perTurnInput.slice(i, i + PRESSURE_WINDOW);\n const avg = window.reduce((sum, value) => sum + value, 0) / PRESSURE_WINDOW;\n if (avg > PRESSURE_TOKENS) pressureWindows += 1;\n if (avg > peakWindowAvg) peakWindowAvg = avg;\n }\n }\n return {\n flagged,\n growthFactor: Math.round(growthFactor * 10) / 10,\n perTurnInput: perTurnInput.slice(0, 260),\n pressureWindows,\n peakWindowAvg: Math.round(peakWindowAvg),\n };\n}\n\nfunction duplicateReadGroups(sessions) {\n const groups = {};\n let duplicateCount = 0;\n for (const session of sessions) {\n const seen = {};\n const steps = (session.steps || []).slice().sort((a, b) => a.index - b.index);\n for (const step of steps) {\n const target = normalizedTarget(step.target);\n if (!target || !target.includes(\"/\")) continue;\n if (step.family === \"write\") {\n delete seen[target];\n continue;\n }\n if (step.family !== \"read\") continue;\n if (!seen[target]) {\n seen[target] = step;\n continue;\n }\n duplicateCount += 1;\n const key = session.sessionId + \"|\" + target;\n if (!groups[key]) {\n groups[key] = {\n path: target,\n sessionId: session.sessionId,\n sessionTitle: session.title,\n count: 1,\n firstIndex: seen[target].index,\n repeated: [],\n };\n }\n groups[key].count += 1;\n groups[key].repeated.push(step.index);\n }\n }\n return { duplicateCount, groups: Object.values(groups).sort((a, b) => b.count - a.count) };\n}\n\nfunction commandRetryGroups(sessions) {\n const retries = [];\n for (const session of sessions) {\n const steps = (session.steps || []).slice().sort((a, b) => a.index - b.index);\n let current = null;\n let streak = [];\n const flush = () => {\n if (current && streak.length >= 3) {\n retries.push({\n command: current,\n sessionId: session.sessionId,\n sessionTitle: session.title,\n count: streak.length,\n steps: streak.map((step) => step.index),\n });\n }\n };\n for (const step of steps) {\n const command = step.family === \"execute\" ? step.normalizedCommand : \"\";\n if (command && command === current) {\n streak.push(step);\n } else {\n flush();\n current = command || null;\n streak = command ? [step] : [];\n }\n }\n flush();\n }\n return retries.sort((a, b) => b.count - a.count);\n}\n\nfunction failedToolLoops(sessions) {\n const loops = [];\n for (const session of sessions) {\n const steps = (session.steps || []).slice().sort((a, b) => a.index - b.index);\n let currentTool = \"\";\n let streak = [];\n const flush = () => {\n if (currentTool && streak.length >= 3) {\n loops.push({\n tool: currentTool,\n sessionId: session.sessionId,\n sessionTitle: session.title,\n count: streak.length,\n steps: streak.map((step) => step.index),\n sample: streak[0] && (streak[0].errorPreview || streak[0].preview),\n });\n }\n };\n for (const step of steps) {\n if (step.isError && step.tool === currentTool) {\n streak.push(step);\n } else {\n flush();\n currentTool = step.isError ? step.tool : \"\";\n streak = step.isError ? [step] : [];\n }\n }\n flush();\n }\n return loops.sort((a, b) => b.count - a.count);\n}\n\nfunction dayKey(value) {\n const date = new Date(value || Date.now());\n if (!Number.isFinite(date.getTime())) return \"unknown\";\n return date.toISOString().slice(0, 10);\n}\n\nfunction analyzeContext(sessions, args, mcpServers) {\n const agg = aggregate(sessions);\n const steps = agg.steps || [];\n const familyCounts = {};\n let failedTools = 0;\n for (const step of steps) {\n addCounter(familyCounts, step.family || \"tool\", 1);\n if (step.isError) failedTools += 1;\n }\n const reads = familyCounts.read || 0;\n const searches = familyCounts.search || 0;\n const writes = familyCounts.write || 0;\n const executes = familyCounts.execute || 0;\n const agents = familyCounts.agent || 0;\n const mcpCalls = familyCounts.mcp || 0;\n const readSearch = reads + searches;\n const explorationRatio = writes ? readSearch / writes : readSearch;\n const usage = agg.usage || emptyUsage();\n const cacheDenom = (usage.inputTokens || 0) + (usage.cacheCreationInputTokens || 0) + (usage.cacheReadInputTokens || 0);\n const cacheHitRatio = cacheDenom ? (usage.cacheReadInputTokens || 0) / cacheDenom : 0;\n const cacheCreationPct = cacheDenom ? (usage.cacheCreationInputTokens || 0) / cacheDenom : 0;\n const duplicateReads = duplicateReadGroups(sessions);\n const retries = commandRetryGroups(sessions);\n const failureLoops = failedToolLoops(sessions);\n const cacheSessions = sessions.map((session) => Object.assign({ sessionId: session.sessionId, sessionTitle: session.title }, cacheStability(session)));\n const growthSessions = sessions.map((session) => Object.assign({ sessionId: session.sessionId, sessionTitle: session.title }, contextGrowth(session)));\n const churning = cacheSessions.filter((item) => item.classification !== \"stable\");\n const growing = growthSessions.filter((item) => item.flagged);\n const pressure = growthSessions.filter((item) => item.pressureWindows > 0);\n const byDay = {};\n for (const session of sessions) {\n const key = dayKey(session.updatedAt || session.startedAt || session.mtime);\n if (!byDay[key]) byDay[key] = { day: key, sessions: 0, tokens: 0, observed: 0, codex: 0, claude: 0 };\n byDay[key].sessions += 1;\n byDay[key].tokens += session.tokens || 0;\n if (session.tokenMethod === \"observed\") byDay[key].observed += 1;\n byDay[key][session.source] = (byDay[key][session.source] || 0) + 1;\n }\n const categoryTotal = Object.values(agg.categories).reduce((sum, value) => sum + value, 0);\n const toolOutputPct = pct(agg.categories.tool_output || 0, categoryTotal);\n const metadataPct = pct(agg.categories.metadata || 0, categoryTotal);\n const assistantPct = pct(agg.categories.assistant || 0, categoryTotal);\n const maxSession = sessions.length ? sessions.reduce((a, b) => a.tokens > b.tokens ? a : b) : null;\n const failureRate = steps.length ? failedTools / steps.length : 0;\n const findings = [];\n if (duplicateReads.duplicateCount > 0) {\n 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\");\n }\n if (retries.length) {\n 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\");\n }\n if (failureLoops.length || (failedTools >= 3 && failureRate > 0.12)) {\n 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\");\n }\n if (explorationRatio > 5 && readSearch >= 10) {\n 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\");\n }\n if (agents > 3) {\n 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\");\n }\n if (churning.length) {\n 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\");\n }\n if (growing.length) {\n 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\");\n }\n if (pressure.length) {\n 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\");\n }\n if (toolOutputPct > 45) {\n 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\");\n }\n if (metadataPct > 18) {\n 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\");\n }\n if (assistantPct > 45) {\n 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\");\n }\n if (maxSession && maxSession.tokens > 80000) {\n 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\");\n }\n if ((mcpServers || []).length > 12 && mcpCalls < 3) {\n 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\");\n }\n let score = 100;\n score -= Math.min(18, duplicateReads.duplicateCount * 2);\n score -= retries.length ? 14 : 0;\n score -= failureLoops.length ? 16 : Math.min(12, Math.round(failureRate * 60));\n score -= explorationRatio > 5 ? Math.min(15, Math.round((explorationRatio - 5) * 2)) : 0;\n score -= churning.length ? 14 : 0;\n score -= growing.length ? 12 : 0;\n score -= pressure.length ? 10 : 0;\n score -= toolOutputPct > 45 ? 8 : 0;\n score = Math.max(0, Math.min(100, Math.round(score)));\n findings.sort((a, b) => b.score - a.score);\n return {\n score,\n scoreLabel: score >= 85 ? \"healthy\" : score >= 70 ? \"watch\" : score >= 50 ? \"strained\" : \"critical\",\n metrics: {\n normalizedSteps: steps.length,\n failedTools,\n failureRate,\n successRate: steps.length ? 1 - failureRate : 1,\n familyCounts,\n reads,\n searches,\n writes,\n executes,\n agents,\n mcpCalls,\n explorationRatio: Math.round(explorationRatio * 10) / 10,\n duplicateReadCount: duplicateReads.duplicateCount,\n retryLoopCount: retries.length,\n failureLoopCount: failureLoops.length,\n cacheHitRatio,\n cacheCreationPct,\n tokenUsage: usage,\n observedSessionCoverage: sessions.length ? sessions.filter((session) => session.tokenMethod === \"observed\").length / sessions.length : 0,\n toolOutputPct,\n metadataPct,\n assistantPct,\n },\n findings,\n evidence: {\n duplicateReads: duplicateReads.groups.slice(0, 20),\n commandRetries: retries.slice(0, 20),\n failureLoops: failureLoops.slice(0, 20),\n cacheStability: cacheSessions,\n contextGrowth: growthSessions,\n },\n trends: {\n byDay: Object.values(byDay).sort((a, b) => a.day.localeCompare(b.day)),\n topTools: sortedEntries(agg.tools, 12),\n topPaths: sortedEntries(agg.paths, 12),\n topMetadata: sortedEntries(agg.metadata, 12),\n },\n sourceModel: {\n sourceProjectSession: sessions.map((session) => ({\n source: session.source,\n project: session.cwd || args.project,\n sessionId: session.sessionId,\n title: session.title,\n tokens: session.tokens,\n updatedAt: session.updatedAt,\n tokenMethod: session.tokenMethod,\n steps: session.totalSteps || 0,\n })),\n privacy: \"Metadata-first: the report stores counters, tool names, paths, short previews, and line offsets where available; transcript bodies stay local.\",\n },\n };\n}\n\nfunction recommendations(sessions, analysis) {\n if (!sessions.length) return [\"No matching sessions found. Try context-xray threads --all-projects --since 2w --open.\"];\n if (analysis && analysis.findings && analysis.findings.length) {\n const seen = {};\n const fromFindings = [];\n for (const finding of analysis.findings) {\n if (!finding.recommendation || seen[finding.recommendation]) continue;\n seen[finding.recommendation] = true;\n fromFindings.push(finding.recommendation);\n }\n if (fromFindings.length) return fromFindings.slice(0, 6);\n }\n const agg = aggregate(sessions);\n const total = Object.values(agg.categories).reduce((sum, value) => sum + value, 0);\n const tips = [];\n const toolOutput = pct(agg.categories.tool_output || 0, total);\n const instructions = pct(agg.categories.instructions || 0, total);\n const assistant = pct(agg.categories.assistant || 0, total);\n const maxSession = sessions.reduce((a, b) => a.tokens > b.tokens ? a : b);\n 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.\");\n 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.\");\n 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.\");\n 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.\");\n const topTool = sortedEntries(agg.tools, 1)[0];\n 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.\");\n const topPath = sortedEntries(agg.paths, 1)[0];\n 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.\");\n 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.\"];\n}\n\nfunction escapeHtml(value) {\n return String(value || \"\").replace(/[&<>\"']/g, (ch) => ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", \"\\\"\": \""\", \"'\": \"'\" }[ch]));\n}\n\nfunction cleanTitle(value) {\n return String(value || \"\").replace(/<[^>]+>/g, \" \").replace(/\\s+/g, \" \").trim();\n}\n\nfunction categoryBar(categories) {\n const total = Object.values(categories || {}).reduce((sum, value) => sum + value, 0);\n if (!total) return \"<div class=\\\"bar\\\"></div>\";\n return \"<div class=\\\"bar\\\">\" + CATEGORIES.map((cat) => {\n const value = categories[cat] || 0;\n if (!value) return \"\";\n const width = Math.max(1, pct(value, total));\n return \"<span title=\\\"\" + escapeHtml(LABELS[cat]) + \"\\\" style=\\\"width:\" + width.toFixed(2) + \"%;background:\" + COLORS[cat] + \"\\\"></span>\";\n }).join(\"\") + \"</div>\";\n}\n\nfunction readJsonFile(file) {\n try {\n return JSON.parse(fs.readFileSync(file, \"utf8\"));\n } catch {\n return null;\n }\n}\n\nfunction safeUrl(value) {\n try {\n const url = new URL(String(value || \"\"));\n return url.origin + url.pathname;\n } catch {\n return String(value || \"\").split(/[?#]/)[0].slice(0, 120);\n }\n}\n\nfunction mcpTarget(definition) {\n if (!definition || typeof definition !== \"object\") return \"configured\";\n if (definition.url) return safeUrl(definition.url);\n if (definition.command) return String(definition.command);\n if (definition.transport) return String(definition.transport);\n return \"configured\";\n}\n\nfunction addMcpServer(out, name, source, definition) {\n if (!name) return;\n const key = source + \":\" + name;\n if (out.some((server) => server.key === key)) return;\n out.push({\n key,\n name: String(name),\n source,\n target: mcpTarget(definition),\n });\n}\n\nfunction readMcpJson(file, source, out) {\n const json = readJsonFile(file);\n if (!json || typeof json !== \"object\") return;\n const servers = json.mcpServers || json.mcp_servers || json.servers;\n if (!servers || typeof servers !== \"object\") return;\n for (const name of Object.keys(servers)) addMcpServer(out, name, source, servers[name]);\n}\n\nfunction readCodexTomlServers(file, out) {\n let text = \"\";\n try {\n text = fs.readFileSync(file, \"utf8\");\n } catch {\n return;\n }\n const lines = text.split(/\\r?\\n/);\n for (let index = 0; index < lines.length; index++) {\n const match = lines[index].match(/^\\s*\\[mcp_servers\\.(?:\"([^\"]+)\"|([^\\]]+))\\]\\s*$/);\n if (!match) continue;\n const name = match[1] || match[2] || \"\";\n const section = {};\n for (let cursor = index + 1; cursor < lines.length; cursor++) {\n if (/^\\s*\\[/.test(lines[cursor])) break;\n const pair = lines[cursor].match(/^\\s*([A-Za-z0-9_-]+)\\s*=\\s*\"?([^\"]+)\"?\\s*$/);\n if (pair) section[pair[1]] = pair[2];\n }\n addMcpServer(out, name, \"Codex config\", section);\n }\n}\n\nfunction readMcpServers(project) {\n const out = [];\n readCodexTomlServers(path.join(CODEX_DIR, \"config.toml\"), out);\n readMcpJson(path.join(HOME, \".claude.json\"), \"Claude user config\", out);\n readMcpJson(path.join(CLAUDE_DIR, \"settings.json\"), \"Claude settings\", out);\n readMcpJson(path.join(path.resolve(project), \".mcp.json\"), \"Project .mcp.json\", out);\n readMcpJson(path.join(path.resolve(project), \".cursor\", \"mcp.json\"), \"Project Cursor MCP\", out);\n readMcpJson(path.join(HOME, \".cowork\", \"mcp.json\"), \"Cowork MCP\", out);\n return out.sort((a, b) => a.name.localeCompare(b.name) || a.source.localeCompare(b.source));\n}\n\nfunction sourceCounts(sessions) {\n return sessions.reduce((counts, session) => {\n counts[session.source] = (counts[session.source] || 0) + 1;\n return counts;\n }, {});\n}\n\nfunction buildReport(sessions, args) {\n const agg = aggregate(sessions);\n const mcpServers = readMcpServers(args.project);\n const analysis = analyzeContext(sessions, args, mcpServers);\n return {\n generatedAt: new Date().toISOString(),\n generatedLabel: new Date().toLocaleString(),\n mode: args.mode,\n source: args.source,\n since: args.since,\n project: path.resolve(args.project),\n sessionCount: sessions.length,\n sourceCounts: sourceCounts(sessions),\n totalTokens: sessions.reduce((sum, s) => sum + s.tokens, 0),\n categories: agg.categories,\n tools: sortedEntries(agg.tools, 50),\n toolTokens: sortedEntries(agg.toolTokens, 50),\n paths: sortedEntries(agg.paths, 50),\n metadata: sortedEntries(agg.metadata, 50),\n mcpUsage: sortedEntries(agg.mcpUsage, 50),\n mcpTools: sortedEntries(agg.mcpTools, 50),\n mcpServers,\n analysis,\n recommendations: recommendations(sessions, analysis),\n toolEvents: agg.toolEvents,\n metadataEvents: agg.metadataEvents,\n steps: agg.steps,\n sessions,\n };\n}\n\nfunction jsonScript(value) {\n return JSON.stringify(value).replace(/</g, \"\\\\u003c\");\n}\n\nfunction overviewEnhancementScript() {\n return \"(\" + function () {\n const dataEl = document.getElementById(\"xray-data\");\n if (!dataEl) return;\n const report = JSON.parse(dataEl.textContent || \"{}\");\n const cats = [\"user\", \"assistant\", \"tool_call\", \"tool_output\", \"reasoning\", \"instructions\", \"attachment\", \"metadata\", \"other\"];\n const labels = {\n user: \"User asks\",\n assistant: \"Assistant text\",\n tool_call: \"Tool calls\",\n tool_output: \"Tool output\",\n reasoning: \"Reasoning\",\n instructions: \"Instructions/context\",\n attachment: \"Attachments\",\n metadata: \"Metadata\",\n other: \"Other\",\n };\n const colors = {\n user: \"#8ba8ff\",\n assistant: \"#55b982\",\n tool_call: \"#f0a85b\",\n tool_output: \"#e06b73\",\n reasoning: \"#a77be8\",\n instructions: \"#6ac3d5\",\n attachment: \"#d6a85a\",\n metadata: \"#9aa3ad\",\n other: \"#c3c8ce\",\n };\n\n function esc(value) {\n return String(value || \"\").replace(/[&<>\"']/g, function (ch) {\n return { \"&\": \"&\", \"<\": \"<\", \">\": \">\", \"\\\"\": \""\", \"'\": \"'\" }[ch];\n });\n }\n\n function fmt(value) {\n const number = Number(value) || 0;\n if (number >= 1000000) return (number / 1000000).toFixed(1) + \"m\";\n if (number >= 1000) return (number / 1000).toFixed(1) + \"k\";\n return String(number);\n }\n\n function tok(chars) {\n chars = Number(chars) || 0;\n return chars > 0 ? Math.max(1, Math.ceil(chars / 4)) : 0;\n }\n\n function totalCounter(counter) {\n return Object.keys(counter || {}).reduce(function (sum, key) {\n return sum + (Number(counter[key]) || 0);\n }, 0);\n }\n\n function pct(part, total) {\n return total > 0 ? Math.max(0, Math.min(100, (part / total) * 100)) : 0;\n }\n\n function actionRow(kind, value, title, subtitle, right, farRight, color) {\n 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>\";\n }\n\n function bar(counter) {\n const total = totalCounter(counter);\n if (!total) return \"<div class=\\\"bar\\\"></div>\";\n return \"<div class=\\\"bar\\\">\" + cats.map(function (cat) {\n const value = (counter || {})[cat] || 0;\n if (!value) return \"\";\n 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>\";\n }).join(\"\") + \"</div>\";\n }\n\n function topTools(limit) {\n return (report.tools || []).slice(0, limit || 8);\n }\n\n function toolTokens(name) {\n const found = (report.toolTokens || []).filter(function (entry) {\n return entry[0] === name;\n })[0];\n return found ? found[1] : 0;\n }\n\n function sessionMatches(kind, value) {\n return (report.sessions || []).filter(function (session) {\n if (kind === \"category\") return !!((session.categories || {})[value]);\n if (kind === \"path\") return !!((session.paths || {})[value]);\n return false;\n }).sort(function (a, b) {\n const left = kind === \"path\" ? (a.paths || {})[value] || 0 : (a.categories || {})[value] || 0;\n const right = kind === \"path\" ? (b.paths || {})[value] || 0 : (b.categories || {})[value] || 0;\n return right - left;\n });\n }\n\n function sampleCards(events) {\n events = (events || []).slice(0, 8);\n if (!events.length) return \"<p class=\\\"empty\\\">No sampled records for this item.</p>\";\n return \"<div class=\\\"detail-list\\\">\" + events.map(function (event) {\n 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>\";\n }).join(\"\") + \"</div>\";\n }\n\n function sessionCards(sessions, kind, value) {\n sessions = (sessions || []).slice(0, 8);\n if (!sessions.length) return \"<p class=\\\"empty\\\">No matching sessions for this item.</p>\";\n return \"<div class=\\\"detail-list\\\">\" + sessions.map(function (session) {\n const amount = kind === \"path\" ? (session.paths || {})[value] || 0 : tok((session.categories || {})[value] || 0);\n const unit = kind === \"path\" ? \"mentions\" : \"tok\";\n 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>\";\n }).join(\"\") + \"</div>\";\n }\n\n function openDetailTab(tab, selector, value) {\n const tabButton = document.querySelector(\"[data-tab=\\\"\" + tab + \"\\\"]\");\n if (tabButton) tabButton.click();\n if (!selector) return;\n window.setTimeout(function () {\n const rows = Array.prototype.slice.call(document.querySelectorAll(selector));\n const row = rows.filter(function (item) {\n return item.getAttribute(selector.slice(1, -1)) === value;\n })[0];\n if (row) row.click();\n }, 0);\n }\n\n function jumpButton(tab, label, selector, value) {\n 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>\";\n }\n\n function setDetail(kind, value) {\n const el = document.getElementById(\"overview-drilldown\");\n if (!el) return;\n if (kind === \"tool\") {\n const events = (report.toolEvents || []).filter(function (event) {\n return event.tool === value;\n });\n 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);\n } else if (kind === \"metadata\") {\n const events = (report.metadataEvents || []).filter(function (event) {\n return event.type === value;\n });\n 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);\n } else if (kind === \"path\") {\n 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\", \"\", \"\");\n } else {\n const chars = (report.categories || {})[value] || 0;\n const sessions = sessionMatches(\"category\", value);\n let extra = \"\";\n if (value === \"tool_call\") {\n extra = \"<div class=\\\"mini-label\\\">Top tools</div>\" + topTools(6).map(function (entry) {\n return actionRow(\"tool\", entry[0], entry[0], \"Click for sampled calls\", \"x\" + entry[1], fmt(toolTokens(entry[0])) + \" tok\");\n }).join(\"\");\n } else if (value === \"metadata\") {\n extra = \"<div class=\\\"mini-label\\\">Metadata types</div>\" + (report.metadata || []).slice(0, 6).map(function (entry) {\n return actionRow(\"metadata\", entry[0], entry[0], \"Click for sampled records\", fmt(tok(entry[1])) + \" tok\", fmt(entry[1]) + \" ch\");\n }).join(\"\");\n }\n 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));\n }\n bindOverview();\n }\n\n function bindOverview() {\n Array.prototype.forEach.call(document.querySelectorAll(\"[data-overview-kind]\"), function (button) {\n button.onclick = function () {\n setDetail(button.getAttribute(\"data-overview-kind\"), button.getAttribute(\"data-overview-value\"));\n };\n });\n Array.prototype.forEach.call(document.querySelectorAll(\"[data-overview-jump]\"), function (button) {\n button.onclick = function () {\n const selector = button.getAttribute(\"data-overview-selector\");\n const value = button.getAttribute(\"data-overview-value\");\n openDetailTab(button.getAttribute(\"data-overview-jump\"), selector, value);\n };\n });\n }\n\n function renderOverview() {\n const panel = document.getElementById(\"panel-overview\");\n if (!panel) return;\n const total = totalCounter(report.categories);\n const categoryRows = cats.map(function (cat) {\n const chars = (report.categories || {})[cat] || 0;\n if (!chars) return \"\";\n return actionRow(\"category\", cat, labels[cat], \"Click for sessions and hotspots\", fmt(tok(chars)) + \" tok\", pct(chars, total).toFixed(0) + \"%\", colors[cat]);\n }).join(\"\");\n const toolRows = topTools(8).map(function (entry) {\n return actionRow(\"tool\", entry[0], entry[0], \"Click for sampled calls\", \"x\" + entry[1], fmt(toolTokens(entry[0])) + \" tok\");\n }).join(\"\") || \"<p class=\\\"empty\\\">No tool calls detected.</p>\";\n const pathRows = (report.paths || []).slice(0, 8).map(function (entry) {\n return actionRow(\"path\", entry[0], entry[0], \"Click for matching sessions\", \"x\" + entry[1], \"\");\n }).join(\"\") || \"<p class=\\\"empty\\\">No repeated paths detected.</p>\";\n const metadataRows = (report.metadata || []).slice(0, 8).map(function (entry) {\n return actionRow(\"metadata\", entry[0], entry[0], \"Click for sampled records\", fmt(tok(entry[1])) + \" tok\", fmt(entry[1]) + \" ch\");\n }).join(\"\") || \"<p class=\\\"empty\\\">No metadata-heavy records detected.</p>\";\n 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>\";\n bindOverview();\n }\n\n renderOverview();\n }.toString() + \")();\";\n}\n\nfunction insightsEnhancementScript() {\n return \"(\" + function () {\n const dataEl = document.getElementById(\"xray-data\");\n if (!dataEl) return;\n const report = JSON.parse(dataEl.textContent || \"{}\");\n const analysis = report.analysis || {};\n const metrics = analysis.metrics || {};\n const findings = analysis.findings || [];\n const cats = [\"user\", \"assistant\", \"tool_call\", \"tool_output\", \"reasoning\", \"instructions\", \"attachment\", \"metadata\", \"other\"];\n const labels = {\n user: \"User asks\",\n assistant: \"Assistant text\",\n tool_call: \"Tool calls\",\n tool_output: \"Tool output\",\n reasoning: \"Reasoning\",\n instructions: \"Instructions/context\",\n attachment: \"Attachments\",\n metadata: \"Metadata\",\n other: \"Other\",\n };\n const colors = {\n user: \"#2563eb\",\n assistant: \"#16a34a\",\n tool_call: \"#d97706\",\n tool_output: \"#dc2626\",\n reasoning: \"#7c3aed\",\n instructions: \"#0891b2\",\n attachment: \"#b45309\",\n metadata: \"#64748b\",\n other: \"#94a3b8\",\n };\n\n function esc(value) {\n return String(value || \"\").replace(/[&<>\"']/g, function (ch) {\n return { \"&\": \"&\", \"<\": \"<\", \">\": \">\", \"\\\"\": \""\", \"'\": \"'\" }[ch];\n });\n }\n\n function fmt(value) {\n const number = Number(value) || 0;\n if (Math.abs(number) >= 1000000) return (number / 1000000).toFixed(1) + \"m\";\n if (Math.abs(number) >= 1000) return (number / 1000).toFixed(1) + \"k\";\n return String(Math.round(number * 10) / 10);\n }\n\n function pctText(value) {\n return Math.round((Number(value) || 0) * 100) + \"%\";\n }\n\n function totalCounter(counter) {\n return Object.keys(counter || {}).reduce(function (sum, key) {\n return sum + (Number(counter[key]) || 0);\n }, 0);\n }\n\n function tok(chars) {\n chars = Number(chars) || 0;\n return chars > 0 ? Math.max(1, Math.ceil(chars / 4)) : 0;\n }\n\n function addStyle() {\n if (document.getElementById(\"xray-insights-style\")) return;\n const style = document.createElement(\"style\");\n style.id = \"xray-insights-style\";\n style.textContent = [\n \".health-shell{display:grid;grid-template-columns:270px minmax(0,1fr);gap:14px;align-items:stretch;margin-bottom:14px}\",\n \".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)}\",\n \".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}\",\n \".health-dial:after{content:\\\"\\\";position:absolute;inset:9px;border-radius:inherit;background:hsl(var(--card))}\",\n \".health-score{position:relative;z-index:1;font-size:30px;font-weight:750;letter-spacing:0}\",\n \".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}\",\n \".metric-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}\",\n \".metric-tile{border:1px solid hsl(var(--border));border-radius:8px;padding:12px;background:hsl(var(--card));min-height:84px}\",\n \".metric-tile strong{display:block;font-size:22px;line-height:1.1;margin-top:7px}\",\n \".metric-tile span{font-size:11px;text-transform:uppercase;letter-spacing:.08em;font-weight:650;color:hsl(var(--muted-foreground))}\",\n \".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}\",\n \".finding-button:hover,.finding-button.active{background:hsl(var(--accent)/.45);border-color:hsl(var(--ring)/.45)}\",\n \".finding-title{font-size:14px;font-weight:650}.finding-summary{font-size:12px;color:hsl(var(--muted-foreground));margin-top:4px;overflow-wrap:anywhere}\",\n \".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))}\",\n \".severity.critical,.severity.high{color:#dc2626;border-color:rgba(220,38,38,.28);background:rgba(220,38,38,.08)}\",\n \".severity.medium{color:#b45309;border-color:rgba(180,83,9,.28);background:rgba(180,83,9,.08)}\",\n \".severity.low,.severity.info{color:#0369a1;border-color:rgba(3,105,161,.26);background:rgba(3,105,161,.08)}\",\n \".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}\",\n \".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)}\",\n \".inline-bar{height:8px;border-radius:999px;background:hsl(var(--muted));overflow:hidden;margin-top:8px}.inline-bar span{display:block;height:100%}\",\n \".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}\",\n \".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)}\",\n \".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}\",\n \".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))}\",\n \"@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}}\",\n \"@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}}\",\n ].join(\"\");\n document.head.appendChild(style);\n }\n\n function ensureTab(name, label) {\n const nav = document.querySelector(\".tabs\");\n if (!nav || document.querySelector(\"[data-tab=\\\"\" + name + \"\\\"]\")) return;\n const button = document.createElement(\"button\");\n button.className = \"tab\";\n button.setAttribute(\"data-tab\", name);\n button.type = \"button\";\n button.textContent = label;\n nav.appendChild(button);\n }\n\n function ensurePanel(name) {\n if (document.getElementById(\"panel-\" + name)) return;\n const footer = document.querySelector(\".footer\");\n const panel = document.createElement(\"section\");\n panel.className = \"panel\";\n panel.id = \"panel-\" + name;\n if (footer && footer.parentNode) footer.parentNode.insertBefore(panel, footer);\n }\n\n function activate(name) {\n Array.prototype.forEach.call(document.querySelectorAll(\".tab\"), function (tab) {\n tab.classList.toggle(\"active\", tab.getAttribute(\"data-tab\") === name);\n });\n Array.prototype.forEach.call(document.querySelectorAll(\".panel\"), function (panel) {\n panel.classList.toggle(\"active\", panel.id === \"panel-\" + name);\n });\n if (location.hash !== \"#\" + name) history.replaceState(null, \"\", \"#\" + name);\n }\n\n function wireTabs() {\n Array.prototype.forEach.call(document.querySelectorAll(\".tab\"), function (tab) {\n tab.onclick = function () {\n activate(tab.getAttribute(\"data-tab\"));\n };\n });\n }\n\n function metric(label, value, detail) {\n return \"<div class=\\\"metric-tile\\\"><span>\" + esc(label) + \"</span><strong>\" + esc(value) + \"</strong><p class=\\\"meta\\\">\" + esc(detail || \"\") + \"</p></div>\";\n }\n\n function findingButton(finding, index) {\n 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>\";\n }\n\n function renderFindingDetail(index, targetId) {\n const finding = findings[index] || findings[0];\n const el = document.getElementById(targetId || \"finding-detail\");\n if (!el) return;\n if (!finding) {\n el.innerHTML = \"<h2>No Findings</h2><p class=\\\"empty\\\">No deterministic warning crossed the reporting threshold.</p>\";\n return;\n }\n const evidence = (finding.evidence || []).map(function (item) {\n return \"<div class=\\\"evidence-item\\\">\" + esc(item) + \"</div>\";\n }).join(\"\") || \"<p class=\\\"empty\\\">No evidence sample captured.</p>\";\n 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>\";\n }\n\n function categoryRows() {\n const total = totalCounter(report.categories);\n return cats.map(function (cat) {\n const chars = (report.categories || {})[cat] || 0;\n if (!chars) return \"\";\n const percentage = total ? chars / total : 0;\n 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>\";\n }).join(\"\");\n }\n\n function renderStats() {\n const stats = document.getElementById(\"stats\");\n if (!stats) return;\n stats.innerHTML = metric(\"Health\", (analysis.score || 0) + \"/100\", analysis.scoreLabel || \"unknown\") +\n metric(\"Findings\", findings.length, findings.length ? findings[0].title : \"no threshold crossed\") +\n metric(\"Cache hit\", pctText(metrics.cacheHitRatio || 0), \"create \" + pctText(metrics.cacheCreationPct || 0)) +\n metric(\"Tool success\", pctText(metrics.successRate == null ? 1 : metrics.successRate), fmt(metrics.normalizedSteps || 0) + \" normalized steps\");\n }\n\n function renderOverview() {\n const panel = document.getElementById(\"panel-overview\");\n if (!panel) return;\n const topFindings = findings.slice(0, 4).map(findingButton).join(\"\") || \"<p class=\\\"empty\\\">No major issues found. Recent sessions look balanced.</p>\";\n 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>\";\n Array.prototype.forEach.call(panel.querySelectorAll(\"[data-finding]\"), function (button) {\n button.onclick = function () {\n Array.prototype.forEach.call(panel.querySelectorAll(\"[data-finding]\"), function (item) { item.classList.remove(\"active\"); });\n button.classList.add(\"active\");\n renderFindingDetail(Number(button.getAttribute(\"data-finding\")), \"overview-finding-detail\");\n };\n });\n Array.prototype.forEach.call(panel.querySelectorAll(\"[data-category]\"), function (button) {\n button.onclick = function () {\n const category = button.getAttribute(\"data-category\");\n const sessions = (report.sessions || []).filter(function (session) { return (session.categories || {})[category]; }).slice(0, 4);\n const detail = document.getElementById(\"overview-finding-detail\");\n if (!detail) return;\n 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>\";\n };\n });\n renderFindingDetail(0, \"overview-finding-detail\");\n const first = panel.querySelector(\"[data-finding]\");\n if (first) first.classList.add(\"active\");\n }\n\n function renderFindings() {\n const panel = document.getElementById(\"panel-findings\");\n if (!panel) return;\n 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>\";\n Array.prototype.forEach.call(panel.querySelectorAll(\"[data-finding]\"), function (button) {\n button.onclick = function () {\n Array.prototype.forEach.call(panel.querySelectorAll(\"[data-finding]\"), function (item) { item.classList.remove(\"active\"); });\n button.classList.add(\"active\");\n renderFindingDetail(Number(button.getAttribute(\"data-finding\")), \"finding-detail\");\n };\n });\n renderFindingDetail(0, \"finding-detail\");\n const first = panel.querySelector(\"[data-finding]\");\n if (first) first.classList.add(\"active\");\n }\n\n function renderTrends() {\n const panel = document.getElementById(\"panel-trends\");\n if (!panel) return;\n const days = (analysis.trends && analysis.trends.byDay || []).slice(-14);\n const maxTokens = days.reduce(function (max, day) { return Math.max(max, day.tokens || 0); }, 1);\n const dayRows = days.map(function (day) {\n 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>\";\n }).join(\"\") || \"<p class=\\\"empty\\\">No trend window found.</p>\";\n const toolRows = (analysis.trends && analysis.trends.topTools || []).slice(0, 10).map(function (entry) {\n return \"<tr><td>\" + esc(entry[0]) + \"</td><td>\" + esc(entry[1]) + \"</td></tr>\";\n }).join(\"\") || \"<tr><td>No tools detected</td><td></td></tr>\";\n const pathRows = (analysis.trends && analysis.trends.topPaths || []).slice(0, 10).map(function (entry) {\n return \"<tr><td>\" + esc(entry[0]) + \"</td><td>\" + esc(entry[1]) + \"</td></tr>\";\n }).join(\"\") || \"<tr><td>No paths detected</td><td></td></tr>\";\n 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>\";\n }\n\n function turnHeight(turn, max) {\n const value = Number(turn.totalTokens || turn.inputTokens || 0);\n return Math.max(4, Math.round((value / Math.max(1, max)) * 40));\n }\n\n function renderTimeline() {\n const panel = document.getElementById(\"panel-timeline\");\n if (!panel) return;\n const sessions = (report.sessions || []).slice(0, 30);\n panel.innerHTML = \"<div class=\\\"timeline-list\\\">\" + (sessions.map(function (session) {\n const series = session.usage && session.usage.series || [];\n const maxTurn = series.reduce(function (max, turn) { return Math.max(max, turn.totalTokens || turn.inputTokens || 0); }, 1);\n const strip = series.length ? series.map(function (turn, index) {\n const cache = (turn.cacheReadInputTokens || 0) + (turn.cachedInputTokens || 0);\n const color = turn.cacheCreationInputTokens > turn.inputTokens ? \"#b45309\" : cache > turn.inputTokens ? \"#16a34a\" : \"#2563eb\";\n return \"<button title=\\\"\" + esc((turn.label || \"turn \" + (index + 1)) + \" · \" + fmt(turn.totalTokens || 0) + \" tokens\") + \"\\\" style=\\\"height:\" + turnHeight(turn, maxTurn) + \"px;background:\" + color + \"\\\"></button>\";\n }).join(\"\") : \"<p class=\\\"empty\\\">No observed per-turn token series in this session.</p>\";\n const cache = cacheStabilityClient(session);\n const growth = contextGrowthClient(session);\n 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>\";\n }).join(\"\") || \"<section class=\\\"card\\\"><p class=\\\"empty\\\">No sessions found.</p></section>\") + \"</div>\";\n }\n\n function cacheStabilityClient(session) {\n const match = (analysis.evidence && analysis.evidence.cacheStability || []).filter(function (item) { return item.sessionId === session.sessionId; })[0];\n return match || { classification: \"unknown\" };\n }\n\n function contextGrowthClient(session) {\n const match = (analysis.evidence && analysis.evidence.contextGrowth || []).filter(function (item) { return item.sessionId === session.sessionId; })[0];\n return match || { growthFactor: 0 };\n }\n\n function renderSources() {\n const panel = document.getElementById(\"panel-sources\");\n if (!panel) return;\n const rows = (analysis.sourceModel && analysis.sourceModel.sourceProjectSession || []).slice(0, 60).map(function (item) {\n 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>\";\n }).join(\"\") || \"<p class=\\\"empty\\\">No source records found.</p>\";\n 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>\";\n }\n\n addStyle();\n ensureTab(\"findings\", \"Findings\");\n ensureTab(\"timeline\", \"Timeline\");\n ensureTab(\"trends\", \"Trends\");\n ensureTab(\"sources\", \"Sources\");\n ensurePanel(\"findings\");\n ensurePanel(\"timeline\");\n ensurePanel(\"trends\");\n ensurePanel(\"sources\");\n wireTabs();\n renderStats();\n renderOverview();\n renderFindings();\n renderTimeline();\n renderTrends();\n renderSources();\n const initial = (location.hash || \"#overview\").slice(1);\n if (document.getElementById(\"panel-\" + initial)) activate(initial);\n }.toString() + \")();\";\n}\n\nfunction renderHtml(sessions, args) {\n const report = buildReport(sessions, args);\n 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>\" +\n \"@import url(\\\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\\\");\" +\n \":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);}\" +\n \"@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;}}\" +\n \"*{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}}\" +\n \"</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>\" +\n \"(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);})();\" +\n \"</script><script>\" + overviewEnhancementScript() + \"</script><script>\" + insightsEnhancementScript() + \"</script></body></html>\";\n}\n\nfunction writeJson(sessions, args, file) {\n fs.writeFileSync(file, JSON.stringify(buildReport(sessions, args), null, 2));\n}\n\nfunction openUrl(url) {\n const cmd = process.platform === \"darwin\" ? \"open\" : process.platform === \"win32\" ? \"cmd\" : \"xdg-open\";\n const args = process.platform === \"win32\" ? [\"/c\", \"start\", \"\", url] : [url];\n try {\n childProcess.spawn(cmd, args, { detached: true, stdio: \"ignore\" }).unref();\n } catch {}\n}\n\nfunction printSummary(sessions, args, file, url) {\n const total = sessions.reduce((sum, s) => sum + s.tokens, 0);\n const analysis = analyzeContext(sessions, args, readMcpServers(args.project));\n const topFinding = analysis.findings && analysis.findings[0];\n console.log(\"Context X-Ray: analyzed \" + sessions.length + \" session(s), about \" + fmtTokens(total) + \" observed/estimated tokens.\");\n if (url) console.log(\"Open: \" + url);\n else console.log(\"Report: \" + file);\n console.log(\"Health: \" + analysis.score + \"/100 (\" + analysis.scoreLabel + \")\" + (topFinding ? \" · \" + topFinding.title : \"\"));\n if (topFinding && topFinding.evidence && topFinding.evidence.length) console.log(\"Evidence: \" + topFinding.evidence[0]);\n console.log(\"\");\n for (const tip of recommendations(sessions, analysis).slice(0, 4)) console.log(\"- \" + tip);\n const tools = sortedEntries(aggregate(sessions).tools, 5);\n if (tools.length) console.log(\"- Frequent tools: \" + tools.map((entry) => entry[0] + \" x\" + entry[1]).join(\", \"));\n}\n\nfunction main() {\n const args = parseArgs(process.argv.slice(2));\n if (args.help) return help();\n const sessions = collectSessions(args);\n mkdirp(OUT_DIR);\n const suffix = args.format === \"json\" ? \"json\" : \"html\";\n const file = args.out || path.join(OUT_DIR, \"context-xray-\" + new Date().toISOString().replace(/[:.]/g, \"-\") + \".\" + suffix);\n mkdirp(path.dirname(file));\n if (args.format === \"json\") writeJson(sessions, args, file);\n else fs.writeFileSync(file, renderHtml(sessions, args));\n const url = args.open && args.format === \"html\" ? pathToFileURL(file).href : \"\";\n if (url) openUrl(url);\n printSummary(sessions, args, file, url);\n}\n\nmain();\n`;\n\nexport const CONTEXT_XRAY_SKILL_MD = `---\nname: context-xray\ndescription: >-\n Visualize local Codex and Claude Code context usage, open an inline/browser\n report, flag warnings, and suggest prompt/tooling optimizations. Use when the\n user types /context-xray, asks where context is going, wants recent local\n coding-agent trends, or wants to improve context efficiency.\nmetadata:\n visibility: exported\n---\n\n# Context X-Ray\n\nUse the locally installed Context X-Ray command to visualize recent Codex and\nClaude Code context usage. It reads local transcript files only and does not\nupload transcript content.\n\nProject-scoped installs write only project \\`.agents\\` skill and command\nartifacts; user-scoped installs write global Codex/Claude instructions.\n\n## Run\n\nCurrent or most recent local thread:\n\n\\`\\`\\`sh\n~/.agent-native/context-xray/context-xray --open\n\\`\\`\\`\n\nThread picker / recent sessions:\n\n\\`\\`\\`sh\n~/.agent-native/context-xray/context-xray threads --open\n\\`\\`\\`\n\nWeekly trends:\n\n\\`\\`\\`sh\n~/.agent-native/context-xray/context-xray trends --since 7d --open\n\\`\\`\\`\n\nExact session when the host exposes one:\n\n\\`\\`\\`sh\n~/.agent-native/context-xray/context-xray --session-id \"$CLAUDE_CODE_SESSION_ID\" --open\n\\`\\`\\`\n\nAfter running, report the link, the number of sessions analyzed, the largest\ncontext buckets, and 3-5 specific optimizations.\n\\`--open\\` opens the generated local HTML file directly and does not keep a\nbackground report server running.\n\n## Interpret\n\n- Treat the Overview score as a triage signal, then open Findings for evidence.\n- Repeated file reads: ask the agent to keep a short file-role note and reopen\n only when exact line numbers are needed.\n- Retry loops or failed tool loops: ask the agent to stop after two identical\n failures, summarize the error, and change strategy before rerunning.\n- Exploration heavy: give an inspection budget, then ask for a short\n implementation plan before more reading.\n- Cache churn or context growth: move stable instructions into skills or repo\n docs and continue large work from a compact handoff summary.\n- Tool output heavy: ask the agent to cap command output, summarize failures,\n and only expand logs when exact lines matter.\n- Metadata heavy: use Metadata/Sources drilldowns for protocol overhead, but\n prioritize prompt changes around user/tool/output buckets first.\n`;\n\nexport const CONTEXT_XRAY_COMMAND_MD = `---\ndescription: Visualize local Codex/Claude context usage and get optimization tips.\nargument-hint: [current|threads|trends|--since 7d]\n---\n\nRun Context X-Ray locally and show the user the generated report link plus the\ntop warnings.\n\nChoose the command from the user's arguments:\n\n- No arguments or \\`current\\`:\n \\`~/.agent-native/context-xray/context-xray --open\\`\n- \\`threads\\`:\n \\`~/.agent-native/context-xray/context-xray threads --open\\`\n- \\`trends\\`:\n \\`~/.agent-native/context-xray/context-xray trends --since 7d --open\\`\n\nIf \\`$ARGUMENTS\\` includes flags such as \\`--since 24h\\`, \\`--last 20\\`, or\n\\`--all-projects\\`, pass them through to the command. If the host exposes\n\\`CLAUDE_CODE_SESSION_ID\\`, prefer:\n\n\\`\\`\\`sh\n~/.agent-native/context-xray/context-xray --session-id \"$CLAUDE_CODE_SESSION_ID\" --open\n\\`\\`\\`\n\n\\`--open\\` opens a local HTML report file directly; there should not be a\nlong-running server process to monitor.\n\nAfter the command finishes, summarize:\n\n- the report link\n- sessions analyzed\n- the health score and most important finding\n- one concrete evidence point\n- two or three promptable ways to improve this thread\n`;\n\nfunction codexHome(): string {\n return process.env.CODEX_HOME?.trim() || path.join(os.homedir(), \".codex\");\n}\n\nfunction writeExecutable(file: string, content: string): void {\n fs.mkdirSync(path.dirname(file), { recursive: true });\n fs.writeFileSync(file, content, \"utf-8\");\n fs.chmodSync(file, 0o755);\n}\n\nfunction writeFile(file: string, content: string, written: string[]): void {\n fs.mkdirSync(path.dirname(file), { recursive: true });\n fs.writeFileSync(file, content, \"utf-8\");\n written.push(file);\n}\n\nfunction installProjectArtifacts(baseDir: string, written: string[]): void {\n writeFile(\n path.join(baseDir, \".agents\", \"skills\", \"context-xray\", \"SKILL.md\"),\n CONTEXT_XRAY_SKILL_MD,\n written,\n );\n writeFile(\n path.join(baseDir, \".agents\", \"commands\", \"context-xray.md\"),\n CONTEXT_XRAY_COMMAND_MD,\n written,\n );\n}\n\nexport function installLocalContextXray(\n options: InstallLocalContextXrayOptions,\n): InstallLocalContextXrayResult {\n const installDir = path.join(os.homedir(), \".agent-native\", \"context-xray\");\n const scriptPath = path.join(installDir, \"context-xray\");\n const binPath = path.join(os.homedir(), \".local\", \"bin\", \"context-xray\");\n const written: string[] = [];\n\n if (options.dryRun) {\n return {\n commands: [\"context-xray --open\"],\n scriptPath,\n written,\n };\n }\n\n writeExecutable(scriptPath, CONTEXT_XRAY_EXECUTABLE);\n written.push(scriptPath);\n if (process.platform === \"win32\") {\n const cmdPath = `${binPath}.cmd`;\n writeExecutable(\n cmdPath,\n `@echo off\\r\\nnode ${JSON.stringify(scriptPath)} %*\\r\\n`,\n );\n written.push(cmdPath);\n } else {\n writeExecutable(\n binPath,\n `#!/usr/bin/env sh\\nexec ${JSON.stringify(scriptPath)} \"$@\"\\n`,\n );\n written.push(binPath);\n }\n\n const clientSet = new Set(options.clients);\n const wantsCodex = clientSet.has(\"codex\");\n const wantsClaude =\n clientSet.has(\"claude-code\") || clientSet.has(\"claude-code-cli\");\n\n if (options.scope === \"project\" && options.baseDir) {\n installProjectArtifacts(options.baseDir, written);\n } else if (wantsCodex) {\n writeFile(\n path.join(codexHome(), \"skills\", \"context-xray\", \"SKILL.md\"),\n CONTEXT_XRAY_SKILL_MD,\n written,\n );\n writeFile(\n path.join(codexHome(), \"commands\", \"context-xray.md\"),\n CONTEXT_XRAY_COMMAND_MD,\n written,\n );\n }\n\n if (options.scope !== \"project\" && wantsClaude) {\n writeFile(\n path.join(os.homedir(), \".claude\", \"skills\", \"context-xray\", \"SKILL.md\"),\n CONTEXT_XRAY_SKILL_MD,\n written,\n );\n writeFile(\n path.join(os.homedir(), \".claude\", \"commands\", \"context-xray.md\"),\n CONTEXT_XRAY_COMMAND_MD,\n written,\n );\n }\n\n return {\n commands: [\"context-xray --open\"],\n scriptPath,\n written,\n };\n}\n"]}
|
package/dist/cli/index.js
CHANGED
|
@@ -648,7 +648,7 @@ Usage:
|
|
|
648
648
|
fallback.
|
|
649
649
|
agent-native app-skill <cmd> Install, launch, or package app-backed skills.
|
|
650
650
|
cmds: ensure | launch | pack
|
|
651
|
-
agent-native skills add assets|design-exploration
|
|
651
|
+
agent-native skills add assets|design-exploration|plans|visual-plan|ui-plan|visualize-plan
|
|
652
652
|
Install app skill instructions and MCP in one step
|
|
653
653
|
agent-native migrate <source> Create an Agent-Native Code /migrate session, or use
|
|
654
654
|
--emit for a portable own-agent dossier.
|