@echomem/mcp 1.4.45 → 1.4.46

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 +116 -4
  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,6 +505,118 @@ 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
@@ -520,12 +632,12 @@ export function writeClaudeCodeConfig(entry, options = {}) {
520
632
  });
521
633
  const runClaude = (args, cwd) => {
522
634
  try {
523
- execFileSync("claude", args, {
635
+ execClaudeCodeSync(args, {
524
636
  cwd,
525
637
  encoding: "utf8",
526
638
  stdio: ["ignore", "pipe", "pipe"],
527
639
  timeout: 10000,
528
- });
640
+ }, options.claudeCommands);
529
641
  return true;
530
642
  }
531
643
  catch {
@@ -765,7 +877,7 @@ function inspectClaudeCodeConfig(client, desiredVersion) {
765
877
  if (!fs.existsSync(home(".claude")))
766
878
  return null;
767
879
  try {
768
- const output = execFileSync("claude", ["mcp", "list"], {
880
+ const output = execClaudeCodeSync(["mcp", "list"], {
769
881
  encoding: "utf8",
770
882
  stdio: ["ignore", "pipe", "ignore"],
771
883
  timeout: 3000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@echomem/mcp",
3
- "version": "1.4.45",
3
+ "version": "1.4.46",
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",