@echomem/mcp 1.4.7 → 1.4.9
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/README.md +35 -9
- package/assets/canonical-scorer/README.md +18 -0
- package/assets/canonical-scorer/analyze-10-problems.mjs +857 -0
- package/assets/canonical-scorer/build-session-waste-dashboard.mjs +1628 -0
- package/assets/canonical-scorer/golden_anchors.mjs +83 -0
- package/assets/canonical-scorer/optimizable_detail.mjs +633 -0
- package/assets/hud/claude.svg +1 -0
- package/assets/hud/codex.svg +1 -0
- package/assets/hud/session-viewer.html +35 -0
- package/dist/city/chaos-to-clarity-pencil.html +582 -0
- package/dist/city/echo-ai-city-only.html +1126 -109
- package/dist/city/echo-ai-city-only.template.html +1126 -109
- package/dist/city/echo-face-cutout.png +0 -0
- package/dist/city/pencil-pie-generator.html +883 -0
- package/dist/city/pencil-webgl-landscape.html +1239 -0
- package/dist/city/spatial-fan-story.html +479 -0
- package/dist/codex-session-files.js +283 -0
- package/dist/codex-sync.js +7 -2
- package/dist/context-analysis/canonical-golden.js +47 -0
- package/dist/context-analysis/claude-native-canonical.js +1193 -0
- package/dist/context-analysis/vendored-canonical.js +793 -0
- package/dist/context-analysis/workspace-report.js +1838 -0
- package/dist/context-metrics/calculate.js +56 -0
- package/dist/context-metrics/model-limits.js +26 -0
- package/dist/context-metrics/types.js +1 -0
- package/dist/forensics-10-problems.js +7 -6
- package/dist/forensics.js +863 -132
- package/dist/hud/adapters.js +8 -4
- package/dist/hud/autostart.js +66 -0
- package/dist/hud/cli.js +31 -0
- package/dist/hud/electron-main.js +182 -19
- package/dist/hud/metric.js +13 -4
- package/dist/hud/monitor.js +171 -84
- package/dist/hud/preload.cjs +3 -0
- package/dist/hud/server.js +321 -4
- package/dist/hud/web.js +880 -270
- package/dist/index.js +122 -24
- package/dist/local-data-paths.js +87 -0
- package/dist/migrate.js +55 -29
- package/dist/report.js +101 -40
- package/dist/setup-page.js +4257 -245
- package/dist/setup-preview.js +245 -0
- package/dist/setup.js +786 -75
- package/dist/v1-contract.js +20 -2
- package/package.json +6 -4
- package/templates/echomem-recall.md +2 -2
package/dist/hud/monitor.js
CHANGED
|
@@ -6,7 +6,6 @@ import { adapterList, adapters } from "./adapters.js";
|
|
|
6
6
|
import { homePath, newestFile, walkFiles } 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
|
-
const FOCUS_GRACE_MS = 8_000; // keep the last-known frontmost client this long when detection momentarily misses
|
|
10
9
|
const RECENT_WINDOW_MS = 12 * 60 * 60 * 1000; // a session is a "recent tab" if its log was touched in the last 12h
|
|
11
10
|
const RECENT_CAP = 16; // most-recent N sessions kept as switchable tabs
|
|
12
11
|
export class HudMonitor extends EventEmitter {
|
|
@@ -19,18 +18,21 @@ export class HudMonitor extends EventEmitter {
|
|
|
19
18
|
labelCache = new Map();
|
|
20
19
|
missing = [];
|
|
21
20
|
frontmostCheckedAt = 0;
|
|
22
|
-
|
|
23
|
-
frontmostClient = null;
|
|
21
|
+
frontmostFamily = null;
|
|
24
22
|
lastActiveClient = null;
|
|
25
23
|
threadCounts = {};
|
|
26
24
|
threadCountsAt = 0;
|
|
27
25
|
recentSessions = [];
|
|
28
26
|
codexTitles = new Map();
|
|
29
27
|
codexTitlesSig = "";
|
|
28
|
+
claudeTitles = new Map();
|
|
29
|
+
claudeTitlesSig = "";
|
|
30
|
+
claudeTitlesCheckedAt = 0;
|
|
30
31
|
claudeTitleCache = new Map();
|
|
31
32
|
lastPersistedRecent = "";
|
|
32
33
|
pinnedId = null;
|
|
33
34
|
pinnedScore = null;
|
|
35
|
+
pinnedFromFamily = null;
|
|
34
36
|
constructor(mode = "auto", pollMs = 750) {
|
|
35
37
|
super();
|
|
36
38
|
this.mode = mode;
|
|
@@ -49,8 +51,9 @@ export class HudMonitor extends EventEmitter {
|
|
|
49
51
|
this.timer = null;
|
|
50
52
|
}
|
|
51
53
|
snapshot() {
|
|
52
|
-
const
|
|
54
|
+
const focusedFamily = this.frontmostPreferredFamily();
|
|
53
55
|
this.refreshRecent();
|
|
56
|
+
this.releasePinAfterForegroundSwitch(focusedFamily);
|
|
54
57
|
const now = Date.now();
|
|
55
58
|
const sessions = [...this.scores.values()].map((score) => {
|
|
56
59
|
// Liveness uses file mtime (real last write), not score.updatedAt — the Claude cache stamps
|
|
@@ -60,22 +63,19 @@ export class HudMonitor extends EventEmitter {
|
|
|
60
63
|
return {
|
|
61
64
|
...score,
|
|
62
65
|
live: lastActiveMs < LIVE_WINDOW_MS,
|
|
63
|
-
focused:
|
|
66
|
+
focused: focusedFamily === agentFamily(score.client),
|
|
64
67
|
label: this.labelFor(score),
|
|
65
68
|
lastActiveMs,
|
|
66
69
|
};
|
|
67
70
|
});
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return a.live ? -1 : 1;
|
|
74
|
-
return a.lastActiveMs - b.lastActiveMs;
|
|
75
|
-
});
|
|
76
|
-
// A user-pinned tab overrides auto-selection: it becomes the primary and never flips away.
|
|
71
|
+
// The user's foreground agent is the primary truth. Background agents can keep writing logs, but
|
|
72
|
+
// they should never steal the main HUD away from Codex/Claude while that app is what the user opened.
|
|
73
|
+
sessions.sort((a, b) => hudSelectionRank(a) - hudSelectionRank(b) || a.lastActiveMs - b.lastActiveMs);
|
|
74
|
+
// A pin selects one session while the user remains in the same agent. Crossing to the other
|
|
75
|
+
// foreground agent releases it so Auto can follow the workflow again.
|
|
77
76
|
const pinned = this.pinnedView(now);
|
|
78
77
|
const ordered = pinned ? [pinned, ...sessions.filter((s) => s.sourcePath !== pinned.sourcePath)] : sessions;
|
|
78
|
+
const pinnedId = pinned ? this.pinnedId : null;
|
|
79
79
|
return {
|
|
80
80
|
mode: this.mode,
|
|
81
81
|
active: ordered[0] || null,
|
|
@@ -83,6 +83,7 @@ export class HudMonitor extends EventEmitter {
|
|
|
83
83
|
sessions: ordered,
|
|
84
84
|
threadCounts: this.threadCounts,
|
|
85
85
|
recentSessions: this.recentSessions,
|
|
86
|
+
pinnedId,
|
|
86
87
|
missing: this.missing,
|
|
87
88
|
updatedAt: new Date().toISOString(),
|
|
88
89
|
};
|
|
@@ -101,6 +102,16 @@ export class HudMonitor extends EventEmitter {
|
|
|
101
102
|
return;
|
|
102
103
|
this.pinnedId = id;
|
|
103
104
|
this.pinnedScore = null;
|
|
105
|
+
this.pinnedFromFamily = id ? this.frontmostPreferredFamily() : null;
|
|
106
|
+
}
|
|
107
|
+
releasePinAfterForegroundSwitch(focusedFamily) {
|
|
108
|
+
if (!this.pinnedId || !this.pinnedFromFamily || !focusedFamily)
|
|
109
|
+
return;
|
|
110
|
+
if (focusedFamily === this.pinnedFromFamily)
|
|
111
|
+
return;
|
|
112
|
+
this.pinnedId = null;
|
|
113
|
+
this.pinnedScore = null;
|
|
114
|
+
this.pinnedFromFamily = null;
|
|
104
115
|
}
|
|
105
116
|
// The user-selected tab, scored on demand (cached by file signature). null if nothing is pinned or
|
|
106
117
|
// the pinned session has aged out of the recent list.
|
|
@@ -127,28 +138,27 @@ export class HudMonitor extends EventEmitter {
|
|
|
127
138
|
}
|
|
128
139
|
}
|
|
129
140
|
const lastActiveMs = Math.max(0, now - stat.mtimeMs);
|
|
130
|
-
return { ...this.pinnedScore.score, live: lastActiveMs < LIVE_WINDOW_MS, focused: true, label: rec.title, lastActiveMs };
|
|
141
|
+
return { ...this.pinnedScore.score, live: lastActiveMs < LIVE_WINDOW_MS, focused: true, label: rec.label || rec.title, lastActiveMs };
|
|
131
142
|
}
|
|
132
|
-
|
|
143
|
+
frontmostPreferredFamily() {
|
|
133
144
|
if (this.mode !== "auto" && this.mode !== "both")
|
|
134
145
|
return null;
|
|
135
146
|
const now = Date.now();
|
|
136
147
|
if (now - this.frontmostCheckedAt > 1500) {
|
|
137
148
|
this.frontmostCheckedAt = now;
|
|
138
|
-
const detected =
|
|
149
|
+
const detected = detectFrontmostFamily();
|
|
139
150
|
if (detected) {
|
|
140
|
-
// Positive detection wins immediately
|
|
141
|
-
|
|
142
|
-
this.
|
|
143
|
-
}
|
|
144
|
-
else if (now - this.frontmostSeenAt > FOCUS_GRACE_MS) {
|
|
145
|
-
// Detection missed (osascript timeout, or a non-agent app like a browser is front). Hold the
|
|
146
|
-
// last-known focus for a grace window so the active slot doesn't flip to a background client
|
|
147
|
-
// just because it's writing — then release once the grace expires.
|
|
148
|
-
this.frontmostClient = null;
|
|
151
|
+
// Positive detection wins immediately. Claude Code and Claude Desktop share one family;
|
|
152
|
+
// recency below decides which Claude source is the conversation the user is actually using.
|
|
153
|
+
this.frontmostFamily = detected;
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
|
-
|
|
156
|
+
if (this.frontmostFamily === "codex")
|
|
157
|
+
return this.scores.has("codex") ? "codex" : null;
|
|
158
|
+
if (this.frontmostFamily === "claude") {
|
|
159
|
+
return this.scores.has("claude-code") || this.scores.has("claude-desktop") ? "claude" : null;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
152
162
|
}
|
|
153
163
|
// One throttled walk over every session file per client → both the ongoing-thread counts and the
|
|
154
164
|
// "recent tabs" list (touched in the last RECENT_WINDOW_MS), which is persisted to disk so the tab
|
|
@@ -159,6 +169,7 @@ export class HudMonitor extends EventEmitter {
|
|
|
159
169
|
return;
|
|
160
170
|
this.threadCountsAt = now;
|
|
161
171
|
this.loadCodexTitles();
|
|
172
|
+
this.loadClaudeTitles();
|
|
162
173
|
const counts = {};
|
|
163
174
|
const recent = [];
|
|
164
175
|
const newestByClient = new Map();
|
|
@@ -200,6 +211,7 @@ export class HudMonitor extends EventEmitter {
|
|
|
200
211
|
id: sessionIdFromPath(file),
|
|
201
212
|
client,
|
|
202
213
|
title: this.titleFor(client, file),
|
|
214
|
+
label: sessionLabel(file, client),
|
|
203
215
|
sourcePath: file,
|
|
204
216
|
lastActiveMs: age,
|
|
205
217
|
live: age < LIVE_WINDOW_MS,
|
|
@@ -213,60 +225,18 @@ export class HudMonitor extends EventEmitter {
|
|
|
213
225
|
return title;
|
|
214
226
|
}
|
|
215
227
|
else {
|
|
216
|
-
const
|
|
217
|
-
if (
|
|
218
|
-
return
|
|
228
|
+
const metadataTitle = this.claudeTitles.get(sessionIdFromPath(file));
|
|
229
|
+
if (metadataTitle)
|
|
230
|
+
return metadataTitle;
|
|
231
|
+
const cached = this.claudeTitleCache.get(file);
|
|
232
|
+
if (cached !== undefined)
|
|
233
|
+
return cached;
|
|
234
|
+
const title = claudeDisplayTitle(file, client);
|
|
235
|
+
this.claudeTitleCache.set(file, title);
|
|
236
|
+
return title;
|
|
219
237
|
}
|
|
220
238
|
return sessionLabel(file, client); // repo/cwd basename fallback
|
|
221
239
|
}
|
|
222
|
-
// Claude has no title index, so derive one from the thread's first real user message (cached per file
|
|
223
|
-
// — the first message never changes). Distinguishes two threads in the same repo.
|
|
224
|
-
claudeTitle(file) {
|
|
225
|
-
const cached = this.claudeTitleCache.get(file);
|
|
226
|
-
if (cached !== undefined)
|
|
227
|
-
return cached;
|
|
228
|
-
let title = "";
|
|
229
|
-
try {
|
|
230
|
-
const fd = fs.openSync(file, "r");
|
|
231
|
-
try {
|
|
232
|
-
const buf = Buffer.alloc(64 * 1024);
|
|
233
|
-
const bytes = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
234
|
-
for (const line of buf.toString("utf8", 0, bytes).split("\n")) {
|
|
235
|
-
if (!line.includes('"user"'))
|
|
236
|
-
continue;
|
|
237
|
-
let obj;
|
|
238
|
-
try {
|
|
239
|
-
obj = JSON.parse(line);
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
continue; // partial/last line
|
|
243
|
-
}
|
|
244
|
-
if (!isRecord(obj) || obj.type !== "user")
|
|
245
|
-
continue;
|
|
246
|
-
const message = isRecord(obj.message) ? obj.message : null;
|
|
247
|
-
const content = message ? message.content : undefined;
|
|
248
|
-
let body = "";
|
|
249
|
-
if (typeof content === "string")
|
|
250
|
-
body = content;
|
|
251
|
-
else if (Array.isArray(content))
|
|
252
|
-
body = content.map((b) => (isRecord(b) && typeof b.text === "string" ? b.text : "")).join(" ");
|
|
253
|
-
body = body.replace(/\s+/g, " ").trim();
|
|
254
|
-
if (!body || body.startsWith("<") || /^#\s*claudeMd\b/i.test(body) || body.startsWith("Caveat:"))
|
|
255
|
-
continue;
|
|
256
|
-
title = body.length > 48 ? `${body.slice(0, 47)}…` : body;
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
finally {
|
|
261
|
-
fs.closeSync(fd);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
catch {
|
|
265
|
-
/* unreadable → falls back to repo label */
|
|
266
|
-
}
|
|
267
|
-
this.claudeTitleCache.set(file, title);
|
|
268
|
-
return title;
|
|
269
|
-
}
|
|
270
240
|
// Codex writes a lightweight index of every thread (id + human title). Load it (mtime-cached) so tabs
|
|
271
241
|
// show real titles like "Verify file state" instead of a UUID.
|
|
272
242
|
loadCodexTitles() {
|
|
@@ -302,6 +272,28 @@ export class HudMonitor extends EventEmitter {
|
|
|
302
272
|
}
|
|
303
273
|
this.codexTitles = map;
|
|
304
274
|
}
|
|
275
|
+
// Claude Desktop keeps its sidebar titles outside the transcripts. Join those records by
|
|
276
|
+
// cliSessionId so the HUD can show the same title the user sees in Claude.
|
|
277
|
+
loadClaudeTitles() {
|
|
278
|
+
const now = Date.now();
|
|
279
|
+
if (this.claudeTitlesCheckedAt && now - this.claudeTitlesCheckedAt < 5000)
|
|
280
|
+
return;
|
|
281
|
+
this.claudeTitlesCheckedAt = now;
|
|
282
|
+
const files = listClaudeSessionMetadataFiles();
|
|
283
|
+
const signature = files.map((file) => {
|
|
284
|
+
try {
|
|
285
|
+
const stat = fs.statSync(file);
|
|
286
|
+
return `${file}:${stat.size}:${stat.mtimeMs}`;
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return "";
|
|
290
|
+
}
|
|
291
|
+
}).join("|");
|
|
292
|
+
if (signature === this.claudeTitlesSig)
|
|
293
|
+
return;
|
|
294
|
+
this.claudeTitlesSig = signature;
|
|
295
|
+
this.claudeTitles = claudeSessionTitlesFromFiles(files);
|
|
296
|
+
}
|
|
305
297
|
persistRecent() {
|
|
306
298
|
const signature = JSON.stringify(this.recentSessions.map((s) => `${s.id}:${s.live}`));
|
|
307
299
|
if (signature === this.lastPersistedRecent)
|
|
@@ -360,21 +352,68 @@ export class HudMonitor extends EventEmitter {
|
|
|
360
352
|
this.emit("state", this.snapshot());
|
|
361
353
|
}
|
|
362
354
|
}
|
|
363
|
-
function
|
|
355
|
+
export function hudSelectionRank(session) {
|
|
356
|
+
if (session.focused)
|
|
357
|
+
return 0;
|
|
358
|
+
if (session.live)
|
|
359
|
+
return 1;
|
|
360
|
+
return 2;
|
|
361
|
+
}
|
|
362
|
+
export function claudeDisplayTitle(file, client) {
|
|
363
|
+
const label = sessionLabel(file, client);
|
|
364
|
+
const stamp = sessionTimeLabel(resolveClaudeTranscript(file) ?? file);
|
|
365
|
+
return label === defaultLabel(client) ? stamp : `${label} · ${stamp}`;
|
|
366
|
+
}
|
|
367
|
+
export function claudeSessionTitlesFromFiles(files) {
|
|
368
|
+
const entries = new Map();
|
|
369
|
+
for (const file of files) {
|
|
370
|
+
try {
|
|
371
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
372
|
+
if (!isRecord(raw))
|
|
373
|
+
continue;
|
|
374
|
+
const id = typeof raw.cliSessionId === "string" ? raw.cliSessionId.trim() : "";
|
|
375
|
+
const title = typeof raw.title === "string" ? raw.title.trim() : "";
|
|
376
|
+
if (!id || !title)
|
|
377
|
+
continue;
|
|
378
|
+
const freshness = Number(raw.lastFocusedAt) || Number(raw.lastActivityAt) || fs.statSync(file).mtimeMs;
|
|
379
|
+
const previous = entries.get(id);
|
|
380
|
+
if (!previous || freshness >= previous.freshness)
|
|
381
|
+
entries.set(id, { title, freshness });
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
/* Ignore incomplete or disappearing live metadata files. */
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return new Map([...entries].map(([id, entry]) => [id, entry.title]));
|
|
388
|
+
}
|
|
389
|
+
function detectFrontmostFamily() {
|
|
364
390
|
if (process.platform !== "darwin")
|
|
365
391
|
return null;
|
|
366
392
|
try {
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
393
|
+
const output = execFileSync("osascript", [
|
|
394
|
+
"-e",
|
|
395
|
+
'tell application "System Events" to tell first application process whose frontmost is true to return (name as text) & linefeed & (bundle identifier as text)',
|
|
396
|
+
], { encoding: "utf8", timeout: 500, stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
397
|
+
const [name = "", bundleId = ""] = output.split("\n");
|
|
398
|
+
return agentFamilyForApplication(name, bundleId);
|
|
372
399
|
}
|
|
373
400
|
catch {
|
|
374
401
|
/* Accessibility may be unavailable; fall back to newest log. */
|
|
375
402
|
}
|
|
376
403
|
return null;
|
|
377
404
|
}
|
|
405
|
+
export function agentFamilyForApplication(name, bundleId = "") {
|
|
406
|
+
const appName = name.trim().toLowerCase();
|
|
407
|
+
const appId = bundleId.trim().toLowerCase();
|
|
408
|
+
if (appId === "com.openai.codex" || appName.includes("codex"))
|
|
409
|
+
return "codex";
|
|
410
|
+
if (appId === "com.anthropic.claudefordesktop" || appName.includes("claude"))
|
|
411
|
+
return "claude";
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
export function agentFamily(client) {
|
|
415
|
+
return client === "codex" ? "codex" : "claude";
|
|
416
|
+
}
|
|
378
417
|
function isRecord(value) {
|
|
379
418
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
380
419
|
}
|
|
@@ -392,6 +431,41 @@ function defaultLabel(client) {
|
|
|
392
431
|
return "Claude Code";
|
|
393
432
|
return "Claude Desktop";
|
|
394
433
|
}
|
|
434
|
+
function listClaudeSessionMetadataFiles() {
|
|
435
|
+
const roots = [
|
|
436
|
+
homePath("Library", "Application Support", "Claude", "claude-code-sessions"),
|
|
437
|
+
homePath("Library", "Application Support", "Claude", "local-agent-mode-sessions"),
|
|
438
|
+
];
|
|
439
|
+
const files = [];
|
|
440
|
+
for (const root of roots) {
|
|
441
|
+
for (const account of childDirectories(root)) {
|
|
442
|
+
for (const organization of childDirectories(account)) {
|
|
443
|
+
let entries = [];
|
|
444
|
+
try {
|
|
445
|
+
entries = fs.readdirSync(organization, { withFileTypes: true });
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
for (const entry of entries) {
|
|
451
|
+
if (entry.isFile() && /^local_[0-9a-f-]+\.json$/i.test(entry.name))
|
|
452
|
+
files.push(path.join(organization, entry.name));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return files.sort();
|
|
458
|
+
}
|
|
459
|
+
function childDirectories(root) {
|
|
460
|
+
try {
|
|
461
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
462
|
+
.filter((entry) => entry.isDirectory())
|
|
463
|
+
.map((entry) => path.join(root, entry.name));
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
}
|
|
395
469
|
// Recognizable label = basename of the session's cwd. Both Codex rollouts and Claude transcripts
|
|
396
470
|
// carry a "cwd" field; the Claude Code active source is the echo-ctx cache (<sessionId>.json, no cwd),
|
|
397
471
|
// so resolve its transcript by sessionId first. Falls back to the client name when cwd is absent.
|
|
@@ -400,6 +474,19 @@ function sessionLabel(file, client) {
|
|
|
400
474
|
const cwd = peekCwd(target);
|
|
401
475
|
return cwd ? path.basename(cwd) : defaultLabel(client);
|
|
402
476
|
}
|
|
477
|
+
function sessionTimeLabel(file) {
|
|
478
|
+
try {
|
|
479
|
+
const date = new Date(fs.statSync(file).mtimeMs);
|
|
480
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
481
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
482
|
+
const hour = String(date.getHours()).padStart(2, "0");
|
|
483
|
+
const minute = String(date.getMinutes()).padStart(2, "0");
|
|
484
|
+
return `${month}/${day} ${hour}:${minute}`;
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return sessionIdFromPath(file).slice(0, 8) || "session";
|
|
488
|
+
}
|
|
489
|
+
}
|
|
403
490
|
// The Claude Code active source is the echo-ctx cache (<sessionId>.json), whose mtime only bumps when
|
|
404
491
|
// the statusline re-renders — stale during a long turn. The transcript grows every tool call, so it's
|
|
405
492
|
// the true liveness signal. Take the fresher of the two. Cached per cache-file (paths are stable).
|
package/dist/hud/preload.cjs
CHANGED
|
@@ -7,6 +7,9 @@ electron_1.contextBridge.exposeInMainWorld("echomemHud", {
|
|
|
7
7
|
setOpen(open, height) {
|
|
8
8
|
electron_1.ipcRenderer.send("hud:set-open", { open, height });
|
|
9
9
|
},
|
|
10
|
+
setMini(mini) {
|
|
11
|
+
electron_1.ipcRenderer.send("hud:set-mini", { mini });
|
|
12
|
+
},
|
|
10
13
|
startDrag(screenX, screenY) {
|
|
11
14
|
electron_1.ipcRenderer.send("hud:drag-start", { screenX, screenY });
|
|
12
15
|
},
|