@echomem/mcp 1.4.45 → 1.4.47

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.
Files changed (2) hide show
  1. package/dist/setup.js +193 -50
  2. package/package.json +1 -1
package/dist/setup.js CHANGED
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import http from "node:http";
17
17
  import { randomUUID } from "node:crypto";
18
- import { execFileSync, spawn } from "node:child_process";
18
+ import { execFileSync, spawn, spawnSync, } from "node:child_process";
19
19
  import { Worker } from "node:worker_threads";
20
20
  import fs from "node:fs";
21
21
  import os from "node:os";
@@ -505,11 +505,124 @@ function claudeCodeLocalEchoMemProjects(configPath) {
505
505
  .map(([projectPath]) => projectPath)
506
506
  .sort();
507
507
  }
508
+ function versionedClaudeCodeExecutables(root, executable) {
509
+ let versions;
510
+ try {
511
+ versions = fs.readdirSync(root, { withFileTypes: true });
512
+ }
513
+ catch {
514
+ return [];
515
+ }
516
+ return versions
517
+ .filter((entry) => entry.isDirectory())
518
+ .sort((left, right) => right.name.localeCompare(left.name, undefined, { numeric: true }))
519
+ .map((entry) => path.join(root, entry.name, executable))
520
+ .filter((candidate) => fs.existsSync(candidate));
521
+ }
522
+ /**
523
+ * Return every safe Claude Code launcher location worth trying. Claude Desktop bundles the CLI,
524
+ * but Windows Store/MSIX installs expose its real files below the package LocalCache instead of
525
+ * the caller's ordinary APPDATA/PATH view.
526
+ */
527
+ export function claudeCodeCommandCandidates(options = {}) {
528
+ const platform = options.platform ?? process.platform;
529
+ const env = options.env ?? process.env;
530
+ const homeDir = options.homeDir ?? os.homedir();
531
+ const candidates = [];
532
+ const configured = env.CLAUDE_CODE_EXECUTABLE?.trim();
533
+ if (configured)
534
+ candidates.push(configured);
535
+ candidates.push("claude");
536
+ if (platform === "win32") {
537
+ const appData = env.APPDATA?.trim() || path.join(homeDir, "AppData", "Roaming");
538
+ const localAppData = env.LOCALAPPDATA?.trim() || path.join(homeDir, "AppData", "Local");
539
+ candidates.push(...versionedClaudeCodeExecutables(path.join(appData, "Claude", "claude-code"), "claude.exe"), ...versionedClaudeCodeExecutables(path.join(localAppData, "Claude", "claude-code"), "claude.exe"));
540
+ const packagesRoot = path.join(localAppData, "Packages");
541
+ try {
542
+ for (const entry of fs.readdirSync(packagesRoot, { withFileTypes: true })) {
543
+ if (!entry.isDirectory() || !/^Claude_/i.test(entry.name))
544
+ continue;
545
+ candidates.push(...versionedClaudeCodeExecutables(path.join(packagesRoot, entry.name, "LocalCache", "Roaming", "Claude", "claude-code"), "claude.exe"));
546
+ }
547
+ }
548
+ catch {
549
+ /* A non-Store install has no Packages directory. */
550
+ }
551
+ const executionAlias = path.join(localAppData, "Microsoft", "WindowsApps", "claude.exe");
552
+ if (fs.existsSync(executionAlias))
553
+ candidates.push(executionAlias);
554
+ }
555
+ else if (platform === "darwin") {
556
+ for (const candidate of [
557
+ "/Applications/Claude.app/Contents/Resources/claude",
558
+ path.join(homeDir, "Applications", "Claude.app", "Contents", "Resources", "claude"),
559
+ "/opt/homebrew/bin/claude",
560
+ "/usr/local/bin/claude",
561
+ ]) {
562
+ if (fs.existsSync(candidate))
563
+ candidates.push(candidate);
564
+ }
565
+ }
566
+ return [...new Set(candidates)];
567
+ }
568
+ function escapeWindowsCmdCommand(value) {
569
+ return value.replace(/([()\][%!^"`<>&|;, *?])/g, "^$1");
570
+ }
571
+ function escapeWindowsCmdArgument(value) {
572
+ let escaped = value
573
+ .replace(/(?=(\\+?)?)\1"/g, "$1$1\\\"")
574
+ .replace(/(?=(\\+?)?)\1$/g, "$1$1");
575
+ escaped = `"${escaped}"`;
576
+ return escaped.replace(/([()\][%!^"`<>&|;, *?])/g, "^$1");
577
+ }
578
+ function execClaudeCodeSync(args, options, commandCandidates) {
579
+ let candidates = commandCandidates ? [...commandCandidates] : claudeCodeCommandCandidates();
580
+ if (process.platform === "win32" && !commandCandidates) {
581
+ try {
582
+ const pathMatches = execFileSync("where.exe", ["claude"], {
583
+ encoding: "utf8",
584
+ stdio: ["ignore", "pipe", "ignore"],
585
+ timeout: 3000,
586
+ windowsHide: true,
587
+ }).split(/\r?\n/).map((candidate) => candidate.trim()).filter(Boolean);
588
+ candidates = [...new Set([...pathMatches, ...candidates])];
589
+ }
590
+ catch {
591
+ /* The bundled Desktop candidates below remain available. */
592
+ }
593
+ }
594
+ let lastError = new Error("Claude Code CLI was not found.");
595
+ for (const command of candidates) {
596
+ try {
597
+ if (process.platform === "win32" && /\.(cmd|bat)$/i.test(command)) {
598
+ const shellCommand = [escapeWindowsCmdCommand(command), ...args.map(escapeWindowsCmdArgument)].join(" ");
599
+ const spawnOptions = {
600
+ ...options,
601
+ windowsHide: true,
602
+ windowsVerbatimArguments: true,
603
+ };
604
+ const result = spawnSync(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `"${shellCommand}"`], spawnOptions);
605
+ if (result.error)
606
+ throw result.error;
607
+ if (result.status !== 0) {
608
+ throw new Error(result.stderr?.trim() || `Claude Code exited with status ${result.status ?? "unknown"}.`);
609
+ }
610
+ return result.stdout || "";
611
+ }
612
+ return execFileSync(command, args, process.platform === "win32" ? { ...options, windowsHide: true } : options);
613
+ }
614
+ catch (error) {
615
+ lastError = error;
616
+ }
617
+ }
618
+ throw lastError;
619
+ }
508
620
  export function writeClaudeCodeConfig(entry, options = {}) {
509
621
  // EchoMem belongs at user scope so every Claude Code project resolves the same durable runtime.
510
622
  // Older CLI versions wrote local/project entries, which take precedence over user scope and can
511
623
  // keep launching a deleted npm cache or stale runtime. Migrate those only after user scope is safe.
512
624
  const configPath = options.configPath ?? home(".claude.json");
625
+ let failureReason;
513
626
  const emptyResult = () => ({
514
627
  state: "unavailable",
515
628
  removedLocalProjects: [],
@@ -517,18 +630,21 @@ export function writeClaudeCodeConfig(entry, options = {}) {
517
630
  failedLocalProjects: [],
518
631
  restoredPreviousUserEntry: false,
519
632
  preservedDesktopManaged: false,
633
+ failureReason,
520
634
  });
521
635
  const runClaude = (args, cwd) => {
522
636
  try {
523
- execFileSync("claude", args, {
637
+ execClaudeCodeSync(args, {
524
638
  cwd,
525
639
  encoding: "utf8",
526
640
  stdio: ["ignore", "pipe", "pipe"],
527
641
  timeout: 10000,
528
- });
642
+ }, options.claudeCommands);
643
+ failureReason = undefined;
529
644
  return true;
530
645
  }
531
- catch {
646
+ catch (error) {
647
+ failureReason = commandFailureMessage(error);
532
648
  return false;
533
649
  }
534
650
  };
@@ -547,8 +663,10 @@ export function writeClaudeCodeConfig(entry, options = {}) {
547
663
  if (previousUserEntry && !removeUser())
548
664
  return emptyResult();
549
665
  if (!addUser(desiredUserEntry)) {
666
+ const addFailureReason = failureReason;
550
667
  if (previousUserEntry)
551
668
  restoredPreviousUserEntry = addUser(previousUserEntry);
669
+ failureReason = addFailureReason;
552
670
  return { ...emptyResult(), restoredPreviousUserEntry };
553
671
  }
554
672
  }
@@ -765,7 +883,7 @@ function inspectClaudeCodeConfig(client, desiredVersion) {
765
883
  if (!fs.existsSync(home(".claude")))
766
884
  return null;
767
885
  try {
768
- const output = execFileSync("claude", ["mcp", "list"], {
886
+ const output = execClaudeCodeSync(["mcp", "list"], {
769
887
  encoding: "utf8",
770
888
  stdio: ["ignore", "pipe", "ignore"],
771
889
  timeout: 3000,
@@ -2821,6 +2939,7 @@ async function cmdSetup(flags) {
2821
2939
  // --dev is already an explicit request to replace the managed runtime with a checkout.
2822
2940
  const forceHeadless = flags["force-headless"] === true || typeof flags.dev === "string";
2823
2941
  const configurationFailures = [];
2942
+ const configuredTargets = [];
2824
2943
  if (targets.length === 0) {
2825
2944
  console.log("No client auto-detected. Add this MCP server entry manually:\n");
2826
2945
  console.log(JSON.stringify({ echomem: entry }, null, 2));
@@ -2828,66 +2947,90 @@ async function cmdSetup(flags) {
2828
2947
  }
2829
2948
  else {
2830
2949
  for (const c of targets) {
2831
- if (c.kind === "json") {
2832
- const result = writeJsonClientConfig(c.configPath, entry, { forceHeadless });
2833
- if (result === "desktop-managed") {
2834
- console.log(`✅ Kept the valid Echo Desktop-managed EchoMem entry for ${c.label}: ${c.configPath}`);
2835
- }
2836
- else {
2837
- console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath}`);
2838
- }
2839
- }
2840
- else if (c.kind === "command") {
2841
- const result = writeCodexConfig(c.configPath, entry, { forceHeadless });
2842
- if (result === "wrote")
2843
- console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath} — start a new Codex session to load it.`);
2844
- else if (result === "desktop-managed")
2845
- console.log(`✅ Kept the valid Echo Desktop-managed EchoMem entry for ${c.label}: ${c.configPath}`);
2846
- else
2847
- console.log(`✅ ${c.label} already has the EchoMem MCP entry: ${c.configPath}`);
2848
- }
2849
- else {
2850
- const result = c.id === "claude-code"
2851
- ? writeClaudeCodeConfig(entry, { forceHeadless })
2852
- : "unavailable";
2853
- if (result !== "unavailable" && result.state === "wrote") {
2854
- if (result.preservedDesktopManaged) {
2855
- console.log(`✅ Kept the valid Echo Desktop-managed EchoMem user entry for ${c.label}.`);
2950
+ try {
2951
+ if (c.kind === "json") {
2952
+ const result = writeJsonClientConfig(c.configPath, entry, { forceHeadless });
2953
+ if (result === "desktop-managed") {
2954
+ console.log(`✅ Kept the valid Echo Desktop-managed EchoMem entry for ${c.label}: ${c.configPath}`);
2856
2955
  }
2857
2956
  else {
2858
- console.log(`✅ Wrote EchoMem MCP entry to ${c.label} via \`claude mcp add-json\` — start a new Claude Code session to load it.`);
2859
- }
2860
- if (result.removedLocalProjects.length > 0) {
2861
- console.log(`✅ Removed ${result.removedLocalProjects.length} stale Claude Code project-local EchoMem ${result.removedLocalProjects.length === 1 ? "entry" : "entries"}.`);
2862
- }
2863
- if (result.skippedLocalProjects.length > 0) {
2864
- console.log(`ℹ️ Ignored ${result.skippedLocalProjects.length} EchoMem local ${result.skippedLocalProjects.length === 1 ? "entry" : "entries"} for deleted project directories; they cannot shadow the user entry.`);
2957
+ console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath}`);
2865
2958
  }
2959
+ configuredTargets.push(c);
2960
+ }
2961
+ else if (c.kind === "command") {
2962
+ const result = writeCodexConfig(c.configPath, entry, { forceHeadless });
2963
+ if (result === "wrote")
2964
+ console.log(`✅ Wrote EchoMem MCP entry to ${c.label}: ${c.configPath} — start a new Codex session to load it.`);
2965
+ else if (result === "desktop-managed")
2966
+ console.log(`✅ Kept the valid Echo Desktop-managed EchoMem entry for ${c.label}: ${c.configPath}`);
2967
+ else
2968
+ console.log(`✅ ${c.label} already has the EchoMem MCP entry: ${c.configPath}`);
2969
+ configuredTargets.push(c);
2866
2970
  }
2867
2971
  else {
2868
- const failedProjects = result === "unavailable" ? [] : result.failedLocalProjects;
2869
- configurationFailures.push(failedProjects.length > 0
2870
- ? `${c.label} still has project-local EchoMem overrides in: ${failedProjects.join(", ")}`
2871
- : `${c.label} user-scoped EchoMem entry could not be verified`);
2972
+ const result = c.id === "claude-code"
2973
+ ? writeClaudeCodeConfig(entry, { forceHeadless })
2974
+ : "unavailable";
2975
+ if (result !== "unavailable" && result.state === "wrote") {
2976
+ if (result.preservedDesktopManaged) {
2977
+ console.log(`✅ Kept the valid Echo Desktop-managed EchoMem user entry for ${c.label}.`);
2978
+ }
2979
+ else {
2980
+ console.log(`✅ Wrote EchoMem MCP entry to ${c.label} via \`claude mcp add-json\` — start a new Claude Code session to load it.`);
2981
+ }
2982
+ if (result.removedLocalProjects.length > 0) {
2983
+ console.log(`✅ Removed ${result.removedLocalProjects.length} stale Claude Code project-local EchoMem ${result.removedLocalProjects.length === 1 ? "entry" : "entries"}.`);
2984
+ }
2985
+ if (result.skippedLocalProjects.length > 0) {
2986
+ console.log(`ℹ️ Ignored ${result.skippedLocalProjects.length} EchoMem local ${result.skippedLocalProjects.length === 1 ? "entry" : "entries"} for deleted project directories; they cannot shadow the user entry.`);
2987
+ }
2988
+ configuredTargets.push(c);
2989
+ }
2990
+ else {
2991
+ const failedProjects = result === "unavailable" ? [] : result.failedLocalProjects;
2992
+ const reason = result === "unavailable" ? undefined : result.failureReason;
2993
+ if (result !== "unavailable" && result.state === "needs-repair")
2994
+ configuredTargets.push(c);
2995
+ configurationFailures.push({
2996
+ client: c,
2997
+ reason: failedProjects.length > 0
2998
+ ? `${c.label} still has project-local EchoMem overrides in: ${failedProjects.join(", ")}`
2999
+ : `${c.label} user-scoped EchoMem entry could not be verified${reason ? `: ${reason}` : ""}`,
3000
+ });
3001
+ }
2872
3002
  }
2873
3003
  }
3004
+ catch (error) {
3005
+ configurationFailures.push({
3006
+ client: c,
3007
+ reason: `${c.label} configuration was left unchanged: ${error instanceof Error ? error.message : String(error)}`,
3008
+ });
3009
+ }
2874
3010
  }
2875
3011
  }
2876
3012
  if (configurationFailures.length > 0) {
2877
- throw new Error([
2878
- "EchoMem MCP configuration is incomplete; onboarding was stopped before login/import.",
2879
- ...configurationFailures.map((failure) => `- ${failure}`),
2880
- `Retry with: ${MCP_UPDATE_COMMAND} --client claude-code`,
2881
- ].join("\n"));
3013
+ const explicitSingleClient = Boolean(requested && requested !== "all" && flags.all !== true);
3014
+ const failureMessage = [
3015
+ explicitSingleClient
3016
+ ? "EchoMem could not configure the requested MCP client."
3017
+ : "EchoMem skipped MCP clients that were unavailable or could not be configured.",
3018
+ ...configurationFailures.map((failure) => `- ${failure.reason}`),
3019
+ ...configurationFailures.map((failure) => `Retry ${failure.client.label} later with: ${MCP_UPDATE_COMMAND} --client ${failure.client.id}`),
3020
+ ].join("\n");
3021
+ if (explicitSingleClient)
3022
+ throw new Error(failureMessage);
3023
+ console.log(`⚠️ ${failureMessage}`);
3024
+ console.log("Continuing with the available clients; account connection and local-history import are not blocked.");
2882
3025
  }
2883
3026
  if (!flags["no-agents-md"]) {
2884
- writeMemoryGuidanceForTargets(targets);
3027
+ writeMemoryGuidanceForTargets(configuredTargets);
2885
3028
  }
2886
3029
  if (!flags["no-codex-skills"]) {
2887
- writeCodexSkillsForTargets(targets);
3030
+ writeCodexSkillsForTargets(configuredTargets);
2888
3031
  }
2889
3032
  if (flags["no-save-hooks"] !== true) {
2890
- writeLifecycleHooksForTargets(targets);
3033
+ writeLifecycleHooksForTargets(configuredTargets);
2891
3034
  }
2892
3035
  console.log("");
2893
3036
  if (flags["skip-login"] || flags["no-login"]) {
@@ -2928,7 +3071,7 @@ async function cmdInit(flags) {
2928
3071
  }
2929
3072
  console.log("");
2930
3073
  console.log("🎉 EchoMem is ready.");
2931
- console.log(" • MCP memory is configured for every coding agent installed on this machine.");
3074
+ console.log(" • MCP memory is configured for every available coding agent EchoMem could connect to on this machine.");
2932
3075
  console.log(" • Echo Desktop shows connection status and manages this device credential.");
2933
3076
  console.log(' • Try it now: ask your agent — "search my EchoMem for what I\'ve been working on and recap it."');
2934
3077
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.45",
3
+ "version": "1.4.47",
4
4
  "description": "EchoMem MCP bridge: cloud-first memory tools and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",