@echomem/mcp 1.4.18 → 1.4.19

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.
@@ -0,0 +1,315 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * Convert a Claude Code transcript into the event vocabulary consumed by the
5
+ * vendored canonical scorer. The scorer itself remains provider-independent:
6
+ * this adapter is the only place where Claude's JSONL shape is interpreted.
7
+ */
8
+ export function normalizeClaudeSessionForCanonical(file) {
9
+ const rows = readRows(file);
10
+ const sessionId = firstString(rows, "sessionId") || path.basename(file, ".jsonl");
11
+ const cwd = firstString(rows, "cwd");
12
+ const dynamicTools = collectToolNames(rows).map((name) => ({ name }));
13
+ const inferredOverheadTokens = inferClaudeOverheadTokens(rows);
14
+ const toolTokens = Math.round(JSON.stringify(dynamicTools).length / 3.3);
15
+ const instructionTokens = Math.max(0, inferredOverheadTokens - toolTokens);
16
+ const records = [codexRecord("session_meta", {
17
+ type: "session_meta",
18
+ id: sessionId,
19
+ cwd,
20
+ base_instructions: { text: "x".repeat(instructionTokens * 4) },
21
+ dynamic_tools: dynamicTools,
22
+ }, firstTimestamp(rows))];
23
+ const calls = new Map();
24
+ let hasUserTurn = false;
25
+ for (const row of rows) {
26
+ const timestamp = stringValue(row.timestamp) || firstTimestamp(rows);
27
+ const type = stringValue(row.type);
28
+ if (type === "user") {
29
+ const message = recordValue(row.message);
30
+ const content = message.content;
31
+ if (isToolResultContent(content)) {
32
+ if (!hasUserTurn)
33
+ continue;
34
+ for (const block of arrayValue(content)) {
35
+ if (!isRecord(block) || block.type !== "tool_result")
36
+ continue;
37
+ const callId = stringValue(block.tool_use_id);
38
+ const call = calls.get(callId);
39
+ if (!call || call.mode === "write")
40
+ continue;
41
+ records.push(codexRecord("response_item", {
42
+ type: "function_call_output",
43
+ call_id: callId,
44
+ output: normalizeToolResult(block.content),
45
+ }, timestamp));
46
+ }
47
+ continue;
48
+ }
49
+ const messageText = extractText(content);
50
+ const imageCount = countImages(content);
51
+ records.push(codexRecord("event_msg", {
52
+ type: "user_message",
53
+ message: messageText,
54
+ ...(imageCount > 0 ? { images: Array.from({ length: imageCount }, () => ({ type: "input_image" })) } : {}),
55
+ }, timestamp));
56
+ hasUserTurn = true;
57
+ continue;
58
+ }
59
+ if (type === "system" && hasUserTurn && (row.subtype === "compact_boundary" || row.subtype === "context_compacted")) {
60
+ records.push(codexRecord("event_msg", { type: "context_compacted" }, timestamp));
61
+ continue;
62
+ }
63
+ if (type !== "assistant" || !hasUserTurn)
64
+ continue;
65
+ const message = recordValue(row.message);
66
+ const usage = recordValue(message.usage);
67
+ const inputTokens = claudeOfficialInputTokens(usage);
68
+ if (inputTokens > 0) {
69
+ records.push(codexRecord("event_msg", {
70
+ type: "token_count",
71
+ info: {
72
+ last_token_usage: {
73
+ input_tokens: inputTokens,
74
+ cached_input_tokens: numberValue(usage.cache_read_input_tokens),
75
+ output_tokens: numberValue(usage.output_tokens),
76
+ },
77
+ },
78
+ }, timestamp));
79
+ }
80
+ for (const block of arrayValue(message.content)) {
81
+ if (!isRecord(block))
82
+ continue;
83
+ if (block.type === "thinking") {
84
+ records.push(codexRecord("response_item", { type: "reasoning" }, timestamp));
85
+ continue;
86
+ }
87
+ if (block.type === "text") {
88
+ const text = stringValue(block.text);
89
+ if (text)
90
+ records.push(codexRecord("event_msg", { type: "agent_message", message: text }, timestamp));
91
+ continue;
92
+ }
93
+ if (block.type !== "tool_use")
94
+ continue;
95
+ const normalized = normalizeToolUse(block);
96
+ if (!normalized)
97
+ continue;
98
+ calls.set(normalized.call.callId, normalized.call);
99
+ records.push(...normalized.records.map((record) => ({ ...record, timestamp })));
100
+ }
101
+ }
102
+ return {
103
+ jsonl: `${records.map((record) => JSON.stringify(record)).join("\n")}\n`,
104
+ sessionId,
105
+ cwd,
106
+ inferredOverheadTokens,
107
+ };
108
+ }
109
+ function normalizeToolUse(block) {
110
+ const name = stringValue(block.name);
111
+ const callId = stringValue(block.id);
112
+ if (!name || !callId)
113
+ return null;
114
+ const input = recordValue(block.input);
115
+ if (/^(Write|Edit|MultiEdit|NotebookEdit)$/i.test(name)) {
116
+ const file = stringValue(input.file_path) || stringValue(input.notebook_path) || stringValue(input.path) || "unknown";
117
+ const patch = syntheticPatch(name, file, input);
118
+ return {
119
+ call: { mode: "write", callId },
120
+ records: [
121
+ codexRecord("response_item", { type: "custom_tool_call", name: "apply_patch", call_id: callId, input: patch }, ""),
122
+ codexRecord("event_msg", { type: "patch_apply_end", changes: { [file]: { kind: /^Write$/i.test(name) ? "added" : "modified" } } }, ""),
123
+ ],
124
+ };
125
+ }
126
+ const command = commandForClaudeTool(name, input);
127
+ if (command) {
128
+ return {
129
+ call: { mode: "function", callId },
130
+ records: [codexRecord("response_item", {
131
+ type: "function_call",
132
+ name: "exec_command",
133
+ call_id: callId,
134
+ arguments: JSON.stringify({ cmd: command }),
135
+ }, "")],
136
+ };
137
+ }
138
+ return {
139
+ call: { mode: "function", callId },
140
+ records: [codexRecord("response_item", {
141
+ type: "function_call",
142
+ name: visualToolName(name) ? "view_image" : name,
143
+ call_id: callId,
144
+ arguments: JSON.stringify(input),
145
+ }, "")],
146
+ };
147
+ }
148
+ function commandForClaudeTool(name, input) {
149
+ if (/^Bash$/i.test(name))
150
+ return stringValue(input.command) || "bash";
151
+ if (/^(Read|NotebookRead)$/i.test(name)) {
152
+ const file = stringValue(input.file_path) || stringValue(input.notebook_path) || stringValue(input.path) || "unknown";
153
+ const offset = Math.max(1, numberValue(input.offset) || 1);
154
+ const limit = Math.max(1, numberValue(input.limit) || 2000);
155
+ return `sed -n '${offset},${offset + limit - 1}p' ${quoteArg(file)}`;
156
+ }
157
+ if (/^Grep$/i.test(name)) {
158
+ const pattern = stringValue(input.pattern) || stringValue(input.query) || "unknown";
159
+ const target = stringValue(input.path) || ".";
160
+ return `rg -n ${quoteArg(pattern)} ${quoteArg(target)}`;
161
+ }
162
+ if (/^Glob$/i.test(name))
163
+ return `rg --files ${quoteArg(stringValue(input.path) || ".")}`;
164
+ if (/^(LS|ListFiles)$/i.test(name))
165
+ return `ls ${quoteArg(stringValue(input.path) || ".")}`;
166
+ if (/^(WebSearch|WebFetch)$/i.test(name)) {
167
+ const query = stringValue(input.query) || stringValue(input.url) || "web";
168
+ return `rg ${quoteArg(`web:${query}`)} web`;
169
+ }
170
+ return null;
171
+ }
172
+ function syntheticPatch(name, file, input) {
173
+ const chunks = [];
174
+ const content = stringValue(input.content) || stringValue(input.new_string) || stringValue(input.new_source);
175
+ if (content)
176
+ chunks.push(content);
177
+ for (const edit of arrayValue(input.edits)) {
178
+ if (!isRecord(edit))
179
+ continue;
180
+ const value = stringValue(edit.new_string) || stringValue(edit.new_source) || stringValue(edit.content);
181
+ if (value)
182
+ chunks.push(value);
183
+ }
184
+ const body = chunks.join("\n") || `${name} ${file}`;
185
+ return `*** Begin Patch\n*** ${/^Write$/i.test(name) ? "Add" : "Update"} File: ${file}\n@@\n${body}\n*** End Patch\n`;
186
+ }
187
+ function inferClaudeOverheadTokens(rows) {
188
+ let firstUserTokens = 0;
189
+ let firstUserImages = 0;
190
+ let awaitingUsage = false;
191
+ for (const row of rows) {
192
+ const type = stringValue(row.type);
193
+ if (type === "user" && !isToolResultContent(recordValue(row.message).content)) {
194
+ if (!awaitingUsage) {
195
+ const content = recordValue(row.message).content;
196
+ firstUserTokens = Math.round(extractText(content).length / 4);
197
+ firstUserImages = countImages(content);
198
+ awaitingUsage = true;
199
+ }
200
+ continue;
201
+ }
202
+ if (type !== "assistant" || !awaitingUsage)
203
+ continue;
204
+ const total = claudeOfficialInputTokens(recordValue(recordValue(row.message).usage));
205
+ if (total <= 0)
206
+ continue;
207
+ const structureFloor = Math.round(total * 0.03);
208
+ return Math.max(0, total - firstUserTokens - firstUserImages * 4000 - structureFloor);
209
+ }
210
+ return 0;
211
+ }
212
+ function claudeOfficialInputTokens(usage) {
213
+ return numberValue(usage.input_tokens)
214
+ + numberValue(usage.cache_read_input_tokens)
215
+ + numberValue(usage.cache_creation_input_tokens);
216
+ }
217
+ function normalizeToolResult(value) {
218
+ if (typeof value === "string")
219
+ return value;
220
+ const output = [];
221
+ for (const block of arrayValue(value)) {
222
+ if (!isRecord(block))
223
+ continue;
224
+ if (block.type === "text" && typeof block.text === "string")
225
+ output.push(block.text);
226
+ else if (block.type === "image")
227
+ output.push({ type: "input_image" });
228
+ else
229
+ output.push(block);
230
+ }
231
+ return output.length === 1 ? output[0] : output;
232
+ }
233
+ function extractText(value) {
234
+ if (typeof value === "string")
235
+ return value;
236
+ return arrayValue(value).map((block) => {
237
+ if (!isRecord(block))
238
+ return "";
239
+ if (typeof block.text === "string")
240
+ return block.text;
241
+ return "";
242
+ }).filter(Boolean).join("\n");
243
+ }
244
+ function countImages(value) {
245
+ if (!value)
246
+ return 0;
247
+ if (Array.isArray(value))
248
+ return value.reduce((sum, item) => sum + countImages(item), 0);
249
+ if (!isRecord(value))
250
+ return 0;
251
+ const own = value.type === "image" || value.type === "input_image" ? 1 : 0;
252
+ return own + Object.values(value).reduce((sum, item) => sum + countImages(item), 0);
253
+ }
254
+ function collectToolNames(rows) {
255
+ const names = new Set();
256
+ for (const row of rows) {
257
+ for (const block of arrayValue(recordValue(row.message).content)) {
258
+ if (isRecord(block) && block.type === "tool_use" && typeof block.name === "string")
259
+ names.add(block.name);
260
+ }
261
+ }
262
+ return [...names].sort();
263
+ }
264
+ function readRows(file) {
265
+ const rows = [];
266
+ for (const line of fs.readFileSync(file, "utf8").split(/\n/)) {
267
+ if (!line.trim())
268
+ continue;
269
+ try {
270
+ const value = JSON.parse(line);
271
+ if (isRecord(value))
272
+ rows.push(value);
273
+ }
274
+ catch {
275
+ // Claude may leave a partial final line while a session is active.
276
+ }
277
+ }
278
+ return rows;
279
+ }
280
+ function firstString(rows, key) {
281
+ for (const row of rows)
282
+ if (typeof row[key] === "string" && row[key])
283
+ return row[key];
284
+ return null;
285
+ }
286
+ function firstTimestamp(rows) {
287
+ return firstString(rows, "timestamp") || new Date(0).toISOString();
288
+ }
289
+ function codexRecord(type, payload, timestamp) {
290
+ return { timestamp, type, payload };
291
+ }
292
+ function isToolResultContent(value) {
293
+ return arrayValue(value).some((block) => isRecord(block) && block.type === "tool_result");
294
+ }
295
+ function visualToolName(name) {
296
+ return /screenshot|image|browser|playwright/i.test(name);
297
+ }
298
+ function quoteArg(value) {
299
+ return `"${value.replace(/["\\]/g, "_")}"`;
300
+ }
301
+ function isRecord(value) {
302
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
303
+ }
304
+ function recordValue(value) {
305
+ return isRecord(value) ? value : {};
306
+ }
307
+ function arrayValue(value) {
308
+ return Array.isArray(value) ? value : [];
309
+ }
310
+ function stringValue(value) {
311
+ return typeof value === "string" ? value : "";
312
+ }
313
+ function numberValue(value) {
314
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
315
+ }
package/dist/hud/cli.js CHANGED
File without changes