@echomem/mcp 1.4.18 → 1.4.20
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 +12 -5
- package/dist/codex-session-files.js +7 -1
- package/dist/hud/server.js +17 -13
- package/dist/hud/web.js +204 -13
- package/dist/index.js +61 -30
- package/dist/keystore.js +9 -19
- package/dist/migrate.js +78 -36
- package/dist/setup-page/client-core.js +31 -1
- package/dist/setup-page/client-extraction.js +581 -157
- package/dist/setup-page/client-lifecycle.js +64 -43
- package/dist/setup-page/client-report-city.js +14 -5
- package/dist/setup-page/styles-extraction.js +1176 -148
- package/dist/setup-page/styles-foundation.js +20 -15
- package/dist/setup-page/styles-website-alignment.js +818 -0
- package/dist/setup-page/styles.js +2 -0
- package/dist/setup-preview.js +64 -1
- package/dist/setup.js +328 -39
- package/dist/v1-contract.js +8 -8
- package/package.json +1 -1
- package/templates/codex-skills/echomem-forget/SKILL.md +16 -0
- package/templates/codex-skills/echomem-forget/agents/openai.yaml +6 -0
- package/templates/codex-skills/echomem-login/SKILL.md +27 -0
- package/templates/codex-skills/echomem-login/agents/openai.yaml +6 -0
- package/templates/codex-skills/echomem-save/SKILL.md +14 -0
- package/templates/codex-skills/echomem-save/agents/openai.yaml +6 -0
- package/templates/codex-skills/echomem-search/SKILL.md +14 -0
- package/templates/codex-skills/echomem-search/agents/openai.yaml +6 -0
package/dist/migrate.js
CHANGED
|
@@ -25,7 +25,8 @@ import readline from "node:readline";
|
|
|
25
25
|
import axios from "axios";
|
|
26
26
|
import { KeyStore, echoConfigDir } from "./keystore.js";
|
|
27
27
|
import { fetchEncryptionConfig } from "./encryption.js";
|
|
28
|
-
import {
|
|
28
|
+
import { discoverCodexSessionFiles } from "./codex-session-files.js";
|
|
29
|
+
import { resolveClaudeProjectsDir } from "./local-data-paths.js";
|
|
29
30
|
import { walk, eachLine } from "./report.js";
|
|
30
31
|
const API_BASE = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
|
|
31
32
|
const RATE_MAX = 28; // stay under the import-jobs /run limit of 30 / 60s
|
|
@@ -184,20 +185,29 @@ export function normalizeCwd(cwd) {
|
|
|
184
185
|
const m = cwd.match(/worktrees\/[^/]+\/(.+)$/);
|
|
185
186
|
return m ? m[1] : cwd;
|
|
186
187
|
}
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
function userCreatedCodexFiles(codexRoot) {
|
|
189
|
+
return discoverCodexSessionFiles({
|
|
190
|
+
...(codexRoot
|
|
191
|
+
? { roots: [{ kind: "active", path: codexRoot, priority: 0 }] }
|
|
192
|
+
: {}),
|
|
193
|
+
includeArchived: false,
|
|
194
|
+
userInitiatedOnly: true,
|
|
195
|
+
}).files.map((file) => file.path);
|
|
196
|
+
}
|
|
197
|
+
function userCreatedClaudeFiles(claudeRoot) {
|
|
198
|
+
return walk(claudeRoot, (candidate) => candidate.endsWith(".jsonl"), (name) => name === "subagents" || name === "workflows").filter(isUserCreatedClaudeSessionFile);
|
|
199
|
+
}
|
|
200
|
+
/** Discover every user-created local session, newest first (by first-turn timestamp). */
|
|
201
|
+
export function discoverSessions(opts = {}) {
|
|
189
202
|
const out = [];
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (s)
|
|
195
|
-
out.push(s);
|
|
196
|
-
}
|
|
203
|
+
for (const f of userCreatedCodexFiles(opts.codexRoot)) {
|
|
204
|
+
const s = assembleCodex(f);
|
|
205
|
+
if (s)
|
|
206
|
+
out.push(s);
|
|
197
207
|
}
|
|
198
|
-
const claudeRoot = resolveClaudeProjectsDir();
|
|
208
|
+
const claudeRoot = opts.claudeRoot ?? resolveClaudeProjectsDir();
|
|
199
209
|
if (claudeRoot) {
|
|
200
|
-
for (const f of
|
|
210
|
+
for (const f of userCreatedClaudeFiles(claudeRoot)) {
|
|
201
211
|
const s = assembleClaude(f);
|
|
202
212
|
if (s)
|
|
203
213
|
out.push(s);
|
|
@@ -250,6 +260,20 @@ function hasClaudeText(content) {
|
|
|
250
260
|
return false;
|
|
251
261
|
return content.some((block) => isRecord(block) && block.type === "text" && typeof block.text === "string" && block.text.trim().length > 0);
|
|
252
262
|
}
|
|
263
|
+
function isUserCreatedClaudeSessionFile(file) {
|
|
264
|
+
return initialJsonObjects(file).some((obj) => {
|
|
265
|
+
if (!isRecord(obj) || obj.type !== "user")
|
|
266
|
+
return false;
|
|
267
|
+
if (obj.userType !== "external" || obj.isSidechain !== false || obj.parentUuid !== null)
|
|
268
|
+
return false;
|
|
269
|
+
if (obj.isMeta === true || obj.isCompactSummary === true)
|
|
270
|
+
return false;
|
|
271
|
+
if (typeof obj.agentId === "string" && obj.agentId.trim())
|
|
272
|
+
return false;
|
|
273
|
+
const message = isRecord(obj.message) ? obj.message : {};
|
|
274
|
+
return hasClaudeText(message.content);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
253
277
|
function fastSessionInfo(file, source) {
|
|
254
278
|
let conversationKey = `${source === "codex" ? "codex" : "claude-code"}:${sha16(file)}`;
|
|
255
279
|
let hasTextTurn = false;
|
|
@@ -287,21 +311,18 @@ function fastSessionInfo(file, source) {
|
|
|
287
311
|
}
|
|
288
312
|
function fastSessionEntries(opts = {}) {
|
|
289
313
|
const out = [];
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if (info.hasTextTurn || info.hasRealKey)
|
|
299
|
-
out.push({ filePath, source: "codex", conversationKey: info.conversationKey, size: stat.size, mtimeMs: stat.mtimeMs });
|
|
300
|
-
}
|
|
314
|
+
for (const filePath of userCreatedCodexFiles(opts.codexRoot)) {
|
|
315
|
+
const stat = statSafe(filePath);
|
|
316
|
+
const info = fastSessionInfo(filePath, "codex");
|
|
317
|
+
// Include if we found text OR a real session id (big sessions can have their first text turn beyond
|
|
318
|
+
// the 1MB probe window — gating only on text dropped them entirely; exact discovery refines later).
|
|
319
|
+
// We require a real key so the fast/exact conversationKey match (no sha16 fallback mismatch).
|
|
320
|
+
if (info.hasTextTurn || info.hasRealKey)
|
|
321
|
+
out.push({ filePath, source: "codex", conversationKey: info.conversationKey, size: stat.size, mtimeMs: stat.mtimeMs });
|
|
301
322
|
}
|
|
302
323
|
const claudeRoot = opts.claudeRoot ?? resolveClaudeProjectsDir();
|
|
303
324
|
if (claudeRoot) {
|
|
304
|
-
for (const filePath of
|
|
325
|
+
for (const filePath of userCreatedClaudeFiles(claudeRoot)) {
|
|
305
326
|
const stat = statSafe(filePath);
|
|
306
327
|
const info = fastSessionInfo(filePath, "claude-code");
|
|
307
328
|
// Same rule as codex: include on text OR a real session id so large sessions aren't undercounted,
|
|
@@ -499,6 +520,11 @@ function parsePositiveIntFlag(value, name) {
|
|
|
499
520
|
throw new Error(`${name} must be a positive integer.`);
|
|
500
521
|
return n;
|
|
501
522
|
}
|
|
523
|
+
export async function cancelQueuedImportSession(client, sessionId) {
|
|
524
|
+
if (!sessionId)
|
|
525
|
+
throw new Error("IMPORT_SESSION_ID_REQUIRED");
|
|
526
|
+
await client.patch(`/api/extension/import-sessions/${encodeURIComponent(sessionId)}`, { action: "cancel" });
|
|
527
|
+
}
|
|
502
528
|
function isRecord(value) {
|
|
503
529
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
504
530
|
}
|
|
@@ -756,7 +782,7 @@ function authedClient(token) {
|
|
|
756
782
|
});
|
|
757
783
|
}
|
|
758
784
|
export function discoverMigratableSessions(opts = {}) {
|
|
759
|
-
let sessions = discoverSessions();
|
|
785
|
+
let sessions = discoverSessions(opts);
|
|
760
786
|
const since = opts.since;
|
|
761
787
|
if (since)
|
|
762
788
|
sessions = sessions.filter((s) => (s.firstTs || "").slice(0, 10) >= since);
|
|
@@ -989,7 +1015,18 @@ export async function startMigration(opts) {
|
|
|
989
1015
|
metricsFile,
|
|
990
1016
|
selection: opts.selection,
|
|
991
1017
|
});
|
|
992
|
-
|
|
1018
|
+
const cancelQueued = async () => {
|
|
1019
|
+
await cancelQueuedImportSession(client, session.id);
|
|
1020
|
+
};
|
|
1021
|
+
return {
|
|
1022
|
+
sessionId: session.id,
|
|
1023
|
+
runId,
|
|
1024
|
+
jobCount: session.jobs.length,
|
|
1025
|
+
metricsFile,
|
|
1026
|
+
...(capped ? { capped } : {}),
|
|
1027
|
+
done,
|
|
1028
|
+
cancelQueued,
|
|
1029
|
+
};
|
|
993
1030
|
}
|
|
994
1031
|
async function runJobs(args) {
|
|
995
1032
|
const ledger = loadLedger();
|
|
@@ -1060,7 +1097,7 @@ async function runJobs(args) {
|
|
|
1060
1097
|
const message = responseMessage(e);
|
|
1061
1098
|
const durationMs = Date.now() - jobStartedAt;
|
|
1062
1099
|
if (status === 422 && errCode === "ENCRYPTION_KEY_REQUIRED") {
|
|
1063
|
-
stoppedReason = "
|
|
1100
|
+
stoppedReason = "vault-locked"; // no usable local vault key — signal workers to stop pulling
|
|
1064
1101
|
done++;
|
|
1065
1102
|
recordMetric(buildMigrationMetric({
|
|
1066
1103
|
runId: args.runId, importSessionId: args.session.id, jobId: job.id, index: done, total,
|
|
@@ -1096,7 +1133,8 @@ async function runJobs(args) {
|
|
|
1096
1133
|
}
|
|
1097
1134
|
};
|
|
1098
1135
|
try {
|
|
1099
|
-
|
|
1136
|
+
const workerCount = Math.min(MIGRATE_CONCURRENCY, Math.max(1, args.session.concurrency), Math.max(1, total));
|
|
1137
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
1100
1138
|
}
|
|
1101
1139
|
catch {
|
|
1102
1140
|
failed++;
|
|
@@ -1236,12 +1274,12 @@ export async function cmdMigrate(flags) {
|
|
|
1236
1274
|
},
|
|
1237
1275
|
});
|
|
1238
1276
|
if (h.capped)
|
|
1239
|
-
console.log(c.yellow(`Your plan
|
|
1277
|
+
console.log(c.yellow(`Your plan has ${h.capped} historical slots remaining — importing the newest ${h.capped}. Upgrade to import more.`));
|
|
1240
1278
|
console.log(c.dim(`Import session ${h.sessionId} — ${h.jobCount} jobs queued (the web dashboard can watch this live).`));
|
|
1241
1279
|
console.log(c.dim(`Metrics: ${h.metricsFile}`));
|
|
1242
1280
|
const r = await h.done;
|
|
1243
|
-
if (r.stoppedReason === "
|
|
1244
|
-
console.error(c.yellow(`\n⚠ Vault locked mid-run.
|
|
1281
|
+
if (r.stoppedReason === "vault-locked") {
|
|
1282
|
+
console.error(c.yellow(`\n⚠ Vault locked mid-run. Open Terminal, run \`echomem-mcp unlock\`, and re-run migrate to resume (${r.migrated} done so far).`));
|
|
1245
1283
|
process.exitCode = 1;
|
|
1246
1284
|
}
|
|
1247
1285
|
console.log("");
|
|
@@ -1258,10 +1296,7 @@ export async function cmdMigrate(flags) {
|
|
|
1258
1296
|
if (code === "NOT_LOGGED_IN")
|
|
1259
1297
|
console.error("Not logged in. Run `echomem-mcp login` first, then re-run migrate.");
|
|
1260
1298
|
else if (code === "VAULT_LOCKED") {
|
|
1261
|
-
|
|
1262
|
-
console.error(store.isKeyExpired()
|
|
1263
|
-
? "Vault key expired. Run `echomem-mcp unlock`, then re-run migrate."
|
|
1264
|
-
: "This account is ENCRYPTED but the vault is locked. Run `echomem-mcp unlock`, then re-run migrate.");
|
|
1299
|
+
console.error("This account is encrypted but the local vault is locked. Open Terminal, run `echomem-mcp unlock`, then re-run migrate.");
|
|
1265
1300
|
}
|
|
1266
1301
|
else if (code === "FORBIDDEN_SCOPE") {
|
|
1267
1302
|
console.error("This device token cannot import history. Re-connect this device with `echomem-mcp login`.");
|
|
@@ -1283,7 +1318,14 @@ async function createImportSession(client, sessions, bareId, userTz, signal) {
|
|
|
1283
1318
|
}));
|
|
1284
1319
|
const res = await client.post("/api/extension/import-sessions", { items }, signal ? { signal } : undefined);
|
|
1285
1320
|
const data = res.data || {};
|
|
1286
|
-
|
|
1321
|
+
const serverConcurrency = Number(data.session?.concurrency);
|
|
1322
|
+
return {
|
|
1323
|
+
id: String(data.session?.id || ""),
|
|
1324
|
+
concurrency: Number.isFinite(serverConcurrency) && serverConcurrency > 0
|
|
1325
|
+
? Math.min(MIGRATE_CONCURRENCY, Math.floor(serverConcurrency))
|
|
1326
|
+
: MIGRATE_CONCURRENCY,
|
|
1327
|
+
jobs: Array.isArray(data.jobs) ? data.jobs : [],
|
|
1328
|
+
};
|
|
1287
1329
|
}
|
|
1288
1330
|
/** Run one queued job: stream its transcript to the server, which extracts it. Retries transient locks/rate limits. */
|
|
1289
1331
|
async function runImportJob(client, jobId, s, userTz, encKey) {
|
|
@@ -13,16 +13,27 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
13
13
|
var statsSlow = false;
|
|
14
14
|
var decisionMade = false;
|
|
15
15
|
var connected = false;
|
|
16
|
+
var localHistoryConsentGranted = false;
|
|
16
17
|
var extractMounted = false; // WebGL extraction-plate iframe mounted once; persists across /progress polls
|
|
18
|
+
var lastExtractionProgress = null; // restores the live extraction view if ending fails
|
|
19
|
+
var extractionRecoveryMessage = ""; // persists pause failures across resumed progress updates
|
|
17
20
|
var exStartAt = 0; // extraction start (ms) — drives the countdown
|
|
18
21
|
var exDeadline = 0; // first honest ETA deadline; passing it while running → "taking longer" banner
|
|
19
22
|
var extractionEnded = false; // user chose to leave the extraction flow before natural completion
|
|
23
|
+
var progressPollRunId = 0; // invalidates older progress loops when extraction resumes
|
|
20
24
|
var dashMounted = false; // dashboard shell (incl. plate iframe) mounted once; persists across /stats polls
|
|
21
25
|
var reportMounted = false; // report shell (city iframe) mounted once; loading is an overlay on it, not a separate page
|
|
22
26
|
var authUrl = "";
|
|
27
|
+
var switchAccountUrl = "";
|
|
23
28
|
var workspacePath = "";
|
|
24
29
|
var billingStatus = null;
|
|
25
|
-
var
|
|
30
|
+
var billingStatusLoading = false;
|
|
31
|
+
var setupPlanChoice = "";
|
|
32
|
+
var selectedSessionKeys = Object.create(null);
|
|
33
|
+
var sessionSelectionTouched = false;
|
|
34
|
+
var sessionPickerQuery = "";
|
|
35
|
+
var sessionPickerSource = "all";
|
|
36
|
+
var billingPollTimer = null;
|
|
26
37
|
var authWindow = null;
|
|
27
38
|
var connectionPollStarted = false;
|
|
28
39
|
var statsPollStarted = false;
|
|
@@ -34,6 +45,20 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
34
45
|
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch];
|
|
35
46
|
});
|
|
36
47
|
}
|
|
48
|
+
function setupIcon(name) {
|
|
49
|
+
var paths = {
|
|
50
|
+
"shield-check": '<path d="M12 3 5 6v5c0 4.6 2.9 8.1 7 10 4.1-1.9 7-5.4 7-10V6l-7-3Z"/><path d="m8.8 12.1 2 2 4.4-4.5"/>',
|
|
51
|
+
"folder-read": '<path d="M3 7.5h6l2-2h4.5A2.5 2.5 0 0 1 18 8v1"/><path d="M3 7.5V18a2 2 0 0 0 2 2h7"/><circle cx="17" cy="15" r="3.5"/><path d="m19.5 17.5 2 2"/>',
|
|
52
|
+
"report": '<path d="M5 20V10"/><path d="M12 20V4"/><path d="M19 20v-7"/><path d="M3 20h18"/>',
|
|
53
|
+
"cloud-off": '<path d="m3 3 18 18"/><path d="M6.7 6.7A5.5 5.5 0 0 0 6.5 17H17"/><path d="M10.7 5.2A7 7 0 0 1 19 12.1 4 4 0 0 1 19.5 19"/>',
|
|
54
|
+
"list-check": '<rect x="4" y="4" width="16" height="16" rx="3"/><path d="m7.5 9 1.4 1.4L11.5 8"/><path d="M13.5 9h3"/><path d="m7.5 14 1.4 1.4 2.6-2.4"/><path d="M13.5 14h3"/>',
|
|
55
|
+
"pause": '<path d="M9 7v10"/><path d="M15 7v10"/>',
|
|
56
|
+
"check": '<path d="m6.5 12.2 3.5 3.5 7.5-7.5"/>',
|
|
57
|
+
"copy": '<rect x="8" y="8" width="11" height="11" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/>',
|
|
58
|
+
"arrow-right": '<path d="M5 12h14"/><path d="m14 7 5 5-5 5"/>'
|
|
59
|
+
};
|
|
60
|
+
return '<svg class="setupGlyph" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><g stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' + (paths[name] || "") + '</g></svg>';
|
|
61
|
+
}
|
|
37
62
|
function setHead(nextTitle, nextStatus) {
|
|
38
63
|
title.textContent = nextTitle;
|
|
39
64
|
status.textContent = nextStatus;
|
|
@@ -151,6 +176,11 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
151
176
|
});
|
|
152
177
|
}
|
|
153
178
|
function onConnectClick() {
|
|
179
|
+
if (!localHistoryConsentGranted) {
|
|
180
|
+
renderLocalScanConsent();
|
|
181
|
+
setConsentStatus("Local history access is required before you can connect EchoMem or continue setup.", "required");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
154
184
|
if (connected) { void waitForStats(); return; }
|
|
155
185
|
if (!openAuthWindow(authUrl)) showPopupFallback("", authUrl);
|
|
156
186
|
startConnectionPoll();
|