@echomem/mcp 1.4.18 → 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/server.js +17 -13
- package/dist/hud/web.js +204 -13
- package/dist/index.js +54 -22
- package/dist/migrate.js +28 -4
- package/dist/setup-page/client-core.js +31 -1
- package/dist/setup-page/client-extraction.js +528 -156
- 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 +162 -25
- 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,6 +1112,7 @@ 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;
|
|
@@ -1129,6 +1158,12 @@ export function startCallbackServer(opts = {}) {
|
|
|
1129
1158
|
const handleCallback = (res, token, key, nonce) => {
|
|
1130
1159
|
if (!checkNonce(nonce))
|
|
1131
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
|
+
}
|
|
1132
1167
|
if (!token)
|
|
1133
1168
|
return void text(res, 400, "missing token");
|
|
1134
1169
|
console.log(`[${new Date().toISOString()}] Browser approval callback received.`);
|
|
@@ -1191,7 +1226,15 @@ export function startCallbackServer(opts = {}) {
|
|
|
1191
1226
|
if (route === "/config" && req.method === "GET") {
|
|
1192
1227
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1193
1228
|
return void text(res, 403, "bad nonce");
|
|
1194
|
-
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
|
+
});
|
|
1195
1238
|
return;
|
|
1196
1239
|
}
|
|
1197
1240
|
if (route === "/launch-agent" && req.method === "POST") {
|
|
@@ -1246,6 +1289,12 @@ export function startCallbackServer(opts = {}) {
|
|
|
1246
1289
|
if (route === "/stats" && req.method === "GET") {
|
|
1247
1290
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1248
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
|
+
}
|
|
1249
1298
|
const payload = opts.getStats ? opts.getStats() : stats;
|
|
1250
1299
|
if (payload == null)
|
|
1251
1300
|
return void res.writeHead(202).end();
|
|
@@ -1255,6 +1304,12 @@ export function startCallbackServer(opts = {}) {
|
|
|
1255
1304
|
if (route === "/billing-status" && req.method === "GET") {
|
|
1256
1305
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1257
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
|
+
}
|
|
1258
1313
|
const token = new KeyStore().getToken();
|
|
1259
1314
|
const pricingUrl = `${PRICING_URL}?source=mcp_onboarding`;
|
|
1260
1315
|
if (!token) {
|
|
@@ -1262,7 +1317,11 @@ export function startCallbackServer(opts = {}) {
|
|
|
1262
1317
|
return;
|
|
1263
1318
|
}
|
|
1264
1319
|
try {
|
|
1265
|
-
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
|
+
]);
|
|
1266
1325
|
const plan = (asString(response.data?.plan) || "free").toLowerCase();
|
|
1267
1326
|
const trialUsed = response.data?.billing?.trialUsed === true;
|
|
1268
1327
|
const trialAvailable = response.data?.billing?.trialAvailable !== false;
|
|
@@ -1271,6 +1330,14 @@ export function startCallbackServer(opts = {}) {
|
|
|
1271
1330
|
paid: ["pro", "power", "team", "enterprise"].includes(plan),
|
|
1272
1331
|
trialAvailable,
|
|
1273
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,
|
|
1274
1341
|
pricingUrl,
|
|
1275
1342
|
});
|
|
1276
1343
|
}
|
|
@@ -1284,6 +1351,12 @@ export function startCallbackServer(opts = {}) {
|
|
|
1284
1351
|
res.setHeader("Cache-Control", "no-store");
|
|
1285
1352
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1286
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
|
+
}
|
|
1287
1360
|
let payload;
|
|
1288
1361
|
try {
|
|
1289
1362
|
payload = opts.getReport ? opts.getReport() : null;
|
|
@@ -1379,6 +1452,12 @@ export function startCallbackServer(opts = {}) {
|
|
|
1379
1452
|
if (route === "/progress" && req.method === "GET") {
|
|
1380
1453
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
1381
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
|
+
}
|
|
1382
1461
|
json(res, 200, progress);
|
|
1383
1462
|
return;
|
|
1384
1463
|
}
|
|
@@ -1394,6 +1473,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1394
1473
|
if (!checkNonce(asString(body.nonce)))
|
|
1395
1474
|
return void text(res, 403, "bad nonce");
|
|
1396
1475
|
const allowed = body.allowed === true;
|
|
1476
|
+
reportConsentGranted = allowed;
|
|
1397
1477
|
opts.onReportConsent?.(allowed);
|
|
1398
1478
|
json(res, 200, { ok: true, allowed });
|
|
1399
1479
|
return;
|
|
@@ -1442,6 +1522,12 @@ export function startCallbackServer(opts = {}) {
|
|
|
1442
1522
|
}
|
|
1443
1523
|
if (!checkNonce(asString(body.nonce)))
|
|
1444
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
|
+
}
|
|
1445
1531
|
if (migrateStarted)
|
|
1446
1532
|
return void json(res, 409, { error: "MIGRATE_IN_PROGRESS" });
|
|
1447
1533
|
migrateStarted = true;
|
|
@@ -1449,7 +1535,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
1449
1535
|
respondMigrate(res, { error: "IMPORT_START_TIMEOUT" }, 504);
|
|
1450
1536
|
}, 30_000);
|
|
1451
1537
|
safety.unref?.();
|
|
1452
|
-
migrateRequest.resolve({ res });
|
|
1538
|
+
migrateRequest.resolve({ res, conversationKeys: asConversationKeys(body.conversationKeys) });
|
|
1453
1539
|
decision.resolve("migrate");
|
|
1454
1540
|
return;
|
|
1455
1541
|
}
|
|
@@ -1863,6 +1949,7 @@ async function cmdLogin(flags) {
|
|
|
1863
1949
|
const srv = await startCallbackServer({
|
|
1864
1950
|
port: devPortRaw,
|
|
1865
1951
|
nonce,
|
|
1952
|
+
requireReportConsent: true,
|
|
1866
1953
|
getStats: () => stats,
|
|
1867
1954
|
getReport: () => forensicReport,
|
|
1868
1955
|
getReportProgress: () => forensicProgress,
|
|
@@ -1886,6 +1973,19 @@ async function cmdLogin(flags) {
|
|
|
1886
1973
|
return;
|
|
1887
1974
|
}
|
|
1888
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
|
+
};
|
|
1889
1989
|
startForensicScan();
|
|
1890
1990
|
},
|
|
1891
1991
|
});
|
|
@@ -2125,13 +2225,13 @@ async function cmdLogin(flags) {
|
|
|
2125
2225
|
migratable = migratableFromDiscovery(initialExact);
|
|
2126
2226
|
latestPendingEstimate = migratable.pending;
|
|
2127
2227
|
sessionSummary = sessionsFromDiscovery(initialExact);
|
|
2128
|
-
const partialPayload = await buildStatsPayload([], {
|
|
2228
|
+
const partialPayload = withCandidateSessions(await buildStatsPayload([], {
|
|
2129
2229
|
partial: true,
|
|
2130
2230
|
skipMemoryCount: true,
|
|
2131
2231
|
sessions: sessionSummary,
|
|
2132
2232
|
migratable,
|
|
2133
2233
|
discovery: { phase: "exact", exact: true },
|
|
2134
|
-
});
|
|
2234
|
+
}), initialExact);
|
|
2135
2235
|
if (generation !== refreshGeneration)
|
|
2136
2236
|
return disc;
|
|
2137
2237
|
stats = partialPayload;
|
|
@@ -2175,13 +2275,13 @@ async function cmdLogin(flags) {
|
|
|
2175
2275
|
migratable = migratableFromDiscovery(reconciled);
|
|
2176
2276
|
latestPendingEstimate = migratable.pending;
|
|
2177
2277
|
sessionSummary = sessionsFromDiscovery(reconciled);
|
|
2178
|
-
const reconciledPayload = await buildStatsPayload([], {
|
|
2278
|
+
const reconciledPayload = withCandidateSessions(await buildStatsPayload([], {
|
|
2179
2279
|
partial: true,
|
|
2180
2280
|
skipMemoryCount: true,
|
|
2181
2281
|
sessions: sessionSummary,
|
|
2182
2282
|
migratable,
|
|
2183
2283
|
discovery: { phase: "exact", exact: true },
|
|
2184
|
-
});
|
|
2284
|
+
}), reconciled);
|
|
2185
2285
|
if (generation !== refreshGeneration)
|
|
2186
2286
|
return;
|
|
2187
2287
|
stats = reconciledPayload;
|
|
@@ -2195,11 +2295,11 @@ async function cmdLogin(flags) {
|
|
|
2195
2295
|
failed: 0,
|
|
2196
2296
|
extracted: 0,
|
|
2197
2297
|
});
|
|
2198
|
-
const fullPayload = await buildStatsPayload(collect(), {
|
|
2298
|
+
const fullPayload = withCandidateSessions(await buildStatsPayload(collect(), {
|
|
2199
2299
|
sessions: sessionSummary,
|
|
2200
2300
|
migratable,
|
|
2201
2301
|
discovery: { phase: "full", exact: true },
|
|
2202
|
-
});
|
|
2302
|
+
}), reconciled);
|
|
2203
2303
|
if (generation !== refreshGeneration)
|
|
2204
2304
|
return;
|
|
2205
2305
|
stats = fullPayload;
|
|
@@ -2220,18 +2320,42 @@ async function cmdLogin(flags) {
|
|
|
2220
2320
|
await refreshLocalStatsForToken(nextToken);
|
|
2221
2321
|
});
|
|
2222
2322
|
await refreshLocalStatsForToken(token);
|
|
2223
|
-
//
|
|
2224
|
-
// import ledger; unfinished conversations are
|
|
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.
|
|
2225
2325
|
let pauseRequested = false;
|
|
2226
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
|
+
};
|
|
2227
2344
|
srv.setMigrationPauseHandler(async () => {
|
|
2228
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();
|
|
2229
2349
|
if (pauseCompletion)
|
|
2230
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();
|
|
2231
2355
|
});
|
|
2232
2356
|
const choice = await srv.decision;
|
|
2233
2357
|
if (choice === "migrate") {
|
|
2234
|
-
const { res } = await srv.migrateRequest;
|
|
2358
|
+
const { res, conversationKeys } = await srv.migrateRequest;
|
|
2235
2359
|
let migrateResponded = false;
|
|
2236
2360
|
const sendMigrate = (body, status = 200) => {
|
|
2237
2361
|
if (migrateResponded)
|
|
@@ -2311,7 +2435,11 @@ async function cmdLogin(flags) {
|
|
|
2311
2435
|
process.exitCode = 1;
|
|
2312
2436
|
return true;
|
|
2313
2437
|
}
|
|
2314
|
-
|
|
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;
|
|
2315
2443
|
const updateProgress = (patch) => {
|
|
2316
2444
|
srv.setProgress({
|
|
2317
2445
|
status: "running",
|
|
@@ -2327,7 +2455,7 @@ async function cmdLogin(flags) {
|
|
|
2327
2455
|
});
|
|
2328
2456
|
};
|
|
2329
2457
|
try {
|
|
2330
|
-
if (
|
|
2458
|
+
if (selectedPending.length === 0) {
|
|
2331
2459
|
srv.setProgress({
|
|
2332
2460
|
status: "completed",
|
|
2333
2461
|
total: 0,
|
|
@@ -2343,7 +2471,7 @@ async function cmdLogin(flags) {
|
|
|
2343
2471
|
console.log("Setup complete — no unprocessed local conversations to extract.");
|
|
2344
2472
|
return true;
|
|
2345
2473
|
}
|
|
2346
|
-
updateProgress({ status: "starting", running: 0, queued:
|
|
2474
|
+
updateProgress({ status: "starting", running: 0, queued: selectedPending.length, latest: "Creating import session." });
|
|
2347
2475
|
// Plan caps limit how many conversations one import session accepts (IMPORT_LIMIT_EXCEEDED →
|
|
2348
2476
|
// startMigration slices to the cap). Instead of making the user re-run setup per batch (3000
|
|
2349
2477
|
// sessions used to mean 3 clicks), loop batches automatically until everything pending is done.
|
|
@@ -2362,7 +2490,7 @@ async function cmdLogin(flags) {
|
|
|
2362
2490
|
...(latestRepo ? { latestRepo } : {}),
|
|
2363
2491
|
});
|
|
2364
2492
|
};
|
|
2365
|
-
let remaining =
|
|
2493
|
+
let remaining = selectedPending;
|
|
2366
2494
|
let stoppedReason;
|
|
2367
2495
|
let planLimitNote;
|
|
2368
2496
|
let batchIndex = 0;
|
|
@@ -2383,10 +2511,12 @@ async function cmdLogin(flags) {
|
|
|
2383
2511
|
if (batchIndex === 1)
|
|
2384
2512
|
throw batchError; // first batch failing = the whole import failed
|
|
2385
2513
|
// A later batch could not start (e.g. plan headroom exhausted). Finish gracefully with a note.
|
|
2386
|
-
planLimitNote = `Imported ${progressDone}
|
|
2514
|
+
planLimitNote = `Imported ${progressDone}. Upgrade your plan to import the remaining ${remaining.length} conversations.`;
|
|
2387
2515
|
break;
|
|
2388
2516
|
}
|
|
2389
2517
|
activeSessionId = h.sessionId;
|
|
2518
|
+
activeMigrationCleanup = h.cancelQueued;
|
|
2519
|
+
activeMigrationCleanupPromise = null;
|
|
2390
2520
|
if (batchIndex === 1) {
|
|
2391
2521
|
updateProgress({ status: "running", sessionId: h.sessionId, jobCount: activeJobCount, total: activeJobCount, latest: "Import session created." });
|
|
2392
2522
|
sendMigrate({ sessionId: h.sessionId, jobCount: activeJobCount });
|
|
@@ -2404,10 +2534,12 @@ async function cmdLogin(flags) {
|
|
|
2404
2534
|
stoppedReason = r.stoppedReason;
|
|
2405
2535
|
break;
|
|
2406
2536
|
}
|
|
2407
|
-
//
|
|
2537
|
+
// A historical allowance is lifetime, not a per-batch cap. Stop cleanly
|
|
2538
|
+
// after the allowed slice instead of attempting another session.
|
|
2408
2539
|
if (h.capped && remaining.length > h.jobCount) {
|
|
2409
|
-
|
|
2410
|
-
|
|
2540
|
+
const left = remaining.length - h.jobCount;
|
|
2541
|
+
planLimitNote = `Import complete for this plan. Upgrade to import the remaining ${left} conversations.`;
|
|
2542
|
+
break;
|
|
2411
2543
|
}
|
|
2412
2544
|
break;
|
|
2413
2545
|
}
|
|
@@ -2422,9 +2554,9 @@ async function cmdLogin(flags) {
|
|
|
2422
2554
|
queued: Math.max(0, activeJobCount - progressDone - progressFailed),
|
|
2423
2555
|
failed: progressFailed,
|
|
2424
2556
|
extracted: progressExtracted,
|
|
2425
|
-
latest: "
|
|
2557
|
+
latest: "Ended for now. Re-run setup to rebuild the remaining conversation list.",
|
|
2426
2558
|
});
|
|
2427
|
-
console.log(`Import
|
|
2559
|
+
console.log(`Import ended for now: ${progressDone} imported, ${progressExtracted} memories, ${progressFailed} failed.`);
|
|
2428
2560
|
completePause();
|
|
2429
2561
|
// /skip sends the browser acknowledgement and closes the localhost bridge after this
|
|
2430
2562
|
// safe pause boundary. Do not close it here first or the page can claim success early.
|
|
@@ -2461,7 +2593,7 @@ async function cmdLogin(flags) {
|
|
|
2461
2593
|
queued: Math.max(0, activeJobCount - progressDone - progressFailed),
|
|
2462
2594
|
failed: progressFailed,
|
|
2463
2595
|
extracted: progressExtracted,
|
|
2464
|
-
latest: "
|
|
2596
|
+
latest: "Ended for now. Re-run setup to rebuild the remaining conversation list.",
|
|
2465
2597
|
});
|
|
2466
2598
|
completePause();
|
|
2467
2599
|
return true;
|
|
@@ -2488,8 +2620,13 @@ async function cmdLogin(flags) {
|
|
|
2488
2620
|
sendMigrate({ error: "NO_PENDING_SESSIONS" }, 409);
|
|
2489
2621
|
else if (e?.code === "IMPORT_START_TIMEOUT")
|
|
2490
2622
|
sendMigrate({ error: "IMPORT_START_TIMEOUT" }, 504);
|
|
2491
|
-
else
|
|
2492
|
-
|
|
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
|
+
}
|
|
2493
2630
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2494
2631
|
srv.close();
|
|
2495
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",
|