@echomem/mcp 1.3.0 → 1.3.2
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 +2 -0
- package/dist/codex-sync.js +469 -0
- package/dist/encryption.js +3 -1
- package/dist/forensics.js +717 -0
- package/dist/index.js +461 -9
- package/dist/migrate.js +950 -125
- package/dist/report.js +154 -7
- package/dist/setup-page.js +1120 -0
- package/dist/setup.js +999 -76
- package/dist/v1-contract.js +68 -0
- package/package.json +2 -2
package/dist/report.js
CHANGED
|
@@ -102,6 +102,31 @@ function classifyClaudeTool(name) {
|
|
|
102
102
|
const base = String(name || "").toLowerCase().replace(/^mcp__.+?__/, "");
|
|
103
103
|
return CLAUDE_READ.has(base) ? "read" : "act";
|
|
104
104
|
}
|
|
105
|
+
function normalizeCwd(cwd) {
|
|
106
|
+
if (!cwd)
|
|
107
|
+
return null;
|
|
108
|
+
const m = cwd.match(/worktrees\/[^/]+\/(.+)$/);
|
|
109
|
+
return m ? m[1] : cwd;
|
|
110
|
+
}
|
|
111
|
+
function basenameProject(cwd) {
|
|
112
|
+
if (!cwd)
|
|
113
|
+
return null;
|
|
114
|
+
const name = path.basename(cwd);
|
|
115
|
+
return name && name !== "." && name !== "/" ? name : null;
|
|
116
|
+
}
|
|
117
|
+
function epochMs(ts) {
|
|
118
|
+
if (!ts)
|
|
119
|
+
return null;
|
|
120
|
+
const n = Date.parse(ts);
|
|
121
|
+
return Number.isFinite(n) ? n : null;
|
|
122
|
+
}
|
|
123
|
+
function durationBetween(startTs, lastTs) {
|
|
124
|
+
const start = epochMs(startTs);
|
|
125
|
+
const last = epochMs(lastTs);
|
|
126
|
+
if (start == null || last == null)
|
|
127
|
+
return 0;
|
|
128
|
+
return Math.max(0, Math.round((last - start) / 1000));
|
|
129
|
+
}
|
|
105
130
|
// ---------------------------------------------------------------------------
|
|
106
131
|
// Parsing
|
|
107
132
|
// ---------------------------------------------------------------------------
|
|
@@ -163,13 +188,20 @@ export function eachLine(file, fn) {
|
|
|
163
188
|
/** Codex rollout: token_count is CUMULATIVE (take the last); function_call is per tool invocation. */
|
|
164
189
|
export function parseCodex(file) {
|
|
165
190
|
let tot = null;
|
|
166
|
-
const meta = { first: null };
|
|
191
|
+
const meta = { first: null, last: null, cwd: null };
|
|
167
192
|
let echo = 0;
|
|
168
193
|
let reads = 0;
|
|
169
194
|
let acts = 0;
|
|
170
195
|
eachLine(file, (o) => {
|
|
171
|
-
if (
|
|
172
|
-
meta.first
|
|
196
|
+
if (typeof o.timestamp === "string") {
|
|
197
|
+
if (meta.first === null)
|
|
198
|
+
meta.first = o.timestamp;
|
|
199
|
+
meta.last = o.timestamp;
|
|
200
|
+
}
|
|
201
|
+
if (o && o.type === "session_meta" && o.payload && typeof o.payload.cwd === "string") {
|
|
202
|
+
meta.cwd = o.payload.cwd;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
173
205
|
const p = o && typeof o.payload === "object" && o.payload ? o.payload : o;
|
|
174
206
|
if (!p || typeof p !== "object")
|
|
175
207
|
return;
|
|
@@ -194,6 +226,10 @@ export function parseCodex(file) {
|
|
|
194
226
|
source: "codex",
|
|
195
227
|
date: meta.first ? meta.first.slice(0, 10) : null,
|
|
196
228
|
month: meta.first ? meta.first.slice(0, 7) : null,
|
|
229
|
+
startTs: meta.first,
|
|
230
|
+
lastTs: meta.last,
|
|
231
|
+
durationSec: durationBetween(meta.first, meta.last),
|
|
232
|
+
cwd: normalizeCwd(meta.cwd),
|
|
197
233
|
total: tot.total_tokens || 0,
|
|
198
234
|
cached: tot.cached_input_tokens || 0,
|
|
199
235
|
output: tot.output_tokens || 0,
|
|
@@ -210,11 +246,29 @@ export function parseClaude(file) {
|
|
|
210
246
|
let echo = 0;
|
|
211
247
|
let reads = 0;
|
|
212
248
|
let acts = 0;
|
|
213
|
-
const meta = {
|
|
249
|
+
const meta = {
|
|
250
|
+
first: null,
|
|
251
|
+
last: null,
|
|
252
|
+
cwd: null,
|
|
253
|
+
durationSec: 0,
|
|
254
|
+
prevMs: null,
|
|
255
|
+
};
|
|
214
256
|
let sawUsage = false;
|
|
215
257
|
eachLine(file, (o) => {
|
|
216
|
-
if (meta.
|
|
217
|
-
meta.
|
|
258
|
+
if (!meta.cwd && typeof o.cwd === "string")
|
|
259
|
+
meta.cwd = o.cwd;
|
|
260
|
+
if (typeof o.timestamp === "string") {
|
|
261
|
+
if (meta.first === null)
|
|
262
|
+
meta.first = o.timestamp;
|
|
263
|
+
meta.last = o.timestamp;
|
|
264
|
+
const ms = epochMs(o.timestamp);
|
|
265
|
+
if (ms != null) {
|
|
266
|
+
if (meta.prevMs != null) {
|
|
267
|
+
meta.durationSec += Math.min(300, Math.max(0, Math.round((ms - meta.prevMs) / 1000)));
|
|
268
|
+
}
|
|
269
|
+
meta.prevMs = ms;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
218
272
|
if (o.type !== "assistant" || !o.message)
|
|
219
273
|
return;
|
|
220
274
|
const u = o.message.usage;
|
|
@@ -248,6 +302,10 @@ export function parseClaude(file) {
|
|
|
248
302
|
source: "claude-code",
|
|
249
303
|
date: meta.first ? meta.first.slice(0, 10) : null,
|
|
250
304
|
month: meta.first ? meta.first.slice(0, 7) : null,
|
|
305
|
+
startTs: meta.first,
|
|
306
|
+
lastTs: meta.last,
|
|
307
|
+
durationSec: meta.durationSec,
|
|
308
|
+
cwd: normalizeCwd(meta.cwd),
|
|
251
309
|
total,
|
|
252
310
|
cached,
|
|
253
311
|
output,
|
|
@@ -326,6 +384,34 @@ export function aggregate(stats) {
|
|
|
326
384
|
const months = [...byMonth.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([month, d]) => ({
|
|
327
385
|
month, tokens: d.t, sessions: d.n, rereadPct: d.t ? Math.round((d.c / d.t) * 100) : 0,
|
|
328
386
|
}));
|
|
387
|
+
const byDay = new Map();
|
|
388
|
+
for (const d of dates)
|
|
389
|
+
byDay.set(d, (byDay.get(d) || 0) + 1);
|
|
390
|
+
const busiestDayEntry = [...byDay.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0];
|
|
391
|
+
const hourHistogram = Array.from({ length: 24 }, () => 0);
|
|
392
|
+
for (const s of stats) {
|
|
393
|
+
const ms = epochMs(s.startTs);
|
|
394
|
+
if (ms == null)
|
|
395
|
+
continue;
|
|
396
|
+
const hour = new Date(ms).getHours();
|
|
397
|
+
if (hour >= 0 && hour < 24)
|
|
398
|
+
hourHistogram[hour] += 1;
|
|
399
|
+
}
|
|
400
|
+
const hourMax = Math.max(...hourHistogram);
|
|
401
|
+
const hourLocal = hourMax > 0 ? hourHistogram.findIndex((n) => n === hourMax) : null;
|
|
402
|
+
const approxHours = stats.reduce((n, s) => n + s.durationSec, 0) / 3600;
|
|
403
|
+
const longestSessionMin = Math.max(0, ...stats.map((s) => s.durationSec)) / 60;
|
|
404
|
+
const byProject = new Map();
|
|
405
|
+
for (const s of stats) {
|
|
406
|
+
const name = basenameProject(s.cwd);
|
|
407
|
+
if (!name)
|
|
408
|
+
continue;
|
|
409
|
+
byProject.set(name, (byProject.get(name) || 0) + 1);
|
|
410
|
+
}
|
|
411
|
+
const topProjects = [...byProject.entries()]
|
|
412
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
413
|
+
.slice(0, 10)
|
|
414
|
+
.map(([name, sessions]) => ({ name, sessions }));
|
|
329
415
|
const totals = stats.map((s) => s.total).sort((a, b) => b - a);
|
|
330
416
|
const topN = Math.max(1, Math.floor(totals.length * 0.1));
|
|
331
417
|
const topShare = total ? Math.round((totals.slice(0, topN).reduce((n, x) => n + x, 0) / total) * 100) : 0;
|
|
@@ -342,7 +428,15 @@ export function aggregate(stats) {
|
|
|
342
428
|
first: dates[0] || null,
|
|
343
429
|
last: dates[dates.length - 1] || null,
|
|
344
430
|
days: new Set(dates).size,
|
|
345
|
-
months,
|
|
431
|
+
months,
|
|
432
|
+
busiestDay: { date: busiestDayEntry?.[0] || null, sessions: busiestDayEntry?.[1] || 0 },
|
|
433
|
+
hourHistogram,
|
|
434
|
+
hourLocal,
|
|
435
|
+
approxHours,
|
|
436
|
+
longestSessionMin,
|
|
437
|
+
topProjects,
|
|
438
|
+
topShare,
|
|
439
|
+
medianSession: median(stats.map((s) => s.total)),
|
|
346
440
|
costLow, costHigh,
|
|
347
441
|
};
|
|
348
442
|
}
|
|
@@ -400,6 +494,59 @@ function renderJson(a, memCount) {
|
|
|
400
494
|
estSpendUsdRough: { low: Math.round(a.costLow), high: Math.round(a.costHigh), note: "cache priced at ~0.1-0.5x; rough" },
|
|
401
495
|
}, null, 2);
|
|
402
496
|
}
|
|
497
|
+
function round1(n) {
|
|
498
|
+
return Math.round(n * 10) / 10;
|
|
499
|
+
}
|
|
500
|
+
/** Build the localhost onboarding dashboard payload. This is intentionally separate from report --json. */
|
|
501
|
+
export async function buildStatsPayload(stats, inject) {
|
|
502
|
+
const a = aggregate(stats);
|
|
503
|
+
const memCount = inject?.skipMemoryCount
|
|
504
|
+
? null
|
|
505
|
+
: Object.prototype.hasOwnProperty.call(inject || {}, "memoriesCaptured")
|
|
506
|
+
? inject?.memoriesCaptured ?? null
|
|
507
|
+
: await fetchMemoryCount();
|
|
508
|
+
const busiestMonth = [...a.months].sort((x, y) => y.sessions - x.sessions || x.month.localeCompare(y.month))[0];
|
|
509
|
+
const sessionCounts = inject?.sessions ?? { total: a.sessions, codex: a.codexN, claudeCode: a.claudeN };
|
|
510
|
+
return {
|
|
511
|
+
schemaVersion: 1,
|
|
512
|
+
...(inject?.partial ? { partial: true } : {}),
|
|
513
|
+
...(inject?.discovery ? { discovery: inject.discovery } : {}),
|
|
514
|
+
generatedFrom: ["~/.codex/sessions", "~/.claude/projects"],
|
|
515
|
+
llmCallsUsed: 0,
|
|
516
|
+
transcriptsUploaded: false,
|
|
517
|
+
window: { first: a.first, last: a.last, activeDays: a.days },
|
|
518
|
+
sessions: sessionCounts,
|
|
519
|
+
migratable: {
|
|
520
|
+
pending: inject?.migratable?.pending ?? 0,
|
|
521
|
+
alreadyMigrated: inject?.migratable?.alreadyMigrated ?? 0,
|
|
522
|
+
...inject?.migratable,
|
|
523
|
+
},
|
|
524
|
+
busiest: {
|
|
525
|
+
month: { month: busiestMonth?.month || null, sessions: busiestMonth?.sessions || 0 },
|
|
526
|
+
day: a.busiestDay,
|
|
527
|
+
hourLocal: a.hourLocal,
|
|
528
|
+
hourHistogram: a.hourHistogram,
|
|
529
|
+
},
|
|
530
|
+
time: { approxHours: round1(a.approxHours), approx: true, longestSessionMin: Math.round(a.longestSessionMin) },
|
|
531
|
+
contextGathering: {
|
|
532
|
+
reads: a.reads,
|
|
533
|
+
memoryRecalls: a.recalls,
|
|
534
|
+
readsPerRecall: a.ratio,
|
|
535
|
+
otherActions: a.acts,
|
|
536
|
+
note: "reads = file reads + searches only; edits/writes/builds/tests excluded (memory doesn't replace them)",
|
|
537
|
+
},
|
|
538
|
+
tokens: {
|
|
539
|
+
totalCumulative: a.total,
|
|
540
|
+
rereadPctOfTokens: a.rereadPct,
|
|
541
|
+
fresh: a.fresh,
|
|
542
|
+
output: a.output,
|
|
543
|
+
note: "cumulative across turns; re-read % is a TOKEN share (context re-sent each turn), NOT time",
|
|
544
|
+
},
|
|
545
|
+
concentration: { topTenPctShare: a.topShare, medianSession: a.medianSession },
|
|
546
|
+
topProjects: a.topProjects,
|
|
547
|
+
memoriesCaptured: memCount,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
403
550
|
/** Render the report to a string. `useColor=false` for the MCP tool (clean text); true for the terminal. */
|
|
404
551
|
function renderText(a, memCount, useColor) {
|
|
405
552
|
const c = makeColor(useColor);
|