@echomem/mcp 1.4.9 → 1.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/assets/hud/github.svg +1 -0
  2. package/dist/city/README.md +9 -0
  3. package/dist/city/echo-ai-city-only.html +54 -93
  4. package/dist/context-analysis/claude-native-canonical.js +35 -12
  5. package/dist/context-metrics/calculate.js +2 -15
  6. package/dist/context-metrics/estimator.js +45 -0
  7. package/dist/context-metrics/ledger.js +507 -0
  8. package/dist/context-metrics/parse-claude.js +227 -0
  9. package/dist/context-metrics/parse-codex.js +276 -0
  10. package/dist/hud/adapters.js +69 -198
  11. package/dist/hud/electron-main.js +3 -2
  12. package/dist/hud/fs.js +14 -0
  13. package/dist/hud/metric.js +4 -98
  14. package/dist/hud/monitor.js +1 -12
  15. package/dist/hud/render.js +4 -3
  16. package/dist/hud/server.js +30 -0
  17. package/dist/hud/web.js +409 -186
  18. package/dist/index.js +1 -0
  19. package/dist/setup-page/client-core.js +475 -0
  20. package/dist/setup-page/client-extraction.js +550 -0
  21. package/dist/setup-page/client-lifecycle.js +116 -0
  22. package/dist/setup-page/client-report-audit.js +818 -0
  23. package/dist/setup-page/client-report-city.js +204 -0
  24. package/dist/setup-page/client-report.js +6 -0
  25. package/dist/setup-page/client.js +15 -0
  26. package/dist/setup-page/document.js +37 -0
  27. package/dist/setup-page/styles-city-report.js +880 -0
  28. package/dist/setup-page/styles-context-audit.js +470 -0
  29. package/dist/setup-page/styles-extraction.js +821 -0
  30. package/dist/setup-page/styles-foundation.js +231 -0
  31. package/dist/setup-page/styles.js +11 -0
  32. package/dist/setup-page.js +6 -5623
  33. package/dist/setup.js +24 -10
  34. package/package.json +4 -4
  35. package/dist/city/10-problems-report.html +0 -649
  36. package/dist/city/_live.html +0 -37
  37. package/dist/city/_serve.mjs +0 -45
  38. package/dist/city/card-data.json +0 -15
  39. package/dist/city/chaos-to-clarity-pencil.html +0 -582
  40. package/dist/city/city-data.json +0 -248
  41. package/dist/city/echo-ai-city-only.template.html +0 -2271
  42. package/dist/city/generate-echo-city-only.mjs +0 -112
  43. package/dist/city/pencil-pie-generator.html +0 -883
  44. package/dist/city/pencil-webgl-landscape.html +0 -1239
  45. package/dist/city/spatial-fan-story.html +0 -479
@@ -0,0 +1,227 @@
1
+ // Claude Code / Claude Desktop transcript JSONL -> SessionLedger (+ HUD Score extras), single pass.
2
+ //
3
+ // Structure facts this parser relies on (verified against real ~/.claude/projects transcripts):
4
+ // - assistant lines carry message.usage; resident context = input + cache_read + cache_creation.
5
+ // - tool_use blocks live in assistant messages; their tool_result comes back in user lines.
6
+ // - system lines with subtype "compact_boundary" mark compactions; the replacement summary is a
7
+ // user line flagged isCompactSummary.
8
+ // - prior-turn thinking blocks are not carried into later payloads (same-turn resident only).
9
+ // - isSidechain lines belong to subagents, not the main thread's context window.
10
+ import { IMG_TOK, R_CODE, R_TEXT, TRUNC_CAP } from "./ledger.js";
11
+ import { addToolFile, baseName, cmdKind, normalizeCommand, readFileArg, readRange, searchPattern, topFilesByTool, } from "./parse-codex.js";
12
+ const READ_TEXT_CAP = 60000;
13
+ const WRITE_CORPUS_CAP = 400000;
14
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
15
+ const num = (value) => (typeof value === "number" && Number.isFinite(value) ? value : 0);
16
+ export function parseClaudeSession(text) {
17
+ let turn = 0;
18
+ const items = [];
19
+ const editEpoch = new Map();
20
+ const compactionTurns = [];
21
+ const usage = new Map();
22
+ const commitTurns = new Set();
23
+ const commitMessages = new Map();
24
+ const userMessages = new Map();
25
+ const readChunks = [];
26
+ const writeChunks = [];
27
+ const readLenByFile = new Map();
28
+ let writeCorpusLen = 0;
29
+ const toolById = new Map();
30
+ let overheadBase = 0, firstUserTokens = 0, sawFirstUsage = false;
31
+ let updatedAt = new Date().toISOString();
32
+ let compactMarkers = 0, patchEdits = 0;
33
+ const toolCounts = {};
34
+ const outputTokensByTool = {};
35
+ const filesByTool = new Map();
36
+ const push = (item) => { items.push({ ...item, seq: items.length }); };
37
+ for (const line of text.split(/\n/)) {
38
+ if (!line.trim())
39
+ continue;
40
+ let r;
41
+ try {
42
+ r = JSON.parse(line);
43
+ }
44
+ catch {
45
+ continue;
46
+ }
47
+ if (!isRecord(r) || r.isSidechain)
48
+ continue;
49
+ if (typeof r.timestamp === "string")
50
+ updatedAt = r.timestamp;
51
+ const t = r.type;
52
+ if (t === "system") {
53
+ if (r.subtype === "compact_boundary") {
54
+ compactionTurns.push(turn);
55
+ compactMarkers += 1;
56
+ }
57
+ continue;
58
+ }
59
+ if (t !== "user" && t !== "assistant")
60
+ continue;
61
+ const msg = isRecord(r.message) ? r.message : {};
62
+ const content = Array.isArray(msg.content)
63
+ ? msg.content
64
+ : typeof msg.content === "string" ? [{ type: "text", text: msg.content }] : [];
65
+ if (t === "user") {
66
+ const hasToolResult = content.some((b) => isRecord(b) && b.type === "tool_result");
67
+ if (r.isCompactSummary) {
68
+ const txt = content.map((b) => (isRecord(b) && typeof b.text === "string" ? b.text : "")).join("");
69
+ push({ turn: Math.max(turn, 1), kind: "conv_user", compactSummary: true, tokens: Math.round(txt.length / R_TEXT) });
70
+ continue;
71
+ }
72
+ if (!hasToolResult && r.userType === "external" && !r.isMeta) {
73
+ const txt = content.filter((b) => isRecord(b) && b.type === "text").map((b) => String(b.text || "")).join("\n");
74
+ const isCommand = /^<(command-name|local-command|bash-input)/.test(txt.trim());
75
+ if (txt.trim() && !isCommand) {
76
+ turn++;
77
+ userMessages.set(turn, txt);
78
+ push({ turn, kind: "conv_user", tokens: Math.round(txt.length / R_TEXT) });
79
+ if (!firstUserTokens)
80
+ firstUserTokens = Math.round(txt.length / R_TEXT);
81
+ for (const b of content)
82
+ if (isRecord(b) && b.type === "image")
83
+ push({ turn, kind: "image", source: "user_reference", target: "user_reference", tokens: IMG_TOK });
84
+ continue;
85
+ }
86
+ }
87
+ if (turn === 0)
88
+ continue;
89
+ for (const b of content) {
90
+ if (!isRecord(b) || b.type !== "tool_result")
91
+ continue;
92
+ const reg = toolById.get(String(b.tool_use_id || "")) || { kind: "command", name: "tool" };
93
+ const blocks = Array.isArray(b.content) ? b.content : typeof b.content === "string" ? [{ type: "text", text: b.content }] : [];
94
+ let resultText = "";
95
+ let images = 0;
96
+ for (const cb of blocks) {
97
+ if (!isRecord(cb))
98
+ continue;
99
+ if (cb.type === "image")
100
+ images++;
101
+ else if (typeof cb.text === "string")
102
+ resultText += cb.text;
103
+ }
104
+ for (let i = 0; i < images; i++) {
105
+ push({ turn, kind: "image", source: "tool_screenshot", target: reg.name || "tool_image", tokens: IMG_TOK });
106
+ outputTokensByTool[reg.name] = (outputTokensByTool[reg.name] || 0) + IMG_TOK;
107
+ }
108
+ if (resultText) {
109
+ const tok = Math.min(Math.round(resultText.length / R_CODE), TRUNC_CAP);
110
+ const kind = reg.kind === "read" ? "read" : reg.kind === "search" ? "search" : "command";
111
+ push({ turn, kind, file: reg.file, range: reg.range, normalizedCommand: reg.normalizedCommand, searchPattern: reg.searchPattern, tokens: tok });
112
+ outputTokensByTool[reg.name] = (outputTokensByTool[reg.name] || 0) + tok;
113
+ if (kind === "read" && reg.file) {
114
+ const seen = readLenByFile.get(reg.file) || 0;
115
+ if (seen < READ_TEXT_CAP) {
116
+ readChunks.push({ file: reg.file, turn, text: resultText.slice(0, READ_TEXT_CAP) });
117
+ readLenByFile.set(reg.file, seen + resultText.length);
118
+ }
119
+ }
120
+ }
121
+ }
122
+ continue;
123
+ }
124
+ // assistant line
125
+ if (turn === 0)
126
+ continue;
127
+ const u = msg.usage;
128
+ if (isRecord(u)) {
129
+ const total = num(u.input_tokens) + num(u.cache_read_input_tokens) + num(u.cache_creation_input_tokens);
130
+ if (total > (usage.get(turn) || 0))
131
+ usage.set(turn, total);
132
+ if (!sawFirstUsage) {
133
+ sawFirstUsage = true;
134
+ overheadBase = Math.max(0, total - firstUserTokens);
135
+ }
136
+ }
137
+ for (const b of content) {
138
+ if (!isRecord(b))
139
+ continue;
140
+ if (b.type === "text" && typeof b.text === "string") {
141
+ push({ turn, kind: "conv_agent", tokens: Math.round(b.text.length / R_TEXT) });
142
+ }
143
+ else if (b.type === "thinking" && typeof b.thinking === "string") {
144
+ push({ turn, kind: "reasoning", tokens: Math.round(b.thinking.length / R_TEXT) });
145
+ }
146
+ else if (b.type === "tool_use") {
147
+ const name = typeof b.name === "string" ? b.name : "tool";
148
+ const input = isRecord(b.input) ? b.input : {};
149
+ toolCounts[name] = (toolCounts[name] || 0) + 1;
150
+ let reg = { kind: "command", name, normalizedCommand: name };
151
+ if (name === "Read" && typeof input.file_path === "string") {
152
+ const start = num(input.offset) || 1;
153
+ const end = input.limit ? start + (num(input.limit) || 1) - 1 : null;
154
+ reg = { kind: "read", name, file: baseName(input.file_path), range: end ? [start, end] : null, normalizedCommand: normalizeCommand(`Read ${input.file_path}`) };
155
+ addToolFile(filesByTool, name, input.file_path);
156
+ }
157
+ else if ((name === "Edit" || name === "Write" || name === "MultiEdit" || name === "NotebookEdit") && typeof input.file_path === "string") {
158
+ const body = typeof input.content === "string" ? input.content : typeof input.new_string === "string" ? input.new_string : JSON.stringify(input.edits || "");
159
+ const f = baseName(input.file_path);
160
+ if (f) {
161
+ editEpoch.set(f, [...(editEpoch.get(f) || []), turn]);
162
+ patchEdits += 1;
163
+ push({ turn, kind: "written", file: f, files: [f], tokens: Math.round(body.length / R_CODE) });
164
+ if (writeCorpusLen < WRITE_CORPUS_CAP) {
165
+ writeChunks.push({ turn, text: body });
166
+ writeCorpusLen += body.length;
167
+ }
168
+ reg = { kind: "edit", name, file: f };
169
+ addToolFile(filesByTool, name, input.file_path);
170
+ }
171
+ }
172
+ else if (name === "Bash" && typeof input.command === "string") {
173
+ const cmd = input.command;
174
+ const cm = cmd.match(/commit[^"']*-m\s+["']([^"']+)["']/) || cmd.match(/-m\s+["']([^"']+)["']/);
175
+ // unlike the golden codex regex, also accept flag-with-argument forms
176
+ // (git -C <path> commit, git -c k=v commit) — a real multi-repo pattern in Claude sessions
177
+ if (/git\s+(?:-[cC]\s+\S+\s+|--?[\w-]+(?:=\S+)?\s+)*commit\b/.test(cmd)) {
178
+ commitTurns.add(turn);
179
+ if (cm)
180
+ commitMessages.set(turn, cm[1]);
181
+ }
182
+ const file = baseName(readFileArg(cmd) || undefined);
183
+ reg = { kind: cmdKind(cmd), name, file, range: readRange(cmd), normalizedCommand: normalizeCommand(cmd), searchPattern: searchPattern(cmd) };
184
+ if (file)
185
+ addToolFile(filesByTool, name, file);
186
+ }
187
+ else if ((name === "Grep" || name === "Glob") && (typeof input.pattern === "string" || typeof input.query === "string")) {
188
+ const pat = String(input.pattern || input.query || "");
189
+ reg = { kind: "search", name, searchPattern: pat.slice(0, 80), normalizedCommand: normalizeCommand(`${name} ${pat}`) };
190
+ }
191
+ else if (name === "WebSearch" || name === "WebFetch") {
192
+ const q = String(input.query || input.url || "");
193
+ reg = { kind: "search", name, searchPattern: q.slice(0, 80), normalizedCommand: normalizeCommand(`${name} ${q}`) };
194
+ }
195
+ if (typeof b.id === "string")
196
+ toolById.set(b.id, reg);
197
+ }
198
+ }
199
+ }
200
+ const extras = {
201
+ updatedAt,
202
+ modelContextWindow: null,
203
+ toolCounts,
204
+ outputTokensByTool,
205
+ filesByTool: topFilesByTool(filesByTool),
206
+ stats: { compactMarkers, patchEdits },
207
+ };
208
+ return {
209
+ ledger: {
210
+ client: "claude",
211
+ overhead: overheadBase,
212
+ items,
213
+ usage,
214
+ reasoningCalls: [],
215
+ compactionTurns,
216
+ editEpoch,
217
+ commitTurns,
218
+ commitMessages,
219
+ userMessages,
220
+ readChunks,
221
+ writeChunks,
222
+ reasoningStyle: "items",
223
+ hardCompactionDrop: true,
224
+ },
225
+ extras,
226
+ };
227
+ }
@@ -0,0 +1,276 @@
1
+ // Codex rollout JSONL -> SessionLedger (+ HUD Score extras), single pass.
2
+ // Event handling and token sizing ported 1:1 from the Context Golden Standard parser.
3
+ import { IMG_TOK, R_CODE, R_TEXT, TRUNC_CAP } from "./ledger.js";
4
+ const READ_VERBS = new Set(["cat", "head", "tail", "sed", "nl", "less", "more", "bat", "strings", "view"]);
5
+ const SEARCH_VERBS = new Set(["rg", "grep", "ag", "ack", "find", "fd", "fgrep", "egrep"]);
6
+ const reTok = /Original token count:\s*(\d+)/;
7
+ const READ_TEXT_CAP = 60000;
8
+ const WRITE_CORPUS_CAP = 400000;
9
+ export const baseName = (p) => {
10
+ if (!p)
11
+ return undefined;
12
+ const out = String(p).split("/").pop();
13
+ return out ? out.toLowerCase() : undefined;
14
+ };
15
+ export function readFileArg(cmd) {
16
+ const seg = cmd.split("|")[0].split(">")[0].trim();
17
+ const t = seg.split(/\s+/);
18
+ const v = (t[0] || "").split("/").pop() || "";
19
+ if (!READ_VERBS.has(v))
20
+ return null;
21
+ for (let i = t.length - 1; i >= 1; i--) {
22
+ const x = t[i].replace(/^['"]|['"]$/g, "");
23
+ if (x && !x.startsWith("-") && (x.includes("/") || x.includes(".")))
24
+ return x;
25
+ }
26
+ return null;
27
+ }
28
+ export function readRange(cmd) {
29
+ const ms = [...cmd.matchAll(/(\d+),(\d+)\s*p/g)];
30
+ if (!ms.length)
31
+ return null;
32
+ return [Math.min(...ms.map((m) => +m[1])), Math.max(...ms.map((m) => +m[2]))];
33
+ }
34
+ export function cmdKind(cmd) {
35
+ const v = (cmd.split("|")[0].split(">")[0].trim().split(/\s+/)[0] || "").split("/").pop() || "";
36
+ if (SEARCH_VERBS.has(v))
37
+ return "search";
38
+ if (READ_VERBS.has(v))
39
+ return "read";
40
+ return "command";
41
+ }
42
+ export function normalizeCommand(cmd) {
43
+ return cmd.replace(/\s+/g, " ").trim().replace(/["'][^"']{40,}["']/g, '"..."').replace(/\b\d{4,}\b/g, "N").slice(0, 220);
44
+ }
45
+ export function searchPattern(cmd) {
46
+ const match = cmd.match(/(?:rg|grep|ag|ack)\s+(?:-[^\s]+\s+)*['"]?([^'"\s][^'"]{0,80})/);
47
+ return match ? match[1].replace(/\s+/g, " ").trim() : normalizeCommand(cmd).slice(0, 80);
48
+ }
49
+ function patchFiles(input) {
50
+ const files = new Set();
51
+ for (const line of input.split(/\n/)) {
52
+ const match = line.match(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/) || line.match(/^\*\*\* Move to: (.+)$/);
53
+ const file = baseName(match?.[1]);
54
+ if (file)
55
+ files.add(file);
56
+ }
57
+ return [...files];
58
+ }
59
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
60
+ const num = (value) => (typeof value === "number" && Number.isFinite(value) ? value : 0);
61
+ function isImageOut(out) {
62
+ return Array.isArray(out) && out.some((b) => isRecord(b) && (b.type === "input_image" || "image_url" in b));
63
+ }
64
+ function countImagesDeep(value) {
65
+ if (!value)
66
+ return 0;
67
+ if (typeof value === "string") {
68
+ return (value.match(/"type"\s*:\s*"input_image"/g) || []).length + (value.match(/data:image\//g) || []).length;
69
+ }
70
+ if (Array.isArray(value))
71
+ return value.reduce((a, x) => a + countImagesDeep(x), 0);
72
+ if (isRecord(value))
73
+ return (value.type === "input_image" ? 1 : 0) + Object.values(value).reduce((a, x) => a + countImagesDeep(x), 0);
74
+ return 0;
75
+ }
76
+ export function addToolFile(map, tool, file) {
77
+ if (!tool || !file)
78
+ return;
79
+ const inner = map.get(tool) || new Map();
80
+ inner.set(file, (inner.get(file) || 0) + 1);
81
+ map.set(tool, inner);
82
+ }
83
+ export function topFilesByTool(map) {
84
+ const out = {};
85
+ for (const [tool, files] of map) {
86
+ out[tool] = [...files.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12).map((entry) => entry[0]);
87
+ }
88
+ return out;
89
+ }
90
+ export function parseCodexSession(text) {
91
+ let turn = 0, baseTok = 0, toolsTok = 0, turnCtxTok = 0;
92
+ const items = [];
93
+ const editEpoch = new Map();
94
+ const compactionTurns = [];
95
+ const usage = new Map();
96
+ const reasoningCalls = [];
97
+ const callKind = new Map();
98
+ const commitTurns = new Set();
99
+ const commitMessages = new Map();
100
+ const userMessages = new Map();
101
+ const readChunks = [];
102
+ const writeChunks = [];
103
+ const readLenByFile = new Map();
104
+ let writeCorpusLen = 0;
105
+ let updatedAt = new Date().toISOString();
106
+ let modelContextWindow = 0;
107
+ let lastTool = "";
108
+ let compactMarkers = 0, patchEdits = 0, functionOutputs = 0, largeFunctionOutputs = 0;
109
+ const toolCounts = {};
110
+ const outputTokensByTool = {};
111
+ const filesByTool = new Map();
112
+ const push = (item) => { items.push({ ...item, seq: items.length }); };
113
+ for (const line of text.split(/\n/)) {
114
+ if (!line.trim())
115
+ continue;
116
+ let r;
117
+ try {
118
+ r = JSON.parse(line);
119
+ }
120
+ catch {
121
+ continue;
122
+ }
123
+ if (!isRecord(r))
124
+ continue;
125
+ const p = isRecord(r.payload) ? r.payload : {};
126
+ const pt = typeof p.type === "string" ? p.type : "";
127
+ if (typeof r.timestamp === "string")
128
+ updatedAt = r.timestamp;
129
+ if (r.type === "session_meta" || pt === "session_meta") {
130
+ const base = isRecord(p.base_instructions) && typeof p.base_instructions.text === "string" ? p.base_instructions.text : "";
131
+ baseTok = Math.max(baseTok, Math.round(base.length / R_TEXT));
132
+ toolsTok = Math.max(toolsTok, Math.round(JSON.stringify(p.dynamic_tools || []).length / R_CODE));
133
+ }
134
+ else if (r.type === "turn_context" || p.collaboration_mode) {
135
+ turnCtxTok = Math.max(turnCtxTok, Math.round(JSON.stringify(p).length / R_TEXT));
136
+ }
137
+ else if (pt === "user_message") {
138
+ turn++;
139
+ const txt = typeof p.message === "string" ? p.message : JSON.stringify(p.message || "");
140
+ userMessages.set(turn, txt);
141
+ push({ turn, kind: "conv_user", tokens: Math.round(txt.length / R_TEXT) });
142
+ const images = (Array.isArray(p.images) ? p.images.length : 0) + (Array.isArray(p.local_images) ? p.local_images.length : 0) + countImagesDeep(p.message || "");
143
+ for (let i = 0; i < images; i++)
144
+ push({ turn, kind: "image", source: "user_reference", target: "user_reference", tokens: IMG_TOK });
145
+ }
146
+ else if (turn === 0) {
147
+ continue;
148
+ }
149
+ else if (pt === "token_count" && isRecord(p.info)) {
150
+ const lu = isRecord(p.info.last_token_usage) ? p.info.last_token_usage : {};
151
+ const input = num(lu.input_tokens);
152
+ if (input > (usage.get(turn) || 0))
153
+ usage.set(turn, input);
154
+ modelContextWindow = num(p.info.model_context_window) || modelContextWindow;
155
+ if (num(lu.reasoning_output_tokens))
156
+ reasoningCalls.push({ turn, tokens: num(lu.reasoning_output_tokens) });
157
+ }
158
+ else if (pt === "message" && p.role === "developer") {
159
+ const txt = Array.isArray(p.content) ? p.content.map((b) => (isRecord(b) && typeof b.text === "string" ? b.text : "")).join("") : "";
160
+ push({ turn, kind: "conv_agent", tokens: Math.round(txt.length / R_TEXT) });
161
+ }
162
+ else if (pt === "context_compacted") {
163
+ compactionTurns.push(turn);
164
+ compactMarkers += 1;
165
+ }
166
+ else if (pt === "agent_message") {
167
+ push({ turn, kind: "conv_agent", tokens: Math.round(String(p.message || "").length / R_TEXT) });
168
+ }
169
+ else if (pt === "patch_apply_end" && isRecord(p.changes)) {
170
+ for (const f of Object.keys(p.changes)) {
171
+ const file = baseName(f);
172
+ if (!file)
173
+ continue;
174
+ editEpoch.set(file, [...(editEpoch.get(file) || []), turn]);
175
+ patchEdits += 1;
176
+ }
177
+ }
178
+ else if (pt === "custom_tool_call" && typeof p.input === "string") {
179
+ const files = patchFiles(p.input);
180
+ push({ turn, kind: "written", file: files.length === 1 ? files[0] : undefined, files, tokens: Math.round(p.input.length / R_CODE) });
181
+ if (writeCorpusLen < WRITE_CORPUS_CAP) {
182
+ writeChunks.push({ turn, text: p.input });
183
+ writeCorpusLen += p.input.length;
184
+ }
185
+ }
186
+ else if (pt === "function_call") {
187
+ const name = typeof p.name === "string" ? p.name : "";
188
+ toolCounts[name || "tool"] = (toolCounts[name || "tool"] || 0) + 1;
189
+ if (name)
190
+ lastTool = name;
191
+ const callId = typeof p.call_id === "string" ? p.call_id : "";
192
+ if (name === "exec_command") {
193
+ let a = p.arguments;
194
+ if (typeof a === "string") {
195
+ try {
196
+ a = JSON.parse(a);
197
+ }
198
+ catch {
199
+ a = { cmd: a };
200
+ }
201
+ }
202
+ const cmd = isRecord(a) ? String(a.cmd || a.command || "") : "";
203
+ const cm = cmd.match(/commit[^"']*-m\s+["']([^"']+)["']/) || cmd.match(/-m\s+["']([^"']+)["']/);
204
+ if (/git\s+(?:-[^\s]+\s+)*commit/.test(cmd)) {
205
+ commitTurns.add(turn);
206
+ if (cm)
207
+ commitMessages.set(turn, cm[1]);
208
+ }
209
+ if (callId) {
210
+ const file = baseName(readFileArg(cmd) || undefined);
211
+ callKind.set(callId, { kind: cmdKind(cmd), file, range: readRange(cmd), normalizedCommand: normalizeCommand(cmd), searchPattern: searchPattern(cmd) });
212
+ if (file && (name === "exec_command" || name === "shell"))
213
+ addToolFile(filesByTool, name, file);
214
+ }
215
+ }
216
+ else if (callId) {
217
+ callKind.set(callId, { kind: "command", target: name || "tool_image", imageProducer: name === "view_image" || name === "js", normalizedCommand: name || "tool_image" });
218
+ }
219
+ }
220
+ else if (pt === "function_call_output") {
221
+ functionOutputs += 1;
222
+ const callId = typeof p.call_id === "string" ? p.call_id : "";
223
+ const m = callKind.get(callId) || { kind: "command" };
224
+ const out = p.output;
225
+ const imageCount = countImagesDeep(out);
226
+ const outTool = lastTool || "output";
227
+ if (isImageOut(out) || (m.imageProducer && typeof out !== "string")) {
228
+ push({ turn, kind: "image", source: "tool_screenshot", target: m.target || "tool_image", tokens: IMG_TOK });
229
+ outputTokensByTool[outTool] = (outputTokensByTool[outTool] || 0) + IMG_TOK * Math.max(1, imageCount);
230
+ }
231
+ else {
232
+ const str = typeof out === "string" ? out : JSON.stringify(out || "");
233
+ const mm = str.match(reTok);
234
+ const tok = Math.min(mm ? +mm[1] : Math.round(str.length / R_CODE), TRUNC_CAP);
235
+ const kind = m.kind === "read" ? "read" : m.kind === "search" ? "search" : "command";
236
+ push({ turn, kind, file: m.file, range: m.range, normalizedCommand: m.normalizedCommand, searchPattern: m.searchPattern, tokens: tok });
237
+ outputTokensByTool[outTool] = (outputTokensByTool[outTool] || 0) + tok;
238
+ if (str.length > 12000)
239
+ largeFunctionOutputs += 1;
240
+ if (kind === "read" && m.file) {
241
+ const seen = readLenByFile.get(m.file) || 0;
242
+ if (seen < READ_TEXT_CAP) {
243
+ readChunks.push({ file: m.file, turn, text: str.slice(0, READ_TEXT_CAP) });
244
+ readLenByFile.set(m.file, seen + str.length);
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+ return {
251
+ ledger: {
252
+ client: "codex",
253
+ overhead: baseTok + toolsTok + turnCtxTok,
254
+ items,
255
+ usage,
256
+ reasoningCalls,
257
+ compactionTurns,
258
+ editEpoch,
259
+ commitTurns,
260
+ commitMessages,
261
+ userMessages,
262
+ readChunks,
263
+ writeChunks,
264
+ reasoningStyle: "cumulative",
265
+ hardCompactionDrop: false,
266
+ },
267
+ extras: {
268
+ updatedAt,
269
+ modelContextWindow: modelContextWindow || null,
270
+ toolCounts,
271
+ outputTokensByTool,
272
+ filesByTool: topFilesByTool(filesByTool),
273
+ stats: { compactMarkers, patchEdits, functionOutputs, largeFunctionOutputs },
274
+ },
275
+ };
276
+ }