@echomem/mcp 1.4.7 → 1.4.8

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
@@ -129,6 +129,51 @@ export function writeCodexConfig(configPath, entry) {
129
129
  fs.appendFileSync(configPath, sep + block + "\n");
130
130
  return "wrote";
131
131
  }
132
+ /**
133
+ * Write the EchoMem guidance block into the agent's GLOBAL memory file (~/.codex/AGENTS.md,
134
+ * ~/.claude/CLAUDE.md) so the agent treats EchoMem as its core memory tool without the tool
135
+ * descriptions having to over-explain "when to call me". Marker-fenced + idempotent: only our
136
+ * block is ever replaced; the user's own content is never touched. Project-level files are
137
+ * deliberately NOT written — those belong to the user.
138
+ */
139
+ const AGENTS_MD_BEGIN = "<!-- echomem:begin -->";
140
+ const AGENTS_MD_END = "<!-- echomem:end -->";
141
+ function echomemGuidanceBlock() {
142
+ return [
143
+ AGENTS_MD_BEGIN,
144
+ "## Memory (EchoMem)",
145
+ "EchoMem is your long-term memory across all coding sessions and tools.",
146
+ "- Before re-deriving past decisions, project context, or anything the user likely settled before: call `search_memories` first.",
147
+ "- When meaningful work wraps up (a decision, a fix, a milestone) or the user asks to remember something: call `save_conversation`.",
148
+ "- If the user pastes a session carryover/checkpoint: it may reference `get_checkpoint_by_context` — use it to pull the checkpoint/decision trail when you need more than the snapshot.",
149
+ "- If the user asks to show, reopen, restart, or bring back the EchoMem HUD (the context-health overlay), run the shell command `echomem-hud app --client auto`.",
150
+ "- If the user wants the HUD to come back after a computer restart, run the shell command `echomem-hud autostart on --client auto`.",
151
+ AGENTS_MD_END,
152
+ ].join("\n");
153
+ }
154
+ export function writeAgentsMemoryGuidance(filePath) {
155
+ let content = "";
156
+ try {
157
+ content = fs.readFileSync(filePath, "utf8");
158
+ }
159
+ catch {
160
+ /* fresh file */
161
+ }
162
+ const block = echomemGuidanceBlock();
163
+ const start = content.indexOf(AGENTS_MD_BEGIN);
164
+ const end = content.indexOf(AGENTS_MD_END);
165
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
166
+ if (start >= 0 && end > start) {
167
+ const current = content.slice(start, end + AGENTS_MD_END.length);
168
+ if (current === block)
169
+ return "exists";
170
+ fs.writeFileSync(filePath, content.slice(0, start) + block + content.slice(end + AGENTS_MD_END.length));
171
+ return "updated";
172
+ }
173
+ const sep = content ? (content.endsWith("\n") ? "\n" : "\n\n") : "";
174
+ fs.appendFileSync(filePath, sep + block + "\n");
175
+ return "wrote";
176
+ }
132
177
  /** Merge the EchoMem entry into a JSON client's `mcpServers` map without clobbering siblings. */
133
178
  export function writeJsonClientConfig(configPath, entry) {
134
179
  let config = {};
@@ -275,8 +320,8 @@ export function resolveServerEntryVersion(entry) {
275
320
  }
276
321
  const candidates = [...args, command].filter((value) => typeof value === "string");
277
322
  for (const candidate of candidates) {
278
- if (!candidate.includes(MCP_PACKAGE_NAME) && !candidate.includes(`${path.sep}echomem-mcp`))
279
- continue;
323
+ // Direct local/dev entries often look like ".../packages/mcp-server/dist/index.js";
324
+ // they do not contain the published package name, but walking upward still finds package.json.
280
325
  const resolved = packageVersionFromPath(candidate);
281
326
  if (resolved)
282
327
  return resolved;
@@ -408,11 +453,54 @@ function openBrowser(url) {
408
453
  /* headless — caller prints the URL */
409
454
  }
410
455
  }
456
+ function openClaudeDesktop() {
457
+ if (process.platform !== "darwin") {
458
+ return { ok: false, message: "Could not auto-open Claude on this system. The prompt is copied - open Claude Desktop and paste it." };
459
+ }
460
+ const installedPath = firstExisting([
461
+ "/Applications/Claude.app",
462
+ path.join(os.homedir(), "Applications", "Claude.app"),
463
+ ]);
464
+ const attempts = [
465
+ ["-b", "com.anthropic.claudefordesktop"],
466
+ ["-a", "Claude"],
467
+ ...(installedPath ? [[installedPath]] : []),
468
+ ];
469
+ const failures = [];
470
+ for (const args of attempts) {
471
+ try {
472
+ execFileSync("open", args, { stdio: "pipe" });
473
+ return { ok: true, message: "Opened Claude Desktop. The prompt is copied - paste it into Claude." };
474
+ }
475
+ catch (error) {
476
+ failures.push(`${args.join(" ")}: ${commandFailureMessage(error)}`);
477
+ }
478
+ }
479
+ return {
480
+ ok: false,
481
+ message: installedPath
482
+ ? "Could not auto-open Claude Desktop. The prompt is copied - open Claude Desktop and paste it."
483
+ : "Claude Desktop was not found by macOS. The prompt is copied - open Claude manually and paste it.",
484
+ detail: failures.join(" | "),
485
+ };
486
+ }
487
+ function commandFailureMessage(error) {
488
+ const maybe = error;
489
+ if (Buffer.isBuffer(maybe.stderr)) {
490
+ const stderr = maybe.stderr.toString("utf8").trim();
491
+ if (stderr)
492
+ return stderr;
493
+ }
494
+ return error instanceof Error ? error.message : String(error);
495
+ }
411
496
  function migratableFromDiscovery(disc) {
412
497
  const eta = estimateMigrationEta(disc.pending, disc.skippedActive);
498
+ const pendingCodex = disc.pendingCodex ?? disc.pending.filter((s) => s.source === "codex").length;
413
499
  return {
414
500
  pending: disc.pending.length,
415
501
  pendingTotal: disc.pendingTotal,
502
+ pendingCodex,
503
+ pendingClaudeCode: disc.pendingClaudeCode ?? disc.pending.length - pendingCodex,
416
504
  alreadyMigrated: disc.alreadyMigrated,
417
505
  skippedActive: disc.skippedActive,
418
506
  limited: disc.limited,
@@ -438,6 +526,8 @@ function migratableFromFastSummary(summary) {
438
526
  return {
439
527
  pending: summary.pending,
440
528
  pendingTotal: summary.pendingTotal,
529
+ pendingCodex: summary.pendingCodex,
530
+ pendingClaudeCode: summary.pendingClaudeCode,
441
531
  alreadyMigrated: summary.alreadyMigrated,
442
532
  skippedActive: summary.skippedActive,
443
533
  eta: summary.eta,
@@ -557,6 +647,107 @@ function serveRepoCityAsset(reqPath, res) {
557
647
  fs.createReadStream(filePath).pipe(res);
558
648
  return true;
559
649
  }
650
+ function hudAssetsRoot() {
651
+ return fileURLToPath(new URL("../assets/hud/", import.meta.url));
652
+ }
653
+ function serveHudAsset(reqPath, res) {
654
+ const root = hudAssetsRoot();
655
+ const rel = decodeURIComponent(reqPath.slice("/hud-assets/".length));
656
+ const filePath = path.resolve(root, rel);
657
+ const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
658
+ if (!filePath.startsWith(rootWithSep)) {
659
+ res.writeHead(403).end("forbidden");
660
+ return true;
661
+ }
662
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
663
+ res.writeHead(404).end("not found");
664
+ return true;
665
+ }
666
+ res.writeHead(200, {
667
+ "Content-Type": CITY_ASSET_TYPES[path.extname(filePath)] || "application/octet-stream",
668
+ "Cache-Control": "no-store",
669
+ });
670
+ fs.createReadStream(filePath).pipe(res);
671
+ return true;
672
+ }
673
+ function firstExisting(paths) {
674
+ for (const candidate of paths) {
675
+ if (fs.existsSync(candidate))
676
+ return candidate;
677
+ }
678
+ return null;
679
+ }
680
+ function appResourcesPath(appName) {
681
+ const app = firstExisting([
682
+ `/Applications/${appName}.app`,
683
+ path.join(os.homedir(), "Applications", `${appName}.app`),
684
+ ]);
685
+ return app ? path.join(app, "Contents", "Resources") : null;
686
+ }
687
+ function agentIconSource(id) {
688
+ const resources = appResourcesPath(id === "codex" ? "Codex" : "Claude");
689
+ if (!resources)
690
+ return null;
691
+ if (id === "codex") {
692
+ return firstExisting([
693
+ path.join(resources, "icon.png"),
694
+ path.join(resources, "icon-codex-dark-color.png"),
695
+ path.join(resources, "icon-codex-light.png"),
696
+ path.join(resources, "icon.icns"),
697
+ path.join(resources, "app.icns"),
698
+ path.join(resources, "electron.icns"),
699
+ ]);
700
+ }
701
+ return firstExisting([
702
+ path.join(resources, "icon.png"),
703
+ path.join(resources, "app.png"),
704
+ path.join(resources, "icon.icns"),
705
+ path.join(resources, "electron.icns"),
706
+ ]);
707
+ }
708
+ function convertedAgentIconPath(id, sourcePath) {
709
+ if (path.extname(sourcePath).toLowerCase() === ".png")
710
+ return sourcePath;
711
+ if (process.platform !== "darwin" || path.extname(sourcePath).toLowerCase() !== ".icns")
712
+ return null;
713
+ const stat = fs.statSync(sourcePath);
714
+ const cacheDir = path.join(os.homedir(), ".echomem", "cache", "agent-icons");
715
+ fs.mkdirSync(cacheDir, { recursive: true });
716
+ const out = path.join(cacheDir, `${id}-${Math.round(stat.mtimeMs)}-${stat.size}.png`);
717
+ if (fs.existsSync(out))
718
+ return out;
719
+ try {
720
+ execFileSync("sips", ["-s", "format", "png", sourcePath, "--out", out], { stdio: "ignore" });
721
+ return fs.existsSync(out) ? out : null;
722
+ }
723
+ catch {
724
+ try {
725
+ fs.rmSync(out, { force: true });
726
+ }
727
+ catch { }
728
+ return null;
729
+ }
730
+ }
731
+ function serveAgentIcon(reqPath, res) {
732
+ const raw = decodeURIComponent(reqPath.slice("/agent-icons/".length)).replace(/\.png$/i, "");
733
+ const id = raw === "codex" || raw === "claude-desktop" ? raw : null;
734
+ if (!id) {
735
+ res.writeHead(404).end("not found");
736
+ return true;
737
+ }
738
+ const source = agentIconSource(id);
739
+ const filePath = source ? convertedAgentIconPath(id, source) : null;
740
+ if (!filePath) {
741
+ res.writeHead(404).end("not found");
742
+ return true;
743
+ }
744
+ res.writeHead(200, {
745
+ "Content-Type": "image/png",
746
+ "Cache-Control": "no-store",
747
+ });
748
+ fs.createReadStream(filePath).pipe(res);
749
+ return true;
750
+ }
560
751
  function discoverMigratableSessionsOffThread() {
561
752
  const migrateUrl = new URL("./migrate.js", import.meta.url).href;
562
753
  const code = `
@@ -739,6 +930,14 @@ export function startCallbackServer(opts = {}) {
739
930
  serveRepoCityAsset(route, res);
740
931
  return;
741
932
  }
933
+ if (route.startsWith("/hud-assets/") && req.method === "GET") {
934
+ serveHudAsset(route, res);
935
+ return;
936
+ }
937
+ if (route.startsWith("/agent-icons/") && req.method === "GET") {
938
+ serveAgentIcon(route, res);
939
+ return;
940
+ }
742
941
  if (route === "/setup" && req.method === "GET") {
743
942
  res.writeHead(200, {
744
943
  "Content-Type": "text/html; charset=utf-8",
@@ -749,7 +948,25 @@ export function startCallbackServer(opts = {}) {
749
948
  if (route === "/config" && req.method === "GET") {
750
949
  if (!checkNonce(url.searchParams.get("nonce") || undefined))
751
950
  return void text(res, 403, "bad nonce");
752
- json(res, 200, { connected, authUrl, switchAccountUrl: switchAccountUrl || authUrl, localOnly: true });
951
+ json(res, 200, { connected, authUrl, switchAccountUrl: switchAccountUrl || authUrl, localOnly: true, workspacePath: process.cwd() });
952
+ return;
953
+ }
954
+ if (route === "/launch-agent" && req.method === "POST") {
955
+ let body;
956
+ try {
957
+ body = await readJsonBody(req);
958
+ }
959
+ catch {
960
+ text(res, 400, "bad json");
961
+ return;
962
+ }
963
+ if (!checkNonce(asString(body.nonce)))
964
+ return void text(res, 403, "bad nonce");
965
+ const agent = asString(body.agent);
966
+ if (agent !== "claude-desktop")
967
+ return void json(res, 400, { ok: false, message: "Unsupported agent." });
968
+ const result = openClaudeDesktop();
969
+ json(res, result.ok ? 200 : 501, result);
753
970
  return;
754
971
  }
755
972
  if (route === "/callback" && req.method === "GET") {
@@ -1052,9 +1269,14 @@ async function cmdSetup(flags) {
1052
1269
  }
1053
1270
  }
1054
1271
  }
1272
+ if (!flags["no-agents-md"]) {
1273
+ writeMemoryGuidanceForTargets(targets);
1274
+ }
1055
1275
  console.log("");
1056
1276
  if (flags["skip-login"] || flags["no-login"]) {
1057
- console.log(`Skipped login; existing EchoMem credentials are unchanged. Current bridge: ${MCP_PACKAGE_LABEL}`);
1277
+ // init drives login itself right after, so the "skipped" note would be misleading there.
1278
+ if (!flags["init-quiet"])
1279
+ console.log(`Skipped login; existing EchoMem credentials are unchanged. Current bridge: ${MCP_PACKAGE_LABEL}`);
1058
1280
  }
1059
1281
  else {
1060
1282
  await cmdLogin(flags);
@@ -1062,6 +1284,63 @@ async function cmdSetup(flags) {
1062
1284
  if (flags["with-hud"])
1063
1285
  await cmdSetupHud(flags);
1064
1286
  }
1287
+ /**
1288
+ * `echomem-mcp init` — the one-command install. Configures EVERY coding agent installed on this
1289
+ * machine (Codex + Claude Code + Claude Desktop, not just auto-detected ones), writes the AGENTS.md
1290
+ * memory guidance, logs in via the browser, and launches the context HUD — the whole product in a
1291
+ * single command. `setup`/`update` remain the granular primitives; init just picks the "do everything"
1292
+ * defaults and frames the result.
1293
+ */
1294
+ async function cmdInit(flags) {
1295
+ console.log("Setting up EchoMem — shared memory for all your coding agents, plus the live context HUD.\n");
1296
+ // 1. Configure every installed agent + write AGENTS.md. Hold login + HUD so we control ordering.
1297
+ await cmdSetup({ ...flags, all: true, "skip-login": true, "with-hud": false, "init-quiet": true });
1298
+ // 2. Bring the HUD up NOW (non-blocking) so everything is already running while onboarding proceeds.
1299
+ if (!flags["no-hud"])
1300
+ await cmdSetupHud(flags);
1301
+ // 3. Start onboarding — opens the browser dashboard (scan → connect → extraction) and waits there.
1302
+ console.log("");
1303
+ if (!flags["skip-login"] && !flags["no-login"])
1304
+ await cmdLogin(flags);
1305
+ console.log("");
1306
+ console.log("🎉 EchoMem is ready.");
1307
+ console.log(" • MCP memory is configured for every coding agent installed on this machine.");
1308
+ if (!flags["no-hud"]) {
1309
+ console.log(' • The context HUD is running (top-right). Right-click it → "Open at login" to keep it,');
1310
+ console.log(' or just tell your agent "open the EchoMem HUD" anytime (it runs: echomem-hud app).');
1311
+ }
1312
+ else {
1313
+ console.log(' • Start the context HUD anytime with: echomem-hud app');
1314
+ }
1315
+ console.log(' • Try it now: ask your agent — "search my EchoMem for what I\'ve been working on and recap it."');
1316
+ }
1317
+ /**
1318
+ * For each configured client, add the EchoMem guidance block to its GLOBAL memory file so the
1319
+ * agent knows to lean on EchoMem for recall/save. Global only (~/.codex/AGENTS.md,
1320
+ * ~/.claude/CLAUDE.md) — project files are the user's. Skip with --no-agents-md.
1321
+ */
1322
+ function writeMemoryGuidanceForTargets(targets) {
1323
+ const files = new Map(); // path → label
1324
+ for (const t of targets) {
1325
+ if (t.id === "codex")
1326
+ files.set(home(".codex", "AGENTS.md"), "Codex");
1327
+ if (t.id === "claude-code" || t.id === "claude-desktop")
1328
+ files.set(home(".claude", "CLAUDE.md"), "Claude");
1329
+ }
1330
+ for (const [file, label] of files) {
1331
+ try {
1332
+ const result = writeAgentsMemoryGuidance(file);
1333
+ if (result === "wrote")
1334
+ console.log(`✅ Added EchoMem memory guidance to ${label}'s global memory file: ${file}`);
1335
+ else if (result === "updated")
1336
+ console.log(`✅ Refreshed EchoMem memory guidance in ${file}`);
1337
+ // "exists" → silent; nothing changed.
1338
+ }
1339
+ catch (error) {
1340
+ console.log(`ℹ️ Could not write memory guidance to ${file}: ${error instanceof Error ? error.message : String(error)}`);
1341
+ }
1342
+ }
1343
+ }
1065
1344
  async function cmdUpdate(flags) {
1066
1345
  await cmdSetup({ ...flags, "skip-login": true });
1067
1346
  console.log(`Update config complete. Start a new MCP session to load ${MCP_PACKAGE_LABEL}.`);
@@ -1544,49 +1823,79 @@ async function cmdLogin(flags) {
1544
1823
  return;
1545
1824
  }
1546
1825
  updateProgress({ status: "starting", running: 0, queued: exact.pending.length, latest: "Creating import session." });
1547
- const controller = new AbortController();
1548
- const h = await withTimeout(startMigration({
1549
- pending: exact.pending,
1550
- signal: controller.signal,
1551
- onProgress: (ev) => {
1552
- let latestRepo;
1553
- if (ev.error) {
1554
- progressFailed += 1;
1555
- }
1556
- else {
1557
- progressDone += 1;
1558
- progressExtracted += ev.memories ?? 0;
1559
- latestRepo = repoLabel(ev.session.cwd); // building this completed conversation belongs to
1560
- }
1561
- updateProgress({
1562
- latest: `${ev.session.source} ${(ev.session.firstTs || "").slice(0, 10)}${ev.error ? ` failed: ${ev.error}` : ""}`,
1563
- ...(latestRepo ? { latestRepo } : {}),
1564
- });
1565
- },
1566
- }), 30_000, "IMPORT_START_TIMEOUT", () => controller.abort());
1567
- activeSessionId = h.sessionId;
1568
- activeJobCount = h.jobCount;
1569
- updateProgress({ status: "running", sessionId: h.sessionId, jobCount: h.jobCount, total: h.jobCount, capped: h.capped, latest: "Import session created." });
1570
- sendMigrate({ sessionId: h.sessionId, jobCount: h.jobCount, ...(h.capped ? { capped: h.capped } : {}) });
1826
+ // Plan caps limit how many conversations one import session accepts (IMPORT_LIMIT_EXCEEDED →
1827
+ // startMigration slices to the cap). Instead of making the user re-run setup per batch (3000
1828
+ // sessions used to mean 3 clicks), loop batches automatically until everything pending is done.
1829
+ const onBatchProgress = (ev) => {
1830
+ let latestRepo;
1831
+ if (ev.error) {
1832
+ progressFailed += 1;
1833
+ }
1834
+ else {
1835
+ progressDone += 1;
1836
+ progressExtracted += ev.memories ?? 0;
1837
+ latestRepo = repoLabel(ev.session.cwd); // building this completed conversation belongs to
1838
+ }
1839
+ updateProgress({
1840
+ latest: `${ev.session.source} ${(ev.session.firstTs || "").slice(0, 10)}${ev.error ? ` failed: ${ev.error}` : ""}`,
1841
+ ...(latestRepo ? { latestRepo } : {}),
1842
+ });
1843
+ };
1844
+ let remaining = exact.pending;
1845
+ let stoppedReason;
1846
+ let planLimitNote;
1847
+ let batchIndex = 0;
1571
1848
  console.log("Migrating your history… keep this terminal open until it completes.");
1572
- console.log(`Migration metrics: ${h.metricsFile}`);
1573
- const r = await h.done;
1849
+ for (;;) {
1850
+ batchIndex += 1;
1851
+ let h;
1852
+ try {
1853
+ const controller = new AbortController();
1854
+ h = await withTimeout(startMigration({ pending: remaining, signal: controller.signal, onProgress: onBatchProgress }), 30_000, "IMPORT_START_TIMEOUT", () => controller.abort());
1855
+ }
1856
+ catch (batchError) {
1857
+ if (batchIndex === 1)
1858
+ throw batchError; // first batch failing = the whole import failed
1859
+ // A later batch could not start (e.g. plan headroom exhausted). Finish gracefully with a note.
1860
+ planLimitNote = `Imported ${progressDone} so far — the rest hit your plan's import limit. Re-run extraction later for the remaining ${remaining.length}.`;
1861
+ break;
1862
+ }
1863
+ activeSessionId = h.sessionId;
1864
+ if (batchIndex === 1) {
1865
+ updateProgress({ status: "running", sessionId: h.sessionId, jobCount: activeJobCount, total: activeJobCount, latest: "Import session created." });
1866
+ sendMigrate({ sessionId: h.sessionId, jobCount: activeJobCount });
1867
+ }
1868
+ else {
1869
+ updateProgress({ latest: `Continuing automatically — batch ${batchIndex} (${remaining.length} left).` });
1870
+ }
1871
+ console.log(`Migration metrics (batch ${batchIndex}): ${h.metricsFile}`);
1872
+ const r = await h.done;
1873
+ if (r.stoppedReason) {
1874
+ stoppedReason = r.stoppedReason;
1875
+ break;
1876
+ }
1877
+ // startMigration sliced to the cap ⇒ more remain: continue with the next batch automatically.
1878
+ if (h.capped && remaining.length > h.jobCount) {
1879
+ remaining = remaining.slice(h.jobCount);
1880
+ continue;
1881
+ }
1882
+ break;
1883
+ }
1574
1884
  srv.setProgress({
1575
- status: r.failed || r.stoppedReason ? "failed" : "completed",
1576
- sessionId: h.sessionId,
1577
- jobCount: h.jobCount,
1578
- capped: h.capped,
1579
- total: h.jobCount,
1580
- completed: r.migrated,
1885
+ status: stoppedReason ? "failed" : "completed",
1886
+ sessionId: activeSessionId || undefined,
1887
+ jobCount: activeJobCount,
1888
+ total: activeJobCount,
1889
+ completed: progressDone,
1581
1890
  running: 0,
1582
1891
  queued: 0,
1583
- failed: r.failed,
1584
- extracted: r.extracted,
1585
- latest: r.stoppedReason ? `Stopped: ${r.stoppedReason}` : "Import complete.",
1586
- ...(r.stoppedReason ? { error: r.stoppedReason } : {}),
1892
+ failed: progressFailed,
1893
+ extracted: progressExtracted,
1894
+ latest: stoppedReason ? `Stopped: ${stoppedReason}` : planLimitNote || "Import complete.",
1895
+ ...(stoppedReason ? { error: stoppedReason } : {}),
1587
1896
  });
1588
- console.log(`Import finished: ${r.migrated} imported, ${r.extracted} memories, ${r.failed} failed.`);
1589
- if (r.stoppedReason || r.failed)
1897
+ console.log(`Import finished: ${progressDone} imported, ${progressExtracted} memories, ${progressFailed} failed.`);
1898
+ if (stoppedReason || progressFailed)
1590
1899
  process.exitCode = 1;
1591
1900
  await new Promise((resolve) => setTimeout(resolve, 2000));
1592
1901
  srv.close();
@@ -1713,6 +2022,7 @@ function cmdLogout() {
1713
2022
  const HELP = `EchoMem MCP — local memory bridge
1714
2023
 
1715
2024
  Usage:
2025
+ echomem-mcp init One command: configure every installed agent + HUD + log in
1716
2026
  echomem-mcp Run the MCP server (stdio; default — used by your editor)
1717
2027
  echomem-mcp setup [--client X] Detect editor, write its MCP config, then log in
1718
2028
  echomem-mcp setup --skip-login Write MCP config without opening login/browser
@@ -1749,6 +2059,9 @@ export async function runCli(argv) {
1749
2059
  const cmd = argv[0];
1750
2060
  const flags = parseFlags(argv.slice(1));
1751
2061
  switch (cmd) {
2062
+ case "init":
2063
+ await cmdInit(flags);
2064
+ return true;
1752
2065
  case "setup":
1753
2066
  await cmdSetup(flags);
1754
2067
  return true;
@@ -16,6 +16,7 @@ export const canonicalToolNames = {
16
16
  recompose: "echo_recompose",
17
17
  delete: "delete_memory",
18
18
  getByContext: "get_memories_by_context",
19
+ checkpointByContext: "get_checkpoint_by_context",
19
20
  };
20
21
  export const legacyAliasToCanonical = {
21
22
  search_memories_by_description_semantic: canonicalToolNames.search,
@@ -114,7 +115,7 @@ export function listToolSpecs(opts = {}) {
114
115
  return [
115
116
  {
116
117
  name: canonicalToolNames.search,
117
- description: withMcpVersion(`Recall the user's prior decisions, preferences, constraints, and project context from EchoMem — their long-term memory across ALL their AI tools (Claude.ai, ChatGPT, other agents), not just this session.${mapSection}\nCall this when the task plausibly relates to that remembered context — a topic above, or when the user refers to past work ("what did we decide", "like before", "the usual") — so you don't re-derive or re-ask what they already settled. Skip it for self-contained tasks with no link to their history (e.g. a generic algorithm question). By default this returns only the ranked memories retrieved for recall and skips EchoMem answer generation; set includeAnswer=true only when you explicitly need EchoMem's legacy synthesized recall. Current time: ${currentTime}.${updateSection}`),
118
+ 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.${mapSection}\nReturns the ranked memories; set includeAnswer=true only if you need the legacy synthesized answer. Current time: ${currentTime}.${updateSection}`),
118
119
  inputSchema: {
119
120
  type: "object",
120
121
  properties: {
@@ -160,7 +161,7 @@ export function listToolSpecs(opts = {}) {
160
161
  },
161
162
  {
162
163
  name: canonicalToolNames.save,
163
- description: "Save conversation into EchoMem for future retrieval. Set passthrough=true to store the text verbatim as a session capsule (no LLM extraction, no embeddings) use this for warm-up capsules that a fresh session will reload via get_memories_by_context.",
164
+ description: "Save this conversation into EchoMem as long-term memory (durable memories are extracted automatically). passthrough=true stores the text verbatim as a session capsule insteadreload it later via get_memories_by_context.",
164
165
  inputSchema: {
165
166
  type: "object",
166
167
  properties: {
@@ -383,6 +384,23 @@ export function listToolSpecs(opts = {}) {
383
384
  required: ["contextId"],
384
385
  },
385
386
  },
387
+ {
388
+ name: canonicalToolNames.checkpointByContext,
389
+ description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. 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}.`),
390
+ inputSchema: {
391
+ type: "object",
392
+ properties: {
393
+ contextId: { type: "string", description: "The EchoMem contextId shown by the HUD or returned by save_conversation / Renew session." },
394
+ limit: { type: "number", default: 100 },
395
+ triggerMessage: {
396
+ type: "string",
397
+ description: "Optional: the user's message that caused this lookup. EchoMem stores only a redacted analytics preview and hash.",
398
+ },
399
+ triggerMessageRole: { type: "string", default: "user" },
400
+ },
401
+ required: ["contextId"],
402
+ },
403
+ },
386
404
  {
387
405
  name: canonicalToolNames.report,
388
406
  description: "Show the user a one-screen audit of THEIR OWN AI coding usage — computed locally from their Codex/Claude Code logs ($0, nothing uploaded): how many tokens their agents spent, how much was re-reading context, reads vs memory recalls, and what changes with EchoMem. Call this when the user asks about their usage, token spend, cost, how much they're wasting, or wants a summary of their agent activity — and you may offer it once right after EchoMem is first connected. Returns formatted text to show the user verbatim. Needs no login.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.7",
3
+ "version": "1.4.8",
4
4
  "description": "EchoMem Cloud-First MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -20,7 +20,8 @@
20
20
  "start": "node dist/index.js",
21
21
  "dev": "tsx src/index.ts",
22
22
  "smoke": "node smoke.mjs",
23
- "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/update-check.test.mjs && node test/delete.test.mjs && node test/migrate.test.mjs && node test/hud.test.mjs",
23
+ "preview:extraction": "npm run build && node scripts/preview-extraction.mjs",
24
+ "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/update-check.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/hud.test.mjs",
24
25
  "prepack": "npm run build && node scripts/bundle-city.mjs"
25
26
  },
26
27
  "dependencies": {