@echomem/mcp 1.3.2 → 1.4.0

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/setup.js CHANGED
@@ -20,13 +20,15 @@ import fs from "node:fs";
20
20
  import os from "node:os";
21
21
  import path from "node:path";
22
22
  import readline from "node:readline";
23
+ import { fileURLToPath } from "node:url";
23
24
  import axios from "axios";
24
25
  import { KeyStore } from "./keystore.js";
25
26
  import { fetchEncryptionConfig, deriveAndVerifyKey, verifyKeyB64 } from "./encryption.js";
26
27
  import { collect, runReport, buildStatsPayload } from "./report.js";
27
- import { cmdMigrate, applyAccountImportStatus, applyFastAccountImportStatus, discoverMigratableFastDiscovery, discoverMigratableSessions, estimateMigrationEta, fetchProcessedImportKeys, isImportStatusUnsupported, markAccountImportStatusFailed, markAccountImportStatusUnavailable, markFastAccountImportStatusUnavailable, startMigration, summarizeFastMigratableDiscovery, MIGRATE_CONCURRENCY, } from "./migrate.js";
28
+ import { cmdMigrate, applyAccountImportStatus, applyFastAccountImportStatus, discoverMigratableFastDiscovery, discoverMigratableSessions, discoverPendingSessionsTargeted, estimateMigrationEta, fetchProcessedImportKeys, isImportStatusUnsupported, markAccountImportStatusFailed, markAccountImportStatusUnavailable, markFastAccountImportStatusUnavailable, startMigration, summarizeFastMigratableDiscovery, MIGRATE_CONCURRENCY, } from "./migrate.js";
28
29
  import { syncCodexUsage } from "./codex-sync.js";
29
30
  import { renderSetupPage } from "./setup-page.js";
31
+ import { repoLabel } from "./forensics.js";
30
32
  // The hosted connect-device page is now only the account-auth/token courier. The dashboard itself is
31
33
  // served by this localhost bridge, where local logs and processed/unprocessed counts never leave the
32
34
  // device unless the user explicitly starts migration. Override the hosted auth origin with ECHO_WEB_URL.
@@ -262,6 +264,43 @@ function withTimeout(promise, ms, code, onTimeout) {
262
264
  function delay(ms) {
263
265
  return new Promise((resolve) => setTimeout(resolve, ms));
264
266
  }
267
+ const CITY_ASSET_TYPES = {
268
+ ".html": "text/html; charset=utf-8",
269
+ ".js": "text/javascript; charset=utf-8",
270
+ ".json": "application/json; charset=utf-8",
271
+ ".wasm": "application/wasm",
272
+ ".riv": "application/octet-stream",
273
+ ".png": "image/png",
274
+ ".svg": "image/svg+xml",
275
+ };
276
+ function repoCityArtifactsRoot() {
277
+ // Monorepo/dev: read the city assets live from repo-root /artifacts.
278
+ const repo = fileURLToPath(new URL("../../../artifacts/", import.meta.url));
279
+ if (fs.existsSync(repo))
280
+ return repo;
281
+ // Published install: fall back to the copy bundled into dist/city by prepack (bundle-city.mjs).
282
+ return fileURLToPath(new URL("./city/", import.meta.url));
283
+ }
284
+ function serveRepoCityAsset(reqPath, res) {
285
+ const root = repoCityArtifactsRoot();
286
+ const rel = reqPath === "/city" || reqPath === "/city/" ? "echo-ai-city-only.html" : decodeURIComponent(reqPath.slice("/city/".length));
287
+ const filePath = path.resolve(root, rel);
288
+ const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
289
+ if (!filePath.startsWith(rootWithSep)) {
290
+ res.writeHead(403).end("forbidden");
291
+ return true;
292
+ }
293
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
294
+ res.writeHead(404).end("not found");
295
+ return true;
296
+ }
297
+ res.writeHead(200, {
298
+ "Content-Type": CITY_ASSET_TYPES[path.extname(filePath)] || "application/octet-stream",
299
+ "Cache-Control": "no-store",
300
+ });
301
+ fs.createReadStream(filePath).pipe(res);
302
+ return true;
303
+ }
265
304
  function discoverMigratableSessionsOffThread() {
266
305
  const migrateUrl = new URL("./migrate.js", import.meta.url).href;
267
306
  const code = `
@@ -440,6 +479,10 @@ export function startCallbackServer(opts = {}) {
440
479
  const url = new URL(req.url || "/", "http://127.0.0.1");
441
480
  const route = url.pathname;
442
481
  const run = async () => {
482
+ if ((route === "/city" || route.startsWith("/city/")) && req.method === "GET") {
483
+ serveRepoCityAsset(route, res);
484
+ return;
485
+ }
443
486
  if (route === "/setup" && req.method === "GET") {
444
487
  res.writeHead(200, {
445
488
  "Content-Type": "text/html; charset=utf-8",
@@ -817,12 +860,15 @@ async function cmdLogin(flags) {
817
860
  let exactDiscovery = Promise.resolve(null);
818
861
  let refreshGeneration = 0;
819
862
  let latestPendingEstimate = 0;
863
+ // The account's already-imported keys, shared so the /migrate sizing can assemble ONLY pending sessions.
864
+ let lastProcessedImportKeys = null;
820
865
  const resetLocalLoginState = () => {
821
866
  refreshGeneration++;
822
867
  stats = null;
823
868
  disc = null;
824
869
  exactDiscovery = Promise.resolve(null);
825
870
  latestPendingEstimate = 0;
871
+ lastProcessedImportKeys = null;
826
872
  srv.setStats(null);
827
873
  srv.setProgress({
828
874
  status: "idle",
@@ -836,7 +882,7 @@ async function cmdLogin(flags) {
836
882
  };
837
883
  const refreshLocalStatsForToken = async (activeToken) => {
838
884
  const generation = ++refreshGeneration;
839
- let lastProcessedImportKeys = null;
885
+ lastProcessedImportKeys = null; // shared with /migrate so it can assemble only the pending sessions
840
886
  let importStatusUnavailable = false;
841
887
  const quickDiscovery = discoverMigratableFastDiscovery();
842
888
  const quick = summarizeFastMigratableDiscovery(quickDiscovery);
@@ -1103,8 +1149,19 @@ async function cmdLogin(flags) {
1103
1149
  }
1104
1150
  let exact = disc;
1105
1151
  if (!exact) {
1106
- // Off-thread sizing normally resolves in ~20s; cap the wait and fall back to an in-process pass so
1107
- // a stalled/contended worker can never leave extraction stuck at "finishing local job sizing".
1152
+ // The full account-reconciled scan (disc) is not ready yet. Do NOT read the whole local history to
1153
+ // size a few new jobs assemble ONLY the sessions the fast scan already flagged as pending (using
1154
+ // the account's processed keys when we have them). This is the fast path for "start extraction".
1155
+ try {
1156
+ exact = discoverPendingSessionsTargeted(lastProcessedImportKeys);
1157
+ }
1158
+ catch (e) {
1159
+ console.error(`Targeted local sizing failed: ${e instanceof Error ? e.message : String(e)}`);
1160
+ }
1161
+ }
1162
+ if (!exact) {
1163
+ // Last resort only: the off-thread full scan, then an in-process full scan. Capped so a stalled
1164
+ // worker can never leave extraction stuck at "finishing local job sizing".
1108
1165
  exact = await withTimeout(exactDiscovery, 40_000, "SIZING_TIMEOUT").catch(() => null);
1109
1166
  if (!exact) {
1110
1167
  srv.setProgress({ status: "starting", total: activeJobCount, completed: 0, running: 0, queued: activeJobCount, failed: 0, extracted: 0, latest: "Sizing your sessions…" });
@@ -1170,15 +1227,18 @@ async function cmdLogin(flags) {
1170
1227
  pending: exact.pending,
1171
1228
  signal: controller.signal,
1172
1229
  onProgress: (ev) => {
1230
+ let latestRepo;
1173
1231
  if (ev.error) {
1174
1232
  progressFailed += 1;
1175
1233
  }
1176
1234
  else {
1177
1235
  progressDone += 1;
1178
1236
  progressExtracted += ev.memories ?? 0;
1237
+ latestRepo = repoLabel(ev.session.cwd); // building this completed conversation belongs to
1179
1238
  }
1180
1239
  updateProgress({
1181
1240
  latest: `${ev.session.source} ${(ev.session.firstTs || "").slice(0, 10)}${ev.error ? ` failed: ${ev.error}` : ""}`,
1241
+ ...(latestRepo ? { latestRepo } : {}),
1182
1242
  });
1183
1243
  },
1184
1244
  }), 30_000, "IMPORT_START_TIMEOUT", () => controller.abort());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "EchoMem Cloud-First MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  "dev": "tsx src/index.ts",
19
19
  "smoke": "node smoke.mjs",
20
20
  "test": "npm run build && node test/crypto.test.mjs && node test/integration.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/tools.test.mjs && node test/delete.test.mjs && node test/migrate.test.mjs",
21
- "prepack": "npm run build"
21
+ "prepack": "npm run build && node scripts/bundle-city.mjs"
22
22
  },
23
23
  "dependencies": {
24
24
  "@modelcontextprotocol/sdk": "^1.0.1",