@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.
- package/dist/cli.cjs +1479 -995
- package/dist/daemon.cjs +111 -36
- package/dist/index.cjs +221 -102
- 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
|
+
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
|
|
72
|
-
var
|
|
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
|
-
|
|
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,
|
|
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("
|
|
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/
|
|
806
|
-
var
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
|
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
|
|
931
|
+
return false;
|
|
822
932
|
}
|
|
823
933
|
}
|
|
824
|
-
function
|
|
825
|
-
return
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
|
|
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
|
-
|
|
840
|
-
const
|
|
841
|
-
return
|
|
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
|
|
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
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
931
|
-
|
|
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
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
1022
|
-
this.activeSocket.write(`${line}
|
|
1023
|
-
`);
|
|
1075
|
+
if (dryRun) {
|
|
1076
|
+
return { changed: true, backupPath: `${configPath}.backup-<timestamp>` };
|
|
1024
1077
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
-
|
|
1070
|
-
|
|
1071
|
-
`);
|
|
1072
|
-
}
|
|
1169
|
+
} else if (repoOutcome === "skipped") {
|
|
1170
|
+
console.log(`[setup-ide-files] Cursor repo MCP config unchanged: ${repoConfigPath}`);
|
|
1073
1171
|
}
|
|
1074
1172
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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
|
|
1151
|
-
var
|
|
1152
|
-
var
|
|
1153
|
-
var
|
|
1154
|
-
var
|
|
1155
|
-
|
|
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
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
-
|
|
1192
|
-
|
|
1215
|
+
const normalized = projectId.trim().toLowerCase();
|
|
1216
|
+
if (!commandLine.includes(`--project-id ${normalized}`)) {
|
|
1217
|
+
return false;
|
|
1193
1218
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
args: ["-y", "@memoraone/mcp@latest"],
|
|
1197
|
-
env
|
|
1198
|
-
};
|
|
1219
|
+
const daemonProjectId = parseDaemonProjectIdFromCommandLine(commandLine);
|
|
1220
|
+
return daemonProjectId !== null && daemonProjectId === normalized;
|
|
1199
1221
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
|
1209
|
-
|
|
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
|
|
1212
|
-
|
|
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
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
-
|
|
1232
|
-
|
|
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
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
|
1352
|
+
return filtered;
|
|
1243
1353
|
}
|
|
1244
|
-
function
|
|
1245
|
-
|
|
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
|
|
1357
|
+
async function defaultRemoveSocket(socketPath) {
|
|
1358
|
+
await fs5.unlink(socketPath);
|
|
1249
1359
|
try {
|
|
1250
|
-
|
|
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
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
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
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
|
1328
|
-
|
|
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
|
|
1331
|
-
return
|
|
1404
|
+
function logPrefix(dryRun) {
|
|
1405
|
+
return dryRun ? "[cleanup][dry-run]" : "[cleanup]";
|
|
1332
1406
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
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
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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
|
-
|
|
1393
|
-
|
|
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
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
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
|
-
|
|
1431
|
-
|
|
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
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
)
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
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 (
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
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 (
|
|
1452
|
-
|
|
1453
|
-
|
|
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
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
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
|
-
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
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
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
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
|
|
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
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
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
|
-
|
|
1585
|
-
|
|
1586
|
-
return false;
|
|
1699
|
+
if (unknown.length) {
|
|
1700
|
+
console.error(`[cleanup] Unknown option(s): ${unknown.join(", ")}`);
|
|
1701
|
+
return 1;
|
|
1587
1702
|
}
|
|
1588
|
-
const
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
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
|
-
|
|
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
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
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
|
|
1733
|
+
return "invalid-json";
|
|
1606
1734
|
}
|
|
1607
1735
|
}
|
|
1608
|
-
function
|
|
1609
|
-
|
|
1610
|
-
|
|
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
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
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
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
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
|
-
}
|
|
1634
|
-
|
|
1635
|
-
|
|
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
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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
|
-
|
|
1645
|
-
if (!opts.quiet) {
|
|
1646
|
-
console.warn(message);
|
|
1885
|
+
hasClientInitialize() {
|
|
1886
|
+
return this.clientInitializeSeen;
|
|
1647
1887
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
-
|
|
1675
|
-
const
|
|
1676
|
-
|
|
1677
|
-
|
|
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
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
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
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
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
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
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
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
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
|
-
|
|
1708
|
-
|
|
1975
|
+
async writeToDaemon(line) {
|
|
1976
|
+
await this.ensureActiveDaemonSocket();
|
|
1977
|
+
this.activeSocket.write(`${line}
|
|
1978
|
+
`);
|
|
1709
1979
|
}
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
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
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
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
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
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
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
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
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
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
|
-
|
|
1786
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2086
|
+
throw new Error(`[memoraone-mcp] Invalid JSON-RPC on stdin: ${String(err)}`);
|
|
1822
2087
|
}
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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 {
|
|
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 (
|
|
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 {
|
|
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
|
|
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
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
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
|
|
3529
|
+
cwd,
|
|
3046
3530
|
dryRun,
|
|
3047
3531
|
allProjects: false,
|
|
3048
3532
|
assumeYes: true
|