@echomem/mcp 1.4.6 → 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 = {};
@@ -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;