@echomem/mcp 1.4.17 → 1.4.19
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/context-analysis/claude-canonical-adapter.js +315 -0
- package/dist/hud/cli.js +0 -0
- package/dist/hud/efficiency.js +447 -0
- package/dist/hud/electron-main.js +7 -2
- package/dist/hud/monitor.js +164 -50
- package/dist/hud/server.js +96 -16
- package/dist/hud/session-launcher.js +395 -0
- package/dist/hud/web.js +540 -177
- package/dist/index.js +54 -22
- package/dist/migrate.js +47 -11
- package/dist/setup-page/client-core.js +31 -1
- package/dist/setup-page/client-extraction.js +533 -149
- package/dist/setup-page/client-lifecycle.js +64 -43
- package/dist/setup-page/styles-extraction.js +1107 -148
- package/dist/setup-page/styles-foundation.js +20 -15
- package/dist/setup-page/styles-website-alignment.js +635 -0
- package/dist/setup-page/styles.js +2 -0
- package/dist/setup-preview.js +49 -0
- package/dist/setup.js +240 -21
- package/dist/v1-contract.js +8 -8
- package/package.json +1 -1
package/dist/setup-preview.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export const SETUP_PREVIEW_STATES = [
|
|
2
|
+
"consent",
|
|
3
|
+
"consent-required",
|
|
2
4
|
"report",
|
|
3
5
|
"extract-counting",
|
|
4
6
|
"extract-ready",
|
|
@@ -200,6 +202,15 @@ function previewWatermarkScript() {
|
|
|
200
202
|
}
|
|
201
203
|
export function renderSetupPreviewBootstrap(state) {
|
|
202
204
|
const watermark = previewWatermarkScript();
|
|
205
|
+
if (state === "consent") {
|
|
206
|
+
return `${watermark}
|
|
207
|
+
renderLocalScanConsent();`;
|
|
208
|
+
}
|
|
209
|
+
if (state === "consent-required") {
|
|
210
|
+
return `${watermark}
|
|
211
|
+
renderLocalScanConsent();
|
|
212
|
+
setConsentStatus("Local history access is required to continue setup. Nothing will be scanned or uploaded unless you allow it.", "required");`;
|
|
213
|
+
}
|
|
203
214
|
if (state === "report") {
|
|
204
215
|
const reportJson = JSON.stringify(previewReport);
|
|
205
216
|
return `${watermark}
|
|
@@ -214,6 +225,17 @@ export function renderSetupPreviewBootstrap(state) {
|
|
|
214
225
|
}
|
|
215
226
|
if (state === "extract-ready") {
|
|
216
227
|
return `${watermark}
|
|
228
|
+
connected = true;
|
|
229
|
+
billingStatus = {
|
|
230
|
+
plan: "free", paid: false, trialAvailable: true,
|
|
231
|
+
pricingUrl: "https://echoknows.com/pricing?source=mcp_onboarding",
|
|
232
|
+
account: {
|
|
233
|
+
displayName: "Erik Hein",
|
|
234
|
+
email: "erik@example.com",
|
|
235
|
+
avatarUrl: "/hud-assets/echo-face-cutout.png"
|
|
236
|
+
},
|
|
237
|
+
historicalConversationQuota: { used: 72, limit: 100, remaining: 28 }
|
|
238
|
+
};
|
|
217
239
|
stats = {
|
|
218
240
|
sessions: { total: 136, codex: 82, claudeCode: 54 },
|
|
219
241
|
migratable: {
|
|
@@ -221,6 +243,19 @@ export function renderSetupPreviewBootstrap(state) {
|
|
|
221
243
|
alreadyMigrated: 108, skippedActive: 0,
|
|
222
244
|
eta: { estimatedLabel: "under 2 minutes" }, estimatedLabel: "under 2 minutes"
|
|
223
245
|
},
|
|
246
|
+
candidateSessions: Array.from({ length: 28 }, function (_, index) {
|
|
247
|
+
var codex = index < 19;
|
|
248
|
+
return {
|
|
249
|
+
key: (codex ? "codex:" : "claude-code:") + "preview-" + index,
|
|
250
|
+
source: codex ? "codex" : "claude-code",
|
|
251
|
+
title: ["Plan the onboarding flow", "Repair memory extraction", "Review pricing limits", "Polish the EchoMem HUD"][index % 4] + " · " + (index + 1),
|
|
252
|
+
project: ["EchoMem Chrome", "MCP Server", "Memory Platform"][index % 3],
|
|
253
|
+
date: new Date(Date.UTC(2026, 6, 18 - (index % 12))).toISOString(),
|
|
254
|
+
characters: 42000 + index * 2700,
|
|
255
|
+
approxInputTokens: 10500 + index * 675,
|
|
256
|
+
turns: 18 + index
|
|
257
|
+
};
|
|
258
|
+
}),
|
|
224
259
|
discovery: { phase: "account", exact: false }
|
|
225
260
|
};
|
|
226
261
|
renderDashboard();`;
|
|
@@ -237,6 +272,20 @@ export function renderSetupPreviewBootstrap(state) {
|
|
|
237
272
|
}
|
|
238
273
|
if (state === "extract-done") {
|
|
239
274
|
return `${watermark}
|
|
275
|
+
stats = {
|
|
276
|
+
candidateSessions: [
|
|
277
|
+
{ key: "codex:done-1", project: "EchoMem Chrome", title: "Polish the onboarding flow" },
|
|
278
|
+
{ key: "codex:done-2", project: "EchoMem Chrome", title: "Repair memory extraction" },
|
|
279
|
+
{ key: "claude-code:done-3", project: "MCP Server", title: "Review recall limits" },
|
|
280
|
+
{ key: "codex:done-4", project: "Memory Platform", title: "Plan the next release" }
|
|
281
|
+
]
|
|
282
|
+
};
|
|
283
|
+
selectedSessionKeys = {
|
|
284
|
+
"codex:done-1": true,
|
|
285
|
+
"codex:done-2": true,
|
|
286
|
+
"claude-code:done-3": true,
|
|
287
|
+
"codex:done-4": true
|
|
288
|
+
};
|
|
240
289
|
renderProgress({ status: "completed", total: 28, completed: 28, extracted: 128, failed: 0 });`;
|
|
241
290
|
}
|
|
242
291
|
if (state === "extract-ending")
|
package/dist/setup.js
CHANGED
|
@@ -629,6 +629,24 @@ function commandFailureMessage(error) {
|
|
|
629
629
|
}
|
|
630
630
|
return error instanceof Error ? error.message : String(error);
|
|
631
631
|
}
|
|
632
|
+
function candidateSessionsFromDiscovery(disc) {
|
|
633
|
+
return disc.pending.map((session) => ({
|
|
634
|
+
key: session.conversationKey,
|
|
635
|
+
source: session.source,
|
|
636
|
+
title: session.title || "Untitled coding session",
|
|
637
|
+
project: repoLabel(session.cwd) || "No project detected",
|
|
638
|
+
date: session.firstTs,
|
|
639
|
+
characters: session.rawData.length,
|
|
640
|
+
approxInputTokens: Math.ceil(session.rawData.length / 4),
|
|
641
|
+
turns: session.turnCount,
|
|
642
|
+
}));
|
|
643
|
+
}
|
|
644
|
+
function withCandidateSessions(payload, disc) {
|
|
645
|
+
const base = payload && typeof payload === "object" && !Array.isArray(payload)
|
|
646
|
+
? payload
|
|
647
|
+
: {};
|
|
648
|
+
return { ...base, candidateSessions: candidateSessionsFromDiscovery(disc) };
|
|
649
|
+
}
|
|
632
650
|
function migratableFromDiscovery(disc) {
|
|
633
651
|
const eta = estimateMigrationEta(disc.pending, disc.skippedActive);
|
|
634
652
|
const pendingCodex = disc.pendingCodex ?? disc.pending.filter((s) => s.source === "codex").length;
|
|
@@ -709,6 +727,16 @@ function isObjectRecord(value) {
|
|
|
709
727
|
function asString(value) {
|
|
710
728
|
return typeof value === "string" && value ? value : undefined;
|
|
711
729
|
}
|
|
730
|
+
function asConversationKeys(value) {
|
|
731
|
+
if (!Array.isArray(value))
|
|
732
|
+
return null;
|
|
733
|
+
const keys = value
|
|
734
|
+
.slice(0, 5000)
|
|
735
|
+
.filter((item) => typeof item === "string")
|
|
736
|
+
.map((item) => item.trim())
|
|
737
|
+
.filter((item) => /^(codex|claude-code):[^\s]{1,180}$/.test(item));
|
|
738
|
+
return Array.from(new Set(keys));
|
|
739
|
+
}
|
|
712
740
|
function readJsonBody(req) {
|
|
713
741
|
return new Promise((resolve, reject) => {
|
|
714
742
|
let body = "";
|
|
@@ -1084,10 +1112,12 @@ export function startCallbackServer(opts = {}) {
|
|
|
1084
1112
|
let authUrl = "";
|
|
1085
1113
|
let switchAccountUrl = "";
|
|
1086
1114
|
let connected = false;
|
|
1115
|
+
let reportConsentGranted = opts.requireReportConsent !== true;
|
|
1087
1116
|
let progress = { status: "idle", total: 0, completed: 0, running: 0, queued: 0, failed: 0, extracted: 0 };
|
|
1088
1117
|
let migrateStarted = false;
|
|
1089
1118
|
let tokenRefreshHandler = null;
|
|
1090
1119
|
let logoutHandler = null;
|
|
1120
|
+
let migrationPauseHandler = null;
|
|
1091
1121
|
let timer;
|
|
1092
1122
|
let closed = false;
|
|
1093
1123
|
let server;
|
|
@@ -1128,6 +1158,12 @@ export function startCallbackServer(opts = {}) {
|
|
|
1128
1158
|
const handleCallback = (res, token, key, nonce) => {
|
|
1129
1159
|
if (!checkNonce(nonce))
|
|
1130
1160
|
return void text(res, 403, "bad nonce");
|
|
1161
|
+
if (opts.requireReportConsent === true && !reportConsentGranted) {
|
|
1162
|
+
return void json(res, 403, {
|
|
1163
|
+
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1164
|
+
message: "Allow local history access in the setup page before connecting EchoMem.",
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1131
1167
|
if (!token)
|
|
1132
1168
|
return void text(res, 400, "missing token");
|
|
1133
1169
|
console.log(`[${new Date().toISOString()}] Browser approval callback received.`);
|
|
@@ -1190,7 +1226,15 @@ export function startCallbackServer(opts = {}) {
|
|
|
1190
1226
|
if (route === "/config" && req.method === "GET") {
|
|
1191
1227
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1192
1228
|
return void text(res, 403, "bad nonce");
|
|
1193
|
-
json(res, 200, {
|
|
1229
|
+
json(res, 200, {
|
|
1230
|
+
connected,
|
|
1231
|
+
authUrl,
|
|
1232
|
+
switchAccountUrl: switchAccountUrl || authUrl,
|
|
1233
|
+
localOnly: true,
|
|
1234
|
+
workspacePath: process.cwd(),
|
|
1235
|
+
consentRequired: opts.requireReportConsent === true,
|
|
1236
|
+
consentGranted: reportConsentGranted,
|
|
1237
|
+
});
|
|
1194
1238
|
return;
|
|
1195
1239
|
}
|
|
1196
1240
|
if (route === "/launch-agent" && req.method === "POST") {
|
|
@@ -1245,6 +1289,12 @@ export function startCallbackServer(opts = {}) {
|
|
|
1245
1289
|
if (route === "/stats" && req.method === "GET") {
|
|
1246
1290
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1247
1291
|
return void text(res, 403, "bad nonce");
|
|
1292
|
+
if (opts.requireReportConsent === true && !reportConsentGranted) {
|
|
1293
|
+
return void json(res, 403, {
|
|
1294
|
+
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1295
|
+
message: "Allow local history access before continuing setup.",
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1248
1298
|
const payload = opts.getStats ? opts.getStats() : stats;
|
|
1249
1299
|
if (payload == null)
|
|
1250
1300
|
return void res.writeHead(202).end();
|
|
@@ -1254,6 +1304,12 @@ export function startCallbackServer(opts = {}) {
|
|
|
1254
1304
|
if (route === "/billing-status" && req.method === "GET") {
|
|
1255
1305
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1256
1306
|
return void text(res, 403, "bad nonce");
|
|
1307
|
+
if (opts.requireReportConsent === true && !reportConsentGranted) {
|
|
1308
|
+
return void json(res, 403, {
|
|
1309
|
+
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1310
|
+
message: "Allow local history access before continuing setup.",
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1257
1313
|
const token = new KeyStore().getToken();
|
|
1258
1314
|
const pricingUrl = `${PRICING_URL}?source=mcp_onboarding`;
|
|
1259
1315
|
if (!token) {
|
|
@@ -1261,7 +1317,11 @@ export function startCallbackServer(opts = {}) {
|
|
|
1261
1317
|
return;
|
|
1262
1318
|
}
|
|
1263
1319
|
try {
|
|
1264
|
-
const
|
|
1320
|
+
const client = authedAxios(token);
|
|
1321
|
+
const [response, profileResponse] = await Promise.all([
|
|
1322
|
+
client.get("/api/extension/account/bootstrap", { timeout: 6000 }),
|
|
1323
|
+
client.get("/api/extension/account/profile-summary", { timeout: 6000 }).catch(() => null),
|
|
1324
|
+
]);
|
|
1265
1325
|
const plan = (asString(response.data?.plan) || "free").toLowerCase();
|
|
1266
1326
|
const trialUsed = response.data?.billing?.trialUsed === true;
|
|
1267
1327
|
const trialAvailable = response.data?.billing?.trialAvailable !== false;
|
|
@@ -1270,6 +1330,14 @@ export function startCallbackServer(opts = {}) {
|
|
|
1270
1330
|
paid: ["pro", "power", "team", "enterprise"].includes(plan),
|
|
1271
1331
|
trialAvailable,
|
|
1272
1332
|
trialUsed,
|
|
1333
|
+
historicalConversationQuota: response.data?.historicalConversationQuota ?? null,
|
|
1334
|
+
memoryProcessingQuota: response.data?.memoryProcessingQuota ?? null,
|
|
1335
|
+
memorySearchQuota: response.data?.memorySearchQuota ?? null,
|
|
1336
|
+
account: profileResponse ? {
|
|
1337
|
+
displayName: asString(profileResponse.data?.displayName) || "EchoMem user",
|
|
1338
|
+
email: asString(profileResponse.data?.email) || "",
|
|
1339
|
+
avatarUrl: asString(profileResponse.data?.avatarUrl) || "",
|
|
1340
|
+
} : null,
|
|
1273
1341
|
pricingUrl,
|
|
1274
1342
|
});
|
|
1275
1343
|
}
|
|
@@ -1283,6 +1351,12 @@ export function startCallbackServer(opts = {}) {
|
|
|
1283
1351
|
res.setHeader("Cache-Control", "no-store");
|
|
1284
1352
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1285
1353
|
return void text(res, 403, "bad nonce");
|
|
1354
|
+
if (opts.requireReportConsent === true && !reportConsentGranted) {
|
|
1355
|
+
return void json(res, 403, {
|
|
1356
|
+
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1357
|
+
message: "Allow local history access before starting the local scan.",
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1286
1360
|
let payload;
|
|
1287
1361
|
try {
|
|
1288
1362
|
payload = opts.getReport ? opts.getReport() : null;
|
|
@@ -1378,6 +1452,12 @@ export function startCallbackServer(opts = {}) {
|
|
|
1378
1452
|
if (route === "/progress" && req.method === "GET") {
|
|
1379
1453
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1380
1454
|
return void text(res, 403, "bad nonce");
|
|
1455
|
+
if (opts.requireReportConsent === true && !reportConsentGranted) {
|
|
1456
|
+
return void json(res, 403, {
|
|
1457
|
+
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1458
|
+
message: "Allow local history access before continuing setup.",
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1381
1461
|
json(res, 200, progress);
|
|
1382
1462
|
return;
|
|
1383
1463
|
}
|
|
@@ -1393,6 +1473,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1393
1473
|
if (!checkNonce(asString(body.nonce)))
|
|
1394
1474
|
return void text(res, 403, "bad nonce");
|
|
1395
1475
|
const allowed = body.allowed === true;
|
|
1476
|
+
reportConsentGranted = allowed;
|
|
1396
1477
|
opts.onReportConsent?.(allowed);
|
|
1397
1478
|
json(res, 200, { ok: true, allowed });
|
|
1398
1479
|
return;
|
|
@@ -1441,6 +1522,12 @@ export function startCallbackServer(opts = {}) {
|
|
|
1441
1522
|
}
|
|
1442
1523
|
if (!checkNonce(asString(body.nonce)))
|
|
1443
1524
|
return void text(res, 403, "bad nonce");
|
|
1525
|
+
if (opts.requireReportConsent === true && !reportConsentGranted) {
|
|
1526
|
+
return void json(res, 403, {
|
|
1527
|
+
error: "LOCAL_HISTORY_CONSENT_REQUIRED",
|
|
1528
|
+
message: "Allow local history access before starting extraction.",
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1444
1531
|
if (migrateStarted)
|
|
1445
1532
|
return void json(res, 409, { error: "MIGRATE_IN_PROGRESS" });
|
|
1446
1533
|
migrateStarted = true;
|
|
@@ -1448,7 +1535,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1448
1535
|
respondMigrate(res, { error: "IMPORT_START_TIMEOUT" }, 504);
|
|
1449
1536
|
}, 30_000);
|
|
1450
1537
|
safety.unref?.();
|
|
1451
|
-
migrateRequest.resolve({ res });
|
|
1538
|
+
migrateRequest.resolve({ res, conversationKeys: asConversationKeys(body.conversationKeys) });
|
|
1452
1539
|
decision.resolve("migrate");
|
|
1453
1540
|
return;
|
|
1454
1541
|
}
|
|
@@ -1463,6 +1550,23 @@ export function startCallbackServer(opts = {}) {
|
|
|
1463
1550
|
}
|
|
1464
1551
|
if (!checkNonce(asString(body.nonce)))
|
|
1465
1552
|
return void text(res, 403, "bad nonce");
|
|
1553
|
+
if (migrateStarted) {
|
|
1554
|
+
if (!migrationPauseHandler) {
|
|
1555
|
+
json(res, 409, { error: "MIGRATION_PAUSE_UNAVAILABLE", message: "Extraction is still starting. Try again in a moment." });
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
try {
|
|
1559
|
+
// Do not claim the work is paused until the import loop has stopped scheduling jobs.
|
|
1560
|
+
await migrationPauseHandler();
|
|
1561
|
+
}
|
|
1562
|
+
catch (error) {
|
|
1563
|
+
json(res, 500, {
|
|
1564
|
+
error: "MIGRATION_PAUSE_FAILED",
|
|
1565
|
+
message: error instanceof Error ? error.message : "Could not pause extraction.",
|
|
1566
|
+
});
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1466
1570
|
json(res, 200, { ok: true });
|
|
1467
1571
|
decision.resolve("skip");
|
|
1468
1572
|
close();
|
|
@@ -1496,6 +1600,9 @@ export function startCallbackServer(opts = {}) {
|
|
|
1496
1600
|
setLogoutHandler: (handler) => {
|
|
1497
1601
|
logoutHandler = handler;
|
|
1498
1602
|
},
|
|
1603
|
+
setMigrationPauseHandler: (handler) => {
|
|
1604
|
+
migrationPauseHandler = handler;
|
|
1605
|
+
},
|
|
1499
1606
|
setAuthUrl: (url, nextSwitchAccountUrl) => {
|
|
1500
1607
|
authUrl = url;
|
|
1501
1608
|
switchAccountUrl = nextSwitchAccountUrl || url;
|
|
@@ -1842,6 +1949,7 @@ async function cmdLogin(flags) {
|
|
|
1842
1949
|
const srv = await startCallbackServer({
|
|
1843
1950
|
port: devPortRaw,
|
|
1844
1951
|
nonce,
|
|
1952
|
+
requireReportConsent: true,
|
|
1845
1953
|
getStats: () => stats,
|
|
1846
1954
|
getReport: () => forensicReport,
|
|
1847
1955
|
getReportProgress: () => forensicProgress,
|
|
@@ -1865,6 +1973,19 @@ async function cmdLogin(flags) {
|
|
|
1865
1973
|
return;
|
|
1866
1974
|
}
|
|
1867
1975
|
forensicConsent = "allowed";
|
|
1976
|
+
const now = Date.now();
|
|
1977
|
+
forensicStage = "starting";
|
|
1978
|
+
forensicStageStartedAt = now;
|
|
1979
|
+
forensicProgress = {
|
|
1980
|
+
status: "running",
|
|
1981
|
+
scanned: 0,
|
|
1982
|
+
total: 0,
|
|
1983
|
+
stage: forensicStage,
|
|
1984
|
+
label: forensicStageLabel(forensicStage),
|
|
1985
|
+
elapsedMs: now - forensicStartedAt,
|
|
1986
|
+
stageElapsedMs: 0,
|
|
1987
|
+
updatedAt: now,
|
|
1988
|
+
};
|
|
1868
1989
|
startForensicScan();
|
|
1869
1990
|
},
|
|
1870
1991
|
});
|
|
@@ -2104,13 +2225,13 @@ async function cmdLogin(flags) {
|
|
|
2104
2225
|
migratable = migratableFromDiscovery(initialExact);
|
|
2105
2226
|
latestPendingEstimate = migratable.pending;
|
|
2106
2227
|
sessionSummary = sessionsFromDiscovery(initialExact);
|
|
2107
|
-
const partialPayload = await buildStatsPayload([], {
|
|
2228
|
+
const partialPayload = withCandidateSessions(await buildStatsPayload([], {
|
|
2108
2229
|
partial: true,
|
|
2109
2230
|
skipMemoryCount: true,
|
|
2110
2231
|
sessions: sessionSummary,
|
|
2111
2232
|
migratable,
|
|
2112
2233
|
discovery: { phase: "exact", exact: true },
|
|
2113
|
-
});
|
|
2234
|
+
}), initialExact);
|
|
2114
2235
|
if (generation !== refreshGeneration)
|
|
2115
2236
|
return disc;
|
|
2116
2237
|
stats = partialPayload;
|
|
@@ -2154,13 +2275,13 @@ async function cmdLogin(flags) {
|
|
|
2154
2275
|
migratable = migratableFromDiscovery(reconciled);
|
|
2155
2276
|
latestPendingEstimate = migratable.pending;
|
|
2156
2277
|
sessionSummary = sessionsFromDiscovery(reconciled);
|
|
2157
|
-
const reconciledPayload = await buildStatsPayload([], {
|
|
2278
|
+
const reconciledPayload = withCandidateSessions(await buildStatsPayload([], {
|
|
2158
2279
|
partial: true,
|
|
2159
2280
|
skipMemoryCount: true,
|
|
2160
2281
|
sessions: sessionSummary,
|
|
2161
2282
|
migratable,
|
|
2162
2283
|
discovery: { phase: "exact", exact: true },
|
|
2163
|
-
});
|
|
2284
|
+
}), reconciled);
|
|
2164
2285
|
if (generation !== refreshGeneration)
|
|
2165
2286
|
return;
|
|
2166
2287
|
stats = reconciledPayload;
|
|
@@ -2174,11 +2295,11 @@ async function cmdLogin(flags) {
|
|
|
2174
2295
|
failed: 0,
|
|
2175
2296
|
extracted: 0,
|
|
2176
2297
|
});
|
|
2177
|
-
const fullPayload = await buildStatsPayload(collect(), {
|
|
2298
|
+
const fullPayload = withCandidateSessions(await buildStatsPayload(collect(), {
|
|
2178
2299
|
sessions: sessionSummary,
|
|
2179
2300
|
migratable,
|
|
2180
2301
|
discovery: { phase: "full", exact: true },
|
|
2181
|
-
});
|
|
2302
|
+
}), reconciled);
|
|
2182
2303
|
if (generation !== refreshGeneration)
|
|
2183
2304
|
return;
|
|
2184
2305
|
stats = fullPayload;
|
|
@@ -2199,9 +2320,42 @@ async function cmdLogin(flags) {
|
|
|
2199
2320
|
await refreshLocalStatsForToken(nextToken);
|
|
2200
2321
|
});
|
|
2201
2322
|
await refreshLocalStatsForToken(token);
|
|
2323
|
+
// Ending is deliberately transient. Completed conversations remain recorded in the normal
|
|
2324
|
+
// import ledger; unfinished conversations are canceled and rediscovered by the next init.
|
|
2325
|
+
let pauseRequested = false;
|
|
2326
|
+
let pauseCompletion = null;
|
|
2327
|
+
let activeMigrationCleanup = null;
|
|
2328
|
+
let activeMigrationCleanupPromise = null;
|
|
2329
|
+
const cancelActiveMigration = async () => {
|
|
2330
|
+
if (!activeMigrationCleanup)
|
|
2331
|
+
return false;
|
|
2332
|
+
if (!activeMigrationCleanupPromise) {
|
|
2333
|
+
const cleanup = activeMigrationCleanup();
|
|
2334
|
+
activeMigrationCleanupPromise = cleanup;
|
|
2335
|
+
cleanup.catch(() => {
|
|
2336
|
+
// A transient backend failure must remain retryable from the restored extraction page.
|
|
2337
|
+
if (activeMigrationCleanupPromise === cleanup)
|
|
2338
|
+
activeMigrationCleanupPromise = null;
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
await activeMigrationCleanupPromise;
|
|
2342
|
+
return true;
|
|
2343
|
+
};
|
|
2344
|
+
srv.setMigrationPauseHandler(async () => {
|
|
2345
|
+
pauseRequested = true;
|
|
2346
|
+
// Cancel the backend session first. This immediately releases every queued reservation and
|
|
2347
|
+
// prevents another job from being claimed while already-running requests finish safely.
|
|
2348
|
+
const canceledBeforePause = await cancelActiveMigration();
|
|
2349
|
+
if (pauseCompletion)
|
|
2350
|
+
await pauseCompletion;
|
|
2351
|
+
// If End for now raced with import-session creation, the cleanup handle becomes available only
|
|
2352
|
+
// after the local worker loop observes pauseRequested. Cancel it before acknowledging the UI.
|
|
2353
|
+
if (!canceledBeforePause)
|
|
2354
|
+
await cancelActiveMigration();
|
|
2355
|
+
});
|
|
2202
2356
|
const choice = await srv.decision;
|
|
2203
2357
|
if (choice === "migrate") {
|
|
2204
|
-
const { res } = await srv.migrateRequest;
|
|
2358
|
+
const { res, conversationKeys } = await srv.migrateRequest;
|
|
2205
2359
|
let migrateResponded = false;
|
|
2206
2360
|
const sendMigrate = (body, status = 200) => {
|
|
2207
2361
|
if (migrateResponded)
|
|
@@ -2214,6 +2368,14 @@ async function cmdLogin(flags) {
|
|
|
2214
2368
|
let progressDone = 0;
|
|
2215
2369
|
let progressFailed = 0;
|
|
2216
2370
|
let progressExtracted = 0;
|
|
2371
|
+
let resolvePauseCompletion = null;
|
|
2372
|
+
pauseCompletion = new Promise((resolve) => {
|
|
2373
|
+
resolvePauseCompletion = resolve;
|
|
2374
|
+
});
|
|
2375
|
+
const completePause = () => {
|
|
2376
|
+
resolvePauseCompletion?.();
|
|
2377
|
+
resolvePauseCompletion = null;
|
|
2378
|
+
};
|
|
2217
2379
|
if (!disc) {
|
|
2218
2380
|
srv.setProgress({
|
|
2219
2381
|
status: "starting",
|
|
@@ -2273,7 +2435,11 @@ async function cmdLogin(flags) {
|
|
|
2273
2435
|
process.exitCode = 1;
|
|
2274
2436
|
return true;
|
|
2275
2437
|
}
|
|
2276
|
-
|
|
2438
|
+
const requestedKeys = conversationKeys ? new Set(conversationKeys) : null;
|
|
2439
|
+
const selectedPending = requestedKeys
|
|
2440
|
+
? exact.pending.filter((session) => requestedKeys.has(session.conversationKey))
|
|
2441
|
+
: exact.pending;
|
|
2442
|
+
activeJobCount = selectedPending.length;
|
|
2277
2443
|
const updateProgress = (patch) => {
|
|
2278
2444
|
srv.setProgress({
|
|
2279
2445
|
status: "running",
|
|
@@ -2289,7 +2455,7 @@ async function cmdLogin(flags) {
|
|
|
2289
2455
|
});
|
|
2290
2456
|
};
|
|
2291
2457
|
try {
|
|
2292
|
-
if (
|
|
2458
|
+
if (selectedPending.length === 0) {
|
|
2293
2459
|
srv.setProgress({
|
|
2294
2460
|
status: "completed",
|
|
2295
2461
|
total: 0,
|
|
@@ -2305,7 +2471,7 @@ async function cmdLogin(flags) {
|
|
|
2305
2471
|
console.log("Setup complete — no unprocessed local conversations to extract.");
|
|
2306
2472
|
return true;
|
|
2307
2473
|
}
|
|
2308
|
-
updateProgress({ status: "starting", running: 0, queued:
|
|
2474
|
+
updateProgress({ status: "starting", running: 0, queued: selectedPending.length, latest: "Creating import session." });
|
|
2309
2475
|
// Plan caps limit how many conversations one import session accepts (IMPORT_LIMIT_EXCEEDED →
|
|
2310
2476
|
// startMigration slices to the cap). Instead of making the user re-run setup per batch (3000
|
|
2311
2477
|
// sessions used to mean 3 clicks), loop batches automatically until everything pending is done.
|
|
@@ -2324,7 +2490,7 @@ async function cmdLogin(flags) {
|
|
|
2324
2490
|
...(latestRepo ? { latestRepo } : {}),
|
|
2325
2491
|
});
|
|
2326
2492
|
};
|
|
2327
|
-
let remaining =
|
|
2493
|
+
let remaining = selectedPending;
|
|
2328
2494
|
let stoppedReason;
|
|
2329
2495
|
let planLimitNote;
|
|
2330
2496
|
let batchIndex = 0;
|
|
@@ -2334,16 +2500,23 @@ async function cmdLogin(flags) {
|
|
|
2334
2500
|
let h;
|
|
2335
2501
|
try {
|
|
2336
2502
|
const controller = new AbortController();
|
|
2337
|
-
h = await withTimeout(startMigration({
|
|
2503
|
+
h = await withTimeout(startMigration({
|
|
2504
|
+
pending: remaining,
|
|
2505
|
+
signal: controller.signal,
|
|
2506
|
+
onProgress: onBatchProgress,
|
|
2507
|
+
shouldStop: () => pauseRequested,
|
|
2508
|
+
}), 30_000, "IMPORT_START_TIMEOUT", () => controller.abort());
|
|
2338
2509
|
}
|
|
2339
2510
|
catch (batchError) {
|
|
2340
2511
|
if (batchIndex === 1)
|
|
2341
2512
|
throw batchError; // first batch failing = the whole import failed
|
|
2342
2513
|
// A later batch could not start (e.g. plan headroom exhausted). Finish gracefully with a note.
|
|
2343
|
-
planLimitNote = `Imported ${progressDone}
|
|
2514
|
+
planLimitNote = `Imported ${progressDone}. Upgrade your plan to import the remaining ${remaining.length} conversations.`;
|
|
2344
2515
|
break;
|
|
2345
2516
|
}
|
|
2346
2517
|
activeSessionId = h.sessionId;
|
|
2518
|
+
activeMigrationCleanup = h.cancelQueued;
|
|
2519
|
+
activeMigrationCleanupPromise = null;
|
|
2347
2520
|
if (batchIndex === 1) {
|
|
2348
2521
|
updateProgress({ status: "running", sessionId: h.sessionId, jobCount: activeJobCount, total: activeJobCount, latest: "Import session created." });
|
|
2349
2522
|
sendMigrate({ sessionId: h.sessionId, jobCount: activeJobCount });
|
|
@@ -2353,17 +2526,42 @@ async function cmdLogin(flags) {
|
|
|
2353
2526
|
}
|
|
2354
2527
|
console.log(`Migration metrics (batch ${batchIndex}): ${h.metricsFile}`);
|
|
2355
2528
|
const r = await h.done;
|
|
2529
|
+
if (r.stoppedReason === "paused" || pauseRequested) {
|
|
2530
|
+
stoppedReason = "paused";
|
|
2531
|
+
break;
|
|
2532
|
+
}
|
|
2356
2533
|
if (r.stoppedReason) {
|
|
2357
2534
|
stoppedReason = r.stoppedReason;
|
|
2358
2535
|
break;
|
|
2359
2536
|
}
|
|
2360
|
-
//
|
|
2537
|
+
// A historical allowance is lifetime, not a per-batch cap. Stop cleanly
|
|
2538
|
+
// after the allowed slice instead of attempting another session.
|
|
2361
2539
|
if (h.capped && remaining.length > h.jobCount) {
|
|
2362
|
-
|
|
2363
|
-
|
|
2540
|
+
const left = remaining.length - h.jobCount;
|
|
2541
|
+
planLimitNote = `Import complete for this plan. Upgrade to import the remaining ${left} conversations.`;
|
|
2542
|
+
break;
|
|
2364
2543
|
}
|
|
2365
2544
|
break;
|
|
2366
2545
|
}
|
|
2546
|
+
if (stoppedReason === "paused") {
|
|
2547
|
+
srv.setProgress({
|
|
2548
|
+
status: "paused",
|
|
2549
|
+
sessionId: activeSessionId || undefined,
|
|
2550
|
+
jobCount: activeJobCount,
|
|
2551
|
+
total: activeJobCount,
|
|
2552
|
+
completed: progressDone,
|
|
2553
|
+
running: 0,
|
|
2554
|
+
queued: Math.max(0, activeJobCount - progressDone - progressFailed),
|
|
2555
|
+
failed: progressFailed,
|
|
2556
|
+
extracted: progressExtracted,
|
|
2557
|
+
latest: "Ended for now. Re-run setup to rebuild the remaining conversation list.",
|
|
2558
|
+
});
|
|
2559
|
+
console.log(`Import ended for now: ${progressDone} imported, ${progressExtracted} memories, ${progressFailed} failed.`);
|
|
2560
|
+
completePause();
|
|
2561
|
+
// /skip sends the browser acknowledgement and closes the localhost bridge after this
|
|
2562
|
+
// safe pause boundary. Do not close it here first or the page can claim success early.
|
|
2563
|
+
return true;
|
|
2564
|
+
}
|
|
2367
2565
|
srv.setProgress({
|
|
2368
2566
|
status: stoppedReason ? "failed" : "completed",
|
|
2369
2567
|
sessionId: activeSessionId || undefined,
|
|
@@ -2384,6 +2582,22 @@ async function cmdLogin(flags) {
|
|
|
2384
2582
|
srv.close();
|
|
2385
2583
|
}
|
|
2386
2584
|
catch (e) {
|
|
2585
|
+
if (pauseRequested) {
|
|
2586
|
+
srv.setProgress({
|
|
2587
|
+
status: "paused",
|
|
2588
|
+
sessionId: activeSessionId || undefined,
|
|
2589
|
+
jobCount: activeJobCount,
|
|
2590
|
+
total: activeJobCount,
|
|
2591
|
+
completed: progressDone,
|
|
2592
|
+
running: 0,
|
|
2593
|
+
queued: Math.max(0, activeJobCount - progressDone - progressFailed),
|
|
2594
|
+
failed: progressFailed,
|
|
2595
|
+
extracted: progressExtracted,
|
|
2596
|
+
latest: "Ended for now. Re-run setup to rebuild the remaining conversation list.",
|
|
2597
|
+
});
|
|
2598
|
+
completePause();
|
|
2599
|
+
return true;
|
|
2600
|
+
}
|
|
2387
2601
|
srv.setProgress({
|
|
2388
2602
|
status: "failed",
|
|
2389
2603
|
sessionId: activeSessionId || undefined,
|
|
@@ -2406,8 +2620,13 @@ async function cmdLogin(flags) {
|
|
|
2406
2620
|
sendMigrate({ error: "NO_PENDING_SESSIONS" }, 409);
|
|
2407
2621
|
else if (e?.code === "IMPORT_START_TIMEOUT")
|
|
2408
2622
|
sendMigrate({ error: "IMPORT_START_TIMEOUT" }, 504);
|
|
2409
|
-
else
|
|
2410
|
-
|
|
2623
|
+
else {
|
|
2624
|
+
const responseData = isObjectRecord(e?.response?.data) ? e.response.data : {};
|
|
2625
|
+
const responseCode = asString(responseData.error) || "IMPORT_START_FAILED";
|
|
2626
|
+
const responseMessage = asString(responseData.message) || String(e?.message || e);
|
|
2627
|
+
const responseStatus = typeof e?.response?.status === "number" ? e.response.status : 500;
|
|
2628
|
+
sendMigrate({ error: responseCode, message: responseMessage }, responseStatus);
|
|
2629
|
+
}
|
|
2411
2630
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2412
2631
|
srv.close();
|
|
2413
2632
|
process.exitCode = 1;
|
package/dist/v1-contract.js
CHANGED
|
@@ -108,7 +108,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
108
108
|
const currentTime = new Date().toISOString();
|
|
109
109
|
const map = opts.map?.trim();
|
|
110
110
|
const updateNotice = opts.updateNotice?.trim();
|
|
111
|
-
const
|
|
111
|
+
const recallPlanNote = "Available on every plan: Free includes 10 searches each week, Pro includes 100, and Power includes 250.";
|
|
112
112
|
const searchBillingReplyInstruction = "If search returns an ACTION REQUIRED subscription message, tell the user to start their trial or subscription and include the exact URL from that result verbatim. Do not respond only with \"connect\" or \"upgrade\".";
|
|
113
113
|
const updateSection = updateNotice ? `\n\nUPDATE NOTICE: ${updateNotice}` : "";
|
|
114
114
|
const mapSection = map
|
|
@@ -117,7 +117,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
117
117
|
return [
|
|
118
118
|
{
|
|
119
119
|
name: canonicalToolNames.search,
|
|
120
|
-
description: withMcpVersion(`Recall the user's prior decisions, preferences, and project context from EchoMem — their long-term memory across ALL their AI tools, not just this session. Use it instead of re-deriving or re-asking what the user already settled. ${
|
|
120
|
+
description: withMcpVersion(`Recall the user's prior decisions, preferences, and project context from EchoMem — their long-term memory across ALL their AI tools, not just this session. Use it instead of re-deriving or re-asking what the user already settled. ${recallPlanNote} ${searchBillingReplyInstruction}${mapSection}\nReturns the ranked memories; set includeAnswer=true only if you need the legacy synthesized answer. Current time: ${currentTime}.${updateSection}`),
|
|
121
121
|
inputSchema: {
|
|
122
122
|
type: "object",
|
|
123
123
|
properties: {
|
|
@@ -140,7 +140,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
142
|
name: "search_memories_by_description_semantic",
|
|
143
|
-
description: `Legacy alias for search_memories. ${
|
|
143
|
+
description: `Legacy alias for search_memories. ${recallPlanNote} ${searchBillingReplyInstruction}`,
|
|
144
144
|
inputSchema: {
|
|
145
145
|
type: "object",
|
|
146
146
|
properties: {
|
|
@@ -163,7 +163,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
163
163
|
},
|
|
164
164
|
{
|
|
165
165
|
name: canonicalToolNames.save,
|
|
166
|
-
description: "Save this conversation into EchoMem as long-term memory (durable memories are extracted automatically).
|
|
166
|
+
description: "Save this conversation into EchoMem as long-term memory (durable memories are extracted automatically). New extraction input uses the plan's weekly processing allowance; if the limit is reached, nothing is saved. passthrough=true stores the text verbatim as a session capsule.",
|
|
167
167
|
inputSchema: {
|
|
168
168
|
type: "object",
|
|
169
169
|
properties: {
|
|
@@ -196,7 +196,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
196
196
|
},
|
|
197
197
|
{
|
|
198
198
|
name: canonicalToolNames.timeRange,
|
|
199
|
-
description: `Retrieve memories within a specific date range. ${
|
|
199
|
+
description: `Retrieve memories within a specific date range. ${recallPlanNote} Current time: ${currentTime}.`,
|
|
200
200
|
inputSchema: {
|
|
201
201
|
type: "object",
|
|
202
202
|
properties: {
|
|
@@ -214,7 +214,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
214
214
|
},
|
|
215
215
|
{
|
|
216
216
|
name: canonicalToolNames.keywords,
|
|
217
|
-
description: `Search memories based on keywords in keys field. ${
|
|
217
|
+
description: `Search memories based on keywords in keys field. ${recallPlanNote}`,
|
|
218
218
|
inputSchema: {
|
|
219
219
|
type: "object",
|
|
220
220
|
properties: {
|
|
@@ -371,7 +371,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
name: canonicalToolNames.getByContext,
|
|
374
|
-
description: withMcpVersion(`Deterministically re-fetch the exact batch of memories saved under one contextId — no semantic search, no ranking, just that session's saved capsule. ${
|
|
374
|
+
description: withMcpVersion(`Deterministically re-fetch the exact batch of memories saved under one contextId — no semantic search, no ranking, just that session's saved capsule. ${recallPlanNote} save_conversation returns a contextId; pass it here to pull back precisely those memories, e.g. to warm up a fresh session with what a prior session saved, or to verify the saved facts are still present. Current time: ${currentTime}.`),
|
|
375
375
|
inputSchema: {
|
|
376
376
|
type: "object",
|
|
377
377
|
properties: {
|
|
@@ -388,7 +388,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
388
388
|
},
|
|
389
389
|
{
|
|
390
390
|
name: canonicalToolNames.checkpointByContext,
|
|
391
|
-
description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. ${
|
|
391
|
+
description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. ${recallPlanNote} Use this when the user or EchoMem HUD gives you a contextId for a renewed coding session and you need the session handoff, not a raw memory dump. It deterministically fetches that context and formats it as orientation state: decisions, carryover, constraints, and checkpoints. Current time: ${currentTime}.`),
|
|
392
392
|
inputSchema: {
|
|
393
393
|
type: "object",
|
|
394
394
|
properties: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.19",
|
|
4
4
|
"description": "EchoMem MCP bridge: cloud-first memory tools, local context HUD, and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|