@echomem/mcp 1.3.2 → 1.4.1
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/dist/city/_live.html +37 -0
- package/dist/city/_serve.mjs +45 -0
- package/dist/city/card-data.json +15 -0
- package/dist/city/city-data.json +248 -0
- package/dist/city/echo-ai-city-only.html +1862 -0
- package/dist/city/echo-ai-city-only.template.html +1254 -0
- package/dist/city/echo-extraction-plate.html +330 -0
- package/dist/city/generate-echo-city-only.mjs +112 -0
- package/dist/city/personality_stickers/bossy.png +0 -0
- package/dist/city/personality_stickers/ghosty.png +0 -0
- package/dist/city/personality_stickers/loopy.png +0 -0
- package/dist/city/personality_stickers/lusty.png +0 -0
- package/dist/city/personality_stickers/maxxy.png +0 -0
- package/dist/city/personality_stickers/tabby.png +0 -0
- package/dist/city/vendor/OrbitControls.js +1417 -0
- package/dist/city/vendor/RoundedBoxGeometry.js +155 -0
- package/dist/city/vendor/echo_general-file-21.riv +0 -0
- package/dist/city/vendor/rive.js +8139 -0
- package/dist/city/vendor/rive.wasm +0 -0
- package/dist/city/vendor/three.module.min.js +6 -0
- package/dist/forensics.js +46 -5
- package/dist/migrate.js +46 -4
- package/dist/setup-page.js +767 -266
- package/dist/setup.js +64 -4
- package/package.json +2 -2
package/dist/forensics.js
CHANGED
|
@@ -17,7 +17,26 @@
|
|
|
17
17
|
import fs from "node:fs";
|
|
18
18
|
import os from "node:os";
|
|
19
19
|
import path from "node:path";
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
20
21
|
import { eachLine, walk } from "./report.js";
|
|
22
|
+
/** Fast, safe per-repo commit count since a date. Metadata only (no diffs) so it stays cheap;
|
|
23
|
+
* `rev-list --count` is git-indexed. Any failure (not a repo, git missing, timeout) → 0. */
|
|
24
|
+
function gitCommitCount(cwd, sinceIso) {
|
|
25
|
+
if (!cwd)
|
|
26
|
+
return 0;
|
|
27
|
+
try {
|
|
28
|
+
const args = ["-C", cwd, "rev-list", "--count"];
|
|
29
|
+
if (sinceIso)
|
|
30
|
+
args.push(`--since=${sinceIso}`);
|
|
31
|
+
args.push("HEAD");
|
|
32
|
+
const out = execFileSync("git", args, { timeout: 3000, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
33
|
+
const n = parseInt(out, 10);
|
|
34
|
+
return Number.isFinite(n) ? n : 0;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
21
40
|
// ---------------------------------------------------------------------------
|
|
22
41
|
// Constants (named on purpose — no magic numbers downstream)
|
|
23
42
|
// ---------------------------------------------------------------------------
|
|
@@ -85,7 +104,7 @@ function round(value, digits = 1) {
|
|
|
85
104
|
function tokensFromChars(chars = 0) {
|
|
86
105
|
return Math.round((chars || 0) / CHARS_PER_TOKEN);
|
|
87
106
|
}
|
|
88
|
-
function repoLabel(cwd) {
|
|
107
|
+
export function repoLabel(cwd) {
|
|
89
108
|
if (!cwd)
|
|
90
109
|
return "home";
|
|
91
110
|
const m = cwd.match(/worktrees\/[^/]+\/(.+)$/);
|
|
@@ -172,9 +191,11 @@ class Forensics {
|
|
|
172
191
|
const name = repoLabel(cwd);
|
|
173
192
|
let r = this.repos.get(name);
|
|
174
193
|
if (!r) {
|
|
175
|
-
r = { name, sessions: new Set(), assistantMessages: 0, cold: 0, cacheWrite: 0, cacheRead: 0, output: 0, reads: 0, rereads: 0, staleRereads: 0, userTimestamps: [] };
|
|
194
|
+
r = { name, cwd: null, sessions: new Set(), assistantMessages: 0, cold: 0, cacheWrite: 0, cacheRead: 0, output: 0, codexTokens: 0, claudeTokens: 0, reads: 0, rereads: 0, staleRereads: 0, userTimestamps: [], byDay: new Map() };
|
|
176
195
|
this.repos.set(name, r);
|
|
177
196
|
}
|
|
197
|
+
if (cwd && !r.cwd)
|
|
198
|
+
r.cwd = cwd; // remember a real working dir for this repo (for the git pass)
|
|
178
199
|
return r;
|
|
179
200
|
}
|
|
180
201
|
fileRecord(p, repo) {
|
|
@@ -201,7 +222,7 @@ class Forensics {
|
|
|
201
222
|
this.allSessionIds.add(id);
|
|
202
223
|
}
|
|
203
224
|
/** assistant-turn token usage (already split into cold/cacheWrite/cacheRead/output). */
|
|
204
|
-
recordUsage(model, cwd, session, u) {
|
|
225
|
+
recordUsage(model, cwd, session, u, provider, ms) {
|
|
205
226
|
const m = this.modelBucket(model);
|
|
206
227
|
m.messages += 1;
|
|
207
228
|
m.cold += u.cold;
|
|
@@ -217,6 +238,16 @@ class Forensics {
|
|
|
217
238
|
r.cacheWrite += u.cacheWrite;
|
|
218
239
|
r.cacheRead += u.cacheRead;
|
|
219
240
|
r.output += u.output;
|
|
241
|
+
// per-repo provider split → drives the city's per-cube colour (Codex green / Claude orange)
|
|
242
|
+
const turnTokens = u.cold + u.cacheWrite + u.cacheRead + u.output;
|
|
243
|
+
if (provider === "codex")
|
|
244
|
+
r.codexTokens += turnTokens;
|
|
245
|
+
else
|
|
246
|
+
r.claudeTokens += turnTokens;
|
|
247
|
+
if (ms != null && turnTokens > 0) {
|
|
248
|
+
const d = localDay(ms);
|
|
249
|
+
r.byDay.set(d, (r.byDay.get(d) || 0) + turnTokens);
|
|
250
|
+
}
|
|
220
251
|
}
|
|
221
252
|
}
|
|
222
253
|
recordEdit(target, cwd) {
|
|
@@ -369,9 +400,13 @@ class Forensics {
|
|
|
369
400
|
const allUserTimestamps = [];
|
|
370
401
|
const dayAttention = new Map();
|
|
371
402
|
const repoOut = [];
|
|
403
|
+
const sinceIso = this.minTs != null ? new Date(this.minTs).toISOString() : null;
|
|
404
|
+
let totalCommits = 0;
|
|
372
405
|
for (const repo of this.repos.values()) {
|
|
373
406
|
allUserTimestamps.push(...repo.userTimestamps);
|
|
374
407
|
const wb = this.workBlocks(repo.userTimestamps);
|
|
408
|
+
const commits = gitCommitCount(repo.cwd, sinceIso); // fast git pass: commits landed in this repo during the scan window
|
|
409
|
+
totalCommits += commits;
|
|
375
410
|
repoOut.push({
|
|
376
411
|
name: repo.name, sessions: repo.sessions.size, assistantMessages: repo.assistantMessages,
|
|
377
412
|
attentionHours: round(wb.attentionHours, 1), activeAttentionDays: wb.activeDays,
|
|
@@ -380,6 +415,10 @@ class Forensics {
|
|
|
380
415
|
tokens: repo.cold + repo.cacheWrite + repo.cacheRead + repo.output,
|
|
381
416
|
cost: round(costOf({ cold: repo.cold, cacheWrite: repo.cacheWrite, cacheRead: repo.cacheRead, output: repo.output }, CLAUDE_DEFAULT), 2),
|
|
382
417
|
reads: repo.reads, rereads: repo.rereads, staleRereads: repo.staleRereads,
|
|
418
|
+
dominantProvider: repo.codexTokens >= repo.claudeTokens ? "codex" : "claude",
|
|
419
|
+
codexTokens: repo.codexTokens, claudeTokens: repo.claudeTokens,
|
|
420
|
+
commits,
|
|
421
|
+
daily: [...repo.byDay.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)),
|
|
383
422
|
});
|
|
384
423
|
for (const b of wb.blocks) {
|
|
385
424
|
const d = localDay(b.start);
|
|
@@ -445,6 +484,7 @@ class Forensics {
|
|
|
445
484
|
sessionCount: this.allSessionIds.size,
|
|
446
485
|
repoCount: repoOut.length,
|
|
447
486
|
activeDays: activeAttentionDays,
|
|
487
|
+
totalCommits,
|
|
448
488
|
totalTokens, totalInputTokens: totalInput, coldInputTokens: cold, cacheWriteTokens: cacheWrite,
|
|
449
489
|
cacheReadTokens: cacheRead, outputTokens: output,
|
|
450
490
|
inputPct: totalTokens ? Math.round((totalInput / totalTokens) * 100) : 0,
|
|
@@ -493,7 +533,7 @@ function feedClaude(file, eng) {
|
|
|
493
533
|
eng.recordUsage(o.message.model || "unknown", cwd, session, {
|
|
494
534
|
cold: u.input_tokens || 0, cacheWrite: u.cache_creation_input_tokens || 0,
|
|
495
535
|
cacheRead: u.cache_read_input_tokens || 0, output: u.output_tokens || 0,
|
|
496
|
-
});
|
|
536
|
+
}, "claude", Number.isFinite(ts) ? ts : undefined);
|
|
497
537
|
const blocks = Array.isArray(o.message.content) ? o.message.content : [];
|
|
498
538
|
for (const b of blocks) {
|
|
499
539
|
if (b?.type !== "tool_use")
|
|
@@ -626,8 +666,9 @@ function replayFile(eng, fe) {
|
|
|
626
666
|
if (fe.lastTs != null)
|
|
627
667
|
eng.noteTs(fe.lastTs);
|
|
628
668
|
eng.noteSession(fe.session);
|
|
669
|
+
const provider = fe.source === "codex" ? "codex" : "claude";
|
|
629
670
|
for (const u of fe.usage)
|
|
630
|
-
eng.recordUsage(u.model, fe.cwd, fe.session, { cold: u.cold, cacheWrite: 0, cacheRead: u.cacheRead, output: u.output });
|
|
671
|
+
eng.recordUsage(u.model, fe.cwd, fe.session, { cold: u.cold, cacheWrite: 0, cacheRead: u.cacheRead, output: u.output }, provider, fe.firstTs ?? undefined);
|
|
631
672
|
for (const e of fe.ev) {
|
|
632
673
|
if (e.t === "m")
|
|
633
674
|
eng.recordUserMsg(fe.cwd, fe.session, e.ms);
|
package/dist/migrate.js
CHANGED
|
@@ -249,13 +249,16 @@ function hasClaudeText(content) {
|
|
|
249
249
|
function fastSessionInfo(file, source) {
|
|
250
250
|
let conversationKey = `${source === "codex" ? "codex" : "claude-code"}:${sha16(file)}`;
|
|
251
251
|
let hasTextTurn = false;
|
|
252
|
+
let hasRealKey = false;
|
|
252
253
|
for (const obj of initialJsonObjects(file)) {
|
|
253
254
|
if (!isRecord(obj))
|
|
254
255
|
continue;
|
|
255
256
|
if (source === "codex" && obj.type === "session_meta") {
|
|
256
257
|
const payload = isRecord(obj.payload) ? obj.payload : {};
|
|
257
|
-
if (typeof payload.id === "string" && payload.id)
|
|
258
|
+
if (typeof payload.id === "string" && payload.id) {
|
|
258
259
|
conversationKey = `codex:${payload.id}`;
|
|
260
|
+
hasRealKey = true;
|
|
261
|
+
}
|
|
259
262
|
continue;
|
|
260
263
|
}
|
|
261
264
|
if (source === "codex") {
|
|
@@ -268,6 +271,7 @@ function fastSessionInfo(file, source) {
|
|
|
268
271
|
}
|
|
269
272
|
if (source === "claude-code" && typeof obj.sessionId === "string" && obj.sessionId) {
|
|
270
273
|
conversationKey = `claude-code:${obj.sessionId}`;
|
|
274
|
+
hasRealKey = true;
|
|
271
275
|
}
|
|
272
276
|
if (source === "claude-code" && (obj.type === "user" || obj.type === "assistant")) {
|
|
273
277
|
const message = isRecord(obj.message) ? obj.message : {};
|
|
@@ -275,7 +279,7 @@ function fastSessionInfo(file, source) {
|
|
|
275
279
|
hasTextTurn = true;
|
|
276
280
|
}
|
|
277
281
|
}
|
|
278
|
-
return { conversationKey, hasTextTurn };
|
|
282
|
+
return { conversationKey, hasTextTurn, hasRealKey };
|
|
279
283
|
}
|
|
280
284
|
function fastSessionEntries(opts = {}) {
|
|
281
285
|
const out = [];
|
|
@@ -283,14 +287,19 @@ function fastSessionEntries(opts = {}) {
|
|
|
283
287
|
for (const filePath of walk(codexRoot, (p) => /rollout-.*\.jsonl$/.test(p), () => false)) {
|
|
284
288
|
const stat = statSafe(filePath);
|
|
285
289
|
const info = fastSessionInfo(filePath, "codex");
|
|
286
|
-
if (
|
|
290
|
+
// Include if we found text OR a real session id (big sessions can have their first text turn beyond
|
|
291
|
+
// the 1MB probe window — gating only on text dropped them entirely; exact discovery refines later).
|
|
292
|
+
// We require a real key so the fast/exact conversationKey match (no sha16 fallback mismatch).
|
|
293
|
+
if (info.hasTextTurn || info.hasRealKey)
|
|
287
294
|
out.push({ filePath, source: "codex", conversationKey: info.conversationKey, size: stat.size, mtimeMs: stat.mtimeMs });
|
|
288
295
|
}
|
|
289
296
|
const claudeRoot = opts.claudeRoot ?? path.join(os.homedir(), ".claude", "projects");
|
|
290
297
|
for (const filePath of walk(claudeRoot, (p) => p.endsWith(".jsonl"), (name) => name === "subagents" || name === "workflows")) {
|
|
291
298
|
const stat = statSafe(filePath);
|
|
292
299
|
const info = fastSessionInfo(filePath, "claude-code");
|
|
293
|
-
|
|
300
|
+
// Same rule as codex: include on text OR a real session id so large sessions aren't undercounted,
|
|
301
|
+
// while keeping the key stable (claude-code sessionId appears on every line, so hasRealKey is reliable).
|
|
302
|
+
if (info.hasTextTurn || info.hasRealKey)
|
|
294
303
|
out.push({ filePath, source: "claude-code", conversationKey: info.conversationKey, size: stat.size, mtimeMs: stat.mtimeMs });
|
|
295
304
|
}
|
|
296
305
|
return out;
|
|
@@ -759,6 +768,39 @@ export function discoverMigratableSessions(opts = {}) {
|
|
|
759
768
|
claudeCount: sessions.length - codexCount,
|
|
760
769
|
};
|
|
761
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* Targeted sizing for extraction start. The fast (stat-only) scan already knows WHICH sessions are
|
|
773
|
+
* pending — by conversation key + file path — without reading any content. This assembles full content
|
|
774
|
+
* for ONLY those pending sessions, never the whole local history. That keeps "start extraction" fast even
|
|
775
|
+
* when a user has gigabytes of already-processed logs (reading all of them just to prepare a few new jobs
|
|
776
|
+
* is what made sizing hang for 30s+).
|
|
777
|
+
*
|
|
778
|
+
* `processedKeys` (from the account import-status check) yields the accurate pending set; without it we
|
|
779
|
+
* fall back to the local-ledger pending. Either way we read at most the pending files, not the archive.
|
|
780
|
+
*/
|
|
781
|
+
export function discoverPendingSessionsTargeted(processedKeys, opts = {}) {
|
|
782
|
+
const fast = discoverMigratableFastDiscovery(opts);
|
|
783
|
+
const filtered = processedKeys ? applyFastAccountImportStatus(fast, processedKeys, opts) : fast;
|
|
784
|
+
const pending = [];
|
|
785
|
+
for (const entry of filtered.pending) {
|
|
786
|
+
const s = entry.source === "codex" ? assembleCodex(entry.filePath) : assembleClaude(entry.filePath);
|
|
787
|
+
if (s)
|
|
788
|
+
pending.push(s);
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
sessions: pending,
|
|
792
|
+
pending,
|
|
793
|
+
pendingTotal: filtered.pendingTotal,
|
|
794
|
+
alreadyMigrated: filtered.alreadyMigrated,
|
|
795
|
+
skippedActive: filtered.skippedActive,
|
|
796
|
+
limited: filtered.limited,
|
|
797
|
+
codexCount: filtered.codexCount,
|
|
798
|
+
claudeCount: filtered.claudeCount,
|
|
799
|
+
accountChecked: filtered.accountChecked,
|
|
800
|
+
accountCheckFailed: filtered.accountCheckFailed,
|
|
801
|
+
accountCheckUnavailable: filtered.accountCheckUnavailable,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
762
804
|
export function applyAccountImportStatus(discovery, processedKeys, opts = {}) {
|
|
763
805
|
const selectableSessions = opts.includeActive
|
|
764
806
|
? discovery.sessions
|