@memoraone/mcp 0.1.34 → 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 +1112 -923
  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.34",
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,477 +885,120 @@ 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;
932
- }
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");
992
- }
993
- recordClientInitialize(line) {
994
- this.lastInitializeLine = line;
995
- this.clientInitializeSeen = true;
996
- }
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");
1002
- }
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
- }
1019
- }
1020
- async writeToDaemon(line) {
1021
- await this.ensureActiveDaemonSocket();
1022
- this.activeSocket.write(`${line}
1023
- `);
1024
- }
1025
- resetSessionState() {
1026
- this.lastInitializeLine = null;
1027
- this.pendingDeferredClientLines = [];
1028
- this.handshakeDeferredClientLines = [];
1029
- this.clientInitializeSeen = false;
1030
- this.activeBinding = null;
1031
- }
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);
1047
- }
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");
1054
- }
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)
1063
- );
1064
- logInitializeDebug(
1065
- this.log,
1066
- this.env,
1067
- `deferred message replay after reconnect count=${this.handshakeDeferredClientLines.length} types=${JSON.stringify(types)}`
1068
- );
1069
- for (const deferredLine of this.handshakeDeferredClientLines) {
1070
- this.activeSocket.write(`${deferredLine}
1071
- `);
1072
- }
1073
- }
1074
- }
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;
1107
- }
1108
- }
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);
1141
- }
1142
- }
1143
-
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
- // 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;
1172
- }
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);
1180
- }
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
- };
1190
- }
1191
- if (!options.npxPath) {
1192
- throw new Error("[setup-ide-files] Cursor MCP config requires a resolved npx path.");
1193
- }
1194
- return {
1195
- command: options.npxPath,
1196
- args: ["-y", "@memoraone/mcp@latest"],
1197
- env
1198
- };
1199
- }
1200
- async function pathExists(filePath) {
1201
- try {
1202
- await fs4.access(filePath);
1203
- return true;
1204
- } catch {
1205
- return false;
1206
- }
1207
- }
1208
- function stripLeadingLineComments(text) {
1209
- return text.split("\n").filter((line) => !/^\s*\/\//.test(line)).join("\n");
1210
- }
1211
- function getKnownCursorGlobalMcpConfigCandidates(homeDir) {
1212
- return [path6.join(homeDir, ".cursor", "mcp.json")];
1213
- }
1214
- async function detectCursorGlobalMcpConfig(options) {
1215
- if (options?.explicitPath) {
1216
- return { ok: true, path: options.explicitPath, detectedExisting: await pathExists(options.explicitPath) };
1217
- }
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);
1223
- }
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
- };
1230
- }
1231
- if (existing.length === 1) {
1232
- return { ok: true, path: existing[0], detectedExisting: true };
1233
- }
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
- };
1241
- }
1242
- return { ok: true, path: defaultPath, detectedExisting: false };
1243
- }
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())}`;
1247
- }
1248
- async function isWorkingNpx(npxPath) {
1249
- 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;
1260
- } catch {
1261
- return false;
1262
- }
1263
- }
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");
1271
- }
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));
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));
1276
1002
  }
1277
1003
  try {
1278
1004
  const lookupCmd = process.platform === "win32" ? "where" : "which";
@@ -1380,539 +1106,1002 @@ async function auditCursorMcpConfig(options) {
1380
1106
  } catch {
1381
1107
  repoHasManagedMemoraone = false;
1382
1108
  }
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;
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;
1116
+ }
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}`
1164
+ );
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}`
1168
+ );
1169
+ } else if (repoOutcome === "skipped") {
1170
+ console.log(`[setup-ide-files] Cursor repo MCP config unchanged: ${repoConfigPath}`);
1171
+ }
1172
+ }
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}`);
1179
+ }
1180
+ } else if (globalConfigPath) {
1181
+ console.log(
1182
+ `[setup-ide-files] Cursor global MCP config unchanged (no managed memoraone to remove): ${globalConfigPath}`
1183
+ );
1184
+ }
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
+ );
1192
+ }
1193
+ }
1194
+
1195
+ // src/cleanup.ts
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);
1203
+ }
1204
+ function parseDaemonProjectIdFromCommandLine(commandLine) {
1205
+ if (!commandLine.includes("--daemon")) {
1206
+ return null;
1207
+ }
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;
1214
+ }
1215
+ const normalized = projectId.trim().toLowerCase();
1216
+ if (!commandLine.includes(`--project-id ${normalized}`)) {
1217
+ return false;
1218
+ }
1219
+ const daemonProjectId = parseDaemonProjectIdFromCommandLine(commandLine);
1220
+ return daemonProjectId !== null && daemonProjectId === normalized;
1221
+ }
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)) {
1230
+ return false;
1231
+ }
1232
+ const daemonIde = parseDaemonIdeFromCommandLine(commandLine);
1233
+ return daemonIde !== void 0 && daemonIde === ide;
1234
+ }
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;
1252
+ }
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();
1259
+ }
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;
1277
+ }
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
+ }
1299
+ }
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;
1314
+ }
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
+ }
1326
+ }
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
+ }
1351
+ }
1352
+ return filtered;
1353
+ }
1354
+ async function defaultKillProcess(pid) {
1355
+ process.kill(pid, "SIGTERM");
1356
+ }
1357
+ async function defaultRemoveSocket(socketPath) {
1358
+ await fs5.unlink(socketPath);
1359
+ try {
1360
+ await fs5.unlink(bindingSidecarPath(socketPath));
1361
+ } catch {
1362
+ }
1363
+ }
1364
+ async function defaultConfirm(message) {
1365
+ if (!import_node_process.stdin.isTTY) {
1366
+ return false;
1367
+ }
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();
1374
+ }
1375
+ }
1376
+ async function resolveCleanupTarget(cwd) {
1377
+ try {
1378
+ const binding = await resolveAuthoritativeBinding([path7.resolve(cwd)]);
1379
+ return {
1380
+ workspaceRoot: binding.workspaceRoot,
1381
+ m1Path: binding.m1Path,
1382
+ projectId: binding.projectId
1383
+ };
1384
+ } catch {
1385
+ return { error: CLEANUP_PROJECT_ID_REQUIRED_ERROR };
1386
+ }
1387
+ }
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);
1390
1400
  }
1391
1401
  }
1392
- return {
1393
- repoConfigPath,
1394
- repoHasManagedMemoraone,
1395
- globalConfigPath,
1396
- globalHasManagedMemoraone,
1397
- conflict: repoHasManagedMemoraone && globalHasManagedMemoraone
1398
- };
1402
+ return { matching, skipped };
1399
1403
  }
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.`
1404
+ function logPrefix(dryRun) {
1405
+ return dryRun ? "[cleanup][dry-run]" : "[cleanup]";
1406
+ }
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.`
1415
1412
  );
1416
- } else if (audit.repoHasManagedMemoraone && !audit.globalHasManagedMemoraone) {
1413
+ } else {
1417
1414
  console.log(
1418
- `${prefix} Cursor MCP is repo-scoped (.cursor/mcp.json) with no global memoraone entry (recommended for multi-repo windows).`
1415
+ `${prefix} Note: Valid IDE connections for this project may disconnect temporarily; they should reconnect automatically. Stale connections will remain cleared.`
1419
1416
  );
1420
1417
  }
1421
1418
  }
1422
- function logCursorMcpCliSummary(info, dryRun, opts) {
1423
- const { repoConfigPath, repoOutcome, npxPath, cliPath, repoBackupPath, globalConfigPath, globalMemoraoneRemoved, globalBackupPath } = info;
1424
- const interactive = opts?.forInteractivePostSetup === true;
1425
- console.log(`[setup-ide-files] Cursor repo MCP config: ${repoConfigPath}`);
1426
- if (cliPath) {
1427
- console.log(`[setup-ide-files] Resolved local CLI: ${cliPath}`);
1428
- } else if (npxPath) {
1429
- console.log(`[setup-ide-files] Resolved npx: ${npxPath}`);
1419
+ function cleanupLog(opts, message) {
1420
+ if (!opts.quiet) {
1421
+ console.log(message);
1430
1422
  }
1431
- if (repoBackupPath) {
1432
- console.log(`[setup-ide-files] Cursor repo MCP config backup: ${repoBackupPath}`);
1423
+ }
1424
+ function cleanupWarn(opts, message) {
1425
+ if (!opts.quiet) {
1426
+ console.warn(message);
1433
1427
  }
1434
- if (!interactive) {
1435
- if (repoOutcome === "created") {
1436
- console.log(
1437
- dryRun ? `[setup-ide-files] Cursor repo MCP config would be created: ${repoConfigPath}` : `[setup-ide-files] Cursor repo MCP config created: ${repoConfigPath}`
1438
- );
1439
- } else if (repoOutcome === "updated") {
1440
- console.log(
1441
- dryRun ? `[setup-ide-files] Cursor repo MCP config would be updated: ${repoConfigPath}` : `[setup-ide-files] Cursor repo MCP config updated: ${repoConfigPath}`
1442
- );
1443
- } else if (repoOutcome === "skipped") {
1444
- console.log(`[setup-ide-files] Cursor repo MCP config unchanged: ${repoConfigPath}`);
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
+ };
1445
1448
  }
1446
- }
1447
- if (globalMemoraoneRemoved && globalConfigPath) {
1448
- console.log(
1449
- dryRun ? `[setup-ide-files] Would remove memoraone from Cursor global MCP config: ${globalConfigPath}` : `[setup-ide-files] Removed memoraone from Cursor global MCP config: ${globalConfigPath}`
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()}.`
1450
1453
  );
1451
- if (globalBackupPath) {
1452
- console.log(`[setup-ide-files] Cursor global MCP config backup: ${globalBackupPath}`);
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
+ }
1453
1485
  }
1454
- } else if (globalConfigPath) {
1455
- console.log(
1456
- `[setup-ide-files] Cursor global MCP config unchanged (no managed memoraone to remove): ${globalConfigPath}`
1457
- );
1458
1486
  }
1459
- console.log(
1460
- "[setup-ide-files] Each Cursor window uses this repo\u2019s .cursor/mcp.json (separate MCP process per repo)."
1461
- );
1462
- if (!interactive) {
1463
- console.log(
1464
- "[setup-ide-files] Fully quit Cursor and reopen this repo for MCP changes to take effect."
1465
- );
1487
+ if (targetProjectId !== null || opts.ide) {
1488
+ logReconnectNotice(opts, prefix, opts.ide);
1466
1489
  }
1467
- }
1468
-
1469
- // src/cleanup.ts
1470
- var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
1471
- 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;
1472
- 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;
1473
- var MEMORAONE_MCP_COMMAND_RE = /memoraone-mcp|memoraOne-mcp|@memoraone\/mcp/;
1474
- var CLEANUP_PROJECT_ID_REQUIRED_ERROR = "Provide --project-id <id> or run from a folder containing memoraone.m1.";
1475
- function isMemoraoneMcpCommandLine(commandLine) {
1476
- return MEMORAONE_MCP_COMMAND_RE.test(commandLine);
1477
- }
1478
- function parseDaemonProjectIdFromCommandLine(commandLine) {
1479
- if (!commandLine.includes("--daemon")) {
1480
- return null;
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
+ }
1481
1516
  }
1482
- const match = commandLine.match(DAEMON_PROJECT_ID_RE);
1483
- return match ? match[1].toLowerCase() : null;
1484
- }
1485
- function parseDaemonIdeFromCommandLine(commandLine) {
1486
- if (!commandLine.includes("--daemon")) {
1487
- return void 0;
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();
1488
1543
  }
1489
- return parseIdeTypeFromCommandLine(commandLine);
1490
- }
1491
- function parseDaemonProcessLines(lines) {
1492
- const processes = [];
1493
- for (const line of lines) {
1494
- const trimmed = line.trim();
1495
- if (!trimmed) continue;
1496
- const spaceIdx = trimmed.indexOf(" ");
1497
- if (spaceIdx <= 0) continue;
1498
- const pid = Number.parseInt(trimmed.slice(0, spaceIdx), 10);
1499
- if (!Number.isFinite(pid) || pid <= 0) continue;
1500
- const command = trimmed.slice(spaceIdx + 1);
1501
- if (!isMemoraoneMcpCommandLine(command)) continue;
1502
- const projectId = parseDaemonProjectIdFromCommandLine(command);
1503
- if (projectId === null) continue;
1504
- const ide = parseDaemonIdeFromCommandLine(command);
1505
- processes.push({ pid, command, projectId, ...ide !== void 0 ? { ide } : {} });
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)"}`
1564
+ );
1506
1565
  }
1507
- return processes;
1508
- }
1509
- function normalizeCleanupProjectId(projectId) {
1510
- const trimmed = projectId.trim();
1511
- if (!PROJECT_ID_RE.test(trimmed)) {
1512
- return { error: `Invalid project id: ${projectId}` };
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}`);
1571
+ }
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.`);
1513
1576
  }
1514
- return trimmed.toLowerCase();
1515
- }
1516
- async function defaultListDaemonProcesses() {
1517
- const { stdout } = await execFileAsync2("ps", ["-eo", "pid=,args="], {
1518
- maxBuffer: 10 * 1024 * 1024
1519
- });
1520
- return parseDaemonProcessLines(stdout.split("\n"));
1521
- }
1522
- async function defaultListSocketPaths(projectId) {
1523
- const baseDir = getMcpBaseDir();
1524
- let entries;
1525
- try {
1526
- entries = await fs5.readdir(baseDir);
1527
- } catch (err) {
1528
- const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
1529
- if (code === "ENOENT") {
1530
- return [];
1577
+ if (socketPaths.length) {
1578
+ cleanupLog(opts, `${prefix} Sockets to remove:`);
1579
+ for (const socketPath of socketPaths) {
1580
+ cleanupLog(opts, `${prefix} ${socketPath}`);
1531
1581
  }
1532
- throw err;
1582
+ } else {
1583
+ cleanupLog(opts, `${prefix} No matching sockets found under ${getMcpBaseDir()}.`);
1533
1584
  }
1534
- const paths = [];
1535
- for (const name of entries) {
1536
- if (!name.endsWith(".sock") || !isMemoraoneSocketFilename(name)) {
1537
- continue;
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}`);
1538
1589
  }
1539
- const socketPath = path7.join(baseDir, name);
1540
- if (projectId === null) {
1541
- paths.push(socketPath);
1542
- continue;
1590
+ }
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
+ };
1543
1605
  }
1544
- const normalizedProjectId = projectId.trim().toLowerCase();
1545
- if (isLegacySocketFilename(name) && isSocketFilenameForProject(name, normalizedProjectId)) {
1546
- paths.push(socketPath);
1547
- continue;
1606
+ }
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
+ };
1620
+ }
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)}`);
1548
1628
  }
1549
- if (isHashSocketFilename(name)) {
1550
- const record = readBindingSidecarRecord(socketPath);
1551
- if (record?.projectId.trim().toLowerCase() === normalizedProjectId) {
1552
- paths.push(socketPath);
1629
+ }
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)}`);
1553
1642
  }
1554
1643
  }
1555
1644
  }
1556
- 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
+ };
1557
1655
  }
1558
- async function filterSocketPathsByIde(socketPaths, projectId, ide) {
1559
- if (ide === void 0) return socketPaths;
1560
- const normalizedProjectId = projectId.trim().toLowerCase();
1561
- const filtered = [];
1562
- for (const socketPath of socketPaths) {
1563
- const basename4 = path7.basename(socketPath);
1564
- if (isLegacySocketFilename(basename4)) {
1565
- if (isSocketFilenameForProjectAndIde(basename4, normalizedProjectId, ide)) {
1566
- 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];
1567
1674
  }
1568
- continue;
1569
- }
1570
- if (isHashSocketFilename(basename4)) {
1571
- const record = readBindingSidecarRecord(socketPath);
1572
- if (record?.projectId.trim().toLowerCase() === normalizedProjectId && record.ideType === ide) {
1573
- 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
+ }
1574
1685
  }
1575
- }
1686
+ } else if (arg.startsWith("-")) unknown.push(arg);
1687
+ else unknown.push(arg);
1576
1688
  }
1577
- return filtered;
1578
- }
1579
- async function defaultKillProcess(pid) {
1580
- process.kill(pid, "SIGTERM");
1689
+ return { dryRun, allProjects, assumeYes, projectId, ide, invalidIde, unknown };
1581
1690
  }
1582
- async function defaultRemoveSocket(socketPath) {
1583
- await fs5.unlink(socketPath);
1584
- try {
1585
- await fs5.unlink(bindingSidecarPath(socketPath));
1586
- } 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;
1587
1698
  }
1588
- }
1589
- async function defaultConfirm(message) {
1590
- if (!import_node_process.stdin.isTTY) {
1591
- return false;
1699
+ if (unknown.length) {
1700
+ console.error(`[cleanup] Unknown option(s): ${unknown.join(", ")}`);
1701
+ return 1;
1592
1702
  }
1593
- const rl = readline3.createInterface({ input: import_node_process.stdin, output: import_node_process.stdout });
1594
- try {
1595
- const answer = await rl.question(`${message} [y/N] `);
1596
- return /^y(es)?$/i.test(answer.trim());
1597
- } finally {
1598
- 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}`);
1599
1713
  }
1714
+ return result.exitCode;
1600
1715
  }
1601
- 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) {
1602
1723
  try {
1603
- const binding = await resolveAuthoritativeBinding([path7.resolve(cwd)]);
1604
- return {
1605
- workspaceRoot: binding.workspaceRoot,
1606
- m1Path: binding.m1Path,
1607
- projectId: binding.projectId
1608
- };
1609
- } catch {
1610
- return { error: CLEANUP_PROJECT_ID_REQUIRED_ERROR };
1611
- }
1612
- }
1613
- function filterProcessesForScope(processes, projectId) {
1614
- if (projectId === null) {
1615
- return { matching: processes, skipped: [] };
1616
- }
1617
- const normalized = projectId.toLowerCase();
1618
- const matching = [];
1619
- const skipped = [];
1620
- for (const proc of processes) {
1621
- if (proc.projectId === normalized) {
1622
- matching.push(proc);
1623
- } else {
1624
- skipped.push(proc);
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)}`;
1625
1730
  }
1731
+ return "jsonrpc";
1732
+ } catch {
1733
+ return "invalid-json";
1626
1734
  }
1627
- return { matching, skipped };
1628
1735
  }
1629
- function logPrefix(dryRun) {
1630
- return dryRun ? "[cleanup][dry-run]" : "[cleanup]";
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
+ });
1631
1750
  }
1632
- function logReconnectNotice(opts, prefix, ide) {
1633
- if (opts.quiet) return;
1634
- if (ide) {
1635
- console.log(
1636
- `${prefix} Note: Valid ${ide} connections for this project may disconnect temporarily; they should reconnect automatically. Stale connections will remain cleared.`
1637
- );
1638
- } else {
1639
- console.log(
1640
- `${prefix} Note: Valid IDE connections for this project may disconnect temporarily; they should reconnect automatically. Stale connections will remain cleared.`
1641
- );
1642
- }
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
+ });
1643
1760
  }
1644
- function cleanupLog(opts, message) {
1645
- if (!opts.quiet) {
1646
- console.log(message);
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;
1647
1769
  }
1648
- }
1649
- function cleanupWarn(opts, message) {
1650
- if (!opts.quiet) {
1651
- console.warn(message);
1770
+ for (const proc of processes) {
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)}`);
1778
+ }
1652
1779
  }
1653
1780
  }
1654
- async function runCleanup(opts) {
1655
- const listProcesses = opts.listProcesses ?? defaultListDaemonProcesses;
1656
- const listSocketPaths = opts.listSocketPaths ?? defaultListSocketPaths;
1657
- const killProcess = opts.killProcess ?? defaultKillProcess;
1658
- const removeSocket = opts.removeSocket ?? defaultRemoveSocket;
1659
- const confirm = opts.confirm ?? defaultConfirm;
1660
- const prefix = logPrefix(opts.dryRun);
1661
- let targetProjectId = null;
1662
- let workspaceRoot;
1663
- let m1Path;
1664
- if (opts.allProjects) {
1665
- if (opts.projectId) {
1666
- return {
1667
- exitCode: 1,
1668
- killedPids: [],
1669
- removedSockets: [],
1670
- skippedProcesses: [],
1671
- error: "Cannot combine --all-projects with --project-id."
1672
- };
1673
- }
1674
- cleanupLog(opts, `${prefix} Mode: all projects (--all-projects)`);
1675
- cleanupWarn(
1676
- opts,
1677
- `${prefix} WARNING: This stops every MemoraOne MCP daemon and removes all project sockets under ${getMcpBaseDir()}.`
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}`
1678
1787
  );
1679
- } else if (opts.projectId) {
1680
- const normalized = normalizeCleanupProjectId(opts.projectId);
1681
- if (typeof normalized !== "string") {
1682
- return { exitCode: 1, killedPids: [], removedSockets: [], skippedProcesses: [], error: normalized.error };
1683
- }
1684
- targetProjectId = normalized;
1685
- cleanupLog(opts, `${prefix} Project id: ${targetProjectId}`);
1686
- if (opts.ide) {
1687
- cleanupLog(opts, `${prefix} IDE filter: ${opts.ide}`);
1688
- }
1689
- } else {
1690
- const target = await resolveCleanupTarget(opts.cwd);
1691
- if ("error" in target) {
1692
- return { exitCode: 1, killedPids: [], removedSockets: [], skippedProcesses: [], error: target.error };
1693
- }
1694
- targetProjectId = target.projectId;
1695
- workspaceRoot = target.workspaceRoot;
1696
- m1Path = target.m1Path;
1697
- cleanupLog(opts, `${prefix} Workspace root: ${workspaceRoot}`);
1698
- cleanupLog(opts, `${prefix} memoraone.m1: ${m1Path}`);
1699
- cleanupLog(opts, `${prefix} Project id: ${targetProjectId}`);
1700
- if (opts.ide) {
1701
- cleanupLog(opts, `${prefix} IDE filter: ${opts.ide}`);
1702
- }
1703
- if (workspaceRoot && (!opts.ide || opts.ide === "cursor")) {
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
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) {
1704
1807
  try {
1705
- const cursorAudit = await auditCursorMcpConfig({ repoRoot: workspaceRoot });
1706
- logCursorMcpConfigAudit(prefix, cursorAudit);
1707
- } catch (err) {
1708
- cleanupWarn(opts, `${prefix} Cursor MCP config audit failed: ${String(err)}`);
1808
+ socket.destroy();
1809
+ } catch {
1709
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);
1710
1824
  }
1711
1825
  }
1712
- if (targetProjectId !== null || opts.ide) {
1713
- logReconnectNotice(opts, prefix, opts.ide);
1714
- }
1715
- const allDaemonProcesses = await listProcesses();
1716
- const { matching: projectProcesses, skipped: skippedProcesses } = filterProcessesForScope(
1717
- allDaemonProcesses,
1718
- targetProjectId
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
1719
1835
  );
1720
- let processesToStop = projectProcesses;
1721
- const ideSkippedProcesses = [];
1722
- if (opts.ide && targetProjectId !== null) {
1723
- processesToStop = [];
1724
- for (const proc of projectProcesses) {
1725
- if (proc.ide === opts.ide) {
1726
- processesToStop.push(proc);
1727
- } else if (proc.ide === void 0) {
1728
- ideSkippedProcesses.push(proc);
1729
- cleanupLog(
1730
- opts,
1731
- `${prefix} Skipped daemon pid=${proc.pid} because IDE could not be safely determined.`
1732
- );
1733
- } else {
1734
- ideSkippedProcesses.push(proc);
1735
- cleanupLog(
1736
- opts,
1737
- `${prefix} Skipped daemon pid=${proc.pid} (IDE ${proc.ide} does not match filter ${opts.ide}).`
1836
+ verifyDaemonSidecarBinding(socketPath, sessionBinding, opts.env);
1837
+ return { socket, binding: sessionBinding, cacheRefreshed: reconciled.cacheRefreshed };
1838
+ }
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}`
1738
1877
  );
1739
- }
1740
- }
1878
+ });
1879
+ child.unref();
1880
+ });
1741
1881
  }
1742
- const allSocketPaths = await listSocketPaths(targetProjectId);
1743
- const socketPaths = targetProjectId === null ? allSocketPaths : await filterSocketPathsByIde(allSocketPaths, targetProjectId, opts.ide);
1744
- if (opts.allProjects) {
1745
- const projectIds = /* @__PURE__ */ new Set();
1746
- for (const proc of processesToStop) {
1747
- projectIds.add(proc.projectId);
1748
- }
1749
- for (const socketPath of socketPaths) {
1750
- const id = extractProjectIdFromSocketFilename(path7.basename(socketPath));
1751
- if (id) {
1752
- projectIds.add(id);
1753
- continue;
1882
+ getActiveBinding() {
1883
+ return this.activeBinding;
1884
+ }
1885
+ hasClientInitialize() {
1886
+ return this.clientInitializeSeen;
1887
+ }
1888
+ async ensureDaemonForInitialize(params) {
1889
+ logInitializeDebug(
1890
+ this.log,
1891
+ this.env,
1892
+ `initialize payload: ${summarizeInitializeParamsForDebug(params)}`
1893
+ );
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
+ );
1754
1904
  }
1755
- const record = readBindingSidecarRecord(socketPath);
1756
- if (record) {
1757
- projectIds.add(record.projectId.trim().toLowerCase());
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
+ );
1758
1922
  }
1759
1923
  }
1760
- cleanupLog(
1761
- opts,
1762
- `${prefix} Projects affected: ${projectIds.size ? [...projectIds].sort().join(", ") : "(none found)"}`
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}`
1763
1931
  );
1764
- }
1765
- if (processesToStop.length) {
1766
- cleanupLog(opts, `${prefix} Daemon processes to stop:`);
1767
- for (const proc of processesToStop) {
1768
- const ideLabel = proc.ide ? ` ide=${proc.ide}` : "";
1769
- cleanupLog(opts, `${prefix} pid=${proc.pid} project=${proc.projectId}${ideLabel}`);
1932
+ if (this.activeBinding && bindingsMatch(this.activeBinding, binding) && this.activeSocket) {
1933
+ return;
1770
1934
  }
1771
- } else if (ideSkippedProcesses.length) {
1772
- cleanupLog(opts, `${prefix} No matching daemon processes for IDE filter ${opts.ide}.`);
1773
- } else {
1774
- cleanupLog(opts, `${prefix} No matching daemon processes found.`);
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;
1943
+ }
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;
1775
1951
  }
1776
- if (socketPaths.length) {
1777
- cleanupLog(opts, `${prefix} Sockets to remove:`);
1778
- for (const socketPath of socketPaths) {
1779
- cleanupLog(opts, `${prefix} ${socketPath}`);
1780
- }
1781
- } else {
1782
- cleanupLog(opts, `${prefix} No matching sockets found under ${getMcpBaseDir()}.`);
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");
1783
1957
  }
1784
- if (skippedProcesses.length) {
1785
- cleanupLog(opts, `${prefix} Skipped unrelated daemon processes:`);
1786
- for (const proc of skippedProcesses) {
1787
- cleanupLog(opts, `${prefix} pid=${proc.pid} project=${proc.projectId}`);
1958
+ async replayDeferredClientMessages() {
1959
+ if (this.pendingDeferredClientLines.length === 0) {
1960
+ return;
1788
1961
  }
1789
- }
1790
- if (opts.allProjects && !opts.dryRun) {
1791
- const ok = opts.assumeYes ? true : await confirm(`${prefix} Proceed with cleanup for ALL projects?`);
1792
- if (!ok) {
1793
- cleanupLog(opts, `${prefix} Aborted.`);
1794
- return {
1795
- exitCode: 1,
1796
- workspaceRoot,
1797
- m1Path,
1798
- projectId: targetProjectId ?? void 0,
1799
- killedPids: [],
1800
- removedSockets: [],
1801
- skippedProcesses: [...skippedProcesses, ...ideSkippedProcesses],
1802
- error: opts.assumeYes ? void 0 : "Aborted (--all requires --yes in non-interactive mode)"
1803
- };
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);
1804
1973
  }
1805
1974
  }
1806
- const killedPids = [];
1807
- const removedSockets = [];
1808
- if (opts.dryRun) {
1809
- cleanupLog(opts, `${prefix} Dry run complete \u2014 no processes stopped, no sockets removed.`);
1810
- return {
1811
- exitCode: 0,
1812
- workspaceRoot,
1813
- m1Path,
1814
- projectId: targetProjectId ?? void 0,
1815
- killedPids: processesToStop.map((p) => p.pid),
1816
- removedSockets: socketPaths,
1817
- skippedProcesses: [...skippedProcesses, ...ideSkippedProcesses]
1818
- };
1975
+ async writeToDaemon(line) {
1976
+ await this.ensureActiveDaemonSocket();
1977
+ this.activeSocket.write(`${line}
1978
+ `);
1819
1979
  }
1820
- for (const proc of processesToStop) {
1821
- try {
1822
- await killProcess(proc.pid);
1823
- killedPids.push(proc.pid);
1824
- cleanupLog(opts, `${prefix} Stopped daemon pid=${proc.pid} project=${proc.projectId}`);
1825
- } catch (err) {
1826
- cleanupWarn(opts, `${prefix} Could not stop pid=${proc.pid}: ${String(err)}`);
1827
- }
1980
+ resetSessionState() {
1981
+ this.lastInitializeLine = null;
1982
+ this.pendingDeferredClientLines = [];
1983
+ this.handshakeDeferredClientLines = [];
1984
+ this.clientInitializeSeen = false;
1985
+ this.activeBinding = null;
1828
1986
  }
1829
- if (killedPids.length) {
1830
- await new Promise((r) => setTimeout(r, 300));
1987
+ async connectActiveDaemon() {
1988
+ if (!this.activeBinding) {
1989
+ throw new Error("[memoraone-mcp] Internal error: connectActiveDaemon without active binding");
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);
1831
2003
  }
1832
- for (const socketPath of socketPaths) {
1833
- try {
1834
- await removeSocket(socketPath);
1835
- removedSockets.push(socketPath);
1836
- cleanupLog(opts, `${prefix} Removed socket ${socketPath}`);
1837
- } catch (err) {
1838
- const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
1839
- if (code !== "ENOENT") {
1840
- cleanupWarn(opts, `${prefix} Could not remove socket ${socketPath}: ${String(err)}`);
2004
+ async ensureActiveDaemonSocket() {
2005
+ if (this.activeSocket && !this.activeSocket.destroyed) {
2006
+ return;
2007
+ }
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
+ `);
1841
2028
  }
1842
2029
  }
1843
2030
  }
1844
- cleanupLog(opts, `${prefix} Done. stopped=${killedPids.length} socketsRemoved=${removedSockets.length}`);
1845
- return {
1846
- exitCode: 0,
1847
- workspaceRoot,
1848
- m1Path,
1849
- projectId: targetProjectId ?? void 0,
1850
- killedPids,
1851
- removedSockets,
1852
- skippedProcesses: [...skippedProcesses, ...ideSkippedProcesses]
1853
- };
1854
- }
1855
- function parseCleanupFlags(argv) {
1856
- let dryRun = false;
1857
- let allProjects = false;
1858
- let assumeYes = false;
1859
- let projectId;
1860
- let ide;
1861
- let invalidIde;
1862
- const unknown = [];
1863
- for (let i = 0; i < argv.length; i++) {
1864
- const arg = argv[i];
1865
- if (arg === "--dry-run") dryRun = true;
1866
- else if (arg === "--all-projects" || arg === "--all") allProjects = true;
1867
- else if (arg === "--yes" || arg === "-y") assumeYes = true;
1868
- else if (arg === "--project-id") {
1869
- if (i + 1 >= argv.length) {
1870
- unknown.push("--project-id (missing value)");
1871
- } else {
1872
- projectId = argv[++i];
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;
1873
2048
  }
1874
- } else if (arg === "--ide") {
1875
- if (i + 1 >= argv.length) {
1876
- unknown.push("--ide (missing value)");
1877
- } else {
1878
- const value = argv[++i];
1879
- if (IDE_TYPES.includes(value)) {
1880
- ide = value;
1881
- } else {
1882
- invalidIde = value;
1883
- }
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;
1884
2056
  }
1885
- } else if (arg.startsWith("-")) unknown.push(arg);
1886
- else unknown.push(arg);
1887
- }
1888
- return { dryRun, allProjects, assumeYes, projectId, ide, invalidIde, unknown };
1889
- }
1890
- async function cliCleanup(argv) {
1891
- const { dryRun, allProjects, assumeYes, projectId, ide, invalidIde, unknown } = parseCleanupFlags(argv);
1892
- if (invalidIde) {
1893
- console.error(
1894
- `[cleanup] Invalid --ide value: ${invalidIde}. Expected one of: ${IDE_TYPES.join(", ")}.`
1895
- );
1896
- return 1;
2057
+ });
1897
2058
  }
1898
- if (unknown.length) {
1899
- console.error(`[cleanup] Unknown option(s): ${unknown.join(", ")}`);
1900
- return 1;
2059
+ detachSocketReader() {
2060
+ if (this.socketLineReader) {
2061
+ this.socketLineReader.close();
2062
+ this.socketLineReader = null;
2063
+ }
1901
2064
  }
1902
- const result = await runCleanup({
1903
- cwd: process.cwd(),
1904
- dryRun,
1905
- allProjects,
1906
- assumeYes,
1907
- projectId,
1908
- ide
1909
- });
1910
- if (result.error) {
1911
- console.error(`[cleanup] ${result.error}`);
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;
2077
+ }
2078
+ const trimmed = line.trim();
2079
+ if (trimmed === "") {
2080
+ continue;
2081
+ }
2082
+ let message;
2083
+ try {
2084
+ message = JSON.parse(trimmed);
2085
+ } catch (err) {
2086
+ throw new Error(`[memoraone-mcp] Invalid JSON-RPC on stdin: ${String(err)}`);
2087
+ }
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;
2095
+ }
2096
+ await router.writeToDaemon(trimmed);
1912
2097
  }
1913
- return result.exitCode;
1914
2098
  }
1915
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
+
1916
2105
  // src/jetbrainsMcpConfig.ts
1917
2106
  var fs6 = __toESM(require("fs/promises"), 1);
1918
2107
  var os3 = __toESM(require("os"), 1);