@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.
@@ -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, { connected, authUrl, switchAccountUrl: switchAccountUrl || authUrl, localOnly: true, workspacePath: process.cwd() });
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 response = await authedAxios(token).get("/api/extension/account/bootstrap", { timeout: 6000 });
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
- // A pause is deliberately transient. Completed conversations remain recorded in the normal
2224
- // import ledger; unfinished conversations are rediscovered and offered again by the next init.
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
- activeJobCount = exact.pending.length;
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 (exact.pending.length === 0) {
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: exact.pending.length, latest: "Creating import session." });
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 = exact.pending;
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} so far — the rest hit your plan's import limit. Re-run extraction later for the remaining ${remaining.length}.`;
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
- // startMigration sliced to the cap more remain: continue with the next batch automatically.
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
- remaining = remaining.slice(h.jobCount);
2410
- continue;
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: "Paused by user. Re-run setup to continue the remaining conversations.",
2557
+ latest: "Ended for now. Re-run setup to rebuild the remaining conversation list.",
2426
2558
  });
2427
- console.log(`Import paused: ${progressDone} imported, ${progressExtracted} memories, ${progressFailed} failed.`);
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: "Paused by user. Re-run setup to continue the remaining conversations.",
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
- sendMigrate({ error: "IMPORT_START_FAILED", message: String(e?.message || e) }, 500);
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;
@@ -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 paidRecallNote = "Requires Echo Pro or Power; saving conversations and memory extraction remain included.";
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. ${paidRecallNote} ${searchBillingReplyInstruction}${mapSection}\nReturns the ranked memories; set includeAnswer=true only if you need the legacy synthesized answer. Current time: ${currentTime}.${updateSection}`),
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. ${paidRecallNote} ${searchBillingReplyInstruction}`,
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). Saving is included. passthrough=true stores the text verbatim as a session capsule; reloading/searching saved memories requires Echo Pro or Power.",
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. ${paidRecallNote} Current time: ${currentTime}.`,
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. ${paidRecallNote}`,
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. ${paidRecallNote} 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}.`),
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. ${paidRecallNote} 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}.`),
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.18",
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",