@agent-native/core 0.32.18 → 0.35.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/dist/agent/context-xray/actions/context-evict.d.ts +3 -0
- package/dist/agent/context-xray/actions/context-evict.d.ts.map +1 -0
- package/dist/agent/context-xray/actions/context-evict.js +35 -0
- package/dist/agent/context-xray/actions/context-evict.js.map +1 -0
- package/dist/agent/context-xray/actions/context-manifest-get.d.ts +3 -0
- package/dist/agent/context-xray/actions/context-manifest-get.d.ts.map +1 -0
- package/dist/agent/context-xray/actions/context-manifest-get.js +67 -0
- package/dist/agent/context-xray/actions/context-manifest-get.js.map +1 -0
- package/dist/agent/context-xray/actions/context-pin.d.ts +3 -0
- package/dist/agent/context-xray/actions/context-pin.d.ts.map +1 -0
- package/dist/agent/context-xray/actions/context-pin.js +35 -0
- package/dist/agent/context-xray/actions/context-pin.js.map +1 -0
- package/dist/agent/context-xray/actions/context-report.d.ts +3 -0
- package/dist/agent/context-xray/actions/context-report.d.ts.map +1 -0
- package/dist/agent/context-xray/actions/context-report.js +59 -0
- package/dist/agent/context-xray/actions/context-report.js.map +1 -0
- package/dist/agent/context-xray/actions/context-restore.d.ts +3 -0
- package/dist/agent/context-xray/actions/context-restore.d.ts.map +1 -0
- package/dist/agent/context-xray/actions/context-restore.js +33 -0
- package/dist/agent/context-xray/actions/context-restore.js.map +1 -0
- package/dist/agent/context-xray/actions/errors.d.ts +7 -0
- package/dist/agent/context-xray/actions/errors.d.ts.map +1 -0
- package/dist/agent/context-xray/actions/errors.js +15 -0
- package/dist/agent/context-xray/actions/errors.js.map +1 -0
- package/dist/agent/context-xray/apply-directives.d.ts +10 -0
- package/dist/agent/context-xray/apply-directives.d.ts.map +1 -0
- package/dist/agent/context-xray/apply-directives.js +100 -0
- package/dist/agent/context-xray/apply-directives.js.map +1 -0
- package/dist/agent/context-xray/directives-store.d.ts +26 -0
- package/dist/agent/context-xray/directives-store.d.ts.map +1 -0
- package/dist/agent/context-xray/directives-store.js +131 -0
- package/dist/agent/context-xray/directives-store.js.map +1 -0
- package/dist/agent/context-xray/identity.d.ts +5 -0
- package/dist/agent/context-xray/identity.d.ts.map +1 -0
- package/dist/agent/context-xray/identity.js +33 -0
- package/dist/agent/context-xray/identity.js.map +1 -0
- package/dist/agent/context-xray/manifest.d.ts +19 -0
- package/dist/agent/context-xray/manifest.d.ts.map +1 -0
- package/dist/agent/context-xray/manifest.js +115 -0
- package/dist/agent/context-xray/manifest.js.map +1 -0
- package/dist/agent/context-xray/migrations.d.ts +3 -0
- package/dist/agent/context-xray/migrations.d.ts.map +1 -0
- package/dist/agent/context-xray/migrations.js +31 -0
- package/dist/agent/context-xray/migrations.js.map +1 -0
- package/dist/agent/context-xray/plugin.d.ts +5 -0
- package/dist/agent/context-xray/plugin.d.ts.map +1 -0
- package/dist/agent/context-xray/plugin.js +15 -0
- package/dist/agent/context-xray/plugin.js.map +1 -0
- package/dist/agent/context-xray/schema.d.ts +249 -0
- package/dist/agent/context-xray/schema.d.ts.map +1 -0
- package/dist/agent/context-xray/schema.js +17 -0
- package/dist/agent/context-xray/schema.js.map +1 -0
- package/dist/agent/context-xray/segments.d.ts +19 -0
- package/dist/agent/context-xray/segments.d.ts.map +1 -0
- package/dist/agent/context-xray/segments.js +154 -0
- package/dist/agent/context-xray/segments.js.map +1 -0
- package/dist/agent/context-xray/tokenize.d.ts +10 -0
- package/dist/agent/context-xray/tokenize.d.ts.map +1 -0
- package/dist/agent/context-xray/tokenize.js +70 -0
- package/dist/agent/context-xray/tokenize.js.map +1 -0
- package/dist/agent/production-agent.d.ts +2 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +45 -5
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/context-xray-local.d.ts +16 -0
- package/dist/cli/context-xray-local.d.ts.map +1 -0
- package/dist/cli/context-xray-local.js +738 -0
- package/dist/cli/context-xray-local.js.map +1 -0
- package/dist/cli/skills.d.ts +3 -0
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +200 -73
- package/dist/cli/skills.js.map +1 -1
- package/dist/cli/templates-meta.js +5 -5
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +2 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/components/ui/sheet.d.ts +16 -0
- package/dist/client/components/ui/sheet.d.ts.map +1 -0
- package/dist/client/components/ui/sheet.js +23 -0
- package/dist/client/components/ui/sheet.js.map +1 -0
- package/dist/client/context-xray/ContextMeter.d.ts +4 -0
- package/dist/client/context-xray/ContextMeter.d.ts.map +1 -0
- package/dist/client/context-xray/ContextMeter.js +76 -0
- package/dist/client/context-xray/ContextMeter.js.map +1 -0
- package/dist/client/context-xray/ContextSegmentRow.d.ts +9 -0
- package/dist/client/context-xray/ContextSegmentRow.d.ts.map +1 -0
- package/dist/client/context-xray/ContextSegmentRow.js +22 -0
- package/dist/client/context-xray/ContextSegmentRow.js.map +1 -0
- package/dist/client/context-xray/ContextTreemap.d.ts +6 -0
- package/dist/client/context-xray/ContextTreemap.d.ts.map +1 -0
- package/dist/client/context-xray/ContextTreemap.js +49 -0
- package/dist/client/context-xray/ContextTreemap.js.map +1 -0
- package/dist/client/context-xray/ContextXRayPanel.d.ts +11 -0
- package/dist/client/context-xray/ContextXRayPanel.d.ts.map +1 -0
- package/dist/client/context-xray/ContextXRayPanel.js +87 -0
- package/dist/client/context-xray/ContextXRayPanel.js.map +1 -0
- package/dist/client/context-xray/SegmentProvenancePopover.d.ts +7 -0
- package/dist/client/context-xray/SegmentProvenancePopover.d.ts.map +1 -0
- package/dist/client/context-xray/SegmentProvenancePopover.js +7 -0
- package/dist/client/context-xray/SegmentProvenancePopover.js.map +1 -0
- package/dist/client/context-xray/format.d.ts +7 -0
- package/dist/client/context-xray/format.d.ts.map +1 -0
- package/dist/client/context-xray/format.js +47 -0
- package/dist/client/context-xray/format.js.map +1 -0
- package/dist/deploy/route-discovery.d.ts.map +1 -1
- package/dist/deploy/route-discovery.js +1 -0
- package/dist/deploy/route-discovery.js.map +1 -1
- package/dist/deploy/workspace-core.d.ts +1 -1
- package/dist/deploy/workspace-core.d.ts.map +1 -1
- package/dist/deploy/workspace-core.js +1 -0
- package/dist/deploy/workspace-core.js.map +1 -1
- package/dist/provider-api/index.d.ts +237 -0
- package/dist/provider-api/index.d.ts.map +1 -0
- package/dist/provider-api/index.js +1533 -0
- package/dist/provider-api/index.js.map +1 -0
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +20 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/framework-request-handler.d.ts.map +1 -1
- package/dist/server/framework-request-handler.js +2 -0
- package/dist/server/framework-request-handler.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/shared/context-xray.d.ts +58 -0
- package/dist/shared/context-xray.d.ts.map +1 -0
- package/dist/shared/context-xray.js +17 -0
- package/dist/shared/context-xray.js.map +1 -0
- package/dist/vite/action-types-plugin.d.ts.map +1 -1
- package/dist/vite/action-types-plugin.js +20 -0
- package/dist/vite/action-types-plugin.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +1 -0
- package/dist/vite/client.js.map +1 -1
- package/package.json +10 -1
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const CONTEXT_XRAY_EXECUTABLE = String.raw `#!/usr/bin/env node
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
const childProcess = require("node:child_process");
|
|
8
|
+
const fs = require("node:fs");
|
|
9
|
+
const os = require("node:os");
|
|
10
|
+
const path = require("node:path");
|
|
11
|
+
const { pathToFileURL } = require("node:url");
|
|
12
|
+
|
|
13
|
+
const HOME = os.homedir();
|
|
14
|
+
const CODEX_DIR = process.env.CODEX_HOME && process.env.CODEX_HOME.trim() ? process.env.CODEX_HOME.trim() : path.join(HOME, ".codex");
|
|
15
|
+
const CLAUDE_DIR = path.join(HOME, ".claude");
|
|
16
|
+
const OUT_DIR = path.join(CODEX_DIR, "context-xray");
|
|
17
|
+
const SESSION_ID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
18
|
+
const CATEGORIES = ["user", "assistant", "tool_call", "tool_output", "reasoning", "instructions", "attachment", "metadata", "other"];
|
|
19
|
+
const LABELS = {
|
|
20
|
+
user: "User asks",
|
|
21
|
+
assistant: "Assistant text",
|
|
22
|
+
tool_call: "Tool calls",
|
|
23
|
+
tool_output: "Tool output",
|
|
24
|
+
reasoning: "Reasoning",
|
|
25
|
+
instructions: "Instructions/context",
|
|
26
|
+
attachment: "Attachments",
|
|
27
|
+
metadata: "Metadata",
|
|
28
|
+
other: "Other",
|
|
29
|
+
};
|
|
30
|
+
const COLORS = {
|
|
31
|
+
user: "#8ba8ff",
|
|
32
|
+
assistant: "#55b982",
|
|
33
|
+
tool_call: "#f0a85b",
|
|
34
|
+
tool_output: "#e06b73",
|
|
35
|
+
reasoning: "#a77be8",
|
|
36
|
+
instructions: "#6ac3d5",
|
|
37
|
+
attachment: "#d6a85a",
|
|
38
|
+
metadata: "#9aa3ad",
|
|
39
|
+
other: "#c3c8ce",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function parseArgs(argv) {
|
|
43
|
+
const out = {
|
|
44
|
+
mode: "current",
|
|
45
|
+
source: "both",
|
|
46
|
+
since: "7d",
|
|
47
|
+
last: 12,
|
|
48
|
+
scanLimit: 80,
|
|
49
|
+
project: process.cwd(),
|
|
50
|
+
allProjects: false,
|
|
51
|
+
sessionId: "",
|
|
52
|
+
format: "html",
|
|
53
|
+
out: "",
|
|
54
|
+
open: false,
|
|
55
|
+
port: 0,
|
|
56
|
+
};
|
|
57
|
+
for (let i = 0; i < argv.length; i++) {
|
|
58
|
+
const arg = argv[i];
|
|
59
|
+
const eat = (flag) => {
|
|
60
|
+
if (arg === flag) return argv[++i] || "";
|
|
61
|
+
if (arg.startsWith(flag + "=")) return arg.slice(flag.length + 1);
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
let value;
|
|
65
|
+
if (arg === "threads" || arg === "--threads") out.mode = "threads";
|
|
66
|
+
else if (arg === "trends" || arg === "--trends") out.mode = "trends";
|
|
67
|
+
else if (arg === "current" || arg === "--current") out.mode = "current";
|
|
68
|
+
else if ((value = eat("--source")) !== undefined) out.source = value;
|
|
69
|
+
else if ((value = eat("--since")) !== undefined) out.since = value;
|
|
70
|
+
else if ((value = eat("--last")) !== undefined) out.last = Number(value) || out.last;
|
|
71
|
+
else if ((value = eat("--scan-limit")) !== undefined) out.scanLimit = Number(value) || out.scanLimit;
|
|
72
|
+
else if ((value = eat("--project")) !== undefined) out.project = value;
|
|
73
|
+
else if ((value = eat("--session-id")) !== undefined) out.sessionId = value;
|
|
74
|
+
else if ((value = eat("--format")) !== undefined) out.format = value;
|
|
75
|
+
else if ((value = eat("--out")) !== undefined) out.out = value;
|
|
76
|
+
else if ((value = eat("--port")) !== undefined) out.port = Number(value) || 0;
|
|
77
|
+
else if (arg === "--all-projects") out.allProjects = true;
|
|
78
|
+
else if (arg === "--open") out.open = true;
|
|
79
|
+
else if (arg === "--json") out.format = "json";
|
|
80
|
+
else if (arg === "--help" || arg === "-h") out.help = true;
|
|
81
|
+
}
|
|
82
|
+
if (process.env.CLAUDE_CODE_SESSION_ID && !out.sessionId && out.mode === "current") {
|
|
83
|
+
out.sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
84
|
+
}
|
|
85
|
+
if (out.mode === "threads") {
|
|
86
|
+
out.allProjects = true;
|
|
87
|
+
out.last = Math.max(out.last, 30);
|
|
88
|
+
}
|
|
89
|
+
if (out.mode === "trends") {
|
|
90
|
+
out.allProjects = true;
|
|
91
|
+
out.last = Math.max(out.last, 60);
|
|
92
|
+
}
|
|
93
|
+
if (out.mode === "current") {
|
|
94
|
+
out.last = 1;
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function help() {
|
|
100
|
+
console.log([
|
|
101
|
+
"Context X-Ray",
|
|
102
|
+
"",
|
|
103
|
+
"Usage:",
|
|
104
|
+
" context-xray --open Visualize the current/recent local thread",
|
|
105
|
+
" context-xray threads --open Pick from recent Codex/Claude sessions",
|
|
106
|
+
" context-xray trends --since 7d --open Show recent usage trends",
|
|
107
|
+
" context-xray --session-id <id> --open Analyze one exact session",
|
|
108
|
+
"",
|
|
109
|
+
"Options:",
|
|
110
|
+
" --source codex|claude|both",
|
|
111
|
+
" --since 24h|7d|2w|ISO",
|
|
112
|
+
" --last <n>",
|
|
113
|
+
" --all-projects",
|
|
114
|
+
" --format html|json",
|
|
115
|
+
" --out <path>",
|
|
116
|
+
].join("\n"));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function mkdirp(dir) {
|
|
120
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseSince(value) {
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
const match = String(value || "7d").trim().toLowerCase().match(/^(\d+)([hdw])$/);
|
|
126
|
+
if (match) {
|
|
127
|
+
const amount = Number(match[1]);
|
|
128
|
+
const unit = match[2];
|
|
129
|
+
const mult = unit === "h" ? 3600000 : unit === "d" ? 86400000 : 604800000;
|
|
130
|
+
return now - amount * mult;
|
|
131
|
+
}
|
|
132
|
+
const parsed = Date.parse(value);
|
|
133
|
+
return Number.isFinite(parsed) ? parsed : now - 7 * 86400000;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readJsonl(file) {
|
|
137
|
+
try {
|
|
138
|
+
return fs.readFileSync(file, "utf8").split(/\r?\n/).filter(Boolean).map((line) => {
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(line);
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}).filter(Boolean);
|
|
145
|
+
} catch {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function compact(value) {
|
|
151
|
+
try {
|
|
152
|
+
const text = JSON.stringify(value);
|
|
153
|
+
return text.length > 250000 ? text.slice(0, 250000) : text;
|
|
154
|
+
} catch {
|
|
155
|
+
return String(value || "");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function textFrom(value, depth) {
|
|
160
|
+
if (depth > 8 || value == null) return "";
|
|
161
|
+
if (typeof value === "string") return value.length > 250000 ? value.slice(0, 250000) : value;
|
|
162
|
+
if (typeof value !== "object") return "";
|
|
163
|
+
if (Array.isArray(value)) return value.map((item) => textFrom(item, depth + 1)).filter(Boolean).join("\n");
|
|
164
|
+
const skip = new Set(["encrypted_content", "id", "uuid", "call_id", "sessionId", "parentUuid"]);
|
|
165
|
+
const keys = new Set(["text", "message", "output", "result", "content", "summary", "arguments", "args", "input", "stdout", "stderr", "attachment"]);
|
|
166
|
+
const parts = [];
|
|
167
|
+
for (const key of Object.keys(value)) {
|
|
168
|
+
if (skip.has(key)) continue;
|
|
169
|
+
const item = value[key];
|
|
170
|
+
if (keys.has(key) || typeof item === "object") {
|
|
171
|
+
const text = textFrom(item, depth + 1);
|
|
172
|
+
if (text) parts.push(text);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return parts.join("\n");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function addCounter(counter, key, amount) {
|
|
179
|
+
if (!key || !amount) return;
|
|
180
|
+
counter[key] = (counter[key] || 0) + amount;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function mergeCounter(into, from) {
|
|
184
|
+
for (const key of Object.keys(from || {})) addCounter(into, key, from[key]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function estimateTokens(chars) {
|
|
188
|
+
return chars > 0 ? Math.max(1, Math.ceil(chars / 4)) : 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function fmtTokens(tokens) {
|
|
192
|
+
if (tokens >= 1000000) return (tokens / 1000000).toFixed(1) + "m";
|
|
193
|
+
if (tokens >= 1000) return (tokens / 1000).toFixed(1) + "k";
|
|
194
|
+
return String(tokens);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function pct(part, total) {
|
|
198
|
+
return total > 0 ? Math.max(0, Math.min(100, (part / total) * 100)) : 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function pathCounts(text) {
|
|
202
|
+
const out = {};
|
|
203
|
+
const matches = String(text || "").match(/(?:(?:\/[\w@.+,=-]+)+|(?:[\w.-]+\/)+[\w.+,=-]+)(?:\.[A-Za-z0-9_+-]+)?/g) || [];
|
|
204
|
+
for (const raw of matches) {
|
|
205
|
+
const value = raw.replace(/['",.)]+$/g, "");
|
|
206
|
+
if (value.length > 5 && !value.startsWith("http") && value.includes("/")) addCounter(out, value, 1);
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function codexTitle(id) {
|
|
212
|
+
const index = path.join(CODEX_DIR, "session_index.jsonl");
|
|
213
|
+
for (const record of readJsonl(index)) {
|
|
214
|
+
if (record.id === id && record.thread_name) return String(record.thread_name);
|
|
215
|
+
}
|
|
216
|
+
return "";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function observedCodexTokens(payload) {
|
|
220
|
+
if (!payload || payload.type !== "token_count" || !payload.info) return 0;
|
|
221
|
+
const last = payload.info.last_token_usage;
|
|
222
|
+
if (last && Number(last.total_tokens)) return Number(last.total_tokens);
|
|
223
|
+
const total = payload.info.total_token_usage;
|
|
224
|
+
if (total && Number(total.total_tokens)) return Number(total.total_tokens);
|
|
225
|
+
if (total && typeof total === "object") {
|
|
226
|
+
return ["input_tokens", "cached_input_tokens", "output_tokens", "reasoning_output_tokens"].reduce((sum, key) => sum + (Number(total[key]) || 0), 0);
|
|
227
|
+
}
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function claudeUsageTokens(usage) {
|
|
232
|
+
if (!usage || typeof usage !== "object") return 0;
|
|
233
|
+
return (Number(usage.input_tokens) || 0) + (Number(usage.output_tokens) || 0) + (Number(usage.cache_creation_input_tokens) || 0) + (Number(usage.cache_read_input_tokens) || 0);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function sessionIdFromPath(file) {
|
|
237
|
+
const match = path.basename(file).match(SESSION_ID_RE);
|
|
238
|
+
return match ? match[0] : path.basename(file, ".jsonl");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function classifyCodex(record) {
|
|
242
|
+
const top = String(record.type || "");
|
|
243
|
+
const payload = record.payload && typeof record.payload === "object" ? record.payload : {};
|
|
244
|
+
const ptype = String(payload.type || "");
|
|
245
|
+
let category = "other";
|
|
246
|
+
const tools = {};
|
|
247
|
+
if (top === "session_meta") category = "metadata";
|
|
248
|
+
else if (top === "turn_context") category = "instructions";
|
|
249
|
+
else if (top === "event_msg") category = ptype === "user_message" ? "user" : "metadata";
|
|
250
|
+
else if (top === "response_item") {
|
|
251
|
+
if (["function_call", "custom_tool_call", "web_search_call", "tool_search_call", "tool_call"].includes(ptype) || payload.name && payload.call_id) {
|
|
252
|
+
category = "tool_call";
|
|
253
|
+
if (payload.name) addCounter(tools, String(payload.name), 1);
|
|
254
|
+
} else if (["function_call_output", "custom_tool_call_output", "tool_search_output", "tool_result"].includes(ptype) || Object.prototype.hasOwnProperty.call(payload, "output")) category = "tool_output";
|
|
255
|
+
else if (ptype === "reasoning" || payload.summary) category = "reasoning";
|
|
256
|
+
else if (payload.role === "assistant") category = "assistant";
|
|
257
|
+
else if (payload.role === "user" || payload.role === "developer") category = "user";
|
|
258
|
+
}
|
|
259
|
+
const text = textFrom(record, 0) || (category === "metadata" ? compact(record) : "");
|
|
260
|
+
return { category, chars: text.length, tools, paths: pathCounts(text) };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function classifyClaude(record) {
|
|
264
|
+
let category = "other";
|
|
265
|
+
const tools = {};
|
|
266
|
+
const message = record.message && typeof record.message === "object" ? record.message : null;
|
|
267
|
+
let text = "";
|
|
268
|
+
if (message) {
|
|
269
|
+
if (message.role === "user") category = "user";
|
|
270
|
+
else if (message.role === "assistant") category = "assistant";
|
|
271
|
+
const content = Array.isArray(message.content) ? message.content : [message.content];
|
|
272
|
+
const parts = [];
|
|
273
|
+
for (const part of content) {
|
|
274
|
+
if (part && typeof part === "object") {
|
|
275
|
+
if (part.type === "tool_use") {
|
|
276
|
+
category = "tool_call";
|
|
277
|
+
if (part.name) addCounter(tools, String(part.name), 1);
|
|
278
|
+
} else if (part.type === "tool_result") category = "tool_output";
|
|
279
|
+
else if (part.type === "thinking") category = "reasoning";
|
|
280
|
+
}
|
|
281
|
+
parts.push(textFrom(part, 0));
|
|
282
|
+
}
|
|
283
|
+
text = parts.join("\n");
|
|
284
|
+
} else if (record.toolUseResult) {
|
|
285
|
+
category = "tool_output";
|
|
286
|
+
text = textFrom(record.toolUseResult, 0);
|
|
287
|
+
} else if (record.attachment) {
|
|
288
|
+
category = "attachment";
|
|
289
|
+
text = textFrom(record.attachment, 0);
|
|
290
|
+
} else {
|
|
291
|
+
text = textFrom(record, 0);
|
|
292
|
+
}
|
|
293
|
+
return { category, chars: text.length, tools, paths: pathCounts(text) };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function summarizeCodex(file) {
|
|
297
|
+
const stat = fs.statSync(file);
|
|
298
|
+
const summary = {
|
|
299
|
+
source: "codex",
|
|
300
|
+
path: file,
|
|
301
|
+
sessionId: sessionIdFromPath(file),
|
|
302
|
+
title: "",
|
|
303
|
+
cwd: "",
|
|
304
|
+
startedAt: "",
|
|
305
|
+
updatedAt: "",
|
|
306
|
+
categories: {},
|
|
307
|
+
tools: {},
|
|
308
|
+
paths: {},
|
|
309
|
+
observedTokens: 0,
|
|
310
|
+
bytes: stat.size,
|
|
311
|
+
mtime: stat.mtimeMs,
|
|
312
|
+
};
|
|
313
|
+
const records = readJsonl(file);
|
|
314
|
+
for (const record of records) {
|
|
315
|
+
const payload = record.payload && typeof record.payload === "object" ? record.payload : {};
|
|
316
|
+
if (record.type === "session_meta") {
|
|
317
|
+
summary.sessionId = String(payload.id || summary.sessionId);
|
|
318
|
+
summary.cwd = String(payload.cwd || summary.cwd);
|
|
319
|
+
summary.startedAt = String(payload.timestamp || summary.startedAt);
|
|
320
|
+
}
|
|
321
|
+
if (payload.cwd && !summary.cwd) summary.cwd = String(payload.cwd);
|
|
322
|
+
if (record.timestamp) summary.updatedAt = String(record.timestamp);
|
|
323
|
+
summary.observedTokens = Math.max(summary.observedTokens, observedCodexTokens(payload));
|
|
324
|
+
const stats = classifyCodex(record);
|
|
325
|
+
addCounter(summary.categories, stats.category, stats.chars);
|
|
326
|
+
mergeCounter(summary.tools, stats.tools);
|
|
327
|
+
mergeCounter(summary.paths, stats.paths);
|
|
328
|
+
}
|
|
329
|
+
summary.title = codexTitle(summary.sessionId) || summary.sessionId;
|
|
330
|
+
return finalizeSummary(summary);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function summarizeClaude(file) {
|
|
334
|
+
const stat = fs.statSync(file);
|
|
335
|
+
const summary = {
|
|
336
|
+
source: "claude",
|
|
337
|
+
path: file,
|
|
338
|
+
sessionId: sessionIdFromPath(file),
|
|
339
|
+
title: "",
|
|
340
|
+
cwd: "",
|
|
341
|
+
startedAt: "",
|
|
342
|
+
updatedAt: "",
|
|
343
|
+
categories: {},
|
|
344
|
+
tools: {},
|
|
345
|
+
paths: {},
|
|
346
|
+
observedTokens: 0,
|
|
347
|
+
bytes: stat.size,
|
|
348
|
+
mtime: stat.mtimeMs,
|
|
349
|
+
};
|
|
350
|
+
const records = readJsonl(file);
|
|
351
|
+
for (const record of records) {
|
|
352
|
+
summary.sessionId = String(record.sessionId || summary.sessionId);
|
|
353
|
+
summary.cwd = String(record.cwd || summary.cwd);
|
|
354
|
+
if (record.timestamp) {
|
|
355
|
+
summary.updatedAt = String(record.timestamp);
|
|
356
|
+
if (!summary.startedAt) summary.startedAt = String(record.timestamp);
|
|
357
|
+
}
|
|
358
|
+
if (record.message && typeof record.message === "object") {
|
|
359
|
+
summary.observedTokens = Math.max(summary.observedTokens, claudeUsageTokens(record.message.usage));
|
|
360
|
+
if (!summary.title && record.message.role === "user") summary.title = cleanTitle(textFrom(record.message, 0)).slice(0, 90);
|
|
361
|
+
}
|
|
362
|
+
const stats = classifyClaude(record);
|
|
363
|
+
addCounter(summary.categories, stats.category, stats.chars);
|
|
364
|
+
mergeCounter(summary.tools, stats.tools);
|
|
365
|
+
mergeCounter(summary.paths, stats.paths);
|
|
366
|
+
}
|
|
367
|
+
if (!summary.title) summary.title = summary.sessionId;
|
|
368
|
+
return finalizeSummary(summary);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function finalizeSummary(summary) {
|
|
372
|
+
const totalChars = Object.values(summary.categories).reduce((sum, value) => sum + value, 0);
|
|
373
|
+
summary.totalChars = totalChars;
|
|
374
|
+
summary.tokens = summary.observedTokens || estimateTokens(totalChars);
|
|
375
|
+
summary.tokenMethod = summary.observedTokens ? "observed" : "estimated";
|
|
376
|
+
return summary;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function walk(root) {
|
|
380
|
+
const out = [];
|
|
381
|
+
if (!fs.existsSync(root)) return out;
|
|
382
|
+
const visit = (dir) => {
|
|
383
|
+
let entries = [];
|
|
384
|
+
try {
|
|
385
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
386
|
+
} catch {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
for (const entry of entries) {
|
|
390
|
+
const file = path.join(dir, entry.name);
|
|
391
|
+
if (entry.isDirectory()) visit(file);
|
|
392
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) out.push(file);
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
visit(root);
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function encodedProject(project) {
|
|
400
|
+
return path.resolve(project).replace(/\//g, "-");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function pathInsideOrEqual(value, parent) {
|
|
404
|
+
const relative = path.relative(parent, value);
|
|
405
|
+
return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function candidateFiles(source, args) {
|
|
409
|
+
if (args.sessionId) {
|
|
410
|
+
const roots = source === "codex" ? [path.join(CODEX_DIR, "sessions"), path.join(CODEX_DIR, "archived_sessions")] : [path.join(CLAUDE_DIR, "projects")];
|
|
411
|
+
return roots.flatMap(walk).filter((file) => file.includes(args.sessionId));
|
|
412
|
+
}
|
|
413
|
+
const since = parseSince(args.since);
|
|
414
|
+
const roots = source === "codex" ? [path.join(CODEX_DIR, "sessions"), path.join(CODEX_DIR, "archived_sessions")] : [path.join(CLAUDE_DIR, "projects")];
|
|
415
|
+
const projectFragment = encodedProject(args.project);
|
|
416
|
+
const files = roots.flatMap(walk).filter((file) => {
|
|
417
|
+
let stat;
|
|
418
|
+
try {
|
|
419
|
+
stat = fs.statSync(file);
|
|
420
|
+
} catch {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
if (stat.mtimeMs < since) return false;
|
|
424
|
+
if (args.allProjects || source === "codex") return true;
|
|
425
|
+
return file.includes(projectFragment) || file.includes(path.basename(args.project));
|
|
426
|
+
}).sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
427
|
+
return source === "codex" && !args.allProjects ? files : files.slice(0, args.scanLimit);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function projectMatches(session, args) {
|
|
431
|
+
if (args.allProjects || args.sessionId) return true;
|
|
432
|
+
const project = path.resolve(args.project);
|
|
433
|
+
if (session.cwd && pathInsideOrEqual(path.resolve(session.cwd), project)) return true;
|
|
434
|
+
return session.path.includes(encodedProject(project));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function collectSessions(args) {
|
|
438
|
+
const sources = args.source === "both" ? ["codex", "claude"] : [args.source];
|
|
439
|
+
const sessions = [];
|
|
440
|
+
for (const source of sources) {
|
|
441
|
+
for (const file of candidateFiles(source, args)) {
|
|
442
|
+
try {
|
|
443
|
+
const summary = source === "codex" ? summarizeCodex(file) : summarizeClaude(file);
|
|
444
|
+
if (projectMatches(summary, args)) sessions.push(summary);
|
|
445
|
+
} catch {}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (args.mode === "current" || args.sessionId) sessions.sort((a, b) => b.mtime - a.mtime);
|
|
449
|
+
else sessions.sort((a, b) => b.tokens - a.tokens || b.mtime - a.mtime);
|
|
450
|
+
return sessions.slice(0, args.last);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function aggregate(sessions) {
|
|
454
|
+
const out = { categories: {}, tools: {}, paths: {} };
|
|
455
|
+
for (const session of sessions) {
|
|
456
|
+
mergeCounter(out.categories, session.categories);
|
|
457
|
+
mergeCounter(out.tools, session.tools);
|
|
458
|
+
mergeCounter(out.paths, session.paths);
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function sortedEntries(counter, limit) {
|
|
464
|
+
return Object.entries(counter || {}).sort((a, b) => b[1] - a[1]).slice(0, limit);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function recommendations(sessions) {
|
|
468
|
+
if (!sessions.length) return ["No matching sessions found. Try context-xray threads --all-projects --since 2w --open."];
|
|
469
|
+
const agg = aggregate(sessions);
|
|
470
|
+
const total = Object.values(agg.categories).reduce((sum, value) => sum + value, 0);
|
|
471
|
+
const tips = [];
|
|
472
|
+
const toolOutput = pct(agg.categories.tool_output || 0, total);
|
|
473
|
+
const instructions = pct(agg.categories.instructions || 0, total);
|
|
474
|
+
const assistant = pct(agg.categories.assistant || 0, total);
|
|
475
|
+
const maxSession = sessions.reduce((a, b) => a.tokens > b.tokens ? a : b);
|
|
476
|
+
if (toolOutput > 45) tips.push("Tool output dominates context. Prefer targeted rg/sed ranges, cap logs, and ask agents to summarize failing blocks instead of pasting full output.");
|
|
477
|
+
if (instructions > 25) tips.push("Instructions are a large share. Move stable workflow rules into skills or AGENTS/CLAUDE files and keep per-turn prompts short.");
|
|
478
|
+
if (assistant > 45) tips.push("Assistant prose is heavy. Ask for terse progress updates during long runs and save rationale only when it changes decisions.");
|
|
479
|
+
if (maxSession.tokens > 80000) tips.push("The largest session is about " + fmtTokens(maxSession.tokens) + " " + maxSession.tokenMethod + " tokens. Compact or start a fresh handoff before another big implementation pass.");
|
|
480
|
+
const topTool = sortedEntries(agg.tools, 1)[0];
|
|
481
|
+
if (topTool && topTool[1] > 20) tips.push(topTool[0] + " appears " + topTool[1] + " times. Batch independent inspection and use parallel reads/searches.");
|
|
482
|
+
const topPath = sortedEntries(agg.paths, 1)[0];
|
|
483
|
+
if (topPath && topPath[1] > 12) tips.push(topPath[0] + " appears repeatedly (" + topPath[1] + " mentions). Pin a short role summary instead of rereading it.");
|
|
484
|
+
return tips.length ? tips.slice(0, 6) : ["Recent sessions look balanced. Keep using focused reads, compact after milestones, and preserve decisions in a skill or repo doc."];
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function escapeHtml(value) {
|
|
488
|
+
return String(value || "").replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[ch]));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function cleanTitle(value) {
|
|
492
|
+
return String(value || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function categoryBar(categories) {
|
|
496
|
+
const total = Object.values(categories || {}).reduce((sum, value) => sum + value, 0);
|
|
497
|
+
if (!total) return "<div class=\"bar\"></div>";
|
|
498
|
+
return "<div class=\"bar\">" + CATEGORIES.map((cat) => {
|
|
499
|
+
const value = categories[cat] || 0;
|
|
500
|
+
if (!value) return "";
|
|
501
|
+
const width = Math.max(1, pct(value, total));
|
|
502
|
+
return "<span title=\"" + escapeHtml(LABELS[cat]) + "\" style=\"width:" + width.toFixed(2) + "%;background:" + COLORS[cat] + "\"></span>";
|
|
503
|
+
}).join("") + "</div>";
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function renderHtml(sessions, args) {
|
|
507
|
+
const agg = aggregate(sessions);
|
|
508
|
+
const totalTokens = sessions.reduce((sum, s) => sum + s.tokens, 0);
|
|
509
|
+
const tips = recommendations(sessions);
|
|
510
|
+
const categoryRows = CATEGORIES.map((cat) => {
|
|
511
|
+
const chars = agg.categories[cat] || 0;
|
|
512
|
+
if (!chars) return "";
|
|
513
|
+
return "<tr><td>" + escapeHtml(LABELS[cat]) + "</td><td>" + fmtTokens(estimateTokens(chars)) + "</td><td>" + pct(chars, Object.values(agg.categories).reduce((sum, value) => sum + value, 0)).toFixed(0) + "%</td></tr>";
|
|
514
|
+
}).join("");
|
|
515
|
+
const sessionCards = sessions.map((session) => {
|
|
516
|
+
const cats = Object.entries(session.categories).sort((a, b) => b[1] - a[1]).map((entry) => "<tr><td>" + escapeHtml(LABELS[entry[0]] || entry[0]) + "</td><td>" + fmtTokens(estimateTokens(entry[1])) + "</td><td>" + pct(entry[1], session.totalChars).toFixed(0) + "%</td></tr>").join("");
|
|
517
|
+
const tools = sortedEntries(session.tools, 6).map((entry) => "<span class=\"badge\">" + escapeHtml(entry[0]) + " x" + entry[1] + "</span>").join("");
|
|
518
|
+
const paths = sortedEntries(session.paths, 5).map((entry) => "<span class=\"badge muted\">" + escapeHtml(entry[0]) + " x" + entry[1] + "</span>").join("");
|
|
519
|
+
return "<article class=\"card session\"><div class=\"session-head\"><div><div class=\"eyebrow\">" + escapeHtml(session.source) + " - " + escapeHtml(session.updatedAt || "unknown time") + "</div><h3>" + escapeHtml(session.title || session.sessionId) + "</h3><p class=\"path\">" + escapeHtml(session.cwd || session.path) + "</p></div><div class=\"token-big\">" + fmtTokens(session.tokens) + "</div></div>" + categoryBar(session.categories) + "<div class=\"session-grid\"><table><tbody>" + cats + "</tbody></table><div><div class=\"mini-label\">Frequent tools</div><div class=\"badges\">" + (tools || "<span class=\"muted-text\">none detected</span>") + "</div><div class=\"mini-label\">Repeated paths</div><div class=\"badges\">" + (paths || "<span class=\"muted-text\">none detected</span>") + "</div></div></div></article>";
|
|
520
|
+
}).join("");
|
|
521
|
+
const topTools = sortedEntries(agg.tools, 10).map((entry) => "<tr><td>" + escapeHtml(entry[0]) + "</td><td>" + entry[1] + "</td></tr>").join("");
|
|
522
|
+
const topPaths = sortedEntries(agg.paths, 10).map((entry) => "<tr><td>" + escapeHtml(entry[0]) + "</td><td>" + entry[1] + "</td></tr>").join("");
|
|
523
|
+
const sourceCounts = sessions.reduce((counts, s) => (counts[s.source] = (counts[s.source] || 0) + 1, counts), {});
|
|
524
|
+
return "<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Context X-Ray</title><style>" +
|
|
525
|
+
"body{margin:0;background:#0e1116;color:#f3f5f8;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;line-height:1.45}main{max-width:1180px;margin:0 auto;padding:32px 20px 56px}header{display:flex;justify-content:space-between;gap:24px;align-items:flex-end;margin-bottom:24px}h1{margin:0;font-size:clamp(28px,5vw,52px);letter-spacing:0}h2{margin:0 0 14px;font-size:18px}h3{margin:3px 0 4px;font-size:17px}p{margin:0}.muted,.path,.eyebrow,.muted-text{color:#9aa3ad}.eyebrow{text-transform:uppercase;letter-spacing:.08em;font-size:11px}.summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:18px 0}.card{background:linear-gradient(180deg,#151922,#1c2230);border:1px solid #2a3140;border-radius:8px;padding:16px;box-shadow:0 12px 40px rgba(0,0,0,.22)}.stat strong{display:block;font-size:28px;line-height:1.1}.grid{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(280px,.8fr);gap:16px;align-items:start}.bar{display:flex;overflow:hidden;height:14px;border-radius:999px;background:#252b37;margin:12px 0}.bar span{display:block;min-width:2px}.tips li{margin:0 0 9px}.session{margin-top:14px}.session-head{display:flex;justify-content:space-between;gap:16px}.token-big{font-weight:700;font-size:28px;color:#8ba8ff;white-space:nowrap}.session-grid{display:grid;grid-template-columns:260px minmax(0,1fr);gap:14px;margin-top:12px}table{width:100%;border-collapse:collapse;font-size:13px}td{border-top:1px solid #2a3140;padding:7px 0;vertical-align:top}td:last-child{text-align:right;color:#9aa3ad}.mini-label{margin:7px 0 6px;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#9aa3ad}.badges{display:flex;flex-wrap:wrap;gap:6px}.badge{display:inline-flex;border:1px solid #2a3140;border-radius:999px;padding:3px 8px;font-size:12px;background:rgba(255,255,255,.04)}.badge.muted{color:#9aa3ad}footer{color:#9aa3ad;margin-top:24px;font-size:12px}@media(max-width:840px){header,.grid,.session-grid{grid-template-columns:1fr;display:grid}.summary{grid-template-columns:repeat(2,minmax(0,1fr))}}" +
|
|
526
|
+
"</style></head><body><main><header><div><div class=\"eyebrow\">Local coding context profile</div><h1>Context X-Ray</h1><p class=\"muted\">Generated " + escapeHtml(new Date().toLocaleString()) + " - mode=" + escapeHtml(args.mode) + " - source=" + escapeHtml(args.source) + " - since=" + escapeHtml(args.since) + "</p></div></header><section class=\"summary\"><div class=\"card stat\"><span class=\"muted\">Sessions</span><strong>" + sessions.length + "</strong></div><div class=\"card stat\"><span class=\"muted\">Observed/estimated tokens</span><strong>" + fmtTokens(totalTokens) + "</strong></div><div class=\"card stat\"><span class=\"muted\">Codex</span><strong>" + (sourceCounts.codex || 0) + "</strong></div><div class=\"card stat\"><span class=\"muted\">Claude</span><strong>" + (sourceCounts.claude || 0) + "</strong></div></section><section class=\"card\"><h2>Where The Context Is Going</h2>" + categoryBar(agg.categories) + "<table><tbody>" + categoryRows + "</tbody></table></section><div class=\"grid\" style=\"margin-top:16px\"><section class=\"card tips\"><h2>Warnings And Optimizations</h2><ol>" + tips.map((tip) => "<li>" + escapeHtml(tip) + "</li>").join("") + "</ol></section><section class=\"card\"><h2>Hotspots</h2><div class=\"mini-label\">Top tools</div><table><tbody>" + (topTools || "<tr><td>None detected</td><td></td></tr>") + "</tbody></table><div class=\"mini-label\">Top paths</div><table><tbody>" + (topPaths || "<tr><td>None detected</td><td></td></tr>") + "</tbody></table></section></div><section style=\"margin-top:16px\"><h2>Sessions</h2>" + sessionCards + "</section><footer>Reads local transcript files only. No transcript content is uploaded.</footer></main></body></html>";
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function writeJson(sessions, args, file) {
|
|
530
|
+
const agg = aggregate(sessions);
|
|
531
|
+
fs.writeFileSync(file, JSON.stringify({
|
|
532
|
+
generatedAt: new Date().toISOString(),
|
|
533
|
+
mode: args.mode,
|
|
534
|
+
source: args.source,
|
|
535
|
+
since: args.since,
|
|
536
|
+
totalTokens: sessions.reduce((sum, s) => sum + s.tokens, 0),
|
|
537
|
+
categories: agg.categories,
|
|
538
|
+
tools: sortedEntries(agg.tools, 25),
|
|
539
|
+
paths: sortedEntries(agg.paths, 25),
|
|
540
|
+
recommendations: recommendations(sessions),
|
|
541
|
+
sessions,
|
|
542
|
+
}, null, 2));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function openUrl(url) {
|
|
546
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
547
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
548
|
+
try {
|
|
549
|
+
childProcess.spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
|
|
550
|
+
} catch {}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function printSummary(sessions, args, file, url) {
|
|
554
|
+
const total = sessions.reduce((sum, s) => sum + s.tokens, 0);
|
|
555
|
+
console.log("Context X-Ray: analyzed " + sessions.length + " session(s), about " + fmtTokens(total) + " observed/estimated tokens.");
|
|
556
|
+
if (url) console.log("Open: " + url);
|
|
557
|
+
else console.log("Report: " + file);
|
|
558
|
+
console.log("");
|
|
559
|
+
for (const tip of recommendations(sessions).slice(0, 4)) console.log("- " + tip);
|
|
560
|
+
const tools = sortedEntries(aggregate(sessions).tools, 5);
|
|
561
|
+
if (tools.length) console.log("- Frequent tools: " + tools.map((entry) => entry[0] + " x" + entry[1]).join(", "));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function main() {
|
|
565
|
+
const args = parseArgs(process.argv.slice(2));
|
|
566
|
+
if (args.help) return help();
|
|
567
|
+
const sessions = collectSessions(args);
|
|
568
|
+
mkdirp(OUT_DIR);
|
|
569
|
+
const suffix = args.format === "json" ? "json" : "html";
|
|
570
|
+
const file = args.out || path.join(OUT_DIR, "context-xray-" + new Date().toISOString().replace(/[:.]/g, "-") + "." + suffix);
|
|
571
|
+
mkdirp(path.dirname(file));
|
|
572
|
+
if (args.format === "json") writeJson(sessions, args, file);
|
|
573
|
+
else fs.writeFileSync(file, renderHtml(sessions, args));
|
|
574
|
+
const url = args.open && args.format === "html" ? pathToFileURL(file).href : "";
|
|
575
|
+
if (url) openUrl(url);
|
|
576
|
+
printSummary(sessions, args, file, url);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
main();
|
|
580
|
+
`;
|
|
581
|
+
export const CONTEXT_XRAY_SKILL_MD = `---
|
|
582
|
+
name: context-xray
|
|
583
|
+
description: >-
|
|
584
|
+
Visualize local Codex and Claude Code context usage, open an inline/browser
|
|
585
|
+
report, flag warnings, and suggest prompt/tooling optimizations. Use when the
|
|
586
|
+
user types /context-xray, asks where context is going, wants recent local
|
|
587
|
+
coding-agent trends, or wants to improve context efficiency.
|
|
588
|
+
metadata:
|
|
589
|
+
visibility: exported
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
# Context X-Ray
|
|
593
|
+
|
|
594
|
+
Use the locally installed Context X-Ray command to visualize recent Codex and
|
|
595
|
+
Claude Code context usage. It reads local transcript files only and does not
|
|
596
|
+
upload transcript content.
|
|
597
|
+
|
|
598
|
+
Project-scoped installs write only project \`.agents\` skill and command
|
|
599
|
+
artifacts; user-scoped installs write global Codex/Claude instructions.
|
|
600
|
+
|
|
601
|
+
## Run
|
|
602
|
+
|
|
603
|
+
Current or most recent local thread:
|
|
604
|
+
|
|
605
|
+
\`\`\`sh
|
|
606
|
+
~/.agent-native/context-xray/context-xray --open
|
|
607
|
+
\`\`\`
|
|
608
|
+
|
|
609
|
+
Thread picker / recent sessions:
|
|
610
|
+
|
|
611
|
+
\`\`\`sh
|
|
612
|
+
~/.agent-native/context-xray/context-xray threads --open
|
|
613
|
+
\`\`\`
|
|
614
|
+
|
|
615
|
+
Weekly trends:
|
|
616
|
+
|
|
617
|
+
\`\`\`sh
|
|
618
|
+
~/.agent-native/context-xray/context-xray trends --since 7d --open
|
|
619
|
+
\`\`\`
|
|
620
|
+
|
|
621
|
+
Exact session when the host exposes one:
|
|
622
|
+
|
|
623
|
+
\`\`\`sh
|
|
624
|
+
~/.agent-native/context-xray/context-xray --session-id "$CLAUDE_CODE_SESSION_ID" --open
|
|
625
|
+
\`\`\`
|
|
626
|
+
|
|
627
|
+
After running, report the link, the number of sessions analyzed, the largest
|
|
628
|
+
context buckets, and 3-5 specific optimizations.
|
|
629
|
+
\`--open\` opens the generated local HTML file directly and does not keep a
|
|
630
|
+
background report server running.
|
|
631
|
+
|
|
632
|
+
## Interpret
|
|
633
|
+
|
|
634
|
+
- Tool output heavy: use narrower commands, smaller file ranges, and summarized
|
|
635
|
+
logs.
|
|
636
|
+
- Instructions heavy: move stable behavior into skills or AGENTS/CLAUDE files.
|
|
637
|
+
- Assistant prose heavy: ask for shorter status updates during long runs.
|
|
638
|
+
- One huge session: compact or start a follow-up thread with a handoff summary.
|
|
639
|
+
- Repeated path: pin a short file-role summary instead of rereading the file.
|
|
640
|
+
- Repeated tool: batch independent searches or delegate parallel inspection.
|
|
641
|
+
`;
|
|
642
|
+
export const CONTEXT_XRAY_COMMAND_MD = `---
|
|
643
|
+
description: Visualize local Codex/Claude context usage and get optimization tips.
|
|
644
|
+
argument-hint: [current|threads|trends|--since 7d]
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
Run Context X-Ray locally and show the user the generated report link plus the
|
|
648
|
+
top warnings.
|
|
649
|
+
|
|
650
|
+
Choose the command from the user's arguments:
|
|
651
|
+
|
|
652
|
+
- No arguments or \`current\`:
|
|
653
|
+
\`~/.agent-native/context-xray/context-xray --open\`
|
|
654
|
+
- \`threads\`:
|
|
655
|
+
\`~/.agent-native/context-xray/context-xray threads --open\`
|
|
656
|
+
- \`trends\`:
|
|
657
|
+
\`~/.agent-native/context-xray/context-xray trends --since 7d --open\`
|
|
658
|
+
|
|
659
|
+
If \`$ARGUMENTS\` includes flags such as \`--since 24h\`, \`--last 20\`, or
|
|
660
|
+
\`--all-projects\`, pass them through to the command. If the host exposes
|
|
661
|
+
\`CLAUDE_CODE_SESSION_ID\`, prefer:
|
|
662
|
+
|
|
663
|
+
\`\`\`sh
|
|
664
|
+
~/.agent-native/context-xray/context-xray --session-id "$CLAUDE_CODE_SESSION_ID" --open
|
|
665
|
+
\`\`\`
|
|
666
|
+
|
|
667
|
+
\`--open\` opens a local HTML report file directly; there should not be a
|
|
668
|
+
long-running server process to monitor.
|
|
669
|
+
|
|
670
|
+
After the command finishes, summarize:
|
|
671
|
+
|
|
672
|
+
- the report link
|
|
673
|
+
- sessions analyzed
|
|
674
|
+
- the largest context bucket
|
|
675
|
+
- the most important warning
|
|
676
|
+
- two or three concrete ways to improve this thread
|
|
677
|
+
`;
|
|
678
|
+
function codexHome() {
|
|
679
|
+
return process.env.CODEX_HOME?.trim() || path.join(os.homedir(), ".codex");
|
|
680
|
+
}
|
|
681
|
+
function writeExecutable(file, content) {
|
|
682
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
683
|
+
fs.writeFileSync(file, content, "utf-8");
|
|
684
|
+
fs.chmodSync(file, 0o755);
|
|
685
|
+
}
|
|
686
|
+
function writeFile(file, content, written) {
|
|
687
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
688
|
+
fs.writeFileSync(file, content, "utf-8");
|
|
689
|
+
written.push(file);
|
|
690
|
+
}
|
|
691
|
+
function installProjectArtifacts(baseDir, written) {
|
|
692
|
+
writeFile(path.join(baseDir, ".agents", "skills", "context-xray", "SKILL.md"), CONTEXT_XRAY_SKILL_MD, written);
|
|
693
|
+
writeFile(path.join(baseDir, ".agents", "commands", "context-xray.md"), CONTEXT_XRAY_COMMAND_MD, written);
|
|
694
|
+
}
|
|
695
|
+
export function installLocalContextXray(options) {
|
|
696
|
+
const installDir = path.join(os.homedir(), ".agent-native", "context-xray");
|
|
697
|
+
const scriptPath = path.join(installDir, "context-xray");
|
|
698
|
+
const binPath = path.join(os.homedir(), ".local", "bin", "context-xray");
|
|
699
|
+
const written = [];
|
|
700
|
+
if (options.dryRun) {
|
|
701
|
+
return {
|
|
702
|
+
commands: ["context-xray --open"],
|
|
703
|
+
scriptPath,
|
|
704
|
+
written,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
writeExecutable(scriptPath, CONTEXT_XRAY_EXECUTABLE);
|
|
708
|
+
written.push(scriptPath);
|
|
709
|
+
if (process.platform === "win32") {
|
|
710
|
+
const cmdPath = `${binPath}.cmd`;
|
|
711
|
+
writeExecutable(cmdPath, `@echo off\r\nnode ${JSON.stringify(scriptPath)} %*\r\n`);
|
|
712
|
+
written.push(cmdPath);
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
writeExecutable(binPath, `#!/usr/bin/env sh\nexec ${JSON.stringify(scriptPath)} "$@"\n`);
|
|
716
|
+
written.push(binPath);
|
|
717
|
+
}
|
|
718
|
+
const clientSet = new Set(options.clients);
|
|
719
|
+
const wantsCodex = clientSet.has("codex");
|
|
720
|
+
const wantsClaude = clientSet.has("claude-code") || clientSet.has("claude-code-cli");
|
|
721
|
+
if (options.scope === "project" && options.baseDir) {
|
|
722
|
+
installProjectArtifacts(options.baseDir, written);
|
|
723
|
+
}
|
|
724
|
+
else if (wantsCodex) {
|
|
725
|
+
writeFile(path.join(codexHome(), "skills", "context-xray", "SKILL.md"), CONTEXT_XRAY_SKILL_MD, written);
|
|
726
|
+
writeFile(path.join(codexHome(), "commands", "context-xray.md"), CONTEXT_XRAY_COMMAND_MD, written);
|
|
727
|
+
}
|
|
728
|
+
if (options.scope !== "project" && wantsClaude) {
|
|
729
|
+
writeFile(path.join(os.homedir(), ".claude", "skills", "context-xray", "SKILL.md"), CONTEXT_XRAY_SKILL_MD, written);
|
|
730
|
+
writeFile(path.join(os.homedir(), ".claude", "commands", "context-xray.md"), CONTEXT_XRAY_COMMAND_MD, written);
|
|
731
|
+
}
|
|
732
|
+
return {
|
|
733
|
+
commands: ["context-xray --open"],
|
|
734
|
+
scriptPath,
|
|
735
|
+
written,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
//# sourceMappingURL=context-xray-local.js.map
|