@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/index.js CHANGED
@@ -126,7 +126,7 @@ 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:",
129
+ "ACTION REQUIRED: tell the user to start a 14-day Pro trial or choose a subscription at this exact URL:",
130
130
  pricingUrl,
131
131
  "When replying, include the exact URL above. Do not reply only with \"connect\" or \"upgrade\".",
132
132
  "",
@@ -545,10 +545,16 @@ class EchoMemApiClient {
545
545
  async fetchMemoryMap() {
546
546
  try {
547
547
  const enc = await this.encState(); // unencrypted → {enabled:false}; encrypted+locked → throws
548
- const today = new Date().toISOString().slice(0, 10);
549
- const response = await this.axios.post("/api/extension/memories/time-range", { startDate: "2000-01-01", endDate: today, limit: 200 }, { timeout: 6000 });
548
+ // Tool-list decoration is not a user-initiated search and must not spend
549
+ // one of the account's weekly searches. Build the best-effort map from
550
+ // the ordinary paginated memory listing instead.
551
+ const response = await this.axios.get("/api/extension/memories?limit=200", { timeout: 6000 });
550
552
  const data = enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
551
- const memories = Array.isArray(data?.memories) ? data.memories : [];
553
+ const memories = Array.isArray(data?.data)
554
+ ? data.data
555
+ : Array.isArray(data?.memories)
556
+ ? data.memories
557
+ : [];
552
558
  const keys = [...new Set(memories.map((m) => String(m?.keys || "").trim()).filter(Boolean))];
553
559
  if (!keys.length)
554
560
  return undefined;
@@ -789,6 +795,7 @@ class EchoMemApiClient {
789
795
  startDate,
790
796
  endDate,
791
797
  limit,
798
+ requestId: randomUUID(),
792
799
  });
793
800
  return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
794
801
  }
@@ -833,21 +840,41 @@ class EchoMemApiClient {
833
840
  // For an encrypted account, hand the server the key transiently in the X-Encryption-Key header
834
841
  // so it encrypts at rest (mirrors the extension's write path, spec §3.1a). Locked → LockedError.
835
842
  const enc = await this.encState();
836
- const config = enc.enabled && enc.key ? { headers: { "X-Encryption-Key": enc.key } } : undefined;
837
- const response = await this.axios.post("/api/extension/memories/ingest", {
838
- rawData,
839
- sourceUrl: parsed.url,
840
- source: parsed.source || "mcp_server",
841
- title: parsed.title,
842
- // Stable per-session id so multiple saves in this coding session group under one context.
843
- conversationKey: this.sessionId,
844
- passthrough: parsed.passthrough || false,
845
- triggerMessage: parsed.triggerMessage ||
846
- lastUserMessageFromMessages(parsed.messages) ||
847
- lastUserMessageFromConversationText(parsed.conversation),
848
- triggerMessageRole: parsed.triggerMessageRole || "user",
849
- }, config);
850
- return response.data;
843
+ const config = {
844
+ headers: {
845
+ "X-EchoMem-Request-Id": randomUUID(),
846
+ ...(enc.enabled && enc.key ? { "X-Encryption-Key": enc.key } : {}),
847
+ },
848
+ };
849
+ try {
850
+ const response = await this.axios.post("/api/extension/memories/ingest", {
851
+ rawData,
852
+ sourceUrl: parsed.url,
853
+ source: parsed.source || "mcp_server",
854
+ title: parsed.title,
855
+ // Stable per-session id so multiple saves in this coding session group under one context.
856
+ conversationKey: this.sessionId,
857
+ passthrough: parsed.passthrough || false,
858
+ triggerMessage: parsed.triggerMessage ||
859
+ lastUserMessageFromMessages(parsed.messages) ||
860
+ lastUserMessageFromConversationText(parsed.conversation),
861
+ triggerMessageRole: parsed.triggerMessageRole || "user",
862
+ }, config);
863
+ return response.data;
864
+ }
865
+ catch (error) {
866
+ if (axios.isAxiosError(error) && error.response?.status === 429) {
867
+ const data = error.response.data;
868
+ if (data?.error === "MEMORY_PROCESSING_QUOTA_EXCEEDED") {
869
+ const message = typeof data.message === "string"
870
+ ? data.message
871
+ : "Weekly memory-processing limit reached. This conversation was not saved.";
872
+ const upgrade = typeof data.upgradeUrl === "string" ? ` Upgrade: ${data.upgradeUrl}` : "";
873
+ throw new McpError(ErrorCode.InvalidRequest, `${message}${upgrade}`);
874
+ }
875
+ }
876
+ throw error;
877
+ }
851
878
  }
852
879
  async deleteMemory(args) {
853
880
  const parsed = deleteMemorySchema.parse(args ?? {});
@@ -900,6 +927,7 @@ class EchoMemApiClient {
900
927
  startDate: parsed.startDate,
901
928
  endDate: parsed.endDate,
902
929
  limit: parsed.limit,
930
+ requestId: randomUUID(),
903
931
  });
904
932
  return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
905
933
  }
@@ -918,6 +946,7 @@ class EchoMemApiClient {
918
946
  const response = await this.axios.post("/api/extension/memories/keywords", {
919
947
  keywords: parsed.keywords,
920
948
  limit: parsed.limit,
949
+ requestId: randomUUID(),
921
950
  });
922
951
  return enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
923
952
  }
@@ -1362,7 +1391,7 @@ Details: ${m.details || "N/A"}`)
1362
1391
  rec.conversation_chars = text.length;
1363
1392
  rec.save_source = typeof a?.source === "string" ? a.source : sourceFallback;
1364
1393
  }
1365
- const { success, memoriesExtracted, extractedMemories, contextId, capsuleId, passthrough: isPassthrough, error } = await this.client.saveConversation(enrichedArgs);
1394
+ const { success, memoriesExtracted, memoriesDiscarded, extractedMemories, contextId, capsuleId, passthrough: isPassthrough, error } = await this.client.saveConversation(enrichedArgs);
1366
1395
  if (!success)
1367
1396
  throw new Error(`EchoMem API Error: ${error}`);
1368
1397
  if (rec)
@@ -1374,7 +1403,7 @@ Details: ${m.details || "N/A"}`)
1374
1403
  typeof capsuleId === "string" && capsuleId ? `Capsule ID: ${capsuleId}` : "",
1375
1404
  typeof contextId === "string" && contextId ? `Context: ${contextId}` : "",
1376
1405
  "",
1377
- `To reload this capsule in a fresh session with Pro/Power recall: get_memories_by_context({ contextId: "${contextId}" })`,
1406
+ `To reload this capsule in a fresh session: get_memories_by_context({ contextId: "${contextId}" })`,
1378
1407
  ].filter(Boolean).join("\n");
1379
1408
  return { content: [{ type: "text", text }] };
1380
1409
  }
@@ -1396,9 +1425,12 @@ Details: ${m.details || "N/A"}`)
1396
1425
  .join("\n\n");
1397
1426
  const text = [
1398
1427
  `Successfully ingested conversation. Extracted ${memoriesExtracted} memory distinct events.`,
1428
+ typeof memoriesDiscarded === "number" && memoriesDiscarded > 0
1429
+ ? `${memoriesDiscarded} additional memories were not stored because your active-memory limit was reached.`
1430
+ : "",
1399
1431
  list,
1400
1432
  saved.length
1401
- ? `Verify these captured the key facts. With Pro/Power recall, re-fetch this exact batch later by searching the ids above${typeof contextId === "string" && contextId ? ` (context ${contextId})` : ""}.`
1433
+ ? `Verify these captured the key facts. Re-fetch this exact batch later by searching the ids above${typeof contextId === "string" && contextId ? ` (context ${contextId})` : ""}.`
1402
1434
  : "",
1403
1435
  ].filter(Boolean).join("\n\n");
1404
1436
  return { content: [{ type: "text", text }] };
package/dist/migrate.js CHANGED
@@ -499,6 +499,11 @@ function parsePositiveIntFlag(value, name) {
499
499
  throw new Error(`${name} must be a positive integer.`);
500
500
  return n;
501
501
  }
502
+ export async function cancelQueuedImportSession(client, sessionId) {
503
+ if (!sessionId)
504
+ throw new Error("IMPORT_SESSION_ID_REQUIRED");
505
+ await client.patch(`/api/extension/import-sessions/${encodeURIComponent(sessionId)}`, { action: "cancel" });
506
+ }
502
507
  function isRecord(value) {
503
508
  return typeof value === "object" && value !== null && !Array.isArray(value);
504
509
  }
@@ -984,29 +989,51 @@ export async function startMigration(opts) {
984
989
  userTz: tz,
985
990
  encKey,
986
991
  onProgress: opts.onProgress,
992
+ shouldStop: opts.shouldStop,
987
993
  runId,
988
994
  metricsFile,
989
995
  selection: opts.selection,
990
996
  });
991
- return { sessionId: session.id, runId, jobCount: session.jobs.length, metricsFile, ...(capped ? { capped } : {}), done };
997
+ const cancelQueued = async () => {
998
+ await cancelQueuedImportSession(client, session.id);
999
+ };
1000
+ return {
1001
+ sessionId: session.id,
1002
+ runId,
1003
+ jobCount: session.jobs.length,
1004
+ metricsFile,
1005
+ ...(capped ? { capped } : {}),
1006
+ done,
1007
+ cancelQueued,
1008
+ };
992
1009
  }
993
1010
  async function runJobs(args) {
994
1011
  const ledger = loadLedger();
995
1012
  const reqTimes = [];
1013
+ let migrated = 0, extracted = 0, failed = 0, done = 0;
1014
+ let stoppedReason;
1015
+ const stopIfRequested = () => {
1016
+ if (args.shouldStop?.()) {
1017
+ stoppedReason = "paused";
1018
+ return true;
1019
+ }
1020
+ return false;
1021
+ };
996
1022
  const throttle = async () => {
997
1023
  for (;;) {
1024
+ if (stopIfRequested())
1025
+ return false;
998
1026
  const now = Date.now();
999
1027
  while (reqTimes.length && now - reqTimes[0] > RATE_WINDOW_MS)
1000
1028
  reqTimes.shift();
1001
1029
  if (reqTimes.length < RATE_MAX) {
1002
1030
  reqTimes.push(Date.now());
1003
- return;
1031
+ return true;
1004
1032
  }
1005
- await sleep(RATE_WINDOW_MS - (now - reqTimes[0]) + 100);
1033
+ // Check a pause request promptly instead of making the user wait for the rate window.
1034
+ await sleep(Math.min(250, RATE_WINDOW_MS - (now - reqTimes[0]) + 100));
1006
1035
  }
1007
1036
  };
1008
- let migrated = 0, extracted = 0, failed = 0, done = 0;
1009
- let stoppedReason;
1010
1037
  const total = args.session.jobs.length;
1011
1038
  const recordMetric = (metric) => {
1012
1039
  try {
@@ -1021,8 +1048,9 @@ async function runJobs(args) {
1021
1048
  const runOne = async (job, s) => {
1022
1049
  const jobStartedAt = Date.now();
1023
1050
  try {
1024
- await throttle();
1025
- if (stoppedReason)
1051
+ if (!await throttle())
1052
+ return;
1053
+ if (stoppedReason || stopIfRequested())
1026
1054
  return; // an earlier job hit an unrecoverable stop — don't start more work
1027
1055
  const r = await runImportJob(args.client, job.id, s, args.userTz, args.encKey);
1028
1056
  extracted += r.memories;
@@ -1071,7 +1099,7 @@ async function runJobs(args) {
1071
1099
  let next = 0;
1072
1100
  const worker = async () => {
1073
1101
  for (;;) {
1074
- if (stoppedReason)
1102
+ if (stoppedReason || stopIfRequested())
1075
1103
  return;
1076
1104
  const myIdx = next++;
1077
1105
  if (myIdx >= total)
@@ -1084,7 +1112,8 @@ async function runJobs(args) {
1084
1112
  }
1085
1113
  };
1086
1114
  try {
1087
- await Promise.all(Array.from({ length: Math.min(MIGRATE_CONCURRENCY, Math.max(1, total)) }, () => worker()));
1115
+ const workerCount = Math.min(MIGRATE_CONCURRENCY, Math.max(1, args.session.concurrency), Math.max(1, total));
1116
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
1088
1117
  }
1089
1118
  catch {
1090
1119
  failed++;
@@ -1224,7 +1253,7 @@ export async function cmdMigrate(flags) {
1224
1253
  },
1225
1254
  });
1226
1255
  if (h.capped)
1227
- console.log(c.yellow(`Your plan imports up to ${h.capped} at a time — importing the newest ${h.capped}; re-run migrate for older sessions.`));
1256
+ console.log(c.yellow(`Your plan has ${h.capped} historical slots remaining — importing the newest ${h.capped}. Upgrade to import more.`));
1228
1257
  console.log(c.dim(`Import session ${h.sessionId} — ${h.jobCount} jobs queued (the web dashboard can watch this live).`));
1229
1258
  console.log(c.dim(`Metrics: ${h.metricsFile}`));
1230
1259
  const r = await h.done;
@@ -1271,7 +1300,14 @@ async function createImportSession(client, sessions, bareId, userTz, signal) {
1271
1300
  }));
1272
1301
  const res = await client.post("/api/extension/import-sessions", { items }, signal ? { signal } : undefined);
1273
1302
  const data = res.data || {};
1274
- return { id: String(data.session?.id || ""), jobs: Array.isArray(data.jobs) ? data.jobs : [] };
1303
+ const serverConcurrency = Number(data.session?.concurrency);
1304
+ return {
1305
+ id: String(data.session?.id || ""),
1306
+ concurrency: Number.isFinite(serverConcurrency) && serverConcurrency > 0
1307
+ ? Math.min(MIGRATE_CONCURRENCY, Math.floor(serverConcurrency))
1308
+ : MIGRATE_CONCURRENCY,
1309
+ jobs: Array.isArray(data.jobs) ? data.jobs : [],
1310
+ };
1275
1311
  }
1276
1312
  /** Run one queued job: stream its transcript to the server, which extracts it. Retries transient locks/rate limits. */
1277
1313
  async function runImportJob(client, jobId, s, userTz, encKey) {
@@ -13,16 +13,27 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
13
13
  var statsSlow = false;
14
14
  var decisionMade = false;
15
15
  var connected = false;
16
+ var localHistoryConsentGranted = false;
16
17
  var extractMounted = false; // WebGL extraction-plate iframe mounted once; persists across /progress polls
18
+ var lastExtractionProgress = null; // restores the live extraction view if ending fails
19
+ var extractionRecoveryMessage = ""; // persists pause failures across resumed progress updates
17
20
  var exStartAt = 0; // extraction start (ms) — drives the countdown
18
21
  var exDeadline = 0; // first honest ETA deadline; passing it while running → "taking longer" banner
19
22
  var extractionEnded = false; // user chose to leave the extraction flow before natural completion
23
+ var progressPollRunId = 0; // invalidates older progress loops when extraction resumes
20
24
  var dashMounted = false; // dashboard shell (incl. plate iframe) mounted once; persists across /stats polls
21
25
  var reportMounted = false; // report shell (city iframe) mounted once; loading is an overlay on it, not a separate page
22
26
  var authUrl = "";
27
+ var switchAccountUrl = "";
23
28
  var workspacePath = "";
24
29
  var billingStatus = null;
25
- var recallGateSkipped = false;
30
+ var billingStatusLoading = false;
31
+ var setupPlanChoice = "";
32
+ var selectedSessionKeys = Object.create(null);
33
+ var sessionSelectionTouched = false;
34
+ var sessionPickerQuery = "";
35
+ var sessionPickerSource = "all";
36
+ var billingPollTimer = null;
26
37
  var authWindow = null;
27
38
  var connectionPollStarted = false;
28
39
  var statsPollStarted = false;
@@ -34,6 +45,20 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
34
45
  return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch];
35
46
  });
36
47
  }
48
+ function setupIcon(name) {
49
+ var paths = {
50
+ "shield-check": '<path d="M12 3 5 6v5c0 4.6 2.9 8.1 7 10 4.1-1.9 7-5.4 7-10V6l-7-3Z"/><path d="m8.8 12.1 2 2 4.4-4.5"/>',
51
+ "folder-read": '<path d="M3 7.5h6l2-2h4.5A2.5 2.5 0 0 1 18 8v1"/><path d="M3 7.5V18a2 2 0 0 0 2 2h7"/><circle cx="17" cy="15" r="3.5"/><path d="m19.5 17.5 2 2"/>',
52
+ "report": '<path d="M5 20V10"/><path d="M12 20V4"/><path d="M19 20v-7"/><path d="M3 20h18"/>',
53
+ "cloud-off": '<path d="m3 3 18 18"/><path d="M6.7 6.7A5.5 5.5 0 0 0 6.5 17H17"/><path d="M10.7 5.2A7 7 0 0 1 19 12.1 4 4 0 0 1 19.5 19"/>',
54
+ "list-check": '<rect x="4" y="4" width="16" height="16" rx="3"/><path d="m7.5 9 1.4 1.4L11.5 8"/><path d="M13.5 9h3"/><path d="m7.5 14 1.4 1.4 2.6-2.4"/><path d="M13.5 14h3"/>',
55
+ "pause": '<path d="M9 7v10"/><path d="M15 7v10"/>',
56
+ "check": '<path d="m6.5 12.2 3.5 3.5 7.5-7.5"/>',
57
+ "copy": '<rect x="8" y="8" width="11" height="11" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/>',
58
+ "arrow-right": '<path d="M5 12h14"/><path d="m14 7 5 5-5 5"/>'
59
+ };
60
+ return '<svg class="setupGlyph" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><g stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' + (paths[name] || "") + '</g></svg>';
61
+ }
37
62
  function setHead(nextTitle, nextStatus) {
38
63
  title.textContent = nextTitle;
39
64
  status.textContent = nextStatus;
@@ -151,6 +176,11 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
151
176
  });
152
177
  }
153
178
  function onConnectClick() {
179
+ if (!localHistoryConsentGranted) {
180
+ renderLocalScanConsent();
181
+ setConsentStatus("Local history access is required before you can connect EchoMem or continue setup.", "required");
182
+ return;
183
+ }
154
184
  if (connected) { void waitForStats(); return; }
155
185
  if (!openAuthWindow(authUrl)) showPopupFallback("", authUrl);
156
186
  startConnectionPoll();