@echomem/mcp 1.4.9 → 1.4.10

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 (48) 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-canonical-adapter.js +315 -0
  5. package/dist/context-analysis/claude-native-canonical.js +35 -12
  6. package/dist/context-metrics/calculate.js +2 -15
  7. package/dist/context-metrics/estimator.js +45 -0
  8. package/dist/context-metrics/ledger.js +507 -0
  9. package/dist/context-metrics/parse-claude.js +227 -0
  10. package/dist/context-metrics/parse-codex.js +276 -0
  11. package/dist/hud/adapters.js +69 -198
  12. package/dist/hud/cli.js +0 -0
  13. package/dist/hud/efficiency.js +447 -0
  14. package/dist/hud/electron-main.js +3 -2
  15. package/dist/hud/fs.js +14 -0
  16. package/dist/hud/metric.js +4 -98
  17. package/dist/hud/monitor.js +1 -12
  18. package/dist/hud/render.js +4 -3
  19. package/dist/hud/server.js +30 -0
  20. package/dist/hud/web.js +409 -186
  21. package/dist/index.js +0 -0
  22. package/dist/setup-page/client-core.js +475 -0
  23. package/dist/setup-page/client-extraction.js +550 -0
  24. package/dist/setup-page/client-lifecycle.js +116 -0
  25. package/dist/setup-page/client-report-audit.js +818 -0
  26. package/dist/setup-page/client-report-city.js +204 -0
  27. package/dist/setup-page/client-report.js +6 -0
  28. package/dist/setup-page/client.js +15 -0
  29. package/dist/setup-page/document.js +37 -0
  30. package/dist/setup-page/styles-city-report.js +880 -0
  31. package/dist/setup-page/styles-context-audit.js +470 -0
  32. package/dist/setup-page/styles-extraction.js +821 -0
  33. package/dist/setup-page/styles-foundation.js +231 -0
  34. package/dist/setup-page/styles.js +11 -0
  35. package/dist/setup-page.js +6 -5623
  36. package/dist/setup.js +24 -10
  37. package/package.json +4 -4
  38. package/dist/city/10-problems-report.html +0 -649
  39. package/dist/city/_live.html +0 -37
  40. package/dist/city/_serve.mjs +0 -45
  41. package/dist/city/card-data.json +0 -15
  42. package/dist/city/chaos-to-clarity-pencil.html +0 -582
  43. package/dist/city/city-data.json +0 -248
  44. package/dist/city/echo-ai-city-only.template.html +0 -2271
  45. package/dist/city/generate-echo-city-only.mjs +0 -112
  46. package/dist/city/pencil-pie-generator.html +0 -883
  47. package/dist/city/pencil-webgl-landscape.html +0 -1239
  48. package/dist/city/spatial-fan-story.html +0 -479
@@ -1,8 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { calculateContextMetrics } from "../context-metrics/calculate.js";
4
+ import { estimateResidentWaste } from "../context-metrics/estimator.js";
5
+ import { parseCodexSession } from "../context-metrics/parse-codex.js";
6
+ import { parseClaudeSession } from "../context-metrics/parse-claude.js";
3
7
  import { resolveClaudeProjectsDir, resolveCodexSessionsDir } from "../local-data-paths.js";
4
- import { homePath, newestFile, readJsonl, walkFiles } from "./fs.js";
5
- import { bumpTurn, newMetricState, recordEdit, recordRead, recordTool, scoreMetric, shellRead, } from "./metric.js";
8
+ import { homePath, newestFile, resolveClaudeTranscript, walkFiles } from "./fs.js";
9
+ import { BUCKETS, newMetricState, qualityColor, scoreMetric } from "./metric.js";
6
10
  export const adapters = {
7
11
  codex: {
8
12
  client: "codex",
@@ -39,87 +43,7 @@ function findActiveCodex() {
39
43
  return newestFile(listCodex());
40
44
  }
41
45
  function scoreCodex(file) {
42
- const state = newMetricState();
43
- let ctTokens = 0;
44
- let modelContextWindow = 0;
45
- let compactMarkers = 0;
46
- let patchEdits = 0;
47
- let functionOutputs = 0;
48
- let largeFunctionOutputs = 0;
49
- let lastTool = "";
50
- const largeByTool = {};
51
- const outputTokensByTool = {};
52
- const filesByTool = {};
53
- let updatedAt = new Date().toISOString();
54
- for (const record of readJsonl(file)) {
55
- const top = isRecord(record) ? record : {};
56
- const payload = isRecord(top.payload) ? top.payload : {};
57
- const payloadType = typeof payload.type === "string" ? payload.type : "";
58
- if (typeof top.timestamp === "string")
59
- updatedAt = top.timestamp;
60
- if (payloadType === "task_started")
61
- bumpTurn(state);
62
- else if (payloadType === "token_count") {
63
- const info = isRecord(payload.info) ? payload.info : {};
64
- const last = isRecord(info.last_token_usage) ? info.last_token_usage : {};
65
- ctTokens = readNumber(last.input_tokens) || ctTokens;
66
- modelContextWindow = readNumber(info.model_context_window) || modelContextWindow;
67
- }
68
- else if (payloadType === "function_call") {
69
- const name = typeof payload.name === "string" ? payload.name : "";
70
- recordTool(state, name);
71
- if (name)
72
- lastTool = name;
73
- const args = parseArguments(payload.arguments);
74
- const cmd = isRecord(args) && typeof args.cmd === "string" ? args.cmd : "";
75
- if ((name === "exec_command" || name === "shell") && cmd) {
76
- const read = shellRead(cmd);
77
- if (read) {
78
- recordRead(state, read.file, read.start, read.end);
79
- addToolFile(filesByTool, name, read.file);
80
- }
81
- }
82
- }
83
- else if (payloadType === "function_call_output") {
84
- functionOutputs += 1;
85
- const outTool = lastTool || "output";
86
- const osize = outputSize(payload.output);
87
- // Image outputs (base64 PNGs) cost ~IMG_TOK, not their byte length — Kobe's calibration.
88
- const outTok = /image/i.test(outTool) ? 4000 : Math.round(osize / 4);
89
- outputTokensByTool[outTool] = (outputTokensByTool[outTool] || 0) + outTok;
90
- if (osize > 12_000) {
91
- largeFunctionOutputs += 1;
92
- largeByTool[outTool] = (largeByTool[outTool] || 0) + 1;
93
- }
94
- }
95
- else if (payloadType === "patch_apply_end") {
96
- const changes = isRecord(payload.changes) ? payload.changes : {};
97
- for (const changed of Object.keys(changes)) {
98
- recordEdit(state, changed);
99
- patchEdits += 1;
100
- }
101
- }
102
- else if (payloadType === "context_compacted" || top.type === "compacted") {
103
- compactMarkers += 1;
104
- }
105
- }
106
- const score = scoreMetric({
107
- client: "codex",
108
- sourcePath: file,
109
- state,
110
- ctTokens,
111
- ctSource: "token_count",
112
- modelContextWindow,
113
- updatedAt,
114
- stats: { compactMarkers, patchEdits, functionOutputs, largeFunctionOutputs },
115
- });
116
- if (Object.keys(largeByTool).length)
117
- score.largeOutputsByTool = largeByTool;
118
- if (Object.keys(outputTokensByTool).length)
119
- score.outputTokensByTool = outputTokensByTool;
120
- if (Object.keys(filesByTool).length)
121
- score.filesByTool = topFilesByTool(filesByTool);
122
- return score;
46
+ return scoreFromParsed("codex", file, parseCodexSession(fs.readFileSync(file, "utf8")), "token_count");
123
47
  }
124
48
  function listClaudeCode() {
125
49
  const root = resolveClaudeProjectsDir();
@@ -135,6 +59,12 @@ function findActiveClaudeCode() {
135
59
  }
136
60
  function scoreClaudeCode(file) {
137
61
  if (file.includes(`${path.sep}.claude${path.sep}echo-ctx${path.sep}`) || file.endsWith(".json")) {
62
+ // statusline cache: the transcript is the real data source; the cache only locates it
63
+ const transcript = resolveClaudeTranscript(file);
64
+ if (transcript) {
65
+ const score = scoreClaudeTranscript(transcript, "claude-code", "usage_estimate");
66
+ return { ...score, sourcePath: file };
67
+ }
138
68
  const raw = JSON.parse(fs.readFileSync(file, "utf8"));
139
69
  return normalizeCachedClaudeScore(raw, file, "claude-code", "statusline");
140
70
  }
@@ -151,101 +81,64 @@ function scoreClaudeDesktop(file) {
151
81
  return scoreClaudeTranscript(file, "claude-desktop", "usage_estimate");
152
82
  }
153
83
  function scoreClaudeTranscript(file, client, ctSource) {
154
- const state = newMetricState();
155
- let ctTokens = 0;
156
- let updatedAt = new Date().toISOString();
157
- const toolById = new Map();
158
- const outputTokensByTool = {};
159
- const filesByTool = {};
160
- for (const record of readJsonl(file)) {
161
- const obj = isRecord(record) ? record : {};
162
- if (typeof obj.timestamp === "string")
163
- updatedAt = obj.timestamp;
164
- const message = isRecord(obj.message) ? obj.message : obj;
165
- const role = typeof obj.type === "string" ? obj.type : typeof message.role === "string" ? message.role : "";
166
- if (role === "assistant")
167
- bumpTurn(state);
168
- const usage = isRecord(message.usage) ? message.usage : isRecord(obj.usage) ? obj.usage : null;
169
- if (usage) {
170
- const input = readNumber(usage.input_tokens) || 0;
171
- const cacheRead = readNumber(usage.cache_read_input_tokens) || 0;
172
- const cacheCreate = readNumber(usage.cache_creation_input_tokens) || 0;
173
- ctTokens = input + cacheRead + cacheCreate || ctTokens;
174
- }
175
- const content = Array.isArray(message.content) ? message.content : [];
176
- for (const block of content) {
177
- if (!isRecord(block))
178
- continue;
179
- if (block.type === "tool_use") {
180
- const name = typeof block.name === "string" ? block.name : "";
181
- if (typeof block.id === "string" && name)
182
- toolById.set(block.id, name);
183
- const input = isRecord(block.input) ? block.input : {};
184
- recordTool(state, name);
185
- if (name === "Read" && typeof input.file_path === "string") {
186
- const start = readNumber(input.offset) || 1;
187
- const end = input.limit ? start + (readNumber(input.limit) || 1) - 1 : 1e9;
188
- recordRead(state, input.file_path, start, end);
189
- addToolFile(filesByTool, name, input.file_path);
190
- }
191
- else if ((name === "Edit" || name === "Write" || name === "MultiEdit") && typeof input.file_path === "string") {
192
- recordEdit(state, input.file_path);
193
- addToolFile(filesByTool, name, input.file_path);
194
- }
195
- else if (name === "Bash" && typeof input.command === "string") {
196
- const read = shellRead(input.command);
197
- if (read) {
198
- recordRead(state, read.file, read.start, read.end);
199
- addToolFile(filesByTool, "Bash", read.file);
200
- }
201
- }
202
- }
203
- else if (block.type === "tool_result") {
204
- const id = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
205
- const tool = toolById.get(id) || "tool";
206
- outputTokensByTool[tool] = (outputTokensByTool[tool] || 0) + claudeResultTokens(block.content);
207
- }
208
- }
209
- }
210
- const score = scoreMetric({ client, sourcePath: file, state, ctTokens, ctSource, updatedAt });
211
- if (Object.keys(outputTokensByTool).length)
212
- score.outputTokensByTool = outputTokensByTool;
213
- if (Object.keys(filesByTool).length)
214
- score.filesByTool = topFilesByTool(filesByTool);
84
+ return scoreFromParsed(client, file, parseClaudeSession(fs.readFileSync(file, "utf8")), ctSource);
85
+ }
86
+ // Assemble the HUD Score from a parsed ledger: the latest local-live partition supplies resident
87
+ // waste, the shared calculator turns it into pollution/useful/health, and the ledger extras carry
88
+ // the session facts (tools, files, stats) the capsule and panels display.
89
+ function scoreFromParsed(client, file, parsed, ctSource) {
90
+ const estimate = estimateResidentWaste(parsed.ledger);
91
+ const empty = {
92
+ turn: 0, latestInputTokens: 0, wasteTokens: 0, usefulTokens: 0, trackedWasteTokens: 0,
93
+ dupTokens: 0, refindTokens: 0, deadReadTokens: 0, deadImageTokens: 0, dupCount: 0,
94
+ turnsSeen: 0, anchorSource: "none",
95
+ };
96
+ const e = estimate ?? empty;
97
+ const modelWindow = parsed.extras.modelContextWindow;
98
+ const metrics = calculateContextMetrics({
99
+ latestInputTokens: e.latestInputTokens,
100
+ modelContextLimitTokens: modelWindow,
101
+ currentResidentWasteTokens: e.wasteTokens,
102
+ });
103
+ const pollution = Math.min(0.95, metrics.noisePct);
104
+ const pollutionPct = Math.round(pollution * 100);
105
+ const saturationPct = metrics.contextFullnessPct !== undefined ? Math.round(metrics.contextFullnessPct * 100) : null;
106
+ const otherDead = Math.max(0, e.wasteTokens - e.dupTokens - e.refindTokens - e.deadReadTokens - e.deadImageTokens);
107
+ const buckets = {
108
+ [BUCKETS.rangeRedundant]: { tokens: e.dupTokens, count: e.dupCount },
109
+ [BUCKETS.staleRead]: { tokens: e.deadReadTokens + otherDead, count: 0 },
110
+ [BUCKETS.supersededImage]: { tokens: e.deadImageTokens, count: 0 },
111
+ [BUCKETS.staleToolOutput]: { tokens: e.refindTokens, count: 0 },
112
+ [BUCKETS.compactionRecoverable]: { tokens: 0, count: 0 },
113
+ };
114
+ const reads = parsed.ledger.items.reduce((n, it) => n + (it.kind === "read" ? 1 : 0), 0);
115
+ const score = {
116
+ client,
117
+ sourcePath: file,
118
+ turn: e.turnsSeen,
119
+ reads,
120
+ redundantCount: e.dupCount,
121
+ usefulPct: Math.round(Math.max(0, Math.min(1, metrics.usefulPct)) * 100),
122
+ healthScorePct: metrics.healthScorePct,
123
+ pollutionPct,
124
+ pollutionTok: e.wasteTokens,
125
+ ctTokens: e.latestInputTokens,
126
+ ctSource,
127
+ modelContextWindow: modelWindow,
128
+ saturationPct,
129
+ color: qualityColor(pollutionPct, saturationPct),
130
+ buckets,
131
+ honesty: "tracked lower bound",
132
+ updatedAt: parsed.extras.updatedAt,
133
+ stats: { ...parsed.extras.stats, trackedWasteTokens: e.trackedWasteTokens, anchorSource: e.anchorSource },
134
+ tools: parsed.extras.toolCounts,
135
+ };
136
+ if (Object.keys(parsed.extras.outputTokensByTool).length)
137
+ score.outputTokensByTool = parsed.extras.outputTokensByTool;
138
+ if (Object.keys(parsed.extras.filesByTool).length)
139
+ score.filesByTool = parsed.extras.filesByTool;
215
140
  return score;
216
141
  }
217
- function addToolFile(map, tool, file) {
218
- if (!tool || !file)
219
- return;
220
- const inner = map[tool] || (map[tool] = {});
221
- inner[file] = (inner[file] || 0) + 1;
222
- }
223
- function topFilesByTool(map) {
224
- const out = {};
225
- for (const tool of Object.keys(map)) {
226
- out[tool] = Object.entries(map[tool]).sort((a, b) => b[1] - a[1]).slice(0, 12).map((entry) => entry[0]);
227
- }
228
- return out;
229
- }
230
- function claudeResultTokens(content) {
231
- if (typeof content === "string")
232
- return Math.round(outputSize(content) / 4);
233
- if (Array.isArray(content)) {
234
- let total = 0;
235
- for (const block of content) {
236
- if (!isRecord(block))
237
- continue;
238
- if (block.type === "image")
239
- total += 4000; // IMG_TOK, not base64 byte-length
240
- else if (typeof block.text === "string")
241
- total += Math.round(outputSize(block.text) / 4);
242
- else
243
- total += Math.round(outputSize(block) / 4);
244
- }
245
- return total;
246
- }
247
- return Math.round(outputSize(content) / 4);
248
- }
249
142
  function normalizeCachedClaudeScore(raw, file, client, ctSource) {
250
143
  const ctTokens = readNumber(raw.ctTokens) || readNumber(raw.size) || readNumber(raw.contextTokens) || 0;
251
144
  const pollutionTok = readNumber(raw.pollutionTok) || readNumber(raw.redundantTok) || readNumber(raw.pollution_tokens) || 0;
@@ -265,28 +158,6 @@ function normalizeCachedClaudeScore(raw, file, client, ctSource) {
265
158
  updatedAt: new Date().toISOString(),
266
159
  });
267
160
  }
268
- function parseArguments(value) {
269
- if (!value)
270
- return {};
271
- if (isRecord(value))
272
- return value;
273
- if (typeof value !== "string")
274
- return {};
275
- try {
276
- return JSON.parse(value);
277
- }
278
- catch {
279
- return {};
280
- }
281
- }
282
- function outputSize(value) {
283
- if (typeof value === "string")
284
- return Buffer.byteLength(value);
285
- return Buffer.byteLength(JSON.stringify(value || ""));
286
- }
287
161
  function readNumber(value) {
288
162
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
289
163
  }
290
- function isRecord(value) {
291
- return typeof value === "object" && value !== null && !Array.isArray(value);
292
- }
package/dist/hud/cli.js CHANGED
File without changes