@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.
- package/assets/hud/github.svg +1 -0
- package/dist/city/README.md +9 -0
- package/dist/city/echo-ai-city-only.html +54 -93
- package/dist/context-analysis/claude-native-canonical.js +35 -12
- package/dist/context-metrics/calculate.js +2 -15
- package/dist/context-metrics/estimator.js +45 -0
- package/dist/context-metrics/ledger.js +507 -0
- package/dist/context-metrics/parse-claude.js +227 -0
- package/dist/context-metrics/parse-codex.js +276 -0
- package/dist/hud/adapters.js +69 -198
- package/dist/hud/electron-main.js +3 -2
- package/dist/hud/fs.js +14 -0
- package/dist/hud/metric.js +4 -98
- package/dist/hud/monitor.js +1 -12
- package/dist/hud/render.js +4 -3
- package/dist/hud/server.js +30 -0
- package/dist/hud/web.js +409 -186
- package/dist/index.js +1 -0
- package/dist/setup-page/client-core.js +475 -0
- package/dist/setup-page/client-extraction.js +550 -0
- package/dist/setup-page/client-lifecycle.js +116 -0
- package/dist/setup-page/client-report-audit.js +818 -0
- package/dist/setup-page/client-report-city.js +204 -0
- package/dist/setup-page/client-report.js +6 -0
- package/dist/setup-page/client.js +15 -0
- package/dist/setup-page/document.js +37 -0
- package/dist/setup-page/styles-city-report.js +880 -0
- package/dist/setup-page/styles-context-audit.js +470 -0
- package/dist/setup-page/styles-extraction.js +821 -0
- package/dist/setup-page/styles-foundation.js +231 -0
- package/dist/setup-page/styles.js +11 -0
- package/dist/setup-page.js +6 -5623
- package/dist/setup.js +24 -10
- package/package.json +4 -4
- package/dist/city/10-problems-report.html +0 -649
- package/dist/city/_live.html +0 -37
- package/dist/city/_serve.mjs +0 -45
- package/dist/city/card-data.json +0 -15
- package/dist/city/chaos-to-clarity-pencil.html +0 -582
- package/dist/city/city-data.json +0 -248
- package/dist/city/echo-ai-city-only.template.html +0 -2271
- package/dist/city/generate-echo-city-only.mjs +0 -112
- package/dist/city/pencil-pie-generator.html +0 -883
- package/dist/city/pencil-webgl-landscape.html +0 -1239
- package/dist/city/spatial-fan-story.html +0 -479
package/dist/hud/adapters.js
CHANGED
|
@@ -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,
|
|
5
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
}
|
|
@@ -12,8 +12,9 @@ const port = typeof flags.port === "string" ? Number(flags.port) || 17377 : 1737
|
|
|
12
12
|
const COLLAPSED_WIDTH = 360;
|
|
13
13
|
const COLLAPSED_HEIGHT = 112;
|
|
14
14
|
const EXPANDED_HEIGHT = 460;
|
|
15
|
-
// Mini mode: a
|
|
16
|
-
|
|
15
|
+
// Mini mode: a slim always-on pill (dot · score · agent icon · session · repo) — wide enough to
|
|
16
|
+
// read the full session name.
|
|
17
|
+
const MINI_WIDTH = 300;
|
|
17
18
|
const MINI_HEIGHT = 48;
|
|
18
19
|
const AGENT_VISIBILITY_POLL_MS = 900;
|
|
19
20
|
let miniPref = false;
|
package/dist/hud/fs.js
CHANGED
|
@@ -61,3 +61,17 @@ export function statSignature(file) {
|
|
|
61
61
|
return "";
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
// The Claude Code active source can be the echo-ctx statusline cache (<sessionId>.json). The real
|
|
65
|
+
// transcript with usage/tool data lives under ~/.claude/projects — resolve it by session id.
|
|
66
|
+
const transcriptCache = new Map();
|
|
67
|
+
export function resolveClaudeTranscript(cacheFile) {
|
|
68
|
+
if (!cacheFile.endsWith(".json") || !cacheFile.includes(`${path.sep}echo-ctx${path.sep}`))
|
|
69
|
+
return null;
|
|
70
|
+
const cached = transcriptCache.get(cacheFile);
|
|
71
|
+
if (cached !== undefined)
|
|
72
|
+
return cached;
|
|
73
|
+
const sessionId = path.basename(cacheFile, ".json");
|
|
74
|
+
const transcript = newestFile(walkFiles(homePath(".claude", "projects"), (f) => path.basename(f) === `${sessionId}.jsonl`));
|
|
75
|
+
transcriptCache.set(cacheFile, transcript);
|
|
76
|
+
return transcript;
|
|
77
|
+
}
|
package/dist/hud/metric.js
CHANGED
|
@@ -6,19 +6,11 @@ export const BUCKETS = {
|
|
|
6
6
|
staleToolOutput: "stale_tool_output",
|
|
7
7
|
compactionRecoverable: "compaction_recoverable",
|
|
8
8
|
};
|
|
9
|
-
const SHELL_READ_BINS = new Set(["cat", "head", "tail", "sed", "nl", "less", "more", "bat"]);
|
|
10
|
-
const EXT = /\.[A-Za-z0-9]{1,8}$/;
|
|
11
|
-
// A re-read counts as redundant only if a prior read substantially re-covers THIS read. A shared
|
|
12
|
-
// boundary line (sequential paging, e.g. sed 1,260p then 260,620p) is not a re-read; requiring ≥50%
|
|
13
|
-
// of the new range to be already-seen keeps paging out while still catching genuine sub-range re-reads.
|
|
14
|
-
const REDUNDANT_OVERLAP_FRACTION = 0.5;
|
|
15
9
|
export function newMetricState() {
|
|
16
10
|
return {
|
|
17
11
|
turn: 0,
|
|
18
12
|
reads: 0,
|
|
19
13
|
redundantCount: 0,
|
|
20
|
-
readHist: new Map(),
|
|
21
|
-
editTurn: new Map(),
|
|
22
14
|
buckets: {
|
|
23
15
|
[BUCKETS.rangeRedundant]: { tokens: 0, count: 0 },
|
|
24
16
|
[BUCKETS.staleRead]: { tokens: 0, count: 0 },
|
|
@@ -29,42 +21,6 @@ export function newMetricState() {
|
|
|
29
21
|
tools: {},
|
|
30
22
|
};
|
|
31
23
|
}
|
|
32
|
-
export function bumpTurn(state) {
|
|
33
|
-
state.turn += 1;
|
|
34
|
-
}
|
|
35
|
-
export function recordTool(state, name) {
|
|
36
|
-
if (!name)
|
|
37
|
-
return;
|
|
38
|
-
state.tools[name] = (state.tools[name] || 0) + 1;
|
|
39
|
-
}
|
|
40
|
-
export function recordEdit(state, file) {
|
|
41
|
-
if (!file)
|
|
42
|
-
return;
|
|
43
|
-
state.editTurn.set(file, state.turn);
|
|
44
|
-
}
|
|
45
|
-
export function recordRead(state, file, start = 1, end = 1e9) {
|
|
46
|
-
if (!file)
|
|
47
|
-
return;
|
|
48
|
-
const safeStart = Number(start) || 1;
|
|
49
|
-
const safeEnd = Number(end) || 1e9;
|
|
50
|
-
const tokens = estimateReadTokens(safeStart, safeEnd);
|
|
51
|
-
const previous = state.readHist.get(file) || [];
|
|
52
|
-
const lastEdit = state.editTurn.get(file) ?? -1;
|
|
53
|
-
const newLines = Math.max(1, safeEnd - safeStart + 1);
|
|
54
|
-
const redundant = previous.some((read) => {
|
|
55
|
-
if (read.turn < lastEdit)
|
|
56
|
-
return false; // an edit since this read invalidated it — re-read is fresh
|
|
57
|
-
const overlap = Math.min(safeEnd, read.end) - Math.max(safeStart, read.start) + 1;
|
|
58
|
-
return overlap > 0 && overlap / newLines >= REDUNDANT_OVERLAP_FRACTION;
|
|
59
|
-
});
|
|
60
|
-
state.reads += 1;
|
|
61
|
-
if (redundant) {
|
|
62
|
-
addBucket(state, BUCKETS.rangeRedundant, tokens, 1);
|
|
63
|
-
state.redundantCount += 1;
|
|
64
|
-
}
|
|
65
|
-
previous.push({ start: safeStart, end: safeEnd, turn: state.turn, tokens });
|
|
66
|
-
state.readHist.set(file, previous);
|
|
67
|
-
}
|
|
68
24
|
export function addBucket(state, bucket, tokens, count = 1) {
|
|
69
25
|
state.buckets[bucket].tokens += Math.max(0, Math.round(tokens));
|
|
70
26
|
state.buckets[bucket].count += Math.max(0, count);
|
|
@@ -105,34 +61,6 @@ export function scoreMetric(params) {
|
|
|
105
61
|
tools: { ...params.state.tools },
|
|
106
62
|
};
|
|
107
63
|
}
|
|
108
|
-
export function shellRead(cmd) {
|
|
109
|
-
const text = String(cmd || "").trim();
|
|
110
|
-
if (!text)
|
|
111
|
-
return null;
|
|
112
|
-
const firstPipeline = text.split(/[|;&]/)[0].trim();
|
|
113
|
-
const tokens = firstPipeline.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
|
|
114
|
-
if (!tokens.length)
|
|
115
|
-
return null;
|
|
116
|
-
let bin = stripQuotes(tokens[0] || "").split("/").pop() || "";
|
|
117
|
-
if (bin === "sudo")
|
|
118
|
-
bin = stripQuotes(tokens[1] || "").split("/").pop() || "";
|
|
119
|
-
if (!SHELL_READ_BINS.has(bin))
|
|
120
|
-
return null;
|
|
121
|
-
let file = "";
|
|
122
|
-
for (let i = tokens.length - 1; i >= 1; i -= 1) {
|
|
123
|
-
const token = stripQuotes(tokens[i] || "");
|
|
124
|
-
if (!token || token.startsWith("-") || /^\d+(,\d+)?p?$/.test(token))
|
|
125
|
-
continue;
|
|
126
|
-
if (token.includes("/") || EXT.test(token)) {
|
|
127
|
-
file = token;
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (!file)
|
|
132
|
-
return null;
|
|
133
|
-
const [start, end] = rangeFromCmd(text);
|
|
134
|
-
return { file, start, end };
|
|
135
|
-
}
|
|
136
64
|
export function formatTokens(tokens) {
|
|
137
65
|
const n = Number(tokens) || 0;
|
|
138
66
|
if (n >= 1_000_000)
|
|
@@ -145,36 +73,14 @@ export function formatGlance(score) {
|
|
|
145
73
|
const dot = score.color === "amber" ? "◑" : "●";
|
|
146
74
|
return `${dot} ${score.healthScorePct}% score · ${formatTokens(score.ctTokens)}`;
|
|
147
75
|
}
|
|
148
|
-
function
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
return Math.min(8000, Math.max(40, lines * 12));
|
|
152
|
-
}
|
|
153
|
-
function qualityColor(pollutionPct, saturationPct) {
|
|
154
|
-
if (pollutionPct >= 35 ||
|
|
155
|
-
(saturationPct !== null && saturationPct >= 95) ||
|
|
156
|
-
(saturationPct !== null && saturationPct >= 85 && pollutionPct >= 20)) {
|
|
76
|
+
export function qualityColor(pollutionPct, saturationPct) {
|
|
77
|
+
const usefulPct = 100 - Math.max(0, Math.min(100, pollutionPct));
|
|
78
|
+
if (usefulPct <= 40)
|
|
157
79
|
return "red";
|
|
158
|
-
|
|
159
|
-
if (pollutionPct >= 18 || (saturationPct !== null && saturationPct >= 75))
|
|
80
|
+
if (usefulPct <= 60)
|
|
160
81
|
return "amber";
|
|
161
82
|
return "green";
|
|
162
83
|
}
|
|
163
|
-
function rangeFromCmd(cmd) {
|
|
164
|
-
const sed = cmd.match(/\bsed\s+(?:[^\n;|&]*?\s)?-n\s*['"]?\s*(\d+)\s*,\s*(\d+)\s*p/);
|
|
165
|
-
if (sed)
|
|
166
|
-
return [Number(sed[1]), Number(sed[2])];
|
|
167
|
-
const nlSed = cmd.match(/\bnl\b[\s\S]*?\|\s*sed\s+-n\s*['"]?\s*(\d+)\s*,\s*(\d+)\s*p/);
|
|
168
|
-
if (nlSed)
|
|
169
|
-
return [Number(nlSed[1]), Number(nlSed[2])];
|
|
170
|
-
const head = cmd.match(/\bhead\s+(?:-n\s*)?(\d+)\b/);
|
|
171
|
-
if (head)
|
|
172
|
-
return [1, Number(head[1])];
|
|
173
|
-
return [1, 1e9];
|
|
174
|
-
}
|
|
175
|
-
function stripQuotes(value) {
|
|
176
|
-
return value.replace(/^['"]|['"]$/g, "");
|
|
177
|
-
}
|
|
178
84
|
function cloneBuckets(buckets) {
|
|
179
85
|
return Object.fromEntries(Object.entries(buckets).map(([name, value]) => [name, { ...value }]));
|
|
180
86
|
}
|
package/dist/hud/monitor.js
CHANGED
|
@@ -3,7 +3,7 @@ import { EventEmitter } from "node:events";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { adapterList, adapters } from "./adapters.js";
|
|
6
|
-
import { homePath,
|
|
6
|
+
import { homePath, resolveClaudeTranscript } from "./fs.js";
|
|
7
7
|
const LIVE_WINDOW_MS = 45_000;
|
|
8
8
|
const ONGOING_WINDOW_MS = 300_000; // a thread counts as "ongoing" if its log was written in the last 5 min
|
|
9
9
|
const RECENT_WINDOW_MS = 12 * 60 * 60 * 1000; // a session is a "recent tab" if its log was touched in the last 12h
|
|
@@ -501,17 +501,6 @@ function liveMtime(client, file, cacheMtimeMs) {
|
|
|
501
501
|
return cacheMtimeMs;
|
|
502
502
|
}
|
|
503
503
|
}
|
|
504
|
-
const transcriptCache = new Map();
|
|
505
|
-
function resolveClaudeTranscript(cacheFile) {
|
|
506
|
-
if (!cacheFile.endsWith(".json") || !cacheFile.includes(`${path.sep}echo-ctx${path.sep}`))
|
|
507
|
-
return null;
|
|
508
|
-
if (transcriptCache.has(cacheFile))
|
|
509
|
-
return transcriptCache.get(cacheFile) ?? null;
|
|
510
|
-
const sessionId = path.basename(cacheFile, ".json");
|
|
511
|
-
const transcript = newestFile(walkFiles(homePath(".claude", "projects"), (f) => path.basename(f) === `${sessionId}.jsonl`));
|
|
512
|
-
transcriptCache.set(cacheFile, transcript);
|
|
513
|
-
return transcript;
|
|
514
|
-
}
|
|
515
504
|
function peekCwd(file) {
|
|
516
505
|
try {
|
|
517
506
|
const fd = fs.openSync(file, "r");
|
package/dist/hud/render.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { formatGlance, formatTokens } from "./metric.js";
|
|
2
2
|
export function renderScoreText(score) {
|
|
3
3
|
const range = score.buckets.range_redundant;
|
|
4
|
+
const snr = score.pollutionTok > 0 ? `${(Math.max(0, score.ctTokens - score.pollutionTok) / score.pollutionTok).toFixed(1)}:1` : "clean";
|
|
4
5
|
const lines = [
|
|
5
6
|
`${clientLabel(score.client)} context health: ${formatGlance(score)}`,
|
|
6
7
|
"",
|
|
7
|
-
`Tracked dead-weight
|
|
8
|
-
`
|
|
8
|
+
`Tracked dead-weight ${formatTokens(score.pollutionTok)} (${score.pollutionPct}% of the window · SNR ${snr}).`,
|
|
9
|
+
`Superseded copies (re-reads + old write versions): ${formatTokens(range.tokens)} · ${range.count} re-read files.`,
|
|
9
10
|
`Reads: ${score.reads} · Edits: ${score.stats?.patchEdits ?? 0} · Large outputs: ${score.stats?.largeFunctionOutputs ?? 0} · Compactions: ${score.stats?.compactMarkers ?? 0}.`,
|
|
10
11
|
score.saturationPct !== null ? `Saturation: ${score.saturationPct}% of ${formatTokens(score.modelContextWindow || 0)}.` : "",
|
|
11
12
|
`Context token source: ${score.ctSource}.`,
|
|
12
13
|
"",
|
|
13
|
-
"Honesty: tracked lower bound
|
|
14
|
+
"Honesty: tracked lower bound — latest local-live waste from the resident context window; provider eviction is not directly observable.",
|
|
14
15
|
].filter(Boolean);
|
|
15
16
|
return lines.join("\n");
|
|
16
17
|
}
|
package/dist/hud/server.js
CHANGED
|
@@ -157,6 +157,35 @@ export async function createHudServer(opts = {}) {
|
|
|
157
157
|
res.end(JSON.stringify({ ok: canReveal, reason: canReveal ? undefined : opts.onReveal ? "unknown_path" : "no_desktop" }));
|
|
158
158
|
return;
|
|
159
159
|
}
|
|
160
|
+
if (url.pathname === "/jump") {
|
|
161
|
+
// Deep-link into the session's own app, at the conversation level:
|
|
162
|
+
// Claude Code CLI session -> claude://resume?session=<uuid> (imports + opens in Claude desktop)
|
|
163
|
+
// Codex thread -> codex://threads/<uuid> (opens in the ChatGPT app's Codex)
|
|
164
|
+
// The uuid is the one already embedded in the tracked transcript filename.
|
|
165
|
+
const id = url.searchParams.get("id") || "";
|
|
166
|
+
const rec = (latest.recentSessions || []).find((s) => s.id === id);
|
|
167
|
+
let link = "";
|
|
168
|
+
if (rec) {
|
|
169
|
+
const uuid = (path.basename(rec.sourcePath).match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i) || [])[0] || "";
|
|
170
|
+
if (uuid)
|
|
171
|
+
link = rec.client === "codex" ? `codex://threads/${uuid}` : `claude://resume?session=${uuid}`;
|
|
172
|
+
else if (rec.client === "claude-desktop")
|
|
173
|
+
link = "claude://";
|
|
174
|
+
}
|
|
175
|
+
let ok = false;
|
|
176
|
+
if (link) {
|
|
177
|
+
try {
|
|
178
|
+
execFileSync("open", [link]);
|
|
179
|
+
ok = true;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
ok = false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
186
|
+
res.end(JSON.stringify({ ok, link: link || undefined }));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
160
189
|
if (url.pathname === "/renew") {
|
|
161
190
|
handleRenew(latest, res).catch((error) => {
|
|
162
191
|
if (res.writableEnded)
|
|
@@ -511,6 +540,7 @@ function serveHudAsset(pathname, res) {
|
|
|
511
540
|
"/assets/hud/echo-face-cutout.png": { path: "../../assets/hud/echo-face-cutout.png", type: "image/png" },
|
|
512
541
|
"/assets/hud/claude.svg": { path: "../../assets/hud/claude.svg", type: "image/svg+xml; charset=utf-8" },
|
|
513
542
|
"/assets/hud/codex.svg": { path: "../../assets/hud/codex.svg", type: "image/svg+xml; charset=utf-8" },
|
|
543
|
+
"/assets/hud/github.svg": { path: "../../assets/hud/github.svg", type: "image/svg+xml; charset=utf-8" },
|
|
514
544
|
};
|
|
515
545
|
const asset = assets[pathname];
|
|
516
546
|
if (!asset)
|