@echomem/mcp 1.4.16 → 1.4.18

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/index.js CHANGED
@@ -126,10 +126,13 @@ function formatUpgradeRequiredResult(error) {
126
126
  pricingUrl,
127
127
  });
128
128
  return [
129
+ "ACTION REQUIRED: tell the user to start a 7-day Pro trial or choose a subscription at this exact URL:",
130
+ pricingUrl,
131
+ "When replying, include the exact URL above. Do not reply only with \"connect\" or \"upgrade\".",
132
+ "",
129
133
  message,
130
134
  "",
131
135
  code ? `Reason: ${code}.` : "",
132
- `Open pricing: ${pricingUrl}`,
133
136
  ].filter(Boolean).join("\n");
134
137
  }
135
138
  /** Map a thrown error to the telemetry error_kind taxonomy. */
package/dist/migrate.js CHANGED
@@ -984,6 +984,7 @@ export async function startMigration(opts) {
984
984
  userTz: tz,
985
985
  encKey,
986
986
  onProgress: opts.onProgress,
987
+ shouldStop: opts.shouldStop,
987
988
  runId,
988
989
  metricsFile,
989
990
  selection: opts.selection,
@@ -993,20 +994,30 @@ export async function startMigration(opts) {
993
994
  async function runJobs(args) {
994
995
  const ledger = loadLedger();
995
996
  const reqTimes = [];
997
+ let migrated = 0, extracted = 0, failed = 0, done = 0;
998
+ let stoppedReason;
999
+ const stopIfRequested = () => {
1000
+ if (args.shouldStop?.()) {
1001
+ stoppedReason = "paused";
1002
+ return true;
1003
+ }
1004
+ return false;
1005
+ };
996
1006
  const throttle = async () => {
997
1007
  for (;;) {
1008
+ if (stopIfRequested())
1009
+ return false;
998
1010
  const now = Date.now();
999
1011
  while (reqTimes.length && now - reqTimes[0] > RATE_WINDOW_MS)
1000
1012
  reqTimes.shift();
1001
1013
  if (reqTimes.length < RATE_MAX) {
1002
1014
  reqTimes.push(Date.now());
1003
- return;
1015
+ return true;
1004
1016
  }
1005
- await sleep(RATE_WINDOW_MS - (now - reqTimes[0]) + 100);
1017
+ // Check a pause request promptly instead of making the user wait for the rate window.
1018
+ await sleep(Math.min(250, RATE_WINDOW_MS - (now - reqTimes[0]) + 100));
1006
1019
  }
1007
1020
  };
1008
- let migrated = 0, extracted = 0, failed = 0, done = 0;
1009
- let stoppedReason;
1010
1021
  const total = args.session.jobs.length;
1011
1022
  const recordMetric = (metric) => {
1012
1023
  try {
@@ -1021,8 +1032,9 @@ async function runJobs(args) {
1021
1032
  const runOne = async (job, s) => {
1022
1033
  const jobStartedAt = Date.now();
1023
1034
  try {
1024
- await throttle();
1025
- if (stoppedReason)
1035
+ if (!await throttle())
1036
+ return;
1037
+ if (stoppedReason || stopIfRequested())
1026
1038
  return; // an earlier job hit an unrecoverable stop — don't start more work
1027
1039
  const r = await runImportJob(args.client, job.id, s, args.userTz, args.encKey);
1028
1040
  extracted += r.memories;
@@ -1071,7 +1083,7 @@ async function runJobs(args) {
1071
1083
  let next = 0;
1072
1084
  const worker = async () => {
1073
1085
  for (;;) {
1074
- if (stoppedReason)
1086
+ if (stoppedReason || stopIfRequested())
1075
1087
  return;
1076
1088
  const myIdx = next++;
1077
1089
  if (myIdx >= total)
@@ -610,11 +610,23 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
610
610
  renderEnding();
611
611
  try {
612
612
  await postJson("/skip", {});
613
- } catch (_) {
614
- /* The bridge may close while ending; the local page can still show the paused state. */
613
+ await sleep(250);
614
+ renderSkipped();
615
+ } catch (error) {
616
+ // A paused screen is a promise: do not show it until the bridge confirms that no more
617
+ // conversations will be scheduled. Keep the active view visible when pausing fails.
618
+ extractionEnded = false;
619
+ var message = error && error.message ? error.message : "Could not pause extraction. It is still running.";
620
+ try {
621
+ renderProgress(await getJson("/progress"));
622
+ var notice = document.getElementById("exText");
623
+ if (notice) notice.insertAdjacentHTML("afterbegin", '<div class="error">' + esc(message) + '</div>');
624
+ } catch (_) {
625
+ setHead("Could not pause extraction", "Still extracting");
626
+ app.className = "notice";
627
+ app.innerHTML = '<h2>Extraction may still be running.</h2><p>' + esc(message) + '</p><p>Keep this page open and try ending it again in a moment.</p>';
628
+ }
615
629
  }
616
- await sleep(650);
617
- renderSkipped();
618
630
  }
619
631
  async function skip() {
620
632
  if (decisionMade) return;
package/dist/setup.js CHANGED
@@ -1088,6 +1088,7 @@ export function startCallbackServer(opts = {}) {
1088
1088
  let migrateStarted = false;
1089
1089
  let tokenRefreshHandler = null;
1090
1090
  let logoutHandler = null;
1091
+ let migrationPauseHandler = null;
1091
1092
  let timer;
1092
1093
  let closed = false;
1093
1094
  let server;
@@ -1463,6 +1464,23 @@ export function startCallbackServer(opts = {}) {
1463
1464
  }
1464
1465
  if (!checkNonce(asString(body.nonce)))
1465
1466
  return void text(res, 403, "bad nonce");
1467
+ if (migrateStarted) {
1468
+ if (!migrationPauseHandler) {
1469
+ json(res, 409, { error: "MIGRATION_PAUSE_UNAVAILABLE", message: "Extraction is still starting. Try again in a moment." });
1470
+ return;
1471
+ }
1472
+ try {
1473
+ // Do not claim the work is paused until the import loop has stopped scheduling jobs.
1474
+ await migrationPauseHandler();
1475
+ }
1476
+ catch (error) {
1477
+ json(res, 500, {
1478
+ error: "MIGRATION_PAUSE_FAILED",
1479
+ message: error instanceof Error ? error.message : "Could not pause extraction.",
1480
+ });
1481
+ return;
1482
+ }
1483
+ }
1466
1484
  json(res, 200, { ok: true });
1467
1485
  decision.resolve("skip");
1468
1486
  close();
@@ -1496,6 +1514,9 @@ export function startCallbackServer(opts = {}) {
1496
1514
  setLogoutHandler: (handler) => {
1497
1515
  logoutHandler = handler;
1498
1516
  },
1517
+ setMigrationPauseHandler: (handler) => {
1518
+ migrationPauseHandler = handler;
1519
+ },
1499
1520
  setAuthUrl: (url, nextSwitchAccountUrl) => {
1500
1521
  authUrl = url;
1501
1522
  switchAccountUrl = nextSwitchAccountUrl || url;
@@ -2199,6 +2220,15 @@ async function cmdLogin(flags) {
2199
2220
  await refreshLocalStatsForToken(nextToken);
2200
2221
  });
2201
2222
  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.
2225
+ let pauseRequested = false;
2226
+ let pauseCompletion = null;
2227
+ srv.setMigrationPauseHandler(async () => {
2228
+ pauseRequested = true;
2229
+ if (pauseCompletion)
2230
+ await pauseCompletion;
2231
+ });
2202
2232
  const choice = await srv.decision;
2203
2233
  if (choice === "migrate") {
2204
2234
  const { res } = await srv.migrateRequest;
@@ -2214,6 +2244,14 @@ async function cmdLogin(flags) {
2214
2244
  let progressDone = 0;
2215
2245
  let progressFailed = 0;
2216
2246
  let progressExtracted = 0;
2247
+ let resolvePauseCompletion = null;
2248
+ pauseCompletion = new Promise((resolve) => {
2249
+ resolvePauseCompletion = resolve;
2250
+ });
2251
+ const completePause = () => {
2252
+ resolvePauseCompletion?.();
2253
+ resolvePauseCompletion = null;
2254
+ };
2217
2255
  if (!disc) {
2218
2256
  srv.setProgress({
2219
2257
  status: "starting",
@@ -2334,7 +2372,12 @@ async function cmdLogin(flags) {
2334
2372
  let h;
2335
2373
  try {
2336
2374
  const controller = new AbortController();
2337
- h = await withTimeout(startMigration({ pending: remaining, signal: controller.signal, onProgress: onBatchProgress }), 30_000, "IMPORT_START_TIMEOUT", () => controller.abort());
2375
+ h = await withTimeout(startMigration({
2376
+ pending: remaining,
2377
+ signal: controller.signal,
2378
+ onProgress: onBatchProgress,
2379
+ shouldStop: () => pauseRequested,
2380
+ }), 30_000, "IMPORT_START_TIMEOUT", () => controller.abort());
2338
2381
  }
2339
2382
  catch (batchError) {
2340
2383
  if (batchIndex === 1)
@@ -2353,6 +2396,10 @@ async function cmdLogin(flags) {
2353
2396
  }
2354
2397
  console.log(`Migration metrics (batch ${batchIndex}): ${h.metricsFile}`);
2355
2398
  const r = await h.done;
2399
+ if (r.stoppedReason === "paused" || pauseRequested) {
2400
+ stoppedReason = "paused";
2401
+ break;
2402
+ }
2356
2403
  if (r.stoppedReason) {
2357
2404
  stoppedReason = r.stoppedReason;
2358
2405
  break;
@@ -2364,6 +2411,25 @@ async function cmdLogin(flags) {
2364
2411
  }
2365
2412
  break;
2366
2413
  }
2414
+ if (stoppedReason === "paused") {
2415
+ srv.setProgress({
2416
+ status: "paused",
2417
+ sessionId: activeSessionId || undefined,
2418
+ jobCount: activeJobCount,
2419
+ total: activeJobCount,
2420
+ completed: progressDone,
2421
+ running: 0,
2422
+ queued: Math.max(0, activeJobCount - progressDone - progressFailed),
2423
+ failed: progressFailed,
2424
+ extracted: progressExtracted,
2425
+ latest: "Paused by user. Re-run setup to continue the remaining conversations.",
2426
+ });
2427
+ console.log(`Import paused: ${progressDone} imported, ${progressExtracted} memories, ${progressFailed} failed.`);
2428
+ completePause();
2429
+ // /skip sends the browser acknowledgement and closes the localhost bridge after this
2430
+ // safe pause boundary. Do not close it here first or the page can claim success early.
2431
+ return true;
2432
+ }
2367
2433
  srv.setProgress({
2368
2434
  status: stoppedReason ? "failed" : "completed",
2369
2435
  sessionId: activeSessionId || undefined,
@@ -2384,6 +2450,22 @@ async function cmdLogin(flags) {
2384
2450
  srv.close();
2385
2451
  }
2386
2452
  catch (e) {
2453
+ if (pauseRequested) {
2454
+ srv.setProgress({
2455
+ status: "paused",
2456
+ sessionId: activeSessionId || undefined,
2457
+ jobCount: activeJobCount,
2458
+ total: activeJobCount,
2459
+ completed: progressDone,
2460
+ running: 0,
2461
+ queued: Math.max(0, activeJobCount - progressDone - progressFailed),
2462
+ failed: progressFailed,
2463
+ extracted: progressExtracted,
2464
+ latest: "Paused by user. Re-run setup to continue the remaining conversations.",
2465
+ });
2466
+ completePause();
2467
+ return true;
2468
+ }
2387
2469
  srv.setProgress({
2388
2470
  status: "failed",
2389
2471
  sessionId: activeSessionId || undefined,
@@ -109,6 +109,7 @@ export function listToolSpecs(opts = {}) {
109
109
  const map = opts.map?.trim();
110
110
  const updateNotice = opts.updateNotice?.trim();
111
111
  const paidRecallNote = "Requires Echo Pro or Power; saving conversations and memory extraction remain included.";
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\".";
112
113
  const updateSection = updateNotice ? `\n\nUPDATE NOTICE: ${updateNotice}` : "";
113
114
  const mapSection = map
114
115
  ? `\n\nThis user's EchoMem currently covers these topics (a relevance guide — recall when the task relates to one of them):\n${map}\n`
@@ -116,7 +117,7 @@ export function listToolSpecs(opts = {}) {
116
117
  return [
117
118
  {
118
119
  name: canonicalToolNames.search,
119
- 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}${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. ${paidRecallNote} ${searchBillingReplyInstruction}${mapSection}\nReturns the ranked memories; set includeAnswer=true only if you need the legacy synthesized answer. Current time: ${currentTime}.${updateSection}`),
120
121
  inputSchema: {
121
122
  type: "object",
122
123
  properties: {
@@ -139,7 +140,7 @@ export function listToolSpecs(opts = {}) {
139
140
  },
140
141
  {
141
142
  name: "search_memories_by_description_semantic",
142
- description: `Legacy alias for search_memories. ${paidRecallNote}`,
143
+ description: `Legacy alias for search_memories. ${paidRecallNote} ${searchBillingReplyInstruction}`,
143
144
  inputSchema: {
144
145
  type: "object",
145
146
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.16",
3
+ "version": "1.4.18",
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",