@memoraone/mcp 0.1.33 → 0.1.35

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 (4) hide show
  1. package/dist/cli.cjs +1479 -995
  2. package/dist/daemon.cjs +111 -36
  3. package/dist/index.cjs +221 -102
  4. package/package.json +11 -12
package/dist/cli.cjs CHANGED
@@ -30,7 +30,7 @@ var require_package = __commonJS({
30
30
  "package.json"(exports2, module2) {
31
31
  module2.exports = {
32
32
  name: "@memoraone/mcp",
33
- version: "0.1.33",
33
+ version: "0.1.35",
34
34
  type: "module",
35
35
  main: "dist/index.cjs",
36
36
  bin: {
@@ -58,8 +58,8 @@ var require_package = __commonJS({
58
58
  zod: "^4.0.0"
59
59
  },
60
60
  devDependencies: {
61
- tsx: "^4.21.0",
62
61
  tsup: "^8.5.1",
62
+ tsx: "^4.21.0",
63
63
  typescript: "^5.9.2"
64
64
  }
65
65
  };
@@ -68,8 +68,8 @@ var require_package = __commonJS({
68
68
 
69
69
  // src/bridgeProxy.ts
70
70
  var net = __toESM(require("net"), 1);
71
- var readline2 = __toESM(require("readline"), 1);
72
- var import_node_child_process = require("child_process");
71
+ var readline3 = __toESM(require("readline"), 1);
72
+ var import_node_child_process3 = require("child_process");
73
73
 
74
74
  // src/bindingIdentity.ts
75
75
  var crypto = __toESM(require("crypto"), 1);
@@ -84,7 +84,9 @@ function hashBindingIdentity(projectId, workspaceRoot, ideType) {
84
84
  return crypto.createHash("sha256").update(input2).digest("hex").slice(0, BINDING_SOCKET_HASH_LENGTH);
85
85
  }
86
86
  function bindingsMatch(a, b) {
87
- return a.projectId.trim().toLowerCase() === b.projectId.trim().toLowerCase() && path.resolve(a.workspaceRoot) === path.resolve(b.workspaceRoot) && path.resolve(a.m1Path) === path.resolve(b.m1Path);
87
+ const envA = a.environment ?? void 0;
88
+ const envB = b.environment ?? void 0;
89
+ return a.projectId.trim().toLowerCase() === b.projectId.trim().toLowerCase() && path.resolve(a.workspaceRoot) === path.resolve(b.workspaceRoot) && path.resolve(a.m1Path) === path.resolve(b.m1Path) && (a.apiKey ?? null) === (b.apiKey ?? null) && envA === envB;
88
90
  }
89
91
  function formatMissingInitializeWorkspaceError(options) {
90
92
  const lines = [
@@ -119,6 +121,10 @@ var path4 = __toESM(require("path"), 1);
119
121
  var fs = __toESM(require("fs/promises"), 1);
120
122
  var path2 = __toESM(require("path"), 1);
121
123
  var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
124
+ var CANONICAL_M1_FILENAME = "memoraone.m1";
125
+ function isCanonicalM1Path(m1Path) {
126
+ return path2.basename(m1Path) === CANONICAL_M1_FILENAME;
127
+ }
122
128
  function normalizeEnvironment(raw) {
123
129
  if (raw === void 0 || raw === null || typeof raw !== "string") {
124
130
  return void 0;
@@ -165,7 +171,7 @@ async function resolveProjectIdFromExplicitM1Path() {
165
171
  async function findM1WalkingUp(workspaceRoot) {
166
172
  let current = path2.resolve(workspaceRoot);
167
173
  while (true) {
168
- const markerPath = path2.join(current, "memoraone.m1");
174
+ const markerPath = path2.join(current, CANONICAL_M1_FILENAME);
169
175
  try {
170
176
  const content = await fs.readFile(markerPath, "utf8");
171
177
  const { projectId, apiKey, environment } = parseAndValidateM1(content, markerPath);
@@ -272,6 +278,45 @@ async function resolveAuthoritativeBinding(workspaceRoot, options = {}) {
272
278
  }
273
279
  return bindings[0];
274
280
  }
281
+ function bindingRelevantValuesMatch(a, b) {
282
+ const envA = a.environment ?? void 0;
283
+ const envB = b.environment ?? void 0;
284
+ return a.projectId.trim().toLowerCase() === b.projectId.trim().toLowerCase() && path2.resolve(a.workspaceRoot) === path2.resolve(b.workspaceRoot) && path2.resolve(a.m1Path) === path2.resolve(b.m1Path) && (a.apiKey ?? null) === (b.apiKey ?? null) && envA === envB;
285
+ }
286
+ async function reconcileResolvedBindingWithDisk(cached) {
287
+ const m1Path = path2.resolve(cached.m1Path);
288
+ if (cached.bindingSource !== "explicit-m1-path" && !isCanonicalM1Path(m1Path)) {
289
+ throw new Error(
290
+ `[memoraone-mcp] Cached binding m1Path is not the canonical ${CANONICAL_M1_FILENAME}: ${m1Path}`
291
+ );
292
+ }
293
+ let content;
294
+ try {
295
+ content = await fs.readFile(m1Path, "utf8");
296
+ } catch (err) {
297
+ if (err?.code === "ENOENT") {
298
+ throw new Error(
299
+ `[memoraone-mcp] Cached binding file missing at ${m1Path}. Open a folder containing ${CANONICAL_M1_FILENAME}.`
300
+ );
301
+ }
302
+ throw err;
303
+ }
304
+ const parsed = parseAndValidateM1(content, m1Path);
305
+ const resolved = resolveApiKeyWithSource(parsed.apiKey);
306
+ const fresh = {
307
+ projectId: parsed.projectId,
308
+ workspaceRoot: path2.resolve(path2.dirname(m1Path)),
309
+ m1Path,
310
+ apiKey: resolved.apiKey,
311
+ ...parsed.environment !== void 0 ? { environment: parsed.environment } : {},
312
+ bindingSource: cached.bindingSource,
313
+ apiKeySource: resolved.apiKeySource
314
+ };
315
+ if (bindingRelevantValuesMatch(cached, fresh)) {
316
+ return { binding: fresh, cacheRefreshed: false };
317
+ }
318
+ return { binding: fresh, cacheRefreshed: true };
319
+ }
275
320
  function encodeResolvedBinding(binding) {
276
321
  return Buffer.from(JSON.stringify(binding), "utf8").toString("base64");
277
322
  }
@@ -444,6 +489,29 @@ function readBindingSidecarRecord(socketPath) {
444
489
  return null;
445
490
  }
446
491
  }
492
+ function readBindingSidecar(socketPath) {
493
+ const record = readBindingSidecarRecord(socketPath);
494
+ if (!record) {
495
+ return null;
496
+ }
497
+ return decodeResolvedBinding(record.binding);
498
+ }
499
+ function removeBindingSidecar(socketPath) {
500
+ try {
501
+ fs3.unlinkSync(bindingSidecarPath(socketPath));
502
+ } catch {
503
+ }
504
+ }
505
+ function removeDaemonSocketArtifacts(socketPath) {
506
+ removeBindingSidecar(socketPath);
507
+ try {
508
+ fs3.unlinkSync(socketPath);
509
+ } catch {
510
+ }
511
+ }
512
+ function isDaemonBindingMismatchError(err) {
513
+ return err instanceof Error && err.message.includes("Daemon socket binding mismatch");
514
+ }
447
515
  function formatBindingMismatchError(socketPath, sidecar, expected, detail) {
448
516
  const lines = [
449
517
  `[memoraone-mcp] Daemon socket binding mismatch at ${path4.basename(socketPath)}.`,
@@ -453,7 +521,7 @@ function formatBindingMismatchError(socketPath, sidecar, expected, detail) {
453
521
  if (detail) {
454
522
  lines.push(` ${detail}`);
455
523
  }
456
- lines.push("Reload MCP in this IDE window or run memoraone-mcp cleanup for the stale socket.");
524
+ lines.push("The bridge will replace this stale daemon automatically from the current memoraone.m1.");
457
525
  return lines.join("\n");
458
526
  }
459
527
  function verifyDaemonSidecarBinding(socketPath, expected, env = process.env) {
@@ -635,6 +703,21 @@ async function requestClientRootsListUris(options) {
635
703
  }
636
704
  }
637
705
 
706
+ // src/cleanup.ts
707
+ var fs5 = __toESM(require("fs/promises"), 1);
708
+ var path7 = __toESM(require("path"), 1);
709
+ var readline2 = __toESM(require("readline/promises"), 1);
710
+ var import_node_child_process2 = require("child_process");
711
+ var import_node_util2 = require("util");
712
+ var import_node_process = require("process");
713
+
714
+ // src/cursorGlobalMcpConfig.ts
715
+ var fs4 = __toESM(require("fs/promises"), 1);
716
+ var os2 = __toESM(require("os"), 1);
717
+ var path6 = __toESM(require("path"), 1);
718
+ var import_node_child_process = require("child_process");
719
+ var import_node_util = require("util");
720
+
638
721
  // src/initializeBinding.ts
639
722
  var path5 = __toESM(require("path"), 1);
640
723
  var import_node_url = require("url");
@@ -802,1112 +885,1223 @@ async function resolveBindingFromInitializeParams(params, options = {}) {
802
885
  });
803
886
  }
804
887
 
805
- // src/bridgeProxy.ts
806
- var defaultLog = (msg) => {
807
- process.stderr.write(`[memoraone-mcp][bridge] ${msg}
808
- `);
809
- };
810
- function summarizeJsonRpcMethod(line) {
811
- try {
812
- const message = JSON.parse(line.trim());
813
- if (typeof message.method === "string") {
814
- return message.method;
815
- }
816
- if (message.id !== void 0) {
817
- return `response:id=${String(message.id)}`;
888
+ // src/cursorGlobalMcpConfig.ts
889
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
890
+ var MEMORAONE_PROD_API_URL = "https://api.memoraone.com";
891
+ var MEMORAONE_LOCAL_API_URL = "http://localhost:3001";
892
+ var MEMORAONE_STAGING_API_URL = "https://memora-api-staging-phbtrzocjq-uk.a.run.app";
893
+ var MEMORAONE_STAGING_API_URL_PREFIX = "https://memora-api-staging-";
894
+ function cursorMcpApiUrl(environment) {
895
+ if (environment === "local") return MEMORAONE_LOCAL_API_URL;
896
+ if (environment === "staging") return MEMORAONE_STAGING_API_URL;
897
+ return MEMORAONE_PROD_API_URL;
898
+ }
899
+ function buildMemoraoneCursorMcpServer(options) {
900
+ const env = {
901
+ MEMORAONE_API_URL: cursorMcpApiUrl(options.environment),
902
+ MEMORAONE_IDE_TYPE: "cursor"
903
+ };
904
+ if (options.workspaceRoot !== void 0) {
905
+ env[MEMORAONE_WORKSPACE_ROOT_ENV] = path6.resolve(options.workspaceRoot);
906
+ }
907
+ if (options.environment === "local") {
908
+ if (!options.cliPath) {
909
+ throw new Error("[setup-ide-files] Local Cursor MCP config requires a built CLI path.");
818
910
  }
819
- return "jsonrpc";
911
+ return {
912
+ command: "node",
913
+ args: [options.cliPath],
914
+ env
915
+ };
916
+ }
917
+ if (!options.npxPath) {
918
+ throw new Error("[setup-ide-files] Cursor MCP config requires a resolved npx path.");
919
+ }
920
+ return {
921
+ command: options.npxPath,
922
+ args: ["-y", "@memoraone/mcp@latest"],
923
+ env
924
+ };
925
+ }
926
+ async function pathExists(filePath) {
927
+ try {
928
+ await fs4.access(filePath);
929
+ return true;
820
930
  } catch {
821
- return "invalid-json";
931
+ return false;
822
932
  }
823
933
  }
824
- function connectWithRetry(socketPath, log, maxRetries, retryDelayMs, connect2) {
825
- return new Promise((resolve9, reject) => {
826
- const tryConnect = (attempt) => {
827
- connect2(socketPath).then(resolve9).catch((err) => {
828
- if (attempt >= maxRetries) {
829
- reject(err);
830
- return;
831
- }
832
- log(`connect attempt ${attempt + 1} failed, retrying in ${retryDelayMs}ms: ${String(err)}`);
833
- setTimeout(() => tryConnect(attempt + 1), retryDelayMs);
834
- });
934
+ function stripLeadingLineComments(text) {
935
+ return text.split("\n").filter((line) => !/^\s*\/\//.test(line)).join("\n");
936
+ }
937
+ function getKnownCursorGlobalMcpConfigCandidates(homeDir) {
938
+ return [path6.join(homeDir, ".cursor", "mcp.json")];
939
+ }
940
+ async function detectCursorGlobalMcpConfig(options) {
941
+ if (options?.explicitPath) {
942
+ return { ok: true, path: options.explicitPath, detectedExisting: await pathExists(options.explicitPath) };
943
+ }
944
+ const homeDir = options?.homeDir ?? os2.homedir();
945
+ const candidates = getKnownCursorGlobalMcpConfigCandidates(homeDir);
946
+ const existing = [];
947
+ for (const candidate of candidates) {
948
+ if (await pathExists(candidate)) existing.push(candidate);
949
+ }
950
+ if (existing.length > 1) {
951
+ return {
952
+ ok: false,
953
+ error: "[setup-ide-files] Multiple Cursor global MCP config paths found. Specify one explicitly.",
954
+ candidates: existing
835
955
  };
836
- tryConnect(0);
837
- });
956
+ }
957
+ if (existing.length === 1) {
958
+ return { ok: true, path: existing[0], detectedExisting: true };
959
+ }
960
+ const defaultPath = candidates[0];
961
+ if (!defaultPath) {
962
+ return {
963
+ ok: false,
964
+ error: "[setup-ide-files] No known Cursor global MCP config path.",
965
+ candidates: []
966
+ };
967
+ }
968
+ return { ok: true, path: defaultPath, detectedExisting: false };
838
969
  }
839
- async function resolveBridgeSessionBinding(params, env = process.env, options = {}) {
840
- const bridgeOptions = getBridgeBindingResolveOptions(env);
841
- return resolveBindingFromInitializeParams(params, {
842
- env,
843
- fallbackWorkspaceRoots: getEnvWorkspaceRootCandidates(),
844
- rootsListUris: options.rootsListUris,
845
- rootsListAttempted: options.rootsListAttempted,
846
- ...bridgeOptions
847
- });
970
+ function formatBackupTimestamp(d = /* @__PURE__ */ new Date()) {
971
+ const pad = (n) => String(n).padStart(2, "0");
972
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
848
973
  }
849
- async function connectOrSpawnDaemonForBinding(binding, opts) {
850
- const socketPath = getBindingSocketPath(binding, opts.env);
851
- opts.log(
852
- `target daemon socket=${socketPath} project=${binding.projectId} workspace=${binding.workspaceRoot}`
853
- );
854
- let socket;
974
+ async function isWorkingNpx(npxPath) {
855
975
  try {
856
- socket = await connectWithRetry(
857
- socketPath,
858
- opts.log,
859
- opts.maxRetries,
860
- opts.retryDelayMs,
861
- opts.connect
862
- );
863
- verifyDaemonSidecarBinding(socketPath, binding, opts.env);
864
- opts.log("reusing running daemon for session binding");
865
- return socket;
866
- } catch (err) {
867
- if (err instanceof Error && err.message.includes("Daemon socket binding mismatch")) {
868
- throw err;
976
+ if (!await pathExists(npxPath)) return false;
977
+ if (process.platform !== "win32") {
978
+ try {
979
+ await fs4.access(npxPath, fs4.constants.X_OK);
980
+ } catch {
981
+ return false;
982
+ }
869
983
  }
984
+ await execFileAsync(npxPath, ["--version"], { timeout: 1e4 });
985
+ return true;
986
+ } catch {
987
+ return false;
870
988
  }
871
- opts.log("daemon not running, spawning...");
872
- await opts.spawnDaemon(binding, socketPath);
873
- await new Promise((r) => setTimeout(r, opts.retryDelayMs));
874
- socket = await connectWithRetry(
875
- socketPath,
876
- opts.log,
877
- opts.maxRetries,
878
- opts.retryDelayMs,
879
- opts.connect
880
- );
881
- verifyDaemonSidecarBinding(socketPath, binding, opts.env);
882
- return socket;
883
989
  }
884
- var BridgeDaemonRouter = class {
885
- constructor(options) {
886
- this.activeSocket = null;
887
- this.activeBinding = null;
888
- this.socketLineReader = null;
889
- this.lastInitializeLine = null;
890
- this.pendingDeferredClientLines = [];
891
- this.handshakeDeferredClientLines = [];
892
- this.clientInitializeSeen = false;
893
- this.env = options.env ?? process.env;
894
- this.stdout = options.stdout ?? process.stdout;
895
- this.log = options.log ?? defaultLog;
896
- this.cliPath = options.cliPath;
897
- this.maxRetries = options.maxRetries ?? 5;
898
- this.retryDelayMs = options.retryDelayMs ?? 200;
899
- this.lineReader = options.lineReader ?? null;
900
- this.connectImpl = options.connect ?? ((socketPath) => new Promise((resolve9, reject) => {
901
- const socket = net.connect(socketPath, () => resolve9(socket));
902
- socket.on("error", reject);
903
- }));
904
- this.spawnDaemonImpl = options.spawnDaemon ?? (async (binding) => {
905
- const child = (0, import_node_child_process.spawn)(
906
- process.execPath,
907
- buildDaemonSpawnArgs(this.cliPath, binding.projectId, this.env),
908
- {
909
- detached: true,
910
- stdio: "ignore",
911
- env: {
912
- ...this.env,
913
- MEMORAONE_DAEMON_BINDING_B64: encodeResolvedBinding(binding)
914
- }
915
- }
916
- );
917
- child.on("exit", (code, signal) => {
918
- logInitializeDebug(
919
- this.log,
920
- this.env,
921
- `spawned daemon exit code=${code ?? "null"} signal=${signal ?? "null"} project=${binding.projectId}`
922
- );
923
- });
924
- child.unref();
925
- });
926
- }
927
- getActiveBinding() {
928
- return this.activeBinding;
990
+ async function resolveNpxPath() {
991
+ const npxName = process.platform === "win32" ? "npx.cmd" : "npx";
992
+ const candidates = [];
993
+ if (process.platform === "darwin") {
994
+ candidates.push("/opt/homebrew/bin/npx", "/usr/local/bin/npx");
995
+ } else if (process.platform === "linux") {
996
+ candidates.push("/usr/local/bin/npx");
929
997
  }
930
- hasClientInitialize() {
931
- return this.clientInitializeSeen;
998
+ const pathSep = process.platform === "win32" ? ";" : ":";
999
+ for (const dir of (process.env.PATH ?? "").split(pathSep)) {
1000
+ if (!dir) continue;
1001
+ candidates.push(path6.join(dir, npxName));
932
1002
  }
933
- async ensureDaemonForInitialize(params) {
934
- logInitializeDebug(
935
- this.log,
936
- this.env,
937
- `initialize payload: ${summarizeInitializeParamsForDebug(params)}`
938
- );
939
- const bridgeOptions = getBridgeBindingResolveOptions(this.env);
940
- const initializeRoots = extractWorkspaceRootsFromInitialize(params);
941
- let rootsListUris;
942
- let rootsListAttempted = false;
943
- const repoHint = getRepoScopedWorkspaceHint(this.env);
944
- if (initializeRoots.length === 0 && bridgeOptions.allowEnvWorkspaceFallback === false && repoHint === null) {
945
- if (!this.lineReader) {
946
- throw new Error(
947
- "[memoraone-mcp] Internal error: Cursor workspace binding requires stdin line reader for roots/list"
948
- );
949
- }
950
- rootsListAttempted = true;
951
- this.log("initialize lacks workspace roots; requesting roots/list from Cursor before binding");
952
- const rootsListResult = await requestClientRootsListUris({
953
- lineReader: this.lineReader,
954
- stdout: this.stdout,
955
- log: this.log,
956
- env: this.env,
957
- initializeParams: params
958
- });
959
- rootsListUris = rootsListResult.uris;
960
- if (rootsListResult.deferredLines.length > 0) {
961
- this.pendingDeferredClientLines = rootsListResult.deferredLines.slice();
962
- logInitializeDebug(
963
- this.log,
964
- this.env,
965
- `queued ${this.pendingDeferredClientLines.length} deferred client line(s) for replay after initialize`
966
- );
967
- }
968
- }
969
- const binding = await resolveBridgeSessionBinding(params, this.env, {
970
- rootsListUris,
971
- rootsListAttempted
972
- });
973
- const environmentLog = binding.environment !== void 0 ? ` environment=${binding.environment}` : "";
974
- this.log(
975
- `session binding project=${binding.projectId} workspace=${binding.workspaceRoot} m1=${binding.m1Path} source=${binding.bindingSource} apiKeySource=${binding.apiKeySource}${environmentLog}`
976
- );
977
- if (this.activeBinding && bindingsMatch(this.activeBinding, binding) && this.activeSocket) {
978
- return;
979
- }
980
- if (this.activeBinding && !bindingsMatch(this.activeBinding, binding)) {
981
- this.log(
982
- `session binding changed from workspace=${this.activeBinding.workspaceRoot} to workspace=${binding.workspaceRoot}; reconnecting daemon`
983
- );
984
- this.resetSessionState();
985
- this.detachSocketReader();
986
- this.activeSocket?.destroy();
987
- this.activeSocket = null;
988
- }
989
- this.activeBinding = binding;
990
- await this.connectActiveDaemon();
991
- this.log("bridge connected");
1003
+ try {
1004
+ const lookupCmd = process.platform === "win32" ? "where" : "which";
1005
+ const { stdout } = await execFileAsync(lookupCmd, [npxName], { timeout: 5e3 });
1006
+ const first = stdout.trim().split(/\r?\n/).map((line) => line.trim()).find(Boolean);
1007
+ if (first) candidates.unshift(first);
1008
+ } catch {
992
1009
  }
993
- recordClientInitialize(line) {
994
- this.lastInitializeLine = line;
995
- this.clientInitializeSeen = true;
1010
+ const seen = /* @__PURE__ */ new Set();
1011
+ for (const candidate of candidates) {
1012
+ const abs = path6.isAbsolute(candidate) ? candidate : path6.resolve(candidate);
1013
+ const key = process.platform === "win32" ? abs.toLowerCase() : abs;
1014
+ if (seen.has(key)) continue;
1015
+ seen.add(key);
1016
+ if (await isWorkingNpx(abs)) return abs;
996
1017
  }
997
- async forwardInitializeToDaemon(line) {
998
- this.recordClientInitialize(line);
999
- await this.writeToDaemon(line);
1000
- logInitializeDebug(this.log, this.env, "initialize replay to daemon");
1001
- this.log("forwarding active");
1018
+ return null;
1019
+ }
1020
+ function mergeCursorRepoMcpConfigObject(existing, writeOptions) {
1021
+ const environment = writeOptions.environment ?? "production";
1022
+ const base = existing && typeof existing === "object" ? { ...existing } : { mcpServers: {} };
1023
+ const mcpServers = typeof base.mcpServers === "object" && base.mcpServers !== null && !Array.isArray(base.mcpServers) ? { ...base.mcpServers } : {};
1024
+ mcpServers.memoraone = buildMemoraoneCursorMcpServer({
1025
+ environment,
1026
+ npxPath: writeOptions.npxPath,
1027
+ cliPath: writeOptions.cliPath,
1028
+ workspaceRoot: writeOptions.repoRoot
1029
+ });
1030
+ return { ...base, mcpServers };
1031
+ }
1032
+ function isMemoraoneManagedApiUrl(url) {
1033
+ if (typeof url !== "string" || url.length === 0) return false;
1034
+ if (url === MEMORAONE_PROD_API_URL) return true;
1035
+ if (url === MEMORAONE_LOCAL_API_URL) return true;
1036
+ return url.startsWith(MEMORAONE_STAGING_API_URL_PREFIX);
1037
+ }
1038
+ function isManagedMemoraoneCursorServer(server) {
1039
+ if (!server || typeof server !== "object") return false;
1040
+ const s = server;
1041
+ if (!Array.isArray(s.args) || s.args.length !== 2) return false;
1042
+ if (s.args[0] !== "-y" || s.args[1] !== "@memoraone/mcp@latest") return false;
1043
+ const env = s.env;
1044
+ if (!env || typeof env !== "object") return false;
1045
+ return memoraoneEnvMatchesManagedCleanupShape(env);
1046
+ }
1047
+ function cursorConfigHasManagedMemoraone(parsed) {
1048
+ if (!parsed || typeof parsed !== "object") return false;
1049
+ const mcpServers = parsed.mcpServers;
1050
+ if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) return false;
1051
+ return isManagedMemoraoneCursorServer(mcpServers.memoraone);
1052
+ }
1053
+ function memoraoneEnvMatchesManagedCleanupShape(env) {
1054
+ return env.MEMORAONE_IDE_TYPE === "cursor" && isMemoraoneManagedApiUrl(env.MEMORAONE_API_URL);
1055
+ }
1056
+ function getCursorRepoMcpConfigPath(repoRoot) {
1057
+ return path6.join(repoRoot, ".cursor", "mcp.json");
1058
+ }
1059
+ async function readCursorMcpConfigObject(configPath) {
1060
+ try {
1061
+ const raw = await fs4.readFile(configPath, "utf8");
1062
+ return JSON.parse(stripLeadingLineComments(raw));
1063
+ } catch (err) {
1064
+ const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
1065
+ if (code === "ENOENT") return null;
1066
+ throw err;
1002
1067
  }
1003
- async replayDeferredClientMessages() {
1004
- if (this.pendingDeferredClientLines.length === 0) {
1005
- return;
1006
- }
1007
- const lines = this.pendingDeferredClientLines.slice();
1008
- this.pendingDeferredClientLines = [];
1009
- this.handshakeDeferredClientLines = lines.slice();
1010
- const types = lines.map((line) => summarizeJsonRpcMethod(line));
1011
- logInitializeDebug(
1012
- this.log,
1013
- this.env,
1014
- `deferred message replay count=${lines.length} types=${JSON.stringify(types)}`
1015
- );
1016
- for (const line of lines) {
1017
- await this.writeToDaemon(line);
1018
- }
1068
+ }
1069
+ async function removeMemoraoneFromCursorGlobalConfig(options) {
1070
+ const { configPath, dryRun } = options;
1071
+ const parsed = await readCursorMcpConfigObject(configPath);
1072
+ if (!parsed || !cursorConfigHasManagedMemoraone(parsed)) {
1073
+ return { changed: false };
1019
1074
  }
1020
- async writeToDaemon(line) {
1021
- await this.ensureActiveDaemonSocket();
1022
- this.activeSocket.write(`${line}
1023
- `);
1075
+ if (dryRun) {
1076
+ return { changed: true, backupPath: `${configPath}.backup-<timestamp>` };
1024
1077
  }
1025
- resetSessionState() {
1026
- this.lastInitializeLine = null;
1027
- this.pendingDeferredClientLines = [];
1028
- this.handshakeDeferredClientLines = [];
1029
- this.clientInitializeSeen = false;
1030
- this.activeBinding = null;
1078
+ const backupPath = `${configPath}.backup-${formatBackupTimestamp()}`;
1079
+ await fs4.copyFile(configPath, backupPath);
1080
+ const mcpServers = typeof parsed.mcpServers === "object" && parsed.mcpServers !== null && !Array.isArray(parsed.mcpServers) ? { ...parsed.mcpServers } : {};
1081
+ delete mcpServers.memoraone;
1082
+ const hasOtherServers = Object.keys(mcpServers).length > 0;
1083
+ if (!hasOtherServers) {
1084
+ await fs4.unlink(configPath);
1085
+ return { changed: true, backupPath };
1031
1086
  }
1032
- async connectActiveDaemon() {
1033
- if (!this.activeBinding) {
1034
- throw new Error("[memoraone-mcp] Internal error: connectActiveDaemon without active binding");
1035
- }
1036
- const socket = await connectOrSpawnDaemonForBinding(this.activeBinding, {
1037
- env: this.env,
1038
- cliPath: this.cliPath,
1039
- log: this.log,
1040
- maxRetries: this.maxRetries,
1041
- retryDelayMs: this.retryDelayMs,
1042
- connect: this.connectImpl,
1043
- spawnDaemon: this.spawnDaemonImpl
1044
- });
1045
- this.activeSocket = socket;
1046
- this.attachSocketReader(socket);
1087
+ const next = { ...parsed, mcpServers };
1088
+ await fs4.mkdir(path6.dirname(configPath), { recursive: true });
1089
+ await fs4.writeFile(configPath, JSON.stringify(next, null, 2) + "\n", "utf8");
1090
+ return { changed: true, backupPath };
1091
+ }
1092
+ async function auditCursorMcpConfig(options) {
1093
+ const repoRoot = path6.resolve(options?.repoRoot ?? process.cwd());
1094
+ const repoConfigPath = getCursorRepoMcpConfigPath(repoRoot);
1095
+ const globalDetection = await detectCursorGlobalMcpConfig({
1096
+ homeDir: options?.homeDir,
1097
+ explicitPath: options?.explicitGlobalPath
1098
+ });
1099
+ const globalConfigPath = globalDetection.ok ? globalDetection.path : getKnownCursorGlobalMcpConfigCandidates(
1100
+ options?.homeDir ?? os2.homedir()
1101
+ )[0];
1102
+ let repoHasManagedMemoraone = false;
1103
+ try {
1104
+ const repoParsed = await readCursorMcpConfigObject(repoConfigPath);
1105
+ repoHasManagedMemoraone = cursorConfigHasManagedMemoraone(repoParsed);
1106
+ } catch {
1107
+ repoHasManagedMemoraone = false;
1047
1108
  }
1048
- async ensureActiveDaemonSocket() {
1049
- if (this.activeSocket && !this.activeSocket.destroyed) {
1050
- return;
1051
- }
1052
- if (!this.clientInitializeSeen || !this.lastInitializeLine || !this.activeBinding) {
1053
- throw new Error("[memoraone-mcp] MCP request before initialize");
1109
+ let globalHasManagedMemoraone = false;
1110
+ if (globalDetection.ok) {
1111
+ try {
1112
+ const globalParsed = await readCursorMcpConfigObject(globalConfigPath);
1113
+ globalHasManagedMemoraone = cursorConfigHasManagedMemoraone(globalParsed);
1114
+ } catch {
1115
+ globalHasManagedMemoraone = false;
1054
1116
  }
1055
- this.log("daemon socket unavailable; reconnecting for session binding");
1056
- await this.connectActiveDaemon();
1057
- logInitializeDebug(this.log, this.env, "initialize replay to daemon after reconnect");
1058
- this.activeSocket.write(`${this.lastInitializeLine}
1059
- `);
1060
- if (this.handshakeDeferredClientLines.length > 0) {
1061
- const types = this.handshakeDeferredClientLines.map(
1062
- (deferredLine) => summarizeJsonRpcMethod(deferredLine)
1117
+ }
1118
+ return {
1119
+ repoConfigPath,
1120
+ repoHasManagedMemoraone,
1121
+ globalConfigPath,
1122
+ globalHasManagedMemoraone,
1123
+ conflict: repoHasManagedMemoraone && globalHasManagedMemoraone
1124
+ };
1125
+ }
1126
+ function logCursorMcpConfigAudit(prefix, audit) {
1127
+ console.log(`${prefix} Cursor MCP config audit:`);
1128
+ console.log(
1129
+ `${prefix} repo ${audit.repoConfigPath}: managed memoraone=${audit.repoHasManagedMemoraone}`
1130
+ );
1131
+ console.log(
1132
+ `${prefix} global ${audit.globalConfigPath}: managed memoraone=${audit.globalHasManagedMemoraone}`
1133
+ );
1134
+ if (audit.conflict) {
1135
+ console.warn(
1136
+ `${prefix} WARNING: Both repo and global Cursor MCP define memoraone. Global shared MCP cannot bind per-window repos; remove global memoraone and use repo .cursor/mcp.json only.`
1137
+ );
1138
+ } else if (audit.globalHasManagedMemoraone && !audit.repoHasManagedMemoraone) {
1139
+ console.warn(
1140
+ `${prefix} WARNING: Cursor global MCP has memoraone but this repo lacks .cursor/mcp.json. Global MCP shares one process across windows (first-window-wins roots). Run setup-ide-files --cursor in this repo.`
1141
+ );
1142
+ } else if (audit.repoHasManagedMemoraone && !audit.globalHasManagedMemoraone) {
1143
+ console.log(
1144
+ `${prefix} Cursor MCP is repo-scoped (.cursor/mcp.json) with no global memoraone entry (recommended for multi-repo windows).`
1145
+ );
1146
+ }
1147
+ }
1148
+ function logCursorMcpCliSummary(info, dryRun, opts) {
1149
+ const { repoConfigPath, repoOutcome, npxPath, cliPath, repoBackupPath, globalConfigPath, globalMemoraoneRemoved, globalBackupPath } = info;
1150
+ const interactive = opts?.forInteractivePostSetup === true;
1151
+ console.log(`[setup-ide-files] Cursor repo MCP config: ${repoConfigPath}`);
1152
+ if (cliPath) {
1153
+ console.log(`[setup-ide-files] Resolved local CLI: ${cliPath}`);
1154
+ } else if (npxPath) {
1155
+ console.log(`[setup-ide-files] Resolved npx: ${npxPath}`);
1156
+ }
1157
+ if (repoBackupPath) {
1158
+ console.log(`[setup-ide-files] Cursor repo MCP config backup: ${repoBackupPath}`);
1159
+ }
1160
+ if (!interactive) {
1161
+ if (repoOutcome === "created") {
1162
+ console.log(
1163
+ dryRun ? `[setup-ide-files] Cursor repo MCP config would be created: ${repoConfigPath}` : `[setup-ide-files] Cursor repo MCP config created: ${repoConfigPath}`
1063
1164
  );
1064
- logInitializeDebug(
1065
- this.log,
1066
- this.env,
1067
- `deferred message replay after reconnect count=${this.handshakeDeferredClientLines.length} types=${JSON.stringify(types)}`
1165
+ } else if (repoOutcome === "updated") {
1166
+ console.log(
1167
+ dryRun ? `[setup-ide-files] Cursor repo MCP config would be updated: ${repoConfigPath}` : `[setup-ide-files] Cursor repo MCP config updated: ${repoConfigPath}`
1068
1168
  );
1069
- for (const deferredLine of this.handshakeDeferredClientLines) {
1070
- this.activeSocket.write(`${deferredLine}
1071
- `);
1072
- }
1169
+ } else if (repoOutcome === "skipped") {
1170
+ console.log(`[setup-ide-files] Cursor repo MCP config unchanged: ${repoConfigPath}`);
1073
1171
  }
1074
1172
  }
1075
- attachSocketReader(socket) {
1076
- this.detachSocketReader();
1077
- this.socketLineReader = readline2.createInterface({ input: socket, crlfDelay: Infinity });
1078
- this.socketLineReader.on("line", (line) => {
1079
- this.stdout.write(`${line}
1080
- `);
1081
- });
1082
- socket.on("close", (hadError) => {
1083
- if (this.activeSocket === socket) {
1084
- logInitializeDebug(
1085
- this.log,
1086
- this.env,
1087
- `daemon socket closed hadError=${String(hadError)}`
1088
- );
1089
- this.log("daemon socket closed; bridge stays alive for reconnect");
1090
- this.detachSocketReader();
1091
- this.activeSocket = null;
1092
- }
1093
- });
1094
- socket.on("error", (err) => {
1095
- this.log(`socket error: ${String(err)}`);
1096
- logInitializeDebug(this.log, this.env, `daemon socket error: ${String(err)}`);
1097
- if (this.activeSocket === socket) {
1098
- this.detachSocketReader();
1099
- this.activeSocket = null;
1100
- }
1101
- });
1102
- }
1103
- detachSocketReader() {
1104
- if (this.socketLineReader) {
1105
- this.socketLineReader.close();
1106
- this.socketLineReader = null;
1173
+ if (globalMemoraoneRemoved && globalConfigPath) {
1174
+ console.log(
1175
+ dryRun ? `[setup-ide-files] Would remove memoraone from Cursor global MCP config: ${globalConfigPath}` : `[setup-ide-files] Removed memoraone from Cursor global MCP config: ${globalConfigPath}`
1176
+ );
1177
+ if (globalBackupPath) {
1178
+ console.log(`[setup-ide-files] Cursor global MCP config backup: ${globalBackupPath}`);
1107
1179
  }
1180
+ } else if (globalConfigPath) {
1181
+ console.log(
1182
+ `[setup-ide-files] Cursor global MCP config unchanged (no managed memoraone to remove): ${globalConfigPath}`
1183
+ );
1108
1184
  }
1109
- };
1110
- async function runBridgeProxy(options) {
1111
- ensureBaseDir();
1112
- const stdin = options.stdin ?? process.stdin;
1113
- const stdout = options.stdout ?? process.stdout;
1114
- const log = options.log ?? defaultLog;
1115
- const lineReader = options.lineReader ?? new StdioLineReader(stdin);
1116
- const router = new BridgeDaemonRouter({ ...options, stdout, lineReader });
1117
- while (true) {
1118
- const line = await lineReader.readLine();
1119
- if (line === null) {
1120
- break;
1121
- }
1122
- const trimmed = line.trim();
1123
- if (trimmed === "") {
1124
- continue;
1125
- }
1126
- let message;
1127
- try {
1128
- message = JSON.parse(trimmed);
1129
- } catch (err) {
1130
- throw new Error(`[memoraone-mcp] Invalid JSON-RPC on stdin: ${String(err)}`);
1131
- }
1132
- if (message.method === "initialize") {
1133
- log("resolve binding from initialize request before daemon connect");
1134
- const params = message.params ?? {};
1135
- await router.ensureDaemonForInitialize(params);
1136
- await router.forwardInitializeToDaemon(trimmed);
1137
- await router.replayDeferredClientMessages();
1138
- continue;
1139
- }
1140
- await router.writeToDaemon(trimmed);
1185
+ console.log(
1186
+ "[setup-ide-files] Each Cursor window uses this repo\u2019s .cursor/mcp.json (separate MCP process per repo)."
1187
+ );
1188
+ if (!interactive) {
1189
+ console.log(
1190
+ "[setup-ide-files] Fully quit Cursor and reopen this repo for MCP changes to take effect."
1191
+ );
1141
1192
  }
1142
1193
  }
1143
1194
 
1144
- // src/setupIdeFiles.ts
1145
- var fs8 = __toESM(require("fs/promises"), 1);
1146
- var os4 = __toESM(require("os"), 1);
1147
- var path10 = __toESM(require("path"), 1);
1148
-
1149
1195
  // src/cleanup.ts
1150
- var fs5 = __toESM(require("fs/promises"), 1);
1151
- var path7 = __toESM(require("path"), 1);
1152
- var readline3 = __toESM(require("readline/promises"), 1);
1153
- var import_node_child_process3 = require("child_process");
1154
- var import_node_util2 = require("util");
1155
- var import_node_process = require("process");
1156
-
1157
- // src/cursorGlobalMcpConfig.ts
1158
- var fs4 = __toESM(require("fs/promises"), 1);
1159
- var os2 = __toESM(require("os"), 1);
1160
- var path6 = __toESM(require("path"), 1);
1161
- var import_node_child_process2 = require("child_process");
1162
- var import_node_util = require("util");
1163
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
1164
- var MEMORAONE_PROD_API_URL = "https://api.memoraone.com";
1165
- var MEMORAONE_LOCAL_API_URL = "http://localhost:3001";
1166
- var MEMORAONE_STAGING_API_URL = "https://memora-api-staging-phbtrzocjq-uk.a.run.app";
1167
- var MEMORAONE_STAGING_API_URL_PREFIX = "https://memora-api-staging-";
1168
- function cursorMcpApiUrl(environment) {
1169
- if (environment === "local") return MEMORAONE_LOCAL_API_URL;
1170
- if (environment === "staging") return MEMORAONE_STAGING_API_URL;
1171
- return MEMORAONE_PROD_API_URL;
1196
+ var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
1197
+ var DAEMON_PROJECT_ID_RE = /--project-id\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i;
1198
+ var PROJECT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1199
+ var MEMORAONE_MCP_COMMAND_RE = /memoraone-mcp|memoraOne-mcp|@memoraone\/mcp/;
1200
+ var CLEANUP_PROJECT_ID_REQUIRED_ERROR = "Provide --project-id <id> or run from a folder containing memoraone.m1.";
1201
+ function isMemoraoneMcpCommandLine(commandLine) {
1202
+ return MEMORAONE_MCP_COMMAND_RE.test(commandLine);
1172
1203
  }
1173
- function buildMemoraoneCursorMcpServer(options) {
1174
- const env = {
1175
- MEMORAONE_API_URL: cursorMcpApiUrl(options.environment),
1176
- MEMORAONE_IDE_TYPE: "cursor"
1177
- };
1178
- if (options.workspaceRoot !== void 0) {
1179
- env[MEMORAONE_WORKSPACE_ROOT_ENV] = path6.resolve(options.workspaceRoot);
1204
+ function parseDaemonProjectIdFromCommandLine(commandLine) {
1205
+ if (!commandLine.includes("--daemon")) {
1206
+ return null;
1180
1207
  }
1181
- if (options.environment === "local") {
1182
- if (!options.cliPath) {
1183
- throw new Error("[setup-ide-files] Local Cursor MCP config requires a built CLI path.");
1184
- }
1185
- return {
1186
- command: "node",
1187
- args: [options.cliPath],
1188
- env
1189
- };
1208
+ const match = commandLine.match(DAEMON_PROJECT_ID_RE);
1209
+ return match ? match[1].toLowerCase() : null;
1210
+ }
1211
+ function isDaemonProcessForProject(commandLine, projectId) {
1212
+ if (!isMemoraoneMcpCommandLine(commandLine)) {
1213
+ return false;
1190
1214
  }
1191
- if (!options.npxPath) {
1192
- throw new Error("[setup-ide-files] Cursor MCP config requires a resolved npx path.");
1215
+ const normalized = projectId.trim().toLowerCase();
1216
+ if (!commandLine.includes(`--project-id ${normalized}`)) {
1217
+ return false;
1193
1218
  }
1194
- return {
1195
- command: options.npxPath,
1196
- args: ["-y", "@memoraone/mcp@latest"],
1197
- env
1198
- };
1219
+ const daemonProjectId = parseDaemonProjectIdFromCommandLine(commandLine);
1220
+ return daemonProjectId !== null && daemonProjectId === normalized;
1199
1221
  }
1200
- async function pathExists(filePath) {
1201
- try {
1202
- await fs4.access(filePath);
1203
- return true;
1204
- } catch {
1222
+ function parseDaemonIdeFromCommandLine(commandLine) {
1223
+ if (!commandLine.includes("--daemon")) {
1224
+ return void 0;
1225
+ }
1226
+ return parseIdeTypeFromCommandLine(commandLine);
1227
+ }
1228
+ function isDaemonProcessForProjectAndIde(commandLine, projectId, ide) {
1229
+ if (!isDaemonProcessForProject(commandLine, projectId)) {
1205
1230
  return false;
1206
1231
  }
1232
+ const daemonIde = parseDaemonIdeFromCommandLine(commandLine);
1233
+ return daemonIde !== void 0 && daemonIde === ide;
1207
1234
  }
1208
- function stripLeadingLineComments(text) {
1209
- return text.split("\n").filter((line) => !/^\s*\/\//.test(line)).join("\n");
1235
+ function parseDaemonProcessLines(lines) {
1236
+ const processes = [];
1237
+ for (const line of lines) {
1238
+ const trimmed = line.trim();
1239
+ if (!trimmed) continue;
1240
+ const spaceIdx = trimmed.indexOf(" ");
1241
+ if (spaceIdx <= 0) continue;
1242
+ const pid = Number.parseInt(trimmed.slice(0, spaceIdx), 10);
1243
+ if (!Number.isFinite(pid) || pid <= 0) continue;
1244
+ const command = trimmed.slice(spaceIdx + 1);
1245
+ if (!isMemoraoneMcpCommandLine(command)) continue;
1246
+ const projectId = parseDaemonProjectIdFromCommandLine(command);
1247
+ if (projectId === null) continue;
1248
+ const ide = parseDaemonIdeFromCommandLine(command);
1249
+ processes.push({ pid, command, projectId, ...ide !== void 0 ? { ide } : {} });
1250
+ }
1251
+ return processes;
1210
1252
  }
1211
- function getKnownCursorGlobalMcpConfigCandidates(homeDir) {
1212
- return [path6.join(homeDir, ".cursor", "mcp.json")];
1253
+ function normalizeCleanupProjectId(projectId) {
1254
+ const trimmed = projectId.trim();
1255
+ if (!PROJECT_ID_RE.test(trimmed)) {
1256
+ return { error: `Invalid project id: ${projectId}` };
1257
+ }
1258
+ return trimmed.toLowerCase();
1213
1259
  }
1214
- async function detectCursorGlobalMcpConfig(options) {
1215
- if (options?.explicitPath) {
1216
- return { ok: true, path: options.explicitPath, detectedExisting: await pathExists(options.explicitPath) };
1260
+ async function defaultListDaemonProcesses() {
1261
+ const { stdout } = await execFileAsync2("ps", ["-eo", "pid=,args="], {
1262
+ maxBuffer: 10 * 1024 * 1024
1263
+ });
1264
+ return parseDaemonProcessLines(stdout.split("\n"));
1265
+ }
1266
+ async function defaultListSocketPaths(projectId) {
1267
+ const baseDir = getMcpBaseDir();
1268
+ let entries;
1269
+ try {
1270
+ entries = await fs5.readdir(baseDir);
1271
+ } catch (err) {
1272
+ const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
1273
+ if (code === "ENOENT") {
1274
+ return [];
1275
+ }
1276
+ throw err;
1217
1277
  }
1218
- const homeDir = options?.homeDir ?? os2.homedir();
1219
- const candidates = getKnownCursorGlobalMcpConfigCandidates(homeDir);
1220
- const existing = [];
1221
- for (const candidate of candidates) {
1222
- if (await pathExists(candidate)) existing.push(candidate);
1278
+ const paths = [];
1279
+ for (const name of entries) {
1280
+ if (!name.endsWith(".sock") || !isMemoraoneSocketFilename(name)) {
1281
+ continue;
1282
+ }
1283
+ const socketPath = path7.join(baseDir, name);
1284
+ if (projectId === null) {
1285
+ paths.push(socketPath);
1286
+ continue;
1287
+ }
1288
+ const normalizedProjectId = projectId.trim().toLowerCase();
1289
+ if (isLegacySocketFilename(name) && isSocketFilenameForProject(name, normalizedProjectId)) {
1290
+ paths.push(socketPath);
1291
+ continue;
1292
+ }
1293
+ if (isHashSocketFilename(name)) {
1294
+ const record = readBindingSidecarRecord(socketPath);
1295
+ if (record?.projectId.trim().toLowerCase() === normalizedProjectId) {
1296
+ paths.push(socketPath);
1297
+ }
1298
+ }
1223
1299
  }
1224
- if (existing.length > 1) {
1225
- return {
1226
- ok: false,
1227
- error: "[setup-ide-files] Multiple Cursor global MCP config paths found. Specify one explicitly.",
1228
- candidates: existing
1229
- };
1300
+ return paths.sort();
1301
+ }
1302
+ async function defaultListSocketPathsForM1Path(m1Path) {
1303
+ const resolvedM1 = path7.resolve(m1Path);
1304
+ const baseDir = getMcpBaseDir();
1305
+ let entries;
1306
+ try {
1307
+ entries = await fs5.readdir(baseDir);
1308
+ } catch (err) {
1309
+ const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
1310
+ if (code === "ENOENT") {
1311
+ return [];
1312
+ }
1313
+ throw err;
1230
1314
  }
1231
- if (existing.length === 1) {
1232
- return { ok: true, path: existing[0], detectedExisting: true };
1315
+ const paths = [];
1316
+ for (const name of entries) {
1317
+ if (!name.endsWith(".sock") || !isHashSocketFilename(name)) {
1318
+ continue;
1319
+ }
1320
+ const socketPath = path7.join(baseDir, name);
1321
+ const record = readBindingSidecarRecord(socketPath);
1322
+ if (!record?.m1Path) continue;
1323
+ if (path7.resolve(record.m1Path) === resolvedM1) {
1324
+ paths.push(socketPath);
1325
+ }
1233
1326
  }
1234
- const defaultPath = candidates[0];
1235
- if (!defaultPath) {
1236
- return {
1237
- ok: false,
1238
- error: "[setup-ide-files] No known Cursor global MCP config path.",
1239
- candidates: []
1240
- };
1327
+ return paths.sort();
1328
+ }
1329
+ async function filterSocketPathsByIdeForCleanup(socketPaths, projectId, ide, m1Path) {
1330
+ if (ide === void 0) return socketPaths;
1331
+ const normalizedProjectId = projectId.trim().toLowerCase();
1332
+ const resolvedM1 = m1Path ? path7.resolve(m1Path) : null;
1333
+ const filtered = [];
1334
+ for (const socketPath of socketPaths) {
1335
+ const basename5 = path7.basename(socketPath);
1336
+ if (isLegacySocketFilename(basename5)) {
1337
+ if (isSocketFilenameForProjectAndIde(basename5, normalizedProjectId, ide)) {
1338
+ filtered.push(socketPath);
1339
+ }
1340
+ continue;
1341
+ }
1342
+ if (isHashSocketFilename(basename5)) {
1343
+ const record = readBindingSidecarRecord(socketPath);
1344
+ if (!record || record.ideType !== ide) continue;
1345
+ const sameProject = record.projectId.trim().toLowerCase() === normalizedProjectId;
1346
+ const sameM1 = resolvedM1 !== null && path7.resolve(record.m1Path) === resolvedM1;
1347
+ if (sameProject || sameM1) {
1348
+ filtered.push(socketPath);
1349
+ }
1350
+ }
1241
1351
  }
1242
- return { ok: true, path: defaultPath, detectedExisting: false };
1352
+ return filtered;
1243
1353
  }
1244
- function formatBackupTimestamp(d = /* @__PURE__ */ new Date()) {
1245
- const pad = (n) => String(n).padStart(2, "0");
1246
- return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
1354
+ async function defaultKillProcess(pid) {
1355
+ process.kill(pid, "SIGTERM");
1247
1356
  }
1248
- async function isWorkingNpx(npxPath) {
1357
+ async function defaultRemoveSocket(socketPath) {
1358
+ await fs5.unlink(socketPath);
1249
1359
  try {
1250
- if (!await pathExists(npxPath)) return false;
1251
- if (process.platform !== "win32") {
1252
- try {
1253
- await fs4.access(npxPath, fs4.constants.X_OK);
1254
- } catch {
1255
- return false;
1256
- }
1257
- }
1258
- await execFileAsync(npxPath, ["--version"], { timeout: 1e4 });
1259
- return true;
1360
+ await fs5.unlink(bindingSidecarPath(socketPath));
1260
1361
  } catch {
1261
- return false;
1262
1362
  }
1263
1363
  }
1264
- async function resolveNpxPath() {
1265
- const npxName = process.platform === "win32" ? "npx.cmd" : "npx";
1266
- const candidates = [];
1267
- if (process.platform === "darwin") {
1268
- candidates.push("/opt/homebrew/bin/npx", "/usr/local/bin/npx");
1269
- } else if (process.platform === "linux") {
1270
- candidates.push("/usr/local/bin/npx");
1364
+ async function defaultConfirm(message) {
1365
+ if (!import_node_process.stdin.isTTY) {
1366
+ return false;
1271
1367
  }
1272
- const pathSep = process.platform === "win32" ? ";" : ":";
1273
- for (const dir of (process.env.PATH ?? "").split(pathSep)) {
1274
- if (!dir) continue;
1275
- candidates.push(path6.join(dir, npxName));
1368
+ const rl = readline2.createInterface({ input: import_node_process.stdin, output: import_node_process.stdout });
1369
+ try {
1370
+ const answer = await rl.question(`${message} [y/N] `);
1371
+ return /^y(es)?$/i.test(answer.trim());
1372
+ } finally {
1373
+ rl.close();
1276
1374
  }
1375
+ }
1376
+ async function resolveCleanupTarget(cwd) {
1277
1377
  try {
1278
- const lookupCmd = process.platform === "win32" ? "where" : "which";
1279
- const { stdout } = await execFileAsync(lookupCmd, [npxName], { timeout: 5e3 });
1280
- const first = stdout.trim().split(/\r?\n/).map((line) => line.trim()).find(Boolean);
1281
- if (first) candidates.unshift(first);
1378
+ const binding = await resolveAuthoritativeBinding([path7.resolve(cwd)]);
1379
+ return {
1380
+ workspaceRoot: binding.workspaceRoot,
1381
+ m1Path: binding.m1Path,
1382
+ projectId: binding.projectId
1383
+ };
1282
1384
  } catch {
1385
+ return { error: CLEANUP_PROJECT_ID_REQUIRED_ERROR };
1283
1386
  }
1284
- const seen = /* @__PURE__ */ new Set();
1285
- for (const candidate of candidates) {
1286
- const abs = path6.isAbsolute(candidate) ? candidate : path6.resolve(candidate);
1287
- const key = process.platform === "win32" ? abs.toLowerCase() : abs;
1288
- if (seen.has(key)) continue;
1289
- seen.add(key);
1290
- if (await isWorkingNpx(abs)) return abs;
1291
- }
1292
- return null;
1293
- }
1294
- function mergeCursorRepoMcpConfigObject(existing, writeOptions) {
1295
- const environment = writeOptions.environment ?? "production";
1296
- const base = existing && typeof existing === "object" ? { ...existing } : { mcpServers: {} };
1297
- const mcpServers = typeof base.mcpServers === "object" && base.mcpServers !== null && !Array.isArray(base.mcpServers) ? { ...base.mcpServers } : {};
1298
- mcpServers.memoraone = buildMemoraoneCursorMcpServer({
1299
- environment,
1300
- npxPath: writeOptions.npxPath,
1301
- cliPath: writeOptions.cliPath,
1302
- workspaceRoot: writeOptions.repoRoot
1303
- });
1304
- return { ...base, mcpServers };
1305
- }
1306
- function isMemoraoneManagedApiUrl(url) {
1307
- if (typeof url !== "string" || url.length === 0) return false;
1308
- if (url === MEMORAONE_PROD_API_URL) return true;
1309
- if (url === MEMORAONE_LOCAL_API_URL) return true;
1310
- return url.startsWith(MEMORAONE_STAGING_API_URL_PREFIX);
1311
- }
1312
- function isManagedMemoraoneCursorServer(server) {
1313
- if (!server || typeof server !== "object") return false;
1314
- const s = server;
1315
- if (!Array.isArray(s.args) || s.args.length !== 2) return false;
1316
- if (s.args[0] !== "-y" || s.args[1] !== "@memoraone/mcp@latest") return false;
1317
- const env = s.env;
1318
- if (!env || typeof env !== "object") return false;
1319
- return memoraoneEnvMatchesManagedCleanupShape(env);
1320
- }
1321
- function cursorConfigHasManagedMemoraone(parsed) {
1322
- if (!parsed || typeof parsed !== "object") return false;
1323
- const mcpServers = parsed.mcpServers;
1324
- if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) return false;
1325
- return isManagedMemoraoneCursorServer(mcpServers.memoraone);
1326
1387
  }
1327
- function memoraoneEnvMatchesManagedCleanupShape(env) {
1328
- return env.MEMORAONE_IDE_TYPE === "cursor" && isMemoraoneManagedApiUrl(env.MEMORAONE_API_URL);
1388
+ function filterProcessesForScope(processes, projectId) {
1389
+ if (projectId === null) {
1390
+ return { matching: processes, skipped: [] };
1391
+ }
1392
+ const normalized = projectId.toLowerCase();
1393
+ const matching = [];
1394
+ const skipped = [];
1395
+ for (const proc of processes) {
1396
+ if (proc.projectId === normalized) {
1397
+ matching.push(proc);
1398
+ } else {
1399
+ skipped.push(proc);
1400
+ }
1401
+ }
1402
+ return { matching, skipped };
1329
1403
  }
1330
- function getCursorRepoMcpConfigPath(repoRoot) {
1331
- return path6.join(repoRoot, ".cursor", "mcp.json");
1404
+ function logPrefix(dryRun) {
1405
+ return dryRun ? "[cleanup][dry-run]" : "[cleanup]";
1332
1406
  }
1333
- async function readCursorMcpConfigObject(configPath) {
1334
- try {
1335
- const raw = await fs4.readFile(configPath, "utf8");
1336
- return JSON.parse(stripLeadingLineComments(raw));
1337
- } catch (err) {
1338
- const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
1339
- if (code === "ENOENT") return null;
1340
- throw err;
1407
+ function logReconnectNotice(opts, prefix, ide) {
1408
+ if (opts.quiet) return;
1409
+ if (ide) {
1410
+ console.log(
1411
+ `${prefix} Note: Valid ${ide} connections for this project may disconnect temporarily; they should reconnect automatically. Stale connections will remain cleared.`
1412
+ );
1413
+ } else {
1414
+ console.log(
1415
+ `${prefix} Note: Valid IDE connections for this project may disconnect temporarily; they should reconnect automatically. Stale connections will remain cleared.`
1416
+ );
1341
1417
  }
1342
1418
  }
1343
- async function removeMemoraoneFromCursorGlobalConfig(options) {
1344
- const { configPath, dryRun } = options;
1345
- const parsed = await readCursorMcpConfigObject(configPath);
1346
- if (!parsed || !cursorConfigHasManagedMemoraone(parsed)) {
1347
- return { changed: false };
1348
- }
1349
- if (dryRun) {
1350
- return { changed: true, backupPath: `${configPath}.backup-<timestamp>` };
1351
- }
1352
- const backupPath = `${configPath}.backup-${formatBackupTimestamp()}`;
1353
- await fs4.copyFile(configPath, backupPath);
1354
- const mcpServers = typeof parsed.mcpServers === "object" && parsed.mcpServers !== null && !Array.isArray(parsed.mcpServers) ? { ...parsed.mcpServers } : {};
1355
- delete mcpServers.memoraone;
1356
- const hasOtherServers = Object.keys(mcpServers).length > 0;
1357
- if (!hasOtherServers) {
1358
- await fs4.unlink(configPath);
1359
- return { changed: true, backupPath };
1419
+ function cleanupLog(opts, message) {
1420
+ if (!opts.quiet) {
1421
+ console.log(message);
1360
1422
  }
1361
- const next = { ...parsed, mcpServers };
1362
- await fs4.mkdir(path6.dirname(configPath), { recursive: true });
1363
- await fs4.writeFile(configPath, JSON.stringify(next, null, 2) + "\n", "utf8");
1364
- return { changed: true, backupPath };
1365
1423
  }
1366
- async function auditCursorMcpConfig(options) {
1367
- const repoRoot = path6.resolve(options?.repoRoot ?? process.cwd());
1368
- const repoConfigPath = getCursorRepoMcpConfigPath(repoRoot);
1369
- const globalDetection = await detectCursorGlobalMcpConfig({
1370
- homeDir: options?.homeDir,
1371
- explicitPath: options?.explicitGlobalPath
1372
- });
1373
- const globalConfigPath = globalDetection.ok ? globalDetection.path : getKnownCursorGlobalMcpConfigCandidates(
1374
- options?.homeDir ?? os2.homedir()
1375
- )[0];
1376
- let repoHasManagedMemoraone = false;
1377
- try {
1378
- const repoParsed = await readCursorMcpConfigObject(repoConfigPath);
1379
- repoHasManagedMemoraone = cursorConfigHasManagedMemoraone(repoParsed);
1380
- } catch {
1381
- repoHasManagedMemoraone = false;
1424
+ function cleanupWarn(opts, message) {
1425
+ if (!opts.quiet) {
1426
+ console.warn(message);
1382
1427
  }
1383
- let globalHasManagedMemoraone = false;
1384
- if (globalDetection.ok) {
1385
- try {
1386
- const globalParsed = await readCursorMcpConfigObject(globalConfigPath);
1387
- globalHasManagedMemoraone = cursorConfigHasManagedMemoraone(globalParsed);
1388
- } catch {
1389
- globalHasManagedMemoraone = false;
1428
+ }
1429
+ async function runCleanup(opts) {
1430
+ const listProcesses = opts.listProcesses ?? defaultListDaemonProcesses;
1431
+ const listSocketPaths = opts.listSocketPaths ?? defaultListSocketPaths;
1432
+ const killProcess = opts.killProcess ?? defaultKillProcess;
1433
+ const removeSocket = opts.removeSocket ?? defaultRemoveSocket;
1434
+ const confirm = opts.confirm ?? defaultConfirm;
1435
+ const prefix = logPrefix(opts.dryRun);
1436
+ let targetProjectId = null;
1437
+ let workspaceRoot;
1438
+ let m1Path;
1439
+ if (opts.allProjects) {
1440
+ if (opts.projectId) {
1441
+ return {
1442
+ exitCode: 1,
1443
+ killedPids: [],
1444
+ removedSockets: [],
1445
+ skippedProcesses: [],
1446
+ error: "Cannot combine --all-projects with --project-id."
1447
+ };
1448
+ }
1449
+ cleanupLog(opts, `${prefix} Mode: all projects (--all-projects)`);
1450
+ cleanupWarn(
1451
+ opts,
1452
+ `${prefix} WARNING: This stops every MemoraOne MCP daemon and removes all project sockets under ${getMcpBaseDir()}.`
1453
+ );
1454
+ } else if (opts.projectId) {
1455
+ const normalized = normalizeCleanupProjectId(opts.projectId);
1456
+ if (typeof normalized !== "string") {
1457
+ return { exitCode: 1, killedPids: [], removedSockets: [], skippedProcesses: [], error: normalized.error };
1458
+ }
1459
+ targetProjectId = normalized;
1460
+ cleanupLog(opts, `${prefix} Project id: ${targetProjectId}`);
1461
+ if (opts.ide) {
1462
+ cleanupLog(opts, `${prefix} IDE filter: ${opts.ide}`);
1463
+ }
1464
+ } else {
1465
+ const target = await resolveCleanupTarget(opts.cwd);
1466
+ if ("error" in target) {
1467
+ return { exitCode: 1, killedPids: [], removedSockets: [], skippedProcesses: [], error: target.error };
1468
+ }
1469
+ targetProjectId = target.projectId;
1470
+ workspaceRoot = target.workspaceRoot;
1471
+ m1Path = target.m1Path;
1472
+ cleanupLog(opts, `${prefix} Workspace root: ${workspaceRoot}`);
1473
+ cleanupLog(opts, `${prefix} memoraone.m1: ${m1Path}`);
1474
+ cleanupLog(opts, `${prefix} Project id: ${targetProjectId}`);
1475
+ if (opts.ide) {
1476
+ cleanupLog(opts, `${prefix} IDE filter: ${opts.ide}`);
1477
+ }
1478
+ if (workspaceRoot && (!opts.ide || opts.ide === "cursor")) {
1479
+ try {
1480
+ const cursorAudit = await auditCursorMcpConfig({ repoRoot: workspaceRoot });
1481
+ logCursorMcpConfigAudit(prefix, cursorAudit);
1482
+ } catch (err) {
1483
+ cleanupWarn(opts, `${prefix} Cursor MCP config audit failed: ${String(err)}`);
1484
+ }
1390
1485
  }
1391
1486
  }
1392
- return {
1393
- repoConfigPath,
1394
- repoHasManagedMemoraone,
1395
- globalConfigPath,
1396
- globalHasManagedMemoraone,
1397
- conflict: repoHasManagedMemoraone && globalHasManagedMemoraone
1398
- };
1399
- }
1400
- function logCursorMcpConfigAudit(prefix, audit) {
1401
- console.log(`${prefix} Cursor MCP config audit:`);
1402
- console.log(
1403
- `${prefix} repo ${audit.repoConfigPath}: managed memoraone=${audit.repoHasManagedMemoraone}`
1404
- );
1405
- console.log(
1406
- `${prefix} global ${audit.globalConfigPath}: managed memoraone=${audit.globalHasManagedMemoraone}`
1407
- );
1408
- if (audit.conflict) {
1409
- console.warn(
1410
- `${prefix} WARNING: Both repo and global Cursor MCP define memoraone. Global shared MCP cannot bind per-window repos; remove global memoraone and use repo .cursor/mcp.json only.`
1411
- );
1412
- } else if (audit.globalHasManagedMemoraone && !audit.repoHasManagedMemoraone) {
1413
- console.warn(
1414
- `${prefix} WARNING: Cursor global MCP has memoraone but this repo lacks .cursor/mcp.json. Global MCP shares one process across windows (first-window-wins roots). Run setup-ide-files --cursor in this repo.`
1415
- );
1416
- } else if (audit.repoHasManagedMemoraone && !audit.globalHasManagedMemoraone) {
1417
- console.log(
1418
- `${prefix} Cursor MCP is repo-scoped (.cursor/mcp.json) with no global memoraone entry (recommended for multi-repo windows).`
1419
- );
1487
+ if (targetProjectId !== null || opts.ide) {
1488
+ logReconnectNotice(opts, prefix, opts.ide);
1420
1489
  }
1421
- }
1422
- function logCursorMcpCliSummary(info, dryRun) {
1423
- const { repoConfigPath, repoOutcome, npxPath, cliPath, repoBackupPath, globalConfigPath, globalMemoraoneRemoved, globalBackupPath } = info;
1424
- console.log(`[setup-ide-files] Cursor repo MCP config: ${repoConfigPath}`);
1425
- if (cliPath) {
1426
- console.log(`[setup-ide-files] Resolved local CLI: ${cliPath}`);
1427
- } else if (npxPath) {
1428
- console.log(`[setup-ide-files] Resolved npx: ${npxPath}`);
1490
+ const allDaemonProcesses = await listProcesses();
1491
+ const { matching: projectProcesses, skipped: skippedProcesses } = filterProcessesForScope(
1492
+ allDaemonProcesses,
1493
+ targetProjectId
1494
+ );
1495
+ let processesToStop = projectProcesses;
1496
+ const ideSkippedProcesses = [];
1497
+ if (opts.ide && targetProjectId !== null) {
1498
+ processesToStop = [];
1499
+ for (const proc of projectProcesses) {
1500
+ if (proc.ide === opts.ide) {
1501
+ processesToStop.push(proc);
1502
+ } else if (proc.ide === void 0) {
1503
+ ideSkippedProcesses.push(proc);
1504
+ cleanupLog(
1505
+ opts,
1506
+ `${prefix} Skipped daemon pid=${proc.pid} because IDE could not be safely determined.`
1507
+ );
1508
+ } else {
1509
+ ideSkippedProcesses.push(proc);
1510
+ cleanupLog(
1511
+ opts,
1512
+ `${prefix} Skipped daemon pid=${proc.pid} (IDE ${proc.ide} does not match filter ${opts.ide}).`
1513
+ );
1514
+ }
1515
+ }
1429
1516
  }
1430
- if (repoBackupPath) {
1431
- console.log(`[setup-ide-files] Cursor repo MCP config backup: ${repoBackupPath}`);
1517
+ let allSocketPaths = await listSocketPaths(targetProjectId);
1518
+ if (m1Path && !opts.allProjects) {
1519
+ const m1Sockets = await defaultListSocketPathsForM1Path(m1Path);
1520
+ const seen = new Set(allSocketPaths);
1521
+ for (const socketPath of m1Sockets) {
1522
+ if (!seen.has(socketPath)) {
1523
+ seen.add(socketPath);
1524
+ allSocketPaths.push(socketPath);
1525
+ }
1526
+ const record = readBindingSidecarRecord(socketPath);
1527
+ const staleProjectId = record?.projectId?.trim().toLowerCase();
1528
+ if (staleProjectId && targetProjectId !== null && staleProjectId !== targetProjectId) {
1529
+ for (const proc of allDaemonProcesses) {
1530
+ if (proc.projectId !== staleProjectId) continue;
1531
+ if (opts.ide && proc.ide !== void 0 && proc.ide !== opts.ide) continue;
1532
+ if (!processesToStop.some((p) => p.pid === proc.pid)) {
1533
+ processesToStop.push(proc);
1534
+ cleanupLog(
1535
+ opts,
1536
+ `${prefix} Including stale daemon pid=${proc.pid} project=${staleProjectId} (sidecar m1=${m1Path})`
1537
+ );
1538
+ }
1539
+ }
1540
+ }
1541
+ }
1542
+ allSocketPaths = [...seen].sort();
1432
1543
  }
1433
- if (repoOutcome === "created") {
1434
- console.log(
1435
- dryRun ? `[setup-ide-files] Cursor repo MCP config would be created: ${repoConfigPath}` : `[setup-ide-files] Cursor repo MCP config created: ${repoConfigPath}`
1436
- );
1437
- } else if (repoOutcome === "updated") {
1438
- console.log(
1439
- dryRun ? `[setup-ide-files] Cursor repo MCP config would be updated: ${repoConfigPath}` : `[setup-ide-files] Cursor repo MCP config updated: ${repoConfigPath}`
1544
+ const socketPaths = targetProjectId === null ? allSocketPaths : await filterSocketPathsByIdeForCleanup(allSocketPaths, targetProjectId, opts.ide, m1Path);
1545
+ if (opts.allProjects) {
1546
+ const projectIds = /* @__PURE__ */ new Set();
1547
+ for (const proc of processesToStop) {
1548
+ projectIds.add(proc.projectId);
1549
+ }
1550
+ for (const socketPath of socketPaths) {
1551
+ const id = extractProjectIdFromSocketFilename(path7.basename(socketPath));
1552
+ if (id) {
1553
+ projectIds.add(id);
1554
+ continue;
1555
+ }
1556
+ const record = readBindingSidecarRecord(socketPath);
1557
+ if (record) {
1558
+ projectIds.add(record.projectId.trim().toLowerCase());
1559
+ }
1560
+ }
1561
+ cleanupLog(
1562
+ opts,
1563
+ `${prefix} Projects affected: ${projectIds.size ? [...projectIds].sort().join(", ") : "(none found)"}`
1440
1564
  );
1441
- } else if (repoOutcome === "skipped") {
1442
- console.log(`[setup-ide-files] Cursor repo MCP config unchanged: ${repoConfigPath}`);
1443
1565
  }
1444
- if (globalMemoraoneRemoved && globalConfigPath) {
1445
- console.log(
1446
- dryRun ? `[setup-ide-files] Would remove memoraone from Cursor global MCP config: ${globalConfigPath}` : `[setup-ide-files] Removed memoraone from Cursor global MCP config: ${globalConfigPath}`
1447
- );
1448
- if (globalBackupPath) {
1449
- console.log(`[setup-ide-files] Cursor global MCP config backup: ${globalBackupPath}`);
1566
+ if (processesToStop.length) {
1567
+ cleanupLog(opts, `${prefix} Daemon processes to stop:`);
1568
+ for (const proc of processesToStop) {
1569
+ const ideLabel = proc.ide ? ` ide=${proc.ide}` : "";
1570
+ cleanupLog(opts, `${prefix} pid=${proc.pid} project=${proc.projectId}${ideLabel}`);
1450
1571
  }
1451
- } else if (globalConfigPath) {
1452
- console.log(
1453
- `[setup-ide-files] Cursor global MCP config unchanged (no managed memoraone to remove): ${globalConfigPath}`
1454
- );
1572
+ } else if (ideSkippedProcesses.length) {
1573
+ cleanupLog(opts, `${prefix} No matching daemon processes for IDE filter ${opts.ide}.`);
1574
+ } else {
1575
+ cleanupLog(opts, `${prefix} No matching daemon processes found.`);
1455
1576
  }
1456
- console.log(
1457
- "[setup-ide-files] Each Cursor window uses this repo\u2019s .cursor/mcp.json (separate MCP process per repo)."
1458
- );
1459
- console.log(
1460
- "[setup-ide-files] Fully quit Cursor and reopen this repo for MCP changes to take effect."
1461
- );
1462
- }
1463
-
1464
- // src/cleanup.ts
1465
- var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
1466
- var DAEMON_PROJECT_ID_RE = /--project-id\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i;
1467
- var PROJECT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1468
- var MEMORAONE_MCP_COMMAND_RE = /memoraone-mcp|memoraOne-mcp|@memoraone\/mcp/;
1469
- var CLEANUP_PROJECT_ID_REQUIRED_ERROR = "Provide --project-id <id> or run from a folder containing memoraone.m1.";
1470
- function isMemoraoneMcpCommandLine(commandLine) {
1471
- return MEMORAONE_MCP_COMMAND_RE.test(commandLine);
1472
- }
1473
- function parseDaemonProjectIdFromCommandLine(commandLine) {
1474
- if (!commandLine.includes("--daemon")) {
1475
- return null;
1577
+ if (socketPaths.length) {
1578
+ cleanupLog(opts, `${prefix} Sockets to remove:`);
1579
+ for (const socketPath of socketPaths) {
1580
+ cleanupLog(opts, `${prefix} ${socketPath}`);
1581
+ }
1582
+ } else {
1583
+ cleanupLog(opts, `${prefix} No matching sockets found under ${getMcpBaseDir()}.`);
1476
1584
  }
1477
- const match = commandLine.match(DAEMON_PROJECT_ID_RE);
1478
- return match ? match[1].toLowerCase() : null;
1479
- }
1480
- function parseDaemonIdeFromCommandLine(commandLine) {
1481
- if (!commandLine.includes("--daemon")) {
1482
- return void 0;
1585
+ if (skippedProcesses.length) {
1586
+ cleanupLog(opts, `${prefix} Skipped unrelated daemon processes:`);
1587
+ for (const proc of skippedProcesses) {
1588
+ cleanupLog(opts, `${prefix} pid=${proc.pid} project=${proc.projectId}`);
1589
+ }
1483
1590
  }
1484
- return parseIdeTypeFromCommandLine(commandLine);
1485
- }
1486
- function parseDaemonProcessLines(lines) {
1487
- const processes = [];
1488
- for (const line of lines) {
1489
- const trimmed = line.trim();
1490
- if (!trimmed) continue;
1491
- const spaceIdx = trimmed.indexOf(" ");
1492
- if (spaceIdx <= 0) continue;
1493
- const pid = Number.parseInt(trimmed.slice(0, spaceIdx), 10);
1494
- if (!Number.isFinite(pid) || pid <= 0) continue;
1495
- const command = trimmed.slice(spaceIdx + 1);
1496
- if (!isMemoraoneMcpCommandLine(command)) continue;
1497
- const projectId = parseDaemonProjectIdFromCommandLine(command);
1498
- if (projectId === null) continue;
1499
- const ide = parseDaemonIdeFromCommandLine(command);
1500
- processes.push({ pid, command, projectId, ...ide !== void 0 ? { ide } : {} });
1591
+ if (opts.allProjects && !opts.dryRun) {
1592
+ const ok = opts.assumeYes ? true : await confirm(`${prefix} Proceed with cleanup for ALL projects?`);
1593
+ if (!ok) {
1594
+ cleanupLog(opts, `${prefix} Aborted.`);
1595
+ return {
1596
+ exitCode: 1,
1597
+ workspaceRoot,
1598
+ m1Path,
1599
+ projectId: targetProjectId ?? void 0,
1600
+ killedPids: [],
1601
+ removedSockets: [],
1602
+ skippedProcesses: [...skippedProcesses, ...ideSkippedProcesses],
1603
+ error: opts.assumeYes ? void 0 : "Aborted (--all requires --yes in non-interactive mode)"
1604
+ };
1605
+ }
1501
1606
  }
1502
- return processes;
1503
- }
1504
- function normalizeCleanupProjectId(projectId) {
1505
- const trimmed = projectId.trim();
1506
- if (!PROJECT_ID_RE.test(trimmed)) {
1507
- return { error: `Invalid project id: ${projectId}` };
1607
+ const killedPids = [];
1608
+ const removedSockets = [];
1609
+ if (opts.dryRun) {
1610
+ cleanupLog(opts, `${prefix} Dry run complete \u2014 no processes stopped, no sockets removed.`);
1611
+ return {
1612
+ exitCode: 0,
1613
+ workspaceRoot,
1614
+ m1Path,
1615
+ projectId: targetProjectId ?? void 0,
1616
+ killedPids: processesToStop.map((p) => p.pid),
1617
+ removedSockets: socketPaths,
1618
+ skippedProcesses: [...skippedProcesses, ...ideSkippedProcesses]
1619
+ };
1508
1620
  }
1509
- return trimmed.toLowerCase();
1510
- }
1511
- async function defaultListDaemonProcesses() {
1512
- const { stdout } = await execFileAsync2("ps", ["-eo", "pid=,args="], {
1513
- maxBuffer: 10 * 1024 * 1024
1514
- });
1515
- return parseDaemonProcessLines(stdout.split("\n"));
1516
- }
1517
- async function defaultListSocketPaths(projectId) {
1518
- const baseDir = getMcpBaseDir();
1519
- let entries;
1520
- try {
1521
- entries = await fs5.readdir(baseDir);
1522
- } catch (err) {
1523
- const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
1524
- if (code === "ENOENT") {
1525
- return [];
1621
+ for (const proc of processesToStop) {
1622
+ try {
1623
+ await killProcess(proc.pid);
1624
+ killedPids.push(proc.pid);
1625
+ cleanupLog(opts, `${prefix} Stopped daemon pid=${proc.pid} project=${proc.projectId}`);
1626
+ } catch (err) {
1627
+ cleanupWarn(opts, `${prefix} Could not stop pid=${proc.pid}: ${String(err)}`);
1526
1628
  }
1527
- throw err;
1528
1629
  }
1529
- const paths = [];
1530
- for (const name of entries) {
1531
- if (!name.endsWith(".sock") || !isMemoraoneSocketFilename(name)) {
1532
- continue;
1533
- }
1534
- const socketPath = path7.join(baseDir, name);
1535
- if (projectId === null) {
1536
- paths.push(socketPath);
1537
- continue;
1538
- }
1539
- const normalizedProjectId = projectId.trim().toLowerCase();
1540
- if (isLegacySocketFilename(name) && isSocketFilenameForProject(name, normalizedProjectId)) {
1541
- paths.push(socketPath);
1542
- continue;
1543
- }
1544
- if (isHashSocketFilename(name)) {
1545
- const record = readBindingSidecarRecord(socketPath);
1546
- if (record?.projectId.trim().toLowerCase() === normalizedProjectId) {
1547
- paths.push(socketPath);
1630
+ if (killedPids.length) {
1631
+ await new Promise((r) => setTimeout(r, 300));
1632
+ }
1633
+ for (const socketPath of socketPaths) {
1634
+ try {
1635
+ await removeSocket(socketPath);
1636
+ removedSockets.push(socketPath);
1637
+ cleanupLog(opts, `${prefix} Removed socket ${socketPath}`);
1638
+ } catch (err) {
1639
+ const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
1640
+ if (code !== "ENOENT") {
1641
+ cleanupWarn(opts, `${prefix} Could not remove socket ${socketPath}: ${String(err)}`);
1548
1642
  }
1549
1643
  }
1550
1644
  }
1551
- return paths.sort();
1645
+ cleanupLog(opts, `${prefix} Done. stopped=${killedPids.length} socketsRemoved=${removedSockets.length}`);
1646
+ return {
1647
+ exitCode: 0,
1648
+ workspaceRoot,
1649
+ m1Path,
1650
+ projectId: targetProjectId ?? void 0,
1651
+ killedPids,
1652
+ removedSockets,
1653
+ skippedProcesses: [...skippedProcesses, ...ideSkippedProcesses]
1654
+ };
1552
1655
  }
1553
- async function filterSocketPathsByIde(socketPaths, projectId, ide) {
1554
- if (ide === void 0) return socketPaths;
1555
- const normalizedProjectId = projectId.trim().toLowerCase();
1556
- const filtered = [];
1557
- for (const socketPath of socketPaths) {
1558
- const basename4 = path7.basename(socketPath);
1559
- if (isLegacySocketFilename(basename4)) {
1560
- if (isSocketFilenameForProjectAndIde(basename4, normalizedProjectId, ide)) {
1561
- filtered.push(socketPath);
1656
+ function parseCleanupFlags(argv) {
1657
+ let dryRun = false;
1658
+ let allProjects = false;
1659
+ let assumeYes = false;
1660
+ let projectId;
1661
+ let ide;
1662
+ let invalidIde;
1663
+ const unknown = [];
1664
+ for (let i = 0; i < argv.length; i++) {
1665
+ const arg = argv[i];
1666
+ if (arg === "--dry-run") dryRun = true;
1667
+ else if (arg === "--all-projects" || arg === "--all") allProjects = true;
1668
+ else if (arg === "--yes" || arg === "-y") assumeYes = true;
1669
+ else if (arg === "--project-id") {
1670
+ if (i + 1 >= argv.length) {
1671
+ unknown.push("--project-id (missing value)");
1672
+ } else {
1673
+ projectId = argv[++i];
1562
1674
  }
1563
- continue;
1564
- }
1565
- if (isHashSocketFilename(basename4)) {
1566
- const record = readBindingSidecarRecord(socketPath);
1567
- if (record?.projectId.trim().toLowerCase() === normalizedProjectId && record.ideType === ide) {
1568
- filtered.push(socketPath);
1675
+ } else if (arg === "--ide") {
1676
+ if (i + 1 >= argv.length) {
1677
+ unknown.push("--ide (missing value)");
1678
+ } else {
1679
+ const value = argv[++i];
1680
+ if (IDE_TYPES.includes(value)) {
1681
+ ide = value;
1682
+ } else {
1683
+ invalidIde = value;
1684
+ }
1569
1685
  }
1570
- }
1686
+ } else if (arg.startsWith("-")) unknown.push(arg);
1687
+ else unknown.push(arg);
1571
1688
  }
1572
- return filtered;
1573
- }
1574
- async function defaultKillProcess(pid) {
1575
- process.kill(pid, "SIGTERM");
1689
+ return { dryRun, allProjects, assumeYes, projectId, ide, invalidIde, unknown };
1576
1690
  }
1577
- async function defaultRemoveSocket(socketPath) {
1578
- await fs5.unlink(socketPath);
1579
- try {
1580
- await fs5.unlink(bindingSidecarPath(socketPath));
1581
- } catch {
1691
+ async function cliCleanup(argv) {
1692
+ const { dryRun, allProjects, assumeYes, projectId, ide, invalidIde, unknown } = parseCleanupFlags(argv);
1693
+ if (invalidIde) {
1694
+ console.error(
1695
+ `[cleanup] Invalid --ide value: ${invalidIde}. Expected one of: ${IDE_TYPES.join(", ")}.`
1696
+ );
1697
+ return 1;
1582
1698
  }
1583
- }
1584
- async function defaultConfirm(message) {
1585
- if (!import_node_process.stdin.isTTY) {
1586
- return false;
1699
+ if (unknown.length) {
1700
+ console.error(`[cleanup] Unknown option(s): ${unknown.join(", ")}`);
1701
+ return 1;
1587
1702
  }
1588
- const rl = readline3.createInterface({ input: import_node_process.stdin, output: import_node_process.stdout });
1589
- try {
1590
- const answer = await rl.question(`${message} [y/N] `);
1591
- return /^y(es)?$/i.test(answer.trim());
1592
- } finally {
1593
- rl.close();
1703
+ const result = await runCleanup({
1704
+ cwd: process.cwd(),
1705
+ dryRun,
1706
+ allProjects,
1707
+ assumeYes,
1708
+ projectId,
1709
+ ide
1710
+ });
1711
+ if (result.error) {
1712
+ console.error(`[cleanup] ${result.error}`);
1594
1713
  }
1714
+ return result.exitCode;
1595
1715
  }
1596
- async function resolveCleanupTarget(cwd) {
1716
+
1717
+ // src/bridgeProxy.ts
1718
+ var defaultLog = (msg) => {
1719
+ process.stderr.write(`[memoraone-mcp][bridge] ${msg}
1720
+ `);
1721
+ };
1722
+ function summarizeJsonRpcMethod(line) {
1597
1723
  try {
1598
- const binding = await resolveAuthoritativeBinding([path7.resolve(cwd)]);
1599
- return {
1600
- workspaceRoot: binding.workspaceRoot,
1601
- m1Path: binding.m1Path,
1602
- projectId: binding.projectId
1603
- };
1724
+ const message = JSON.parse(line.trim());
1725
+ if (typeof message.method === "string") {
1726
+ return message.method;
1727
+ }
1728
+ if (message.id !== void 0) {
1729
+ return `response:id=${String(message.id)}`;
1730
+ }
1731
+ return "jsonrpc";
1604
1732
  } catch {
1605
- return { error: CLEANUP_PROJECT_ID_REQUIRED_ERROR };
1733
+ return "invalid-json";
1606
1734
  }
1607
1735
  }
1608
- function filterProcessesForScope(processes, projectId) {
1609
- if (projectId === null) {
1610
- return { matching: processes, skipped: [] };
1736
+ function connectWithRetry(socketPath, log, maxRetries, retryDelayMs, connect2) {
1737
+ return new Promise((resolve9, reject) => {
1738
+ const tryConnect = (attempt) => {
1739
+ connect2(socketPath).then(resolve9).catch((err) => {
1740
+ if (attempt >= maxRetries) {
1741
+ reject(err);
1742
+ return;
1743
+ }
1744
+ log(`connect attempt ${attempt + 1} failed, retrying in ${retryDelayMs}ms: ${String(err)}`);
1745
+ setTimeout(() => tryConnect(attempt + 1), retryDelayMs);
1746
+ });
1747
+ };
1748
+ tryConnect(0);
1749
+ });
1750
+ }
1751
+ async function resolveBridgeSessionBinding(params, env = process.env, options = {}) {
1752
+ const bridgeOptions = getBridgeBindingResolveOptions(env);
1753
+ return resolveBindingFromInitializeParams(params, {
1754
+ env,
1755
+ fallbackWorkspaceRoots: getEnvWorkspaceRootCandidates(),
1756
+ rootsListUris: options.rootsListUris,
1757
+ rootsListAttempted: options.rootsListAttempted,
1758
+ ...bridgeOptions
1759
+ });
1760
+ }
1761
+ async function stopDaemonsForStaleBinding(stale, env, log) {
1762
+ const ideType = resolveBindingIdeType(env);
1763
+ let processes;
1764
+ try {
1765
+ processes = await defaultListDaemonProcesses();
1766
+ } catch (err) {
1767
+ log(`could not list daemon processes while replacing stale binding: ${String(err)}`);
1768
+ return;
1611
1769
  }
1612
- const normalized = projectId.toLowerCase();
1613
- const matching = [];
1614
- const skipped = [];
1615
1770
  for (const proc of processes) {
1616
- if (proc.projectId === normalized) {
1617
- matching.push(proc);
1618
- } else {
1619
- skipped.push(proc);
1771
+ const matchesIde = ideType === "" ? isDaemonProcessForProject(proc.command, stale.projectId) : isDaemonProcessForProjectAndIde(proc.command, stale.projectId, ideType);
1772
+ if (!matchesIde) continue;
1773
+ try {
1774
+ process.kill(proc.pid, "SIGTERM");
1775
+ log(`stopped stale daemon pid=${proc.pid} project=${stale.projectId}`);
1776
+ } catch (err) {
1777
+ log(`failed to stop stale daemon pid=${proc.pid}: ${String(err)}`);
1620
1778
  }
1621
1779
  }
1622
- return { matching, skipped };
1623
1780
  }
1624
- function logPrefix(dryRun) {
1625
- return dryRun ? "[cleanup][dry-run]" : "[cleanup]";
1626
- }
1627
- function logReconnectNotice(opts, prefix, ide) {
1628
- if (opts.quiet) return;
1629
- if (ide) {
1630
- console.log(
1631
- `${prefix} Note: Valid ${ide} connections for this project may disconnect temporarily; they should reconnect automatically. Stale connections will remain cleared.`
1781
+ async function connectOrSpawnDaemonForBinding(binding, opts) {
1782
+ const reconciled = await reconcileResolvedBindingWithDisk(binding);
1783
+ const sessionBinding = reconciled.binding;
1784
+ if (reconciled.cacheRefreshed) {
1785
+ opts.log(
1786
+ `refreshed stale binding from ${sessionBinding.m1Path}: project=${sessionBinding.projectId}`
1632
1787
  );
1633
- } else {
1634
- console.log(
1635
- `${prefix} Note: Valid IDE connections for this project may disconnect temporarily; they should reconnect automatically. Stale connections will remain cleared.`
1788
+ }
1789
+ const socketPath = getBindingSocketPath(sessionBinding, opts.env);
1790
+ opts.log(
1791
+ `target daemon socket=${socketPath} project=${sessionBinding.projectId} workspace=${sessionBinding.workspaceRoot}`
1792
+ );
1793
+ let socket;
1794
+ try {
1795
+ socket = await connectWithRetry(
1796
+ socketPath,
1797
+ opts.log,
1798
+ opts.maxRetries,
1799
+ opts.retryDelayMs,
1800
+ opts.connect
1636
1801
  );
1802
+ verifyDaemonSidecarBinding(socketPath, sessionBinding, opts.env);
1803
+ opts.log("reusing running daemon for session binding");
1804
+ return { socket, binding: sessionBinding, cacheRefreshed: reconciled.cacheRefreshed };
1805
+ } catch (err) {
1806
+ if (socket) {
1807
+ try {
1808
+ socket.destroy();
1809
+ } catch {
1810
+ }
1811
+ socket = void 0;
1812
+ }
1813
+ if (isDaemonBindingMismatchError(err)) {
1814
+ opts.log("stale daemon binding detected; replacing daemon for current memoraone.m1");
1815
+ const staleSidecar = readBindingSidecar(socketPath);
1816
+ if (staleSidecar) {
1817
+ await stopDaemonsForStaleBinding(staleSidecar, opts.env, opts.log);
1818
+ }
1819
+ await stopDaemonsForStaleBinding(sessionBinding, opts.env, opts.log);
1820
+ if (!bindingsMatch(binding, sessionBinding)) {
1821
+ await stopDaemonsForStaleBinding(binding, opts.env, opts.log);
1822
+ }
1823
+ removeDaemonSocketArtifacts(socketPath);
1824
+ }
1637
1825
  }
1826
+ opts.log("daemon not running, spawning...");
1827
+ await opts.spawnDaemon(sessionBinding, socketPath);
1828
+ await new Promise((r) => setTimeout(r, opts.retryDelayMs));
1829
+ socket = await connectWithRetry(
1830
+ socketPath,
1831
+ opts.log,
1832
+ opts.maxRetries,
1833
+ opts.retryDelayMs,
1834
+ opts.connect
1835
+ );
1836
+ verifyDaemonSidecarBinding(socketPath, sessionBinding, opts.env);
1837
+ return { socket, binding: sessionBinding, cacheRefreshed: reconciled.cacheRefreshed };
1638
1838
  }
1639
- function cleanupLog(opts, message) {
1640
- if (!opts.quiet) {
1641
- console.log(message);
1839
+ var BridgeDaemonRouter = class {
1840
+ constructor(options) {
1841
+ this.activeSocket = null;
1842
+ this.activeBinding = null;
1843
+ this.socketLineReader = null;
1844
+ this.lastInitializeLine = null;
1845
+ this.pendingDeferredClientLines = [];
1846
+ this.handshakeDeferredClientLines = [];
1847
+ this.clientInitializeSeen = false;
1848
+ this.env = options.env ?? process.env;
1849
+ this.stdout = options.stdout ?? process.stdout;
1850
+ this.log = options.log ?? defaultLog;
1851
+ this.cliPath = options.cliPath;
1852
+ this.maxRetries = options.maxRetries ?? 5;
1853
+ this.retryDelayMs = options.retryDelayMs ?? 200;
1854
+ this.lineReader = options.lineReader ?? null;
1855
+ this.connectImpl = options.connect ?? ((socketPath) => new Promise((resolve9, reject) => {
1856
+ const socket = net.connect(socketPath, () => resolve9(socket));
1857
+ socket.on("error", reject);
1858
+ }));
1859
+ this.spawnDaemonImpl = options.spawnDaemon ?? (async (binding) => {
1860
+ const child = (0, import_node_child_process3.spawn)(
1861
+ process.execPath,
1862
+ buildDaemonSpawnArgs(this.cliPath, binding.projectId, this.env),
1863
+ {
1864
+ detached: true,
1865
+ stdio: "ignore",
1866
+ env: {
1867
+ ...this.env,
1868
+ MEMORAONE_DAEMON_BINDING_B64: encodeResolvedBinding(binding)
1869
+ }
1870
+ }
1871
+ );
1872
+ child.on("exit", (code, signal) => {
1873
+ logInitializeDebug(
1874
+ this.log,
1875
+ this.env,
1876
+ `spawned daemon exit code=${code ?? "null"} signal=${signal ?? "null"} project=${binding.projectId}`
1877
+ );
1878
+ });
1879
+ child.unref();
1880
+ });
1881
+ }
1882
+ getActiveBinding() {
1883
+ return this.activeBinding;
1642
1884
  }
1643
- }
1644
- function cleanupWarn(opts, message) {
1645
- if (!opts.quiet) {
1646
- console.warn(message);
1885
+ hasClientInitialize() {
1886
+ return this.clientInitializeSeen;
1647
1887
  }
1648
- }
1649
- async function runCleanup(opts) {
1650
- const listProcesses = opts.listProcesses ?? defaultListDaemonProcesses;
1651
- const listSocketPaths = opts.listSocketPaths ?? defaultListSocketPaths;
1652
- const killProcess = opts.killProcess ?? defaultKillProcess;
1653
- const removeSocket = opts.removeSocket ?? defaultRemoveSocket;
1654
- const confirm = opts.confirm ?? defaultConfirm;
1655
- const prefix = logPrefix(opts.dryRun);
1656
- let targetProjectId = null;
1657
- let workspaceRoot;
1658
- let m1Path;
1659
- if (opts.allProjects) {
1660
- if (opts.projectId) {
1661
- return {
1662
- exitCode: 1,
1663
- killedPids: [],
1664
- removedSockets: [],
1665
- skippedProcesses: [],
1666
- error: "Cannot combine --all-projects with --project-id."
1667
- };
1668
- }
1669
- cleanupLog(opts, `${prefix} Mode: all projects (--all-projects)`);
1670
- cleanupWarn(
1671
- opts,
1672
- `${prefix} WARNING: This stops every MemoraOne MCP daemon and removes all project sockets under ${getMcpBaseDir()}.`
1888
+ async ensureDaemonForInitialize(params) {
1889
+ logInitializeDebug(
1890
+ this.log,
1891
+ this.env,
1892
+ `initialize payload: ${summarizeInitializeParamsForDebug(params)}`
1673
1893
  );
1674
- } else if (opts.projectId) {
1675
- const normalized = normalizeCleanupProjectId(opts.projectId);
1676
- if (typeof normalized !== "string") {
1677
- return { exitCode: 1, killedPids: [], removedSockets: [], skippedProcesses: [], error: normalized.error };
1894
+ const bridgeOptions = getBridgeBindingResolveOptions(this.env);
1895
+ const initializeRoots = extractWorkspaceRootsFromInitialize(params);
1896
+ let rootsListUris;
1897
+ let rootsListAttempted = false;
1898
+ const repoHint = getRepoScopedWorkspaceHint(this.env);
1899
+ if (initializeRoots.length === 0 && bridgeOptions.allowEnvWorkspaceFallback === false && repoHint === null) {
1900
+ if (!this.lineReader) {
1901
+ throw new Error(
1902
+ "[memoraone-mcp] Internal error: Cursor workspace binding requires stdin line reader for roots/list"
1903
+ );
1904
+ }
1905
+ rootsListAttempted = true;
1906
+ this.log("initialize lacks workspace roots; requesting roots/list from Cursor before binding");
1907
+ const rootsListResult = await requestClientRootsListUris({
1908
+ lineReader: this.lineReader,
1909
+ stdout: this.stdout,
1910
+ log: this.log,
1911
+ env: this.env,
1912
+ initializeParams: params
1913
+ });
1914
+ rootsListUris = rootsListResult.uris;
1915
+ if (rootsListResult.deferredLines.length > 0) {
1916
+ this.pendingDeferredClientLines = rootsListResult.deferredLines.slice();
1917
+ logInitializeDebug(
1918
+ this.log,
1919
+ this.env,
1920
+ `queued ${this.pendingDeferredClientLines.length} deferred client line(s) for replay after initialize`
1921
+ );
1922
+ }
1678
1923
  }
1679
- targetProjectId = normalized;
1680
- cleanupLog(opts, `${prefix} Project id: ${targetProjectId}`);
1681
- if (opts.ide) {
1682
- cleanupLog(opts, `${prefix} IDE filter: ${opts.ide}`);
1924
+ const binding = await resolveBridgeSessionBinding(params, this.env, {
1925
+ rootsListUris,
1926
+ rootsListAttempted
1927
+ });
1928
+ const environmentLog = binding.environment !== void 0 ? ` environment=${binding.environment}` : "";
1929
+ this.log(
1930
+ `session binding project=${binding.projectId} workspace=${binding.workspaceRoot} m1=${binding.m1Path} source=${binding.bindingSource} apiKeySource=${binding.apiKeySource}${environmentLog}`
1931
+ );
1932
+ if (this.activeBinding && bindingsMatch(this.activeBinding, binding) && this.activeSocket) {
1933
+ return;
1683
1934
  }
1684
- } else {
1685
- const target = await resolveCleanupTarget(opts.cwd);
1686
- if ("error" in target) {
1687
- return { exitCode: 1, killedPids: [], removedSockets: [], skippedProcesses: [], error: target.error };
1935
+ if (this.activeBinding && !bindingsMatch(this.activeBinding, binding)) {
1936
+ this.log(
1937
+ `session binding changed from workspace=${this.activeBinding.workspaceRoot} to workspace=${binding.workspaceRoot}; reconnecting daemon`
1938
+ );
1939
+ this.resetSessionState();
1940
+ this.detachSocketReader();
1941
+ this.activeSocket?.destroy();
1942
+ this.activeSocket = null;
1688
1943
  }
1689
- targetProjectId = target.projectId;
1690
- workspaceRoot = target.workspaceRoot;
1691
- m1Path = target.m1Path;
1692
- cleanupLog(opts, `${prefix} Workspace root: ${workspaceRoot}`);
1693
- cleanupLog(opts, `${prefix} memoraone.m1: ${m1Path}`);
1694
- cleanupLog(opts, `${prefix} Project id: ${targetProjectId}`);
1695
- if (opts.ide) {
1696
- cleanupLog(opts, `${prefix} IDE filter: ${opts.ide}`);
1944
+ this.activeBinding = binding;
1945
+ await this.connectActiveDaemon();
1946
+ this.log("bridge connected");
1947
+ }
1948
+ recordClientInitialize(line) {
1949
+ this.lastInitializeLine = line;
1950
+ this.clientInitializeSeen = true;
1951
+ }
1952
+ async forwardInitializeToDaemon(line) {
1953
+ this.recordClientInitialize(line);
1954
+ await this.writeToDaemon(line);
1955
+ logInitializeDebug(this.log, this.env, "initialize replay to daemon");
1956
+ this.log("forwarding active");
1957
+ }
1958
+ async replayDeferredClientMessages() {
1959
+ if (this.pendingDeferredClientLines.length === 0) {
1960
+ return;
1697
1961
  }
1698
- if (workspaceRoot && (!opts.ide || opts.ide === "cursor")) {
1699
- try {
1700
- const cursorAudit = await auditCursorMcpConfig({ repoRoot: workspaceRoot });
1701
- logCursorMcpConfigAudit(prefix, cursorAudit);
1702
- } catch (err) {
1703
- cleanupWarn(opts, `${prefix} Cursor MCP config audit failed: ${String(err)}`);
1704
- }
1962
+ const lines = this.pendingDeferredClientLines.slice();
1963
+ this.pendingDeferredClientLines = [];
1964
+ this.handshakeDeferredClientLines = lines.slice();
1965
+ const types = lines.map((line) => summarizeJsonRpcMethod(line));
1966
+ logInitializeDebug(
1967
+ this.log,
1968
+ this.env,
1969
+ `deferred message replay count=${lines.length} types=${JSON.stringify(types)}`
1970
+ );
1971
+ for (const line of lines) {
1972
+ await this.writeToDaemon(line);
1705
1973
  }
1706
1974
  }
1707
- if (targetProjectId !== null || opts.ide) {
1708
- logReconnectNotice(opts, prefix, opts.ide);
1975
+ async writeToDaemon(line) {
1976
+ await this.ensureActiveDaemonSocket();
1977
+ this.activeSocket.write(`${line}
1978
+ `);
1709
1979
  }
1710
- const allDaemonProcesses = await listProcesses();
1711
- const { matching: projectProcesses, skipped: skippedProcesses } = filterProcessesForScope(
1712
- allDaemonProcesses,
1713
- targetProjectId
1714
- );
1715
- let processesToStop = projectProcesses;
1716
- const ideSkippedProcesses = [];
1717
- if (opts.ide && targetProjectId !== null) {
1718
- processesToStop = [];
1719
- for (const proc of projectProcesses) {
1720
- if (proc.ide === opts.ide) {
1721
- processesToStop.push(proc);
1722
- } else if (proc.ide === void 0) {
1723
- ideSkippedProcesses.push(proc);
1724
- cleanupLog(
1725
- opts,
1726
- `${prefix} Skipped daemon pid=${proc.pid} because IDE could not be safely determined.`
1727
- );
1728
- } else {
1729
- ideSkippedProcesses.push(proc);
1730
- cleanupLog(
1731
- opts,
1732
- `${prefix} Skipped daemon pid=${proc.pid} (IDE ${proc.ide} does not match filter ${opts.ide}).`
1733
- );
1734
- }
1980
+ resetSessionState() {
1981
+ this.lastInitializeLine = null;
1982
+ this.pendingDeferredClientLines = [];
1983
+ this.handshakeDeferredClientLines = [];
1984
+ this.clientInitializeSeen = false;
1985
+ this.activeBinding = null;
1986
+ }
1987
+ async connectActiveDaemon() {
1988
+ if (!this.activeBinding) {
1989
+ throw new Error("[memoraone-mcp] Internal error: connectActiveDaemon without active binding");
1735
1990
  }
1991
+ const connected = await connectOrSpawnDaemonForBinding(this.activeBinding, {
1992
+ env: this.env,
1993
+ cliPath: this.cliPath,
1994
+ log: this.log,
1995
+ maxRetries: this.maxRetries,
1996
+ retryDelayMs: this.retryDelayMs,
1997
+ connect: this.connectImpl,
1998
+ spawnDaemon: this.spawnDaemonImpl
1999
+ });
2000
+ this.activeBinding = connected.binding;
2001
+ this.activeSocket = connected.socket;
2002
+ this.attachSocketReader(connected.socket);
1736
2003
  }
1737
- const allSocketPaths = await listSocketPaths(targetProjectId);
1738
- const socketPaths = targetProjectId === null ? allSocketPaths : await filterSocketPathsByIde(allSocketPaths, targetProjectId, opts.ide);
1739
- if (opts.allProjects) {
1740
- const projectIds = /* @__PURE__ */ new Set();
1741
- for (const proc of processesToStop) {
1742
- projectIds.add(proc.projectId);
2004
+ async ensureActiveDaemonSocket() {
2005
+ if (this.activeSocket && !this.activeSocket.destroyed) {
2006
+ return;
1743
2007
  }
1744
- for (const socketPath of socketPaths) {
1745
- const id = extractProjectIdFromSocketFilename(path7.basename(socketPath));
1746
- if (id) {
1747
- projectIds.add(id);
1748
- continue;
1749
- }
1750
- const record = readBindingSidecarRecord(socketPath);
1751
- if (record) {
1752
- projectIds.add(record.projectId.trim().toLowerCase());
2008
+ if (!this.clientInitializeSeen || !this.lastInitializeLine || !this.activeBinding) {
2009
+ throw new Error("[memoraone-mcp] MCP request before initialize");
2010
+ }
2011
+ this.log("daemon socket unavailable; reconnecting for session binding");
2012
+ await this.connectActiveDaemon();
2013
+ logInitializeDebug(this.log, this.env, "initialize replay to daemon after reconnect");
2014
+ this.activeSocket.write(`${this.lastInitializeLine}
2015
+ `);
2016
+ if (this.handshakeDeferredClientLines.length > 0) {
2017
+ const types = this.handshakeDeferredClientLines.map(
2018
+ (deferredLine) => summarizeJsonRpcMethod(deferredLine)
2019
+ );
2020
+ logInitializeDebug(
2021
+ this.log,
2022
+ this.env,
2023
+ `deferred message replay after reconnect count=${this.handshakeDeferredClientLines.length} types=${JSON.stringify(types)}`
2024
+ );
2025
+ for (const deferredLine of this.handshakeDeferredClientLines) {
2026
+ this.activeSocket.write(`${deferredLine}
2027
+ `);
1753
2028
  }
1754
2029
  }
1755
- cleanupLog(
1756
- opts,
1757
- `${prefix} Projects affected: ${projectIds.size ? [...projectIds].sort().join(", ") : "(none found)"}`
1758
- );
1759
2030
  }
1760
- if (processesToStop.length) {
1761
- cleanupLog(opts, `${prefix} Daemon processes to stop:`);
1762
- for (const proc of processesToStop) {
1763
- const ideLabel = proc.ide ? ` ide=${proc.ide}` : "";
1764
- cleanupLog(opts, `${prefix} pid=${proc.pid} project=${proc.projectId}${ideLabel}`);
1765
- }
1766
- } else if (ideSkippedProcesses.length) {
1767
- cleanupLog(opts, `${prefix} No matching daemon processes for IDE filter ${opts.ide}.`);
1768
- } else {
1769
- cleanupLog(opts, `${prefix} No matching daemon processes found.`);
2031
+ attachSocketReader(socket) {
2032
+ this.detachSocketReader();
2033
+ this.socketLineReader = readline3.createInterface({ input: socket, crlfDelay: Infinity });
2034
+ this.socketLineReader.on("line", (line) => {
2035
+ this.stdout.write(`${line}
2036
+ `);
2037
+ });
2038
+ socket.on("close", (hadError) => {
2039
+ if (this.activeSocket === socket) {
2040
+ logInitializeDebug(
2041
+ this.log,
2042
+ this.env,
2043
+ `daemon socket closed hadError=${String(hadError)}`
2044
+ );
2045
+ this.log("daemon socket closed; bridge stays alive for reconnect");
2046
+ this.detachSocketReader();
2047
+ this.activeSocket = null;
2048
+ }
2049
+ });
2050
+ socket.on("error", (err) => {
2051
+ this.log(`socket error: ${String(err)}`);
2052
+ logInitializeDebug(this.log, this.env, `daemon socket error: ${String(err)}`);
2053
+ if (this.activeSocket === socket) {
2054
+ this.detachSocketReader();
2055
+ this.activeSocket = null;
2056
+ }
2057
+ });
1770
2058
  }
1771
- if (socketPaths.length) {
1772
- cleanupLog(opts, `${prefix} Sockets to remove:`);
1773
- for (const socketPath of socketPaths) {
1774
- cleanupLog(opts, `${prefix} ${socketPath}`);
2059
+ detachSocketReader() {
2060
+ if (this.socketLineReader) {
2061
+ this.socketLineReader.close();
2062
+ this.socketLineReader = null;
1775
2063
  }
1776
- } else {
1777
- cleanupLog(opts, `${prefix} No matching sockets found under ${getMcpBaseDir()}.`);
1778
2064
  }
1779
- if (skippedProcesses.length) {
1780
- cleanupLog(opts, `${prefix} Skipped unrelated daemon processes:`);
1781
- for (const proc of skippedProcesses) {
1782
- cleanupLog(opts, `${prefix} pid=${proc.pid} project=${proc.projectId}`);
2065
+ };
2066
+ async function runBridgeProxy(options) {
2067
+ ensureBaseDir();
2068
+ const stdin = options.stdin ?? process.stdin;
2069
+ const stdout = options.stdout ?? process.stdout;
2070
+ const log = options.log ?? defaultLog;
2071
+ const lineReader = options.lineReader ?? new StdioLineReader(stdin);
2072
+ const router = new BridgeDaemonRouter({ ...options, stdout, lineReader });
2073
+ while (true) {
2074
+ const line = await lineReader.readLine();
2075
+ if (line === null) {
2076
+ break;
1783
2077
  }
1784
- }
1785
- if (opts.allProjects && !opts.dryRun) {
1786
- const ok = opts.assumeYes ? true : await confirm(`${prefix} Proceed with cleanup for ALL projects?`);
1787
- if (!ok) {
1788
- cleanupLog(opts, `${prefix} Aborted.`);
1789
- return {
1790
- exitCode: 1,
1791
- workspaceRoot,
1792
- m1Path,
1793
- projectId: targetProjectId ?? void 0,
1794
- killedPids: [],
1795
- removedSockets: [],
1796
- skippedProcesses: [...skippedProcesses, ...ideSkippedProcesses],
1797
- error: opts.assumeYes ? void 0 : "Aborted (--all requires --yes in non-interactive mode)"
1798
- };
2078
+ const trimmed = line.trim();
2079
+ if (trimmed === "") {
2080
+ continue;
1799
2081
  }
1800
- }
1801
- const killedPids = [];
1802
- const removedSockets = [];
1803
- if (opts.dryRun) {
1804
- cleanupLog(opts, `${prefix} Dry run complete \u2014 no processes stopped, no sockets removed.`);
1805
- return {
1806
- exitCode: 0,
1807
- workspaceRoot,
1808
- m1Path,
1809
- projectId: targetProjectId ?? void 0,
1810
- killedPids: processesToStop.map((p) => p.pid),
1811
- removedSockets: socketPaths,
1812
- skippedProcesses: [...skippedProcesses, ...ideSkippedProcesses]
1813
- };
1814
- }
1815
- for (const proc of processesToStop) {
2082
+ let message;
1816
2083
  try {
1817
- await killProcess(proc.pid);
1818
- killedPids.push(proc.pid);
1819
- cleanupLog(opts, `${prefix} Stopped daemon pid=${proc.pid} project=${proc.projectId}`);
2084
+ message = JSON.parse(trimmed);
1820
2085
  } catch (err) {
1821
- cleanupWarn(opts, `${prefix} Could not stop pid=${proc.pid}: ${String(err)}`);
2086
+ throw new Error(`[memoraone-mcp] Invalid JSON-RPC on stdin: ${String(err)}`);
1822
2087
  }
1823
- }
1824
- if (killedPids.length) {
1825
- await new Promise((r) => setTimeout(r, 300));
1826
- }
1827
- for (const socketPath of socketPaths) {
1828
- try {
1829
- await removeSocket(socketPath);
1830
- removedSockets.push(socketPath);
1831
- cleanupLog(opts, `${prefix} Removed socket ${socketPath}`);
1832
- } catch (err) {
1833
- const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
1834
- if (code !== "ENOENT") {
1835
- cleanupWarn(opts, `${prefix} Could not remove socket ${socketPath}: ${String(err)}`);
1836
- }
2088
+ if (message.method === "initialize") {
2089
+ log("resolve binding from initialize request before daemon connect");
2090
+ const params = message.params ?? {};
2091
+ await router.ensureDaemonForInitialize(params);
2092
+ await router.forwardInitializeToDaemon(trimmed);
2093
+ await router.replayDeferredClientMessages();
2094
+ continue;
1837
2095
  }
2096
+ await router.writeToDaemon(trimmed);
1838
2097
  }
1839
- cleanupLog(opts, `${prefix} Done. stopped=${killedPids.length} socketsRemoved=${removedSockets.length}`);
1840
- return {
1841
- exitCode: 0,
1842
- workspaceRoot,
1843
- m1Path,
1844
- projectId: targetProjectId ?? void 0,
1845
- killedPids,
1846
- removedSockets,
1847
- skippedProcesses: [...skippedProcesses, ...ideSkippedProcesses]
1848
- };
1849
- }
1850
- function parseCleanupFlags(argv) {
1851
- let dryRun = false;
1852
- let allProjects = false;
1853
- let assumeYes = false;
1854
- let projectId;
1855
- let ide;
1856
- let invalidIde;
1857
- const unknown = [];
1858
- for (let i = 0; i < argv.length; i++) {
1859
- const arg = argv[i];
1860
- if (arg === "--dry-run") dryRun = true;
1861
- else if (arg === "--all-projects" || arg === "--all") allProjects = true;
1862
- else if (arg === "--yes" || arg === "-y") assumeYes = true;
1863
- else if (arg === "--project-id") {
1864
- if (i + 1 >= argv.length) {
1865
- unknown.push("--project-id (missing value)");
1866
- } else {
1867
- projectId = argv[++i];
1868
- }
1869
- } else if (arg === "--ide") {
1870
- if (i + 1 >= argv.length) {
1871
- unknown.push("--ide (missing value)");
1872
- } else {
1873
- const value = argv[++i];
1874
- if (IDE_TYPES.includes(value)) {
1875
- ide = value;
1876
- } else {
1877
- invalidIde = value;
1878
- }
1879
- }
1880
- } else if (arg.startsWith("-")) unknown.push(arg);
1881
- else unknown.push(arg);
1882
- }
1883
- return { dryRun, allProjects, assumeYes, projectId, ide, invalidIde, unknown };
1884
- }
1885
- async function cliCleanup(argv) {
1886
- const { dryRun, allProjects, assumeYes, projectId, ide, invalidIde, unknown } = parseCleanupFlags(argv);
1887
- if (invalidIde) {
1888
- console.error(
1889
- `[cleanup] Invalid --ide value: ${invalidIde}. Expected one of: ${IDE_TYPES.join(", ")}.`
1890
- );
1891
- return 1;
1892
- }
1893
- if (unknown.length) {
1894
- console.error(`[cleanup] Unknown option(s): ${unknown.join(", ")}`);
1895
- return 1;
1896
- }
1897
- const result = await runCleanup({
1898
- cwd: process.cwd(),
1899
- dryRun,
1900
- allProjects,
1901
- assumeYes,
1902
- projectId,
1903
- ide
1904
- });
1905
- if (result.error) {
1906
- console.error(`[cleanup] ${result.error}`);
1907
- }
1908
- return result.exitCode;
1909
2098
  }
1910
2099
 
2100
+ // src/setupIdeFiles.ts
2101
+ var fs8 = __toESM(require("fs/promises"), 1);
2102
+ var os4 = __toESM(require("os"), 1);
2103
+ var path10 = __toESM(require("path"), 1);
2104
+
1911
2105
  // src/jetbrainsMcpConfig.ts
1912
2106
  var fs6 = __toESM(require("fs/promises"), 1);
1913
2107
  var os3 = __toESM(require("os"), 1);
@@ -2391,6 +2585,234 @@ async function resolveBuiltCliPathAsync(options) {
2391
2585
  return null;
2392
2586
  }
2393
2587
 
2588
+ // src/openCursorMcpSettings.ts
2589
+ var import_node_child_process5 = require("child_process");
2590
+ var readline4 = __toESM(require("readline/promises"), 1);
2591
+ var import_node_util3 = require("util");
2592
+
2593
+ // src/terminalPresentation.ts
2594
+ var ANSI = {
2595
+ reset: "\x1B[0m",
2596
+ bold: "\x1B[1m",
2597
+ dim: "\x1B[2m",
2598
+ green: "\x1B[32m",
2599
+ yellow: "\x1B[33m",
2600
+ cyan: "\x1B[36m"
2601
+ };
2602
+ function isCiLikeEnv(env = process.env) {
2603
+ if (env.CI === "true" || env.CI === "1") return true;
2604
+ if (env.GITHUB_ACTIONS === "true" || env.GITHUB_ACTIONS === "1") return true;
2605
+ if (env.GITLAB_CI === "true" || env.GITLAB_CI === "1") return true;
2606
+ if (env.CIRCLECI === "true" || env.CIRCLECI === "1") return true;
2607
+ if (env.BUILDKITE === "true" || env.BUILDKITE === "1") return true;
2608
+ if (typeof env.CI === "string" && env.CI.trim() !== "" && env.CI !== "0" && env.CI !== "false") {
2609
+ return true;
2610
+ }
2611
+ return false;
2612
+ }
2613
+ function shouldEnableAnsiColor(opts = {}) {
2614
+ if (typeof opts.color === "boolean") return opts.color;
2615
+ const env = opts.env ?? process.env;
2616
+ if (env.NO_COLOR !== void 0) return false;
2617
+ if (isCiLikeEnv(env)) return false;
2618
+ const tty = opts.stdoutIsTty ?? process.stdout.isTTY === true;
2619
+ return tty;
2620
+ }
2621
+ function shouldUseUnicodeSymbols(opts = {}) {
2622
+ if (typeof opts.unicode === "boolean") return opts.unicode;
2623
+ const env = opts.env ?? process.env;
2624
+ const tty = opts.stdoutIsTty ?? process.stdout.isTTY === true;
2625
+ if (!tty) return false;
2626
+ if (env.TERM === "dumb") return false;
2627
+ if (process.platform === "win32") {
2628
+ return Boolean(
2629
+ env.WT_SESSION || env.WT_PROFILE_ID || env.ConEmuANSI === "ON" || env.TERM_PROGRAM === "vscode" || env.TERM_PROGRAM === "cursor" || typeof env.TERM === "string" && env.TERM !== "" && env.TERM !== "dumb"
2630
+ );
2631
+ }
2632
+ return true;
2633
+ }
2634
+ function paint(enabled, code, text) {
2635
+ if (!enabled || text === "") return text;
2636
+ return `${code}${text}${ANSI.reset}`;
2637
+ }
2638
+ function createTerminalPresentation(opts = {}) {
2639
+ const color = shouldEnableAnsiColor(opts);
2640
+ const unicode = shouldUseUnicodeSymbols(opts);
2641
+ const successSymbol = unicode ? "\u2713" : "[OK]";
2642
+ const warningSymbol = unicode ? "\u26A0" : "[WARN]";
2643
+ const nextActionPrefix = unicode ? "\u2192" : "Next:";
2644
+ return {
2645
+ color,
2646
+ unicode,
2647
+ bold: (text) => paint(color, ANSI.bold, text),
2648
+ green: (text) => paint(color, ANSI.green, text),
2649
+ cyan: (text) => paint(color, ANSI.cyan, text),
2650
+ yellow: (text) => paint(color, ANSI.yellow, text),
2651
+ dim: (text) => paint(color, ANSI.dim, text),
2652
+ successSymbol,
2653
+ warningSymbol,
2654
+ nextActionPrefix,
2655
+ successLine: (message) => paint(color, ANSI.green, `${successSymbol} ${message}`),
2656
+ warningLine: (message) => paint(color, ANSI.yellow, `${warningSymbol} ${message}`),
2657
+ nextActionLine: (message) => `${nextActionPrefix} ${message}`,
2658
+ heading: (text) => paint(color, ANSI.bold, text),
2659
+ indent: (text) => ` ${text}`
2660
+ };
2661
+ }
2662
+
2663
+ // src/openCursorMcpSettings.ts
2664
+ var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process5.execFile);
2665
+ var OPEN_CURSOR_MCP_SETTINGS_PROMPT = "Open Cursor MCP settings now? [Y/n] ";
2666
+ function resolvePresentation(deps) {
2667
+ if (deps.presentation) return deps.presentation;
2668
+ const opts = {
2669
+ env: deps.env ?? process.env,
2670
+ stdoutIsTty: deps.stdoutIsTty ?? process.stdout.isTTY === true,
2671
+ color: deps.color,
2672
+ unicode: deps.unicode
2673
+ };
2674
+ return createTerminalPresentation(opts);
2675
+ }
2676
+ function shouldPromptOpenCursorMcpSettings(opts) {
2677
+ if (!opts.explicitCursor) return false;
2678
+ if (opts.all) return false;
2679
+ if (opts.dryRun) return false;
2680
+ if (!opts.stdinIsTty) return false;
2681
+ if (isCiLikeEnv(opts.env ?? process.env)) return false;
2682
+ return true;
2683
+ }
2684
+ function isYesDefaultAnswer(answer) {
2685
+ const trimmed = answer.trim();
2686
+ if (trimmed === "") return true;
2687
+ if (/^y(es)?$/i.test(trimmed)) return true;
2688
+ if (/^n(o)?$/i.test(trimmed)) return false;
2689
+ return false;
2690
+ }
2691
+ async function confirmYesDefault(question, deps = {}) {
2692
+ if (deps.ask) {
2693
+ return isYesDefaultAnswer(await deps.ask(question));
2694
+ }
2695
+ const input2 = deps.input ?? process.stdin;
2696
+ const output2 = deps.output ?? process.stdout;
2697
+ if (!("isTTY" in input2) || !input2.isTTY) {
2698
+ return false;
2699
+ }
2700
+ const rl = readline4.createInterface({ input: input2, output: output2 });
2701
+ try {
2702
+ const answer = await rl.question(question.endsWith(" ") ? question : `${question} `);
2703
+ return isYesDefaultAnswer(answer);
2704
+ } finally {
2705
+ rl.close();
2706
+ }
2707
+ }
2708
+ function collectOutcomePaths(outcomes) {
2709
+ const created = [];
2710
+ const updated = [];
2711
+ for (const [file, outcome] of Object.entries(outcomes)) {
2712
+ if (outcome === "created") created.push(file);
2713
+ else if (outcome === "updated") updated.push(file);
2714
+ }
2715
+ return { created, updated };
2716
+ }
2717
+ function formatCursorSetupCompletedSummary(opts, presentation = createTerminalPresentation({ color: false, unicode: false })) {
2718
+ const { created, updated } = collectOutcomePaths(opts.outcomes);
2719
+ const tp = presentation;
2720
+ const lines = [
2721
+ tp.successLine("MemoraOne setup completed for Cursor"),
2722
+ "",
2723
+ tp.heading("Repository"),
2724
+ tp.indent(tp.cyan(opts.repoRoot)),
2725
+ "",
2726
+ tp.heading("Changes")
2727
+ ];
2728
+ if (created.length === 0 && updated.length === 0) {
2729
+ lines.push(tp.indent(tp.dim("No file changes needed")));
2730
+ } else {
2731
+ if (created.length) {
2732
+ lines.push(tp.indent(`Created: ${created.join(", ")}`));
2733
+ }
2734
+ if (updated.length) {
2735
+ lines.push(tp.indent(`Updated: ${updated.join(", ")}`));
2736
+ }
2737
+ }
2738
+ return lines;
2739
+ }
2740
+ function printCursorSetupCompletedSummary(opts, println = console.log, presentation) {
2741
+ const tp = presentation ?? createTerminalPresentation();
2742
+ for (const line of formatCursorSetupCompletedSummary(opts, tp)) {
2743
+ println(line);
2744
+ }
2745
+ }
2746
+ function macosOpenCursorMcpSettingsAppleScript() {
2747
+ return [
2748
+ 'tell application "Cursor" to activate',
2749
+ "delay 0.5",
2750
+ 'tell application "System Events"',
2751
+ 'keystroke "p" using {command down, shift down}',
2752
+ "delay 0.4",
2753
+ 'keystroke "View: Open MCP Settings"',
2754
+ "delay 0.4",
2755
+ "key code 36",
2756
+ "end tell"
2757
+ ].join("\n");
2758
+ }
2759
+ async function openCursorMcpSettingsViaOsascript(execFileImpl = execFileAsync3) {
2760
+ await execFileImpl("osascript", ["-e", macosOpenCursorMcpSettingsAppleScript()], {
2761
+ timeout: 3e4
2762
+ });
2763
+ }
2764
+ function manualCursorMcpSettingsSteps(platform) {
2765
+ const chord = platform === "darwin" ? "Command + Shift + P" : "Ctrl + Shift + P";
2766
+ return [
2767
+ "To finish setup manually:",
2768
+ "1. Open this repository in Cursor.",
2769
+ `2. Press ${chord}.`,
2770
+ '3. Run "View: Open MCP Settings".',
2771
+ '4. Find "memoraone" and enable it.',
2772
+ "5. Confirm it turns green.",
2773
+ "6. Return to MemoraOne Studio and refresh Sources."
2774
+ ];
2775
+ }
2776
+ function printManualCursorMcpSettingsSteps(platform, println = console.log) {
2777
+ for (const line of manualCursorMcpSettingsSteps(platform)) {
2778
+ println(line);
2779
+ }
2780
+ }
2781
+ function formatOpenCursorMcpSettingsPrompt(presentation = createTerminalPresentation({ color: false, unicode: false })) {
2782
+ return `${presentation.indent(OPEN_CURSOR_MCP_SETTINGS_PROMPT.trimEnd())} `;
2783
+ }
2784
+ async function runOpenCursorMcpSettingsFlow(deps = {}) {
2785
+ const platform = deps.platform ?? process.platform;
2786
+ const println = deps.println ?? console.log;
2787
+ const tp = resolvePresentation(deps);
2788
+ const confirm = deps.confirm ?? ((question) => confirmYesDefault(question));
2789
+ println("");
2790
+ println(tp.heading("Next"));
2791
+ const yes = await confirm(formatOpenCursorMcpSettingsPrompt(tp));
2792
+ if (!yes) {
2793
+ printManualCursorMcpSettingsSteps(platform, println);
2794
+ return;
2795
+ }
2796
+ if (platform === "darwin") {
2797
+ println(tp.warningLine("macOS may request Automation or Accessibility permission."));
2798
+ try {
2799
+ const open = deps.openViaOsascript ?? (() => openCursorMcpSettingsViaOsascript(deps.execFile ?? execFileAsync3));
2800
+ await open();
2801
+ println(tp.successLine('Confirm "memoraone" is enabled and green'));
2802
+ println(tp.nextActionLine("Return to MemoraOne Studio and refresh Sources"));
2803
+ } catch (err) {
2804
+ const detail = err instanceof Error ? err.message : String(err);
2805
+ println(tp.warningLine(`Could not open Cursor MCP settings automatically (${detail}).`));
2806
+ println(
2807
+ "Permission may be required at: System Settings \u2192 Privacy & Security \u2192 Accessibility"
2808
+ );
2809
+ printManualCursorMcpSettingsSteps("darwin", println);
2810
+ }
2811
+ return;
2812
+ }
2813
+ printManualCursorMcpSettingsSteps(platform, println);
2814
+ }
2815
+
2394
2816
  // src/setupIdeFiles.ts
2395
2817
  var MANAGED_MARKER = "<!-- MemoraOne managed IDE helper -->";
2396
2818
  var GITIGNORE_MEMORAONE_COMMENT = "# MemoraOne local project binding / API key";
@@ -2627,7 +3049,21 @@ function parseSetupIdeFlags(argv) {
2627
3049
  if (local && staging) {
2628
3050
  flagError = "[setup-ide-files] --local and --staging are mutually exclusive.";
2629
3051
  }
2630
- return { targets, force, dryRun, noGitignore, cleanup, devMode, repair, local, staging, all, unknown, flagError };
3052
+ return {
3053
+ targets,
3054
+ force,
3055
+ dryRun,
3056
+ noGitignore,
3057
+ cleanup,
3058
+ devMode,
3059
+ repair,
3060
+ local,
3061
+ staging,
3062
+ all,
3063
+ explicitCursor: cursor,
3064
+ unknown,
3065
+ flagError
3066
+ };
2631
3067
  }
2632
3068
  function cursorEnvironmentFromFlags(local, staging) {
2633
3069
  if (local) return "local";
@@ -2892,7 +3328,7 @@ description: MemoraOne MCP \u2014 IDE agent instructions
2892
3328
  homeDir: o.homeDir,
2893
3329
  explicitPath: o.cursorGlobalMcpConfigPath
2894
3330
  });
2895
- if (!globalDetection.ok) {
3331
+ if (globalDetection.ok === false) {
2896
3332
  return {
2897
3333
  exitCode: 1,
2898
3334
  repoRoot,
@@ -2991,8 +3427,22 @@ description: MemoraOne MCP \u2014 IDE agent instructions
2991
3427
  }
2992
3428
  return { exitCode: 0, repoRoot, outcomes, cursorMcp, jetbrainsMcp, daemonCleanup };
2993
3429
  }
2994
- async function cliSetupIdeFiles(argv) {
2995
- const { targets, force, dryRun, noGitignore, cleanup, devMode, repair, local, staging, unknown, flagError } = parseSetupIdeFlags(argv);
3430
+ async function cliSetupIdeFiles(argv, options = {}) {
3431
+ const {
3432
+ targets,
3433
+ force,
3434
+ dryRun,
3435
+ noGitignore,
3436
+ cleanup,
3437
+ devMode,
3438
+ repair,
3439
+ local,
3440
+ staging,
3441
+ all,
3442
+ explicitCursor,
3443
+ unknown,
3444
+ flagError
3445
+ } = parseSetupIdeFlags(argv);
2996
3446
  if (flagError) {
2997
3447
  console.error(flagError);
2998
3448
  return 1;
@@ -3001,15 +3451,27 @@ async function cliSetupIdeFiles(argv) {
3001
3451
  console.error(`[setup-ide-files] Unknown option(s): ${unknown.join(", ")}`);
3002
3452
  return 1;
3003
3453
  }
3454
+ const cwd = options.cwd ?? process.cwd();
3455
+ const openDeps = options.openCursorMcpSettings ?? {};
3456
+ const stdinIsTty = openDeps.stdinIsTty ?? process.stdin.isTTY === true;
3457
+ const env = openDeps.env ?? process.env;
3458
+ const promptOpenCursorSettings = shouldPromptOpenCursorMcpSettings({
3459
+ explicitCursor,
3460
+ all,
3461
+ dryRun,
3462
+ stdinIsTty,
3463
+ env
3464
+ });
3004
3465
  const result = await runSetupIdeFiles({
3005
- cwd: process.cwd(),
3466
+ cwd,
3006
3467
  targets,
3007
3468
  force,
3008
3469
  dryRun,
3009
- noGitignore,
3470
+ noGitignore: options.setupOverrides?.noGitignore ?? noGitignore,
3010
3471
  devMode,
3011
3472
  repair,
3012
- cursorEnvironment: cursorEnvironmentFromFlags(local, staging)
3473
+ cursorEnvironment: cursorEnvironmentFromFlags(local, staging),
3474
+ ...options.setupOverrides
3013
3475
  });
3014
3476
  if (result.error) {
3015
3477
  console.error(result.error);
@@ -3026,23 +3488,45 @@ async function cliSetupIdeFiles(argv) {
3026
3488
  logSetupIdeCleanupSummary(result.daemonCleanup);
3027
3489
  }
3028
3490
  if (targets.cursor && result.cursorMcp) {
3029
- logCursorMcpCliSummary(result.cursorMcp, dryRun);
3491
+ logCursorMcpCliSummary(result.cursorMcp, dryRun, {
3492
+ forInteractivePostSetup: promptOpenCursorSettings
3493
+ });
3030
3494
  }
3031
3495
  if (targets.jetbrains && result.jetbrainsMcp) {
3032
3496
  logJetBrainsMcpCliSummary(result.jetbrainsMcp, dryRun);
3033
3497
  }
3034
- summarizeOutcomes(result.outcomes);
3035
- if (dryRun) {
3036
- console.log("[setup-ide-files] Dry run: no files written.");
3037
- if (result.daemonCleanup && !result.daemonCleanup.skipped) {
3038
- console.log("[setup-ide-files] Dry run: no daemons stopped, no sockets removed.");
3498
+ if (promptOpenCursorSettings && result.repoRoot) {
3499
+ const presentation = openDeps.presentation ?? createTerminalPresentation({
3500
+ env,
3501
+ stdoutIsTty: openDeps.stdoutIsTty ?? process.stdout.isTTY === true,
3502
+ color: openDeps.color,
3503
+ unicode: openDeps.unicode
3504
+ });
3505
+ printCursorSetupCompletedSummary(
3506
+ { repoRoot: result.repoRoot, outcomes: result.outcomes },
3507
+ openDeps.println,
3508
+ presentation
3509
+ );
3510
+ await runOpenCursorMcpSettingsFlow({
3511
+ ...openDeps,
3512
+ stdinIsTty,
3513
+ env,
3514
+ presentation
3515
+ });
3516
+ } else {
3517
+ summarizeOutcomes(result.outcomes);
3518
+ if (dryRun) {
3519
+ console.log("[setup-ide-files] Dry run: no files written.");
3520
+ if (result.daemonCleanup && !result.daemonCleanup.skipped) {
3521
+ console.log("[setup-ide-files] Dry run: no daemons stopped, no sockets removed.");
3522
+ }
3039
3523
  }
3524
+ console.log(`[setup-ide-files] ${restartIdeInstruction(targets)}`);
3040
3525
  }
3041
- console.log(`[setup-ide-files] ${restartIdeInstruction(targets)}`);
3042
3526
  if (cleanup) {
3043
3527
  console.log("[setup-ide-files] Running additional full-project cleanup (--cleanup)...");
3044
3528
  const cleanupResult = await runCleanup({
3045
- cwd: process.cwd(),
3529
+ cwd,
3046
3530
  dryRun,
3047
3531
  allProjects: false,
3048
3532
  assumeYes: true