@memoraone/mcp 0.1.30 → 0.1.32
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 +1827 -747
- package/dist/daemon.cjs +426 -146
- package/dist/index.cjs +371 -120
- package/package.json +1 -1
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.32",
|
|
34
34
|
type: "module",
|
|
35
35
|
main: "dist/index.cjs",
|
|
36
36
|
bin: {
|
|
@@ -66,88 +66,57 @@ var require_package = __commonJS({
|
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
// src/
|
|
70
|
-
var path7 = __toESM(require("path"), 1);
|
|
69
|
+
// src/bridgeProxy.ts
|
|
71
70
|
var net = __toESM(require("net"), 1);
|
|
72
|
-
var
|
|
71
|
+
var readline2 = __toESM(require("readline"), 1);
|
|
72
|
+
var import_node_child_process = require("child_process");
|
|
73
73
|
|
|
74
|
-
// src/
|
|
75
|
-
var
|
|
74
|
+
// src/bindingIdentity.ts
|
|
75
|
+
var crypto = __toESM(require("crypto"), 1);
|
|
76
76
|
var path = __toESM(require("path"), 1);
|
|
77
|
-
var
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (ideType) {
|
|
108
|
-
return path.join(BASE_DIR, `mcp-${projectId}-${ideType}.sock`);
|
|
109
|
-
}
|
|
110
|
-
return path.join(BASE_DIR, `mcp-${projectId}.sock`);
|
|
111
|
-
}
|
|
112
|
-
function getDaemonSocketPath(projectId, env = process.env) {
|
|
113
|
-
return getSocketPath(projectId, resolveIdeTypeFromEnv(env));
|
|
114
|
-
}
|
|
115
|
-
function ensureBaseDir() {
|
|
116
|
-
fs.mkdirSync(BASE_DIR, { recursive: true });
|
|
117
|
-
return BASE_DIR;
|
|
118
|
-
}
|
|
119
|
-
function isMemoraoneSocketFilename(filename) {
|
|
120
|
-
return SOCKET_PROJECT_ID_RE.test(path.basename(filename));
|
|
121
|
-
}
|
|
122
|
-
function extractProjectIdFromSocketFilename(filename) {
|
|
123
|
-
const match = path.basename(filename).match(SOCKET_PROJECT_ID_RE);
|
|
124
|
-
return match ? match[1].toLowerCase() : null;
|
|
125
|
-
}
|
|
126
|
-
function isSocketFilenameForProject(filename, projectId) {
|
|
127
|
-
const extracted = extractProjectIdFromSocketFilename(filename);
|
|
128
|
-
return extracted !== null && extracted === projectId.trim().toLowerCase();
|
|
129
|
-
}
|
|
130
|
-
function extractIdeTypeFromSocketFilename(filename) {
|
|
131
|
-
const match = path.basename(filename).match(SOCKET_PROJECT_ID_RE);
|
|
132
|
-
if (!match) return null;
|
|
133
|
-
const suffix = match[2];
|
|
134
|
-
if (suffix === void 0) return "legacy";
|
|
135
|
-
if (IDE_TYPE_SET.has(suffix)) return suffix;
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
function isSocketFilenameForProjectAndIde(filename, projectId, ide) {
|
|
139
|
-
if (!isSocketFilenameForProject(filename, projectId)) return false;
|
|
140
|
-
if (ide === void 0) return true;
|
|
141
|
-
const socketIde = extractIdeTypeFromSocketFilename(filename);
|
|
142
|
-
if (socketIde === null) return false;
|
|
143
|
-
if (ide === "cursor") {
|
|
144
|
-
return socketIde === "cursor" || socketIde === "legacy";
|
|
77
|
+
var BINDING_SOCKET_HASH_LENGTH = 16;
|
|
78
|
+
function hashBindingIdentity(projectId, workspaceRoot, ideType) {
|
|
79
|
+
const input2 = [
|
|
80
|
+
projectId.trim().toLowerCase(),
|
|
81
|
+
path.resolve(workspaceRoot),
|
|
82
|
+
ideType
|
|
83
|
+
].join("|");
|
|
84
|
+
return crypto.createHash("sha256").update(input2).digest("hex").slice(0, BINDING_SOCKET_HASH_LENGTH);
|
|
85
|
+
}
|
|
86
|
+
function bindingsMatch(a, b) {
|
|
87
|
+
return a.projectId.trim().toLowerCase() === b.projectId.trim().toLowerCase() && path.resolve(a.workspaceRoot) === path.resolve(b.workspaceRoot) && path.resolve(a.m1Path) === path.resolve(b.m1Path);
|
|
88
|
+
}
|
|
89
|
+
function formatMissingInitializeWorkspaceError(options) {
|
|
90
|
+
const lines = [
|
|
91
|
+
"[memoraone-mcp] Could not resolve workspace from MCP initialize params."
|
|
92
|
+
];
|
|
93
|
+
if (options?.rootsListAttempted) {
|
|
94
|
+
lines.push(
|
|
95
|
+
"Cursor initialize lacked workspaceFolders/rootUri; roots/list was attempted but returned no usable repo root."
|
|
96
|
+
);
|
|
97
|
+
if (options.rootsListUris && options.rootsListUris.length > 0) {
|
|
98
|
+
lines.push(`roots/list URIs: ${options.rootsListUris.join(", ")}`);
|
|
99
|
+
}
|
|
100
|
+
lines.push(
|
|
101
|
+
"Global Cursor MCP cannot safely bind per-window repos without a workspace signal from Cursor (initialize roots or roots/list)."
|
|
102
|
+
);
|
|
103
|
+
lines.push(
|
|
104
|
+
"Reload MCP in this Cursor window, or ensure this repo has a managed .cursor/mcp.json from setup-ide-files --cursor."
|
|
105
|
+
);
|
|
106
|
+
return lines.join("\n");
|
|
145
107
|
}
|
|
146
|
-
|
|
108
|
+
lines.push(
|
|
109
|
+
"Reload MCP in this Cursor window so initialize includes workspaceFolders, rootUri, or a usable roots/list response for this repo."
|
|
110
|
+
);
|
|
111
|
+
return lines.join("\n");
|
|
147
112
|
}
|
|
148
113
|
|
|
114
|
+
// src/bindingSidecar.ts
|
|
115
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
116
|
+
var path4 = __toESM(require("path"), 1);
|
|
117
|
+
|
|
149
118
|
// src/projectBinding.ts
|
|
150
|
-
var
|
|
119
|
+
var fs = __toESM(require("fs/promises"), 1);
|
|
151
120
|
var path2 = __toESM(require("path"), 1);
|
|
152
121
|
var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
153
122
|
function normalizeEnvironment(raw) {
|
|
@@ -183,7 +152,7 @@ async function resolveProjectIdFromExplicitM1Path() {
|
|
|
183
152
|
}
|
|
184
153
|
const markerPath = path2.resolve(raw);
|
|
185
154
|
try {
|
|
186
|
-
const content = await
|
|
155
|
+
const content = await fs.readFile(markerPath, "utf8");
|
|
187
156
|
const { projectId, apiKey, environment } = parseAndValidateM1(content, markerPath);
|
|
188
157
|
return environment === void 0 ? { projectId, apiKey, foundAt: markerPath } : { projectId, apiKey, environment, foundAt: markerPath };
|
|
189
158
|
} catch (err) {
|
|
@@ -198,7 +167,7 @@ async function findM1WalkingUp(workspaceRoot) {
|
|
|
198
167
|
while (true) {
|
|
199
168
|
const markerPath = path2.join(current, "memoraone.m1");
|
|
200
169
|
try {
|
|
201
|
-
const content = await
|
|
170
|
+
const content = await fs.readFile(markerPath, "utf8");
|
|
202
171
|
const { projectId, apiKey, environment } = parseAndValidateM1(content, markerPath);
|
|
203
172
|
const repoRoot = path2.dirname(markerPath);
|
|
204
173
|
return environment === void 0 ? { projectId, apiKey, repoRoot, markerPath } : { projectId, apiKey, environment, repoRoot, markerPath };
|
|
@@ -252,29 +221,33 @@ function resolveApiKeyWithSource(fileApiKey) {
|
|
|
252
221
|
}
|
|
253
222
|
return { apiKey: null, apiKeySource: "none" };
|
|
254
223
|
}
|
|
255
|
-
async function resolveAuthoritativeBinding(workspaceRoot) {
|
|
256
|
-
const
|
|
257
|
-
if (
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
224
|
+
async function resolveAuthoritativeBinding(workspaceRoot, options = {}) {
|
|
225
|
+
const respectExplicitM1Path = options.respectExplicitM1Path !== false;
|
|
226
|
+
if (respectExplicitM1Path) {
|
|
227
|
+
const explicitBinding = await resolveProjectIdFromExplicitM1Path();
|
|
228
|
+
if (explicitBinding) {
|
|
229
|
+
const resolved = resolveApiKeyWithSource(explicitBinding.apiKey);
|
|
230
|
+
return {
|
|
231
|
+
projectId: explicitBinding.projectId,
|
|
232
|
+
workspaceRoot: path2.dirname(explicitBinding.foundAt),
|
|
233
|
+
m1Path: explicitBinding.foundAt,
|
|
234
|
+
apiKey: resolved.apiKey,
|
|
235
|
+
...explicitBinding.environment !== void 0 ? { environment: explicitBinding.environment } : {},
|
|
236
|
+
bindingSource: "explicit-m1-path",
|
|
237
|
+
apiKeySource: resolved.apiKeySource
|
|
238
|
+
};
|
|
239
|
+
}
|
|
268
240
|
}
|
|
269
241
|
const candidates = normalizeWorkspaceSearchRoots(workspaceRoot);
|
|
270
242
|
if (candidates.length === 0) {
|
|
271
243
|
throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
|
|
272
244
|
}
|
|
245
|
+
const bindings = [];
|
|
273
246
|
for (const root of candidates) {
|
|
274
247
|
const binding = await findM1WalkingUp(root);
|
|
275
248
|
if (binding) {
|
|
276
249
|
const resolved = resolveApiKeyWithSource(binding.apiKey);
|
|
277
|
-
|
|
250
|
+
bindings.push({
|
|
278
251
|
projectId: binding.projectId,
|
|
279
252
|
workspaceRoot: binding.repoRoot,
|
|
280
253
|
m1Path: binding.markerPath,
|
|
@@ -282,159 +255,1348 @@ async function resolveAuthoritativeBinding(workspaceRoot) {
|
|
|
282
255
|
...binding.environment !== void 0 ? { environment: binding.environment } : {},
|
|
283
256
|
bindingSource: "workspace-search",
|
|
284
257
|
apiKeySource: resolved.apiKeySource
|
|
285
|
-
};
|
|
258
|
+
});
|
|
286
259
|
}
|
|
287
260
|
}
|
|
288
|
-
|
|
261
|
+
if (bindings.length === 0) {
|
|
262
|
+
throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
|
|
263
|
+
}
|
|
264
|
+
const distinctProjectIds = new Set(bindings.map((b) => b.projectId));
|
|
265
|
+
if (distinctProjectIds.size > 1) {
|
|
266
|
+
const lines = bindings.map(
|
|
267
|
+
(b) => ` - workspace=${b.workspaceRoot} project=${b.projectId} m1=${b.m1Path}`
|
|
268
|
+
);
|
|
269
|
+
throw new Error(
|
|
270
|
+
"[memoraone-mcp] Ambiguous workspace binding: multiple open roots map to different MemoraOne projects.\n" + lines.join("\n") + "\nOpen one repo per Cursor window, or use repo-scoped .cursor/mcp.json from setup-ide-files --cursor."
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return bindings[0];
|
|
289
274
|
}
|
|
290
275
|
function encodeResolvedBinding(binding) {
|
|
291
276
|
return Buffer.from(JSON.stringify(binding), "utf8").toString("base64");
|
|
292
277
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
var fs6 = __toESM(require("fs/promises"), 1);
|
|
296
|
-
var os4 = __toESM(require("os"), 1);
|
|
297
|
-
var path6 = __toESM(require("path"), 1);
|
|
298
|
-
|
|
299
|
-
// src/cleanup.ts
|
|
300
|
-
var fs3 = __toESM(require("fs/promises"), 1);
|
|
301
|
-
var path3 = __toESM(require("path"), 1);
|
|
302
|
-
var readline = __toESM(require("readline/promises"), 1);
|
|
303
|
-
var import_node_child_process = require("child_process");
|
|
304
|
-
var import_node_util = require("util");
|
|
305
|
-
var import_node_process = require("process");
|
|
306
|
-
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
307
|
-
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;
|
|
308
|
-
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;
|
|
309
|
-
var MEMORAONE_MCP_COMMAND_RE = /memoraone-mcp|memoraOne-mcp|@memoraone\/mcp/;
|
|
310
|
-
var CLEANUP_PROJECT_ID_REQUIRED_ERROR = "Provide --project-id <id> or run from a folder containing memoraone.m1.";
|
|
311
|
-
function isMemoraoneMcpCommandLine(commandLine) {
|
|
312
|
-
return MEMORAONE_MCP_COMMAND_RE.test(commandLine);
|
|
313
|
-
}
|
|
314
|
-
function parseDaemonProjectIdFromCommandLine(commandLine) {
|
|
315
|
-
if (!commandLine.includes("--daemon")) {
|
|
278
|
+
function decodeResolvedBinding(value) {
|
|
279
|
+
if (!value) {
|
|
316
280
|
return null;
|
|
317
281
|
}
|
|
318
|
-
|
|
319
|
-
|
|
282
|
+
let parsed;
|
|
283
|
+
try {
|
|
284
|
+
parsed = JSON.parse(Buffer.from(value, "base64").toString("utf8"));
|
|
285
|
+
} catch {
|
|
286
|
+
throw new Error("[memoraone-mcp] Invalid encoded binding payload");
|
|
287
|
+
}
|
|
288
|
+
const projectId = parsed?.projectId;
|
|
289
|
+
const workspaceRoot = parsed?.workspaceRoot;
|
|
290
|
+
const m1Path = parsed?.m1Path;
|
|
291
|
+
const apiKey = parsed?.apiKey;
|
|
292
|
+
const environment = normalizeEnvironment(parsed?.environment);
|
|
293
|
+
const bindingSource = parsed?.bindingSource;
|
|
294
|
+
const apiKeySource = parsed?.apiKeySource;
|
|
295
|
+
if (!projectId || typeof projectId !== "string" || !uuidRegex.test(projectId.trim())) {
|
|
296
|
+
throw new Error("[memoraone-mcp] Invalid binding projectId");
|
|
297
|
+
}
|
|
298
|
+
if (!workspaceRoot || typeof workspaceRoot !== "string") {
|
|
299
|
+
throw new Error("[memoraone-mcp] Invalid binding workspaceRoot");
|
|
300
|
+
}
|
|
301
|
+
if (!m1Path || typeof m1Path !== "string") {
|
|
302
|
+
throw new Error("[memoraone-mcp] Invalid binding m1Path");
|
|
303
|
+
}
|
|
304
|
+
if (apiKey !== null && apiKey !== void 0 && typeof apiKey !== "string") {
|
|
305
|
+
throw new Error("[memoraone-mcp] Invalid binding apiKey");
|
|
306
|
+
}
|
|
307
|
+
if (bindingSource !== "explicit-m1-path" && bindingSource !== "workspace-search") {
|
|
308
|
+
throw new Error("[memoraone-mcp] Invalid binding source");
|
|
309
|
+
}
|
|
310
|
+
if (apiKeySource !== "env" && apiKeySource !== "memoraone.m1" && apiKeySource !== "none") {
|
|
311
|
+
throw new Error("[memoraone-mcp] Invalid binding apiKeySource");
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
projectId: projectId.trim(),
|
|
315
|
+
workspaceRoot,
|
|
316
|
+
m1Path,
|
|
317
|
+
apiKey: typeof apiKey === "string" && apiKey.trim() !== "" ? apiKey.trim() : null,
|
|
318
|
+
...environment !== void 0 ? { environment } : {},
|
|
319
|
+
bindingSource,
|
|
320
|
+
apiKeySource
|
|
321
|
+
};
|
|
320
322
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
+
|
|
324
|
+
// src/socketPaths.ts
|
|
325
|
+
var os = __toESM(require("os"), 1);
|
|
326
|
+
var path3 = __toESM(require("path"), 1);
|
|
327
|
+
var fs2 = __toESM(require("fs"), 1);
|
|
328
|
+
var BASE_DIR = process.env.MEMORAONE_MCP_LOCK_DIR || path3.join(os.homedir(), ".memoraone-mcp");
|
|
329
|
+
var HASH_SOCKET_FILENAME_RE = new RegExp(
|
|
330
|
+
`^mcp-[0-9a-f]{${BINDING_SOCKET_HASH_LENGTH}}\\.sock$`,
|
|
331
|
+
"i"
|
|
332
|
+
);
|
|
333
|
+
var LEGACY_SOCKET_FILENAME_RE = /^mcp-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:-([0-9a-f]{12}))?(?:-(cursor|jetbrains|copilot-vscode))?\.sock$/i;
|
|
334
|
+
var LEGACY_SOCKET_PROJECT_ID_RE = LEGACY_SOCKET_FILENAME_RE;
|
|
335
|
+
function getMcpBaseDir() {
|
|
336
|
+
return BASE_DIR;
|
|
337
|
+
}
|
|
338
|
+
var IDE_TYPES = ["cursor", "copilot-vscode", "jetbrains"];
|
|
339
|
+
var IDE_TYPE_SET = new Set(IDE_TYPES);
|
|
340
|
+
function parseIdeType(value) {
|
|
341
|
+
if (value === void 0 || value.trim() === "" || !IDE_TYPE_SET.has(value)) {
|
|
323
342
|
return void 0;
|
|
324
343
|
}
|
|
325
|
-
return
|
|
344
|
+
return value;
|
|
326
345
|
}
|
|
327
|
-
function
|
|
328
|
-
|
|
329
|
-
for (const line of lines) {
|
|
330
|
-
const trimmed = line.trim();
|
|
331
|
-
if (!trimmed) continue;
|
|
332
|
-
const spaceIdx = trimmed.indexOf(" ");
|
|
333
|
-
if (spaceIdx <= 0) continue;
|
|
334
|
-
const pid = Number.parseInt(trimmed.slice(0, spaceIdx), 10);
|
|
335
|
-
if (!Number.isFinite(pid) || pid <= 0) continue;
|
|
336
|
-
const command = trimmed.slice(spaceIdx + 1);
|
|
337
|
-
if (!isMemoraoneMcpCommandLine(command)) continue;
|
|
338
|
-
const projectId = parseDaemonProjectIdFromCommandLine(command);
|
|
339
|
-
if (projectId === null) continue;
|
|
340
|
-
const ide = parseDaemonIdeFromCommandLine(command);
|
|
341
|
-
processes.push({ pid, command, projectId, ...ide !== void 0 ? { ide } : {} });
|
|
342
|
-
}
|
|
343
|
-
return processes;
|
|
346
|
+
function resolveIdeTypeFromEnv(env = process.env) {
|
|
347
|
+
return parseIdeType(env.MEMORAONE_IDE_TYPE);
|
|
344
348
|
}
|
|
345
|
-
function
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
+
function parseIdeTypeFromCommandLine(commandLine) {
|
|
350
|
+
const match = commandLine.match(/--ide\s+(cursor|copilot-vscode|jetbrains)(?:\s|$)/);
|
|
351
|
+
return match ? match[1] : void 0;
|
|
352
|
+
}
|
|
353
|
+
function buildDaemonSpawnArgs(scriptPath, projectId, env = process.env) {
|
|
354
|
+
const args2 = [scriptPath, "--daemon", "--project-id", projectId];
|
|
355
|
+
const ideType = resolveIdeTypeFromEnv(env);
|
|
356
|
+
if (ideType) {
|
|
357
|
+
args2.push("--ide", ideType);
|
|
349
358
|
}
|
|
350
|
-
return
|
|
359
|
+
return args2;
|
|
351
360
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
maxBuffer: 10 * 1024 * 1024
|
|
355
|
-
});
|
|
356
|
-
return parseDaemonProcessLines(stdout.split("\n"));
|
|
361
|
+
function resolveBindingIdeType(env = process.env) {
|
|
362
|
+
return resolveIdeTypeFromEnv(env) ?? "";
|
|
357
363
|
}
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
entries = await fs3.readdir(baseDir);
|
|
363
|
-
} catch (err) {
|
|
364
|
-
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
365
|
-
if (code === "ENOENT") {
|
|
366
|
-
return [];
|
|
367
|
-
}
|
|
368
|
-
throw err;
|
|
369
|
-
}
|
|
370
|
-
const paths = [];
|
|
371
|
-
for (const name of entries) {
|
|
372
|
-
if (projectId === null) {
|
|
373
|
-
if (isMemoraoneSocketFilename(name)) {
|
|
374
|
-
paths.push(path3.join(baseDir, name));
|
|
375
|
-
}
|
|
376
|
-
} else if (isSocketFilenameForProject(name, projectId)) {
|
|
377
|
-
paths.push(path3.join(baseDir, name));
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
return paths.sort();
|
|
364
|
+
function getBindingSocketFilename(binding, env = process.env) {
|
|
365
|
+
const ideType = resolveBindingIdeType(env);
|
|
366
|
+
const hash = hashBindingIdentity(binding.projectId, binding.workspaceRoot, ideType);
|
|
367
|
+
return `mcp-${hash}.sock`;
|
|
381
368
|
}
|
|
382
|
-
function
|
|
383
|
-
|
|
384
|
-
return socketPaths.filter(
|
|
385
|
-
(socketPath) => isSocketFilenameForProjectAndIde(path3.basename(socketPath), projectId, ide)
|
|
386
|
-
);
|
|
369
|
+
function getBindingSocketPath(binding, env = process.env) {
|
|
370
|
+
return path3.join(BASE_DIR, getBindingSocketFilename(binding, env));
|
|
387
371
|
}
|
|
388
|
-
|
|
389
|
-
|
|
372
|
+
function ensureBaseDir() {
|
|
373
|
+
fs2.mkdirSync(BASE_DIR, { recursive: true });
|
|
374
|
+
return BASE_DIR;
|
|
390
375
|
}
|
|
391
|
-
|
|
392
|
-
|
|
376
|
+
function isHashSocketFilename(filename) {
|
|
377
|
+
return HASH_SOCKET_FILENAME_RE.test(path3.basename(filename));
|
|
393
378
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
379
|
+
function isLegacySocketFilename(filename) {
|
|
380
|
+
return LEGACY_SOCKET_FILENAME_RE.test(path3.basename(filename));
|
|
381
|
+
}
|
|
382
|
+
function isMemoraoneSocketFilename(filename) {
|
|
383
|
+
const base = path3.basename(filename);
|
|
384
|
+
return HASH_SOCKET_FILENAME_RE.test(base) || LEGACY_SOCKET_PROJECT_ID_RE.test(base);
|
|
385
|
+
}
|
|
386
|
+
function extractProjectIdFromSocketFilename(filename) {
|
|
387
|
+
const match = path3.basename(filename).match(LEGACY_SOCKET_PROJECT_ID_RE);
|
|
388
|
+
return match ? match[1].toLowerCase() : null;
|
|
389
|
+
}
|
|
390
|
+
function isSocketFilenameForProject(filename, projectId) {
|
|
391
|
+
const extracted = extractProjectIdFromSocketFilename(filename);
|
|
392
|
+
return extracted !== null && extracted === projectId.trim().toLowerCase();
|
|
393
|
+
}
|
|
394
|
+
function extractIdeTypeFromSocketFilename(filename) {
|
|
395
|
+
const match = path3.basename(filename).match(LEGACY_SOCKET_FILENAME_RE);
|
|
396
|
+
if (!match) return null;
|
|
397
|
+
const ide = match[3];
|
|
398
|
+
if (ide === void 0) return "legacy";
|
|
399
|
+
if (IDE_TYPE_SET.has(ide)) return ide;
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
function isSocketFilenameForProjectAndIde(filename, projectId, ide) {
|
|
403
|
+
if (!isSocketFilenameForProject(filename, projectId)) return false;
|
|
404
|
+
if (ide === void 0) return true;
|
|
405
|
+
const socketIde = extractIdeTypeFromSocketFilename(filename);
|
|
406
|
+
if (socketIde === null) return false;
|
|
407
|
+
if (ide === "cursor") {
|
|
408
|
+
return socketIde === "cursor" || socketIde === "legacy";
|
|
397
409
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
410
|
+
return socketIde === ide;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/bindingSidecar.ts
|
|
414
|
+
function bindingSidecarPath(socketPath) {
|
|
415
|
+
if (socketPath.endsWith(".sock")) {
|
|
416
|
+
return `${socketPath.slice(0, -".sock".length)}.binding.json`;
|
|
404
417
|
}
|
|
418
|
+
return `${socketPath}.binding.json`;
|
|
405
419
|
}
|
|
406
|
-
|
|
420
|
+
function parseSidecarRecord(raw) {
|
|
407
421
|
try {
|
|
408
|
-
const
|
|
422
|
+
const parsed = JSON.parse(raw);
|
|
423
|
+
if (!parsed?.binding) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
const binding = decodeResolvedBinding(parsed.binding);
|
|
409
427
|
return {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
projectId: binding.projectId
|
|
428
|
+
v: typeof parsed.v === "number" ? parsed.v : 1,
|
|
429
|
+
...parsed.ideType ? { ideType: parsed.ideType } : {},
|
|
430
|
+
projectId: parsed.projectId ?? binding.projectId,
|
|
431
|
+
workspaceRoot: parsed.workspaceRoot ?? binding.workspaceRoot,
|
|
432
|
+
m1Path: parsed.m1Path ?? binding.m1Path,
|
|
433
|
+
binding: parsed.binding
|
|
413
434
|
};
|
|
414
435
|
} catch {
|
|
415
|
-
return
|
|
436
|
+
return null;
|
|
416
437
|
}
|
|
417
438
|
}
|
|
418
|
-
function
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const skipped = [];
|
|
425
|
-
for (const proc of processes) {
|
|
426
|
-
if (proc.projectId === normalized) {
|
|
427
|
-
matching.push(proc);
|
|
428
|
-
} else {
|
|
429
|
-
skipped.push(proc);
|
|
430
|
-
}
|
|
439
|
+
function readBindingSidecarRecord(socketPath) {
|
|
440
|
+
try {
|
|
441
|
+
const raw = fs3.readFileSync(bindingSidecarPath(socketPath), "utf8");
|
|
442
|
+
return parseSidecarRecord(raw);
|
|
443
|
+
} catch {
|
|
444
|
+
return null;
|
|
431
445
|
}
|
|
432
|
-
|
|
446
|
+
}
|
|
447
|
+
function formatBindingMismatchError(socketPath, sidecar, expected, detail) {
|
|
448
|
+
const lines = [
|
|
449
|
+
`[memoraone-mcp] Daemon socket binding mismatch at ${path4.basename(socketPath)}.`,
|
|
450
|
+
` socket: project=${sidecar.projectId} workspace=${sidecar.workspaceRoot} m1=${sidecar.m1Path}`,
|
|
451
|
+
` session: project=${expected.projectId} workspace=${expected.workspaceRoot} m1=${expected.m1Path}`
|
|
452
|
+
];
|
|
453
|
+
if (detail) {
|
|
454
|
+
lines.push(` ${detail}`);
|
|
455
|
+
}
|
|
456
|
+
lines.push("Reload MCP in this IDE window or run memoraone-mcp cleanup for the stale socket.");
|
|
457
|
+
return lines.join("\n");
|
|
458
|
+
}
|
|
459
|
+
function verifyDaemonSidecarBinding(socketPath, expected, env = process.env) {
|
|
460
|
+
const record = readBindingSidecarRecord(socketPath);
|
|
461
|
+
if (!record) {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
const sidecar = decodeResolvedBinding(record.binding);
|
|
465
|
+
if (!bindingsMatch(sidecar, expected)) {
|
|
466
|
+
throw new Error(formatBindingMismatchError(socketPath, sidecar, expected));
|
|
467
|
+
}
|
|
468
|
+
const expectedIdeType = resolveBindingIdeType(env);
|
|
469
|
+
if (record.ideType !== void 0 && record.ideType !== expectedIdeType) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
formatBindingMismatchError(
|
|
472
|
+
socketPath,
|
|
473
|
+
sidecar,
|
|
474
|
+
expected,
|
|
475
|
+
`ideType: socket=${record.ideType} session=${expectedIdeType || "(none)"}`
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
return sidecar;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/bridgeClientRoots.ts
|
|
483
|
+
var readline = __toESM(require("readline"), 1);
|
|
484
|
+
var TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
485
|
+
function isInitializeDebugEnabled(env = process.env) {
|
|
486
|
+
return TRUTHY.has(String(env.MEMORAONE_DEBUG_INIT ?? "").trim().toLowerCase());
|
|
487
|
+
}
|
|
488
|
+
function logInitializeDebug(log, env, msg) {
|
|
489
|
+
if (isInitializeDebugEnabled(env)) {
|
|
490
|
+
log(`[init-debug] ${msg}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function clientDeclaresRootsCapability(params) {
|
|
494
|
+
const capabilities = params?.capabilities;
|
|
495
|
+
if (capabilities === null || typeof capabilities !== "object") {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
return Object.prototype.hasOwnProperty.call(capabilities, "roots");
|
|
499
|
+
}
|
|
500
|
+
function summarizeInitializeParamsForDebug(params) {
|
|
501
|
+
if (!params) {
|
|
502
|
+
return "(no params)";
|
|
503
|
+
}
|
|
504
|
+
const folders = Array.isArray(params.workspaceFolders) ? params.workspaceFolders.map((folder) => {
|
|
505
|
+
if (folder && typeof folder === "object") {
|
|
506
|
+
const entry = folder;
|
|
507
|
+
return { name: entry.name, uri: entry.uri };
|
|
508
|
+
}
|
|
509
|
+
return folder;
|
|
510
|
+
}) : params.workspaceFolders;
|
|
511
|
+
const capabilityKeys = params.capabilities && typeof params.capabilities === "object" ? Object.keys(params.capabilities) : [];
|
|
512
|
+
return JSON.stringify({
|
|
513
|
+
rootUri: params.rootUri,
|
|
514
|
+
workspaceFolders: folders,
|
|
515
|
+
clientInfo: params.clientInfo,
|
|
516
|
+
capabilityKeys
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
function extractRootsUrisFromListResult(result) {
|
|
520
|
+
if (result === null || typeof result !== "object") {
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
const roots = result.roots;
|
|
524
|
+
if (!Array.isArray(roots)) {
|
|
525
|
+
return [];
|
|
526
|
+
}
|
|
527
|
+
const uris = [];
|
|
528
|
+
for (const root of roots) {
|
|
529
|
+
if (root && typeof root === "object") {
|
|
530
|
+
const uri = root.uri;
|
|
531
|
+
if (typeof uri === "string" && uri.trim() !== "") {
|
|
532
|
+
uris.push(uri);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return uris;
|
|
537
|
+
}
|
|
538
|
+
var StdioLineReader = class {
|
|
539
|
+
constructor(input2) {
|
|
540
|
+
this.queue = [];
|
|
541
|
+
this.waiters = [];
|
|
542
|
+
this.closed = false;
|
|
543
|
+
this.rl = readline.createInterface({ input: input2, crlfDelay: Infinity });
|
|
544
|
+
this.rl.on("line", (line) => {
|
|
545
|
+
if (this.waiters.length > 0) {
|
|
546
|
+
this.waiters.shift()(line);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
this.queue.push(line);
|
|
550
|
+
});
|
|
551
|
+
this.rl.on("close", () => {
|
|
552
|
+
this.closed = true;
|
|
553
|
+
while (this.waiters.length > 0) {
|
|
554
|
+
this.waiters.shift()(null);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
async readLine() {
|
|
559
|
+
if (this.queue.length > 0) {
|
|
560
|
+
return this.queue.shift() ?? null;
|
|
561
|
+
}
|
|
562
|
+
if (this.closed) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
return new Promise((resolve8) => {
|
|
566
|
+
this.waiters.push(resolve8);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
/** Re-queue lines read during an intermediate protocol step (e.g. roots/list) for the main bridge loop. */
|
|
570
|
+
prependLines(lines) {
|
|
571
|
+
if (lines.length === 0) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
575
|
+
this.queue.unshift(lines[i]);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
close() {
|
|
579
|
+
this.rl.close();
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
var nextBridgeRequestId = 1e5;
|
|
583
|
+
async function requestClientRootsListUris(options) {
|
|
584
|
+
const env = options.env ?? process.env;
|
|
585
|
+
const log = options.log ?? (() => {
|
|
586
|
+
});
|
|
587
|
+
const requestId = nextBridgeRequestId++;
|
|
588
|
+
const deferredLines = [];
|
|
589
|
+
const request = {
|
|
590
|
+
jsonrpc: "2.0",
|
|
591
|
+
id: requestId,
|
|
592
|
+
method: "roots/list",
|
|
593
|
+
params: {}
|
|
594
|
+
};
|
|
595
|
+
logInitializeDebug(
|
|
596
|
+
log,
|
|
597
|
+
env,
|
|
598
|
+
`sending roots/list id=${requestId} clientDeclaresRoots=${clientDeclaresRootsCapability(options.initializeParams)}`
|
|
599
|
+
);
|
|
600
|
+
options.stdout.write(`${JSON.stringify(request)}
|
|
601
|
+
`);
|
|
602
|
+
while (true) {
|
|
603
|
+
const line = await options.lineReader.readLine();
|
|
604
|
+
if (line === null) {
|
|
605
|
+
throw new Error("[memoraone-mcp] Client closed stdin before roots/list response");
|
|
606
|
+
}
|
|
607
|
+
const trimmed = line.trim();
|
|
608
|
+
if (trimmed === "") {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
let message;
|
|
612
|
+
try {
|
|
613
|
+
message = JSON.parse(trimmed);
|
|
614
|
+
} catch (err) {
|
|
615
|
+
logInitializeDebug(log, env, `ignored non-JSON line while waiting for roots/list: ${String(err)}`);
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
if (message.id !== requestId) {
|
|
619
|
+
logInitializeDebug(
|
|
620
|
+
log,
|
|
621
|
+
env,
|
|
622
|
+
`deferred JSON-RPC while waiting for roots/list id=${requestId}: ${trimmed.slice(0, 200)}`
|
|
623
|
+
);
|
|
624
|
+
deferredLines.push(trimmed);
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (message.error) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
`[memoraone-mcp] roots/list failed: ${JSON.stringify(message.error)}`
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
const uris = extractRootsUrisFromListResult(message.result);
|
|
633
|
+
logInitializeDebug(log, env, `roots/list id=${requestId} returned ${uris.length}: ${JSON.stringify(uris)}`);
|
|
634
|
+
return { uris, deferredLines };
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/initializeBinding.ts
|
|
639
|
+
var path5 = __toESM(require("path"), 1);
|
|
640
|
+
var import_node_url = require("url");
|
|
641
|
+
var MEMORAONE_WORKSPACE_ROOT_ENV = "MEMORAONE_WORKSPACE_ROOT";
|
|
642
|
+
function getBridgeBindingResolveOptions(env = process.env) {
|
|
643
|
+
const ideType = resolveIdeTypeFromEnv(env);
|
|
644
|
+
if (ideType === "cursor") {
|
|
645
|
+
return { respectExplicitM1Path: false, allowEnvWorkspaceFallback: false };
|
|
646
|
+
}
|
|
647
|
+
return { respectExplicitM1Path: true, allowEnvWorkspaceFallback: true };
|
|
648
|
+
}
|
|
649
|
+
function uriToPath(uri) {
|
|
650
|
+
if (uri.startsWith("file://")) {
|
|
651
|
+
return (0, import_node_url.fileURLToPath)(uri);
|
|
652
|
+
}
|
|
653
|
+
return uri;
|
|
654
|
+
}
|
|
655
|
+
function getEnvWorkspaceRootCandidates() {
|
|
656
|
+
const raw = process.env.WORKSPACE_FOLDER_PATHS;
|
|
657
|
+
const parts = [];
|
|
658
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
659
|
+
for (const p of raw.split(path5.delimiter).map((s) => s.trim()).filter(Boolean)) {
|
|
660
|
+
parts.push(path5.resolve(p));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
parts.push(process.cwd());
|
|
664
|
+
const seen = /* @__PURE__ */ new Set();
|
|
665
|
+
const deduped = [];
|
|
666
|
+
for (const p of parts) {
|
|
667
|
+
if (!seen.has(p)) {
|
|
668
|
+
seen.add(p);
|
|
669
|
+
deduped.push(p);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return deduped;
|
|
673
|
+
}
|
|
674
|
+
function extractWorkspaceRootsFromInitialize(params) {
|
|
675
|
+
if (!params) {
|
|
676
|
+
return [];
|
|
677
|
+
}
|
|
678
|
+
const seen = /* @__PURE__ */ new Set();
|
|
679
|
+
const roots = [];
|
|
680
|
+
const addRoot = (uri) => {
|
|
681
|
+
if (uri === void 0 || uri.trim() === "") {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const resolved = path5.resolve(uriToPath(uri));
|
|
685
|
+
if (!seen.has(resolved)) {
|
|
686
|
+
seen.add(resolved);
|
|
687
|
+
roots.push(resolved);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
if (Array.isArray(params.workspaceFolders) && params.workspaceFolders.length > 0) {
|
|
691
|
+
for (const folder of params.workspaceFolders) {
|
|
692
|
+
addRoot(folder?.uri);
|
|
693
|
+
}
|
|
694
|
+
return roots;
|
|
695
|
+
}
|
|
696
|
+
if (params.rootUri) {
|
|
697
|
+
addRoot(params.rootUri);
|
|
698
|
+
}
|
|
699
|
+
return roots;
|
|
700
|
+
}
|
|
701
|
+
function getRepoScopedWorkspaceHint(env = process.env) {
|
|
702
|
+
const raw = env[MEMORAONE_WORKSPACE_ROOT_ENV];
|
|
703
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
return path5.resolve(raw.trim());
|
|
707
|
+
}
|
|
708
|
+
function formatWorkspaceAmbiguityError(bindings) {
|
|
709
|
+
const lines = bindings.map(
|
|
710
|
+
(b) => ` - workspace=${b.workspaceRoot} project=${b.projectId} m1=${b.m1Path}`
|
|
711
|
+
);
|
|
712
|
+
return "[memoraone-mcp] Ambiguous workspace binding: multiple open roots map to different MemoraOne projects.\n" + lines.join("\n") + `
|
|
713
|
+
Open one repo per Cursor window, or ensure this repo's managed .cursor/mcp.json includes ${MEMORAONE_WORKSPACE_ROOT_ENV} from setup-ide-files --cursor.`;
|
|
714
|
+
}
|
|
715
|
+
function formatRepoHintInitializeMismatchError(repoHintRoot, initializeBinding) {
|
|
716
|
+
return `[memoraone-mcp] Repo-scoped workspace hint conflicts with MCP initialize workspace.
|
|
717
|
+
${MEMORAONE_WORKSPACE_ROOT_ENV}: ${repoHintRoot}
|
|
718
|
+
initialize: project=${initializeBinding.projectId} workspace=${initializeBinding.workspaceRoot} m1=${initializeBinding.m1Path}
|
|
719
|
+
Reload MCP in the Cursor window for this repo, or re-run setup-ide-files --cursor here if the hint is stale.`;
|
|
720
|
+
}
|
|
721
|
+
function formatRepoHintNotInRootsListError(repoHintRoot, rootsListPaths) {
|
|
722
|
+
return `[memoraone-mcp] Repo-scoped workspace hint does not match any Cursor roots/list entry.
|
|
723
|
+
${MEMORAONE_WORKSPACE_ROOT_ENV}: ${repoHintRoot}
|
|
724
|
+
roots/list paths: ${rootsListPaths.join(", ")}
|
|
725
|
+
Reload MCP in the Cursor window for this repo, or re-run setup-ide-files --cursor if the hint is stale.`;
|
|
726
|
+
}
|
|
727
|
+
async function resolveBindingFromWorkspaceRoots(workspaceRoots, options = {}) {
|
|
728
|
+
if (workspaceRoots.length === 0) {
|
|
729
|
+
throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
|
|
730
|
+
}
|
|
731
|
+
const bindings = [];
|
|
732
|
+
for (const root of workspaceRoots) {
|
|
733
|
+
try {
|
|
734
|
+
bindings.push(
|
|
735
|
+
await resolveAuthoritativeBinding(root, {
|
|
736
|
+
respectExplicitM1Path: options.respectExplicitM1Path
|
|
737
|
+
})
|
|
738
|
+
);
|
|
739
|
+
} catch (err) {
|
|
740
|
+
if (err instanceof Error && err.message.includes("Could not find memoraone.m1")) {
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
throw err;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (bindings.length === 0) {
|
|
747
|
+
throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
|
|
748
|
+
}
|
|
749
|
+
const distinctProjectIds = new Set(bindings.map((b) => b.projectId));
|
|
750
|
+
if (distinctProjectIds.size > 1) {
|
|
751
|
+
throw new Error(formatWorkspaceAmbiguityError(bindings));
|
|
752
|
+
}
|
|
753
|
+
return bindings[0];
|
|
754
|
+
}
|
|
755
|
+
async function resolveBindingFromInitializeParams(params, options = {}) {
|
|
756
|
+
const env = options.env ?? process.env;
|
|
757
|
+
const resolveOpts = {
|
|
758
|
+
respectExplicitM1Path: options.respectExplicitM1Path,
|
|
759
|
+
allowEnvWorkspaceFallback: options.allowEnvWorkspaceFallback
|
|
760
|
+
};
|
|
761
|
+
const repoHint = getRepoScopedWorkspaceHint(env);
|
|
762
|
+
const initializeRoots = extractWorkspaceRootsFromInitialize(params);
|
|
763
|
+
if (initializeRoots.length > 0) {
|
|
764
|
+
const binding = await resolveBindingFromWorkspaceRoots(initializeRoots, resolveOpts);
|
|
765
|
+
if (repoHint !== null) {
|
|
766
|
+
const hintBinding = await resolveAuthoritativeBinding(repoHint, {
|
|
767
|
+
respectExplicitM1Path: false
|
|
768
|
+
});
|
|
769
|
+
if (!bindingsMatch(binding, hintBinding)) {
|
|
770
|
+
throw new Error(formatRepoHintInitializeMismatchError(repoHint, binding));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return binding;
|
|
774
|
+
}
|
|
775
|
+
const rootsListUris = options.rootsListUris ?? [];
|
|
776
|
+
const rootsListPaths = rootsListUris.map((uri) => uriToPath(uri)).filter(Boolean);
|
|
777
|
+
if (rootsListPaths.length > 1 && repoHint !== null) {
|
|
778
|
+
const hintResolved = path5.resolve(repoHint);
|
|
779
|
+
const matchingRoot = rootsListPaths.find((root) => path5.resolve(root) === hintResolved);
|
|
780
|
+
if (!matchingRoot) {
|
|
781
|
+
throw new Error(formatRepoHintNotInRootsListError(repoHint, rootsListPaths));
|
|
782
|
+
}
|
|
783
|
+
return resolveAuthoritativeBinding(repoHint, { respectExplicitM1Path: false });
|
|
784
|
+
}
|
|
785
|
+
if (rootsListPaths.length > 0) {
|
|
786
|
+
return resolveBindingFromWorkspaceRoots(rootsListPaths, resolveOpts);
|
|
787
|
+
}
|
|
788
|
+
if (repoHint !== null) {
|
|
789
|
+
return resolveAuthoritativeBinding(repoHint, { respectExplicitM1Path: false });
|
|
790
|
+
}
|
|
791
|
+
if (options.allowEnvWorkspaceFallback === false) {
|
|
792
|
+
throw new Error(
|
|
793
|
+
formatMissingInitializeWorkspaceError({
|
|
794
|
+
rootsListAttempted: options.rootsListAttempted === true,
|
|
795
|
+
rootsListUris
|
|
796
|
+
})
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
const fallbackRoots = options.fallbackWorkspaceRoots ?? getEnvWorkspaceRootCandidates();
|
|
800
|
+
return resolveAuthoritativeBinding(fallbackRoots, {
|
|
801
|
+
respectExplicitM1Path: options.respectExplicitM1Path
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/bridgeProxy.ts
|
|
806
|
+
var defaultLog = (msg) => {
|
|
807
|
+
process.stderr.write(`[memoraone-mcp][bridge] ${msg}
|
|
808
|
+
`);
|
|
809
|
+
};
|
|
810
|
+
function summarizeJsonRpcMethod(line) {
|
|
811
|
+
try {
|
|
812
|
+
const message = JSON.parse(line.trim());
|
|
813
|
+
if (typeof message.method === "string") {
|
|
814
|
+
return message.method;
|
|
815
|
+
}
|
|
816
|
+
if (message.id !== void 0) {
|
|
817
|
+
return `response:id=${String(message.id)}`;
|
|
818
|
+
}
|
|
819
|
+
return "jsonrpc";
|
|
820
|
+
} catch {
|
|
821
|
+
return "invalid-json";
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function connectWithRetry(socketPath, log, maxRetries, retryDelayMs, connect2) {
|
|
825
|
+
return new Promise((resolve8, reject) => {
|
|
826
|
+
const tryConnect = (attempt) => {
|
|
827
|
+
connect2(socketPath).then(resolve8).catch((err) => {
|
|
828
|
+
if (attempt >= maxRetries) {
|
|
829
|
+
reject(err);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
log(`connect attempt ${attempt + 1} failed, retrying in ${retryDelayMs}ms: ${String(err)}`);
|
|
833
|
+
setTimeout(() => tryConnect(attempt + 1), retryDelayMs);
|
|
834
|
+
});
|
|
835
|
+
};
|
|
836
|
+
tryConnect(0);
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
async function resolveBridgeSessionBinding(params, env = process.env, options = {}) {
|
|
840
|
+
const bridgeOptions = getBridgeBindingResolveOptions(env);
|
|
841
|
+
return resolveBindingFromInitializeParams(params, {
|
|
842
|
+
env,
|
|
843
|
+
fallbackWorkspaceRoots: getEnvWorkspaceRootCandidates(),
|
|
844
|
+
rootsListUris: options.rootsListUris,
|
|
845
|
+
rootsListAttempted: options.rootsListAttempted,
|
|
846
|
+
...bridgeOptions
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
async function connectOrSpawnDaemonForBinding(binding, opts) {
|
|
850
|
+
const socketPath = getBindingSocketPath(binding, opts.env);
|
|
851
|
+
opts.log(
|
|
852
|
+
`target daemon socket=${socketPath} project=${binding.projectId} workspace=${binding.workspaceRoot}`
|
|
853
|
+
);
|
|
854
|
+
let socket;
|
|
855
|
+
try {
|
|
856
|
+
socket = await connectWithRetry(
|
|
857
|
+
socketPath,
|
|
858
|
+
opts.log,
|
|
859
|
+
opts.maxRetries,
|
|
860
|
+
opts.retryDelayMs,
|
|
861
|
+
opts.connect
|
|
862
|
+
);
|
|
863
|
+
verifyDaemonSidecarBinding(socketPath, binding, opts.env);
|
|
864
|
+
opts.log("reusing running daemon for session binding");
|
|
865
|
+
return socket;
|
|
866
|
+
} catch (err) {
|
|
867
|
+
if (err instanceof Error && err.message.includes("Daemon socket binding mismatch")) {
|
|
868
|
+
throw err;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
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
|
+
}
|
|
884
|
+
var BridgeDaemonRouter = class {
|
|
885
|
+
constructor(options) {
|
|
886
|
+
this.activeSocket = null;
|
|
887
|
+
this.activeBinding = null;
|
|
888
|
+
this.socketLineReader = null;
|
|
889
|
+
this.lastInitializeLine = null;
|
|
890
|
+
this.pendingDeferredClientLines = [];
|
|
891
|
+
this.handshakeDeferredClientLines = [];
|
|
892
|
+
this.clientInitializeSeen = false;
|
|
893
|
+
this.env = options.env ?? process.env;
|
|
894
|
+
this.stdout = options.stdout ?? process.stdout;
|
|
895
|
+
this.log = options.log ?? defaultLog;
|
|
896
|
+
this.cliPath = options.cliPath;
|
|
897
|
+
this.maxRetries = options.maxRetries ?? 5;
|
|
898
|
+
this.retryDelayMs = options.retryDelayMs ?? 200;
|
|
899
|
+
this.lineReader = options.lineReader ?? null;
|
|
900
|
+
this.connectImpl = options.connect ?? ((socketPath) => new Promise((resolve8, reject) => {
|
|
901
|
+
const socket = net.connect(socketPath, () => resolve8(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;
|
|
929
|
+
}
|
|
930
|
+
hasClientInitialize() {
|
|
931
|
+
return this.clientInitializeSeen;
|
|
932
|
+
}
|
|
933
|
+
async ensureDaemonForInitialize(params) {
|
|
934
|
+
logInitializeDebug(
|
|
935
|
+
this.log,
|
|
936
|
+
this.env,
|
|
937
|
+
`initialize payload: ${summarizeInitializeParamsForDebug(params)}`
|
|
938
|
+
);
|
|
939
|
+
const bridgeOptions = getBridgeBindingResolveOptions(this.env);
|
|
940
|
+
const initializeRoots = extractWorkspaceRootsFromInitialize(params);
|
|
941
|
+
let rootsListUris;
|
|
942
|
+
let rootsListAttempted = false;
|
|
943
|
+
const repoHint = getRepoScopedWorkspaceHint(this.env);
|
|
944
|
+
if (initializeRoots.length === 0 && bridgeOptions.allowEnvWorkspaceFallback === false && repoHint === null) {
|
|
945
|
+
if (!this.lineReader) {
|
|
946
|
+
throw new Error(
|
|
947
|
+
"[memoraone-mcp] Internal error: Cursor workspace binding requires stdin line reader for roots/list"
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
rootsListAttempted = true;
|
|
951
|
+
this.log("initialize lacks workspace roots; requesting roots/list from Cursor before binding");
|
|
952
|
+
const rootsListResult = await requestClientRootsListUris({
|
|
953
|
+
lineReader: this.lineReader,
|
|
954
|
+
stdout: this.stdout,
|
|
955
|
+
log: this.log,
|
|
956
|
+
env: this.env,
|
|
957
|
+
initializeParams: params
|
|
958
|
+
});
|
|
959
|
+
rootsListUris = rootsListResult.uris;
|
|
960
|
+
if (rootsListResult.deferredLines.length > 0) {
|
|
961
|
+
this.pendingDeferredClientLines = rootsListResult.deferredLines.slice();
|
|
962
|
+
logInitializeDebug(
|
|
963
|
+
this.log,
|
|
964
|
+
this.env,
|
|
965
|
+
`queued ${this.pendingDeferredClientLines.length} deferred client line(s) for replay after initialize`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const binding = await resolveBridgeSessionBinding(params, this.env, {
|
|
970
|
+
rootsListUris,
|
|
971
|
+
rootsListAttempted
|
|
972
|
+
});
|
|
973
|
+
const environmentLog = binding.environment !== void 0 ? ` environment=${binding.environment}` : "";
|
|
974
|
+
this.log(
|
|
975
|
+
`session binding project=${binding.projectId} workspace=${binding.workspaceRoot} m1=${binding.m1Path} source=${binding.bindingSource} apiKeySource=${binding.apiKeySource}${environmentLog}`
|
|
976
|
+
);
|
|
977
|
+
if (this.activeBinding && bindingsMatch(this.activeBinding, binding) && this.activeSocket) {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (this.activeBinding && !bindingsMatch(this.activeBinding, binding)) {
|
|
981
|
+
this.log(
|
|
982
|
+
`session binding changed from workspace=${this.activeBinding.workspaceRoot} to workspace=${binding.workspaceRoot}; reconnecting daemon`
|
|
983
|
+
);
|
|
984
|
+
this.resetSessionState();
|
|
985
|
+
this.detachSocketReader();
|
|
986
|
+
this.activeSocket?.destroy();
|
|
987
|
+
this.activeSocket = null;
|
|
988
|
+
}
|
|
989
|
+
this.activeBinding = binding;
|
|
990
|
+
await this.connectActiveDaemon();
|
|
991
|
+
this.log("bridge connected");
|
|
992
|
+
}
|
|
993
|
+
recordClientInitialize(line) {
|
|
994
|
+
this.lastInitializeLine = line;
|
|
995
|
+
this.clientInitializeSeen = true;
|
|
996
|
+
}
|
|
997
|
+
async forwardInitializeToDaemon(line) {
|
|
998
|
+
this.recordClientInitialize(line);
|
|
999
|
+
await this.writeToDaemon(line);
|
|
1000
|
+
logInitializeDebug(this.log, this.env, "initialize replay to daemon");
|
|
1001
|
+
this.log("forwarding active");
|
|
1002
|
+
}
|
|
1003
|
+
async replayDeferredClientMessages() {
|
|
1004
|
+
if (this.pendingDeferredClientLines.length === 0) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const lines = this.pendingDeferredClientLines.slice();
|
|
1008
|
+
this.pendingDeferredClientLines = [];
|
|
1009
|
+
this.handshakeDeferredClientLines = lines.slice();
|
|
1010
|
+
const types = lines.map((line) => summarizeJsonRpcMethod(line));
|
|
1011
|
+
logInitializeDebug(
|
|
1012
|
+
this.log,
|
|
1013
|
+
this.env,
|
|
1014
|
+
`deferred message replay count=${lines.length} types=${JSON.stringify(types)}`
|
|
1015
|
+
);
|
|
1016
|
+
for (const line of lines) {
|
|
1017
|
+
await this.writeToDaemon(line);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async writeToDaemon(line) {
|
|
1021
|
+
await this.ensureActiveDaemonSocket();
|
|
1022
|
+
this.activeSocket.write(`${line}
|
|
1023
|
+
`);
|
|
1024
|
+
}
|
|
1025
|
+
resetSessionState() {
|
|
1026
|
+
this.lastInitializeLine = null;
|
|
1027
|
+
this.pendingDeferredClientLines = [];
|
|
1028
|
+
this.handshakeDeferredClientLines = [];
|
|
1029
|
+
this.clientInitializeSeen = false;
|
|
1030
|
+
this.activeBinding = null;
|
|
1031
|
+
}
|
|
1032
|
+
async connectActiveDaemon() {
|
|
1033
|
+
if (!this.activeBinding) {
|
|
1034
|
+
throw new Error("[memoraone-mcp] Internal error: connectActiveDaemon without active binding");
|
|
1035
|
+
}
|
|
1036
|
+
const socket = await connectOrSpawnDaemonForBinding(this.activeBinding, {
|
|
1037
|
+
env: this.env,
|
|
1038
|
+
cliPath: this.cliPath,
|
|
1039
|
+
log: this.log,
|
|
1040
|
+
maxRetries: this.maxRetries,
|
|
1041
|
+
retryDelayMs: this.retryDelayMs,
|
|
1042
|
+
connect: this.connectImpl,
|
|
1043
|
+
spawnDaemon: this.spawnDaemonImpl
|
|
1044
|
+
});
|
|
1045
|
+
this.activeSocket = socket;
|
|
1046
|
+
this.attachSocketReader(socket);
|
|
1047
|
+
}
|
|
1048
|
+
async ensureActiveDaemonSocket() {
|
|
1049
|
+
if (this.activeSocket && !this.activeSocket.destroyed) {
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
if (!this.clientInitializeSeen || !this.lastInitializeLine || !this.activeBinding) {
|
|
1053
|
+
throw new Error("[memoraone-mcp] MCP request before initialize");
|
|
1054
|
+
}
|
|
1055
|
+
this.log("daemon socket unavailable; reconnecting for session binding");
|
|
1056
|
+
await this.connectActiveDaemon();
|
|
1057
|
+
logInitializeDebug(this.log, this.env, "initialize replay to daemon after reconnect");
|
|
1058
|
+
this.activeSocket.write(`${this.lastInitializeLine}
|
|
1059
|
+
`);
|
|
1060
|
+
if (this.handshakeDeferredClientLines.length > 0) {
|
|
1061
|
+
const types = this.handshakeDeferredClientLines.map(
|
|
1062
|
+
(deferredLine) => summarizeJsonRpcMethod(deferredLine)
|
|
1063
|
+
);
|
|
1064
|
+
logInitializeDebug(
|
|
1065
|
+
this.log,
|
|
1066
|
+
this.env,
|
|
1067
|
+
`deferred message replay after reconnect count=${this.handshakeDeferredClientLines.length} types=${JSON.stringify(types)}`
|
|
1068
|
+
);
|
|
1069
|
+
for (const deferredLine of this.handshakeDeferredClientLines) {
|
|
1070
|
+
this.activeSocket.write(`${deferredLine}
|
|
1071
|
+
`);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
attachSocketReader(socket) {
|
|
1076
|
+
this.detachSocketReader();
|
|
1077
|
+
this.socketLineReader = readline2.createInterface({ input: socket, crlfDelay: Infinity });
|
|
1078
|
+
this.socketLineReader.on("line", (line) => {
|
|
1079
|
+
this.stdout.write(`${line}
|
|
1080
|
+
`);
|
|
1081
|
+
});
|
|
1082
|
+
socket.on("close", (hadError) => {
|
|
1083
|
+
if (this.activeSocket === socket) {
|
|
1084
|
+
logInitializeDebug(
|
|
1085
|
+
this.log,
|
|
1086
|
+
this.env,
|
|
1087
|
+
`daemon socket closed hadError=${String(hadError)}`
|
|
1088
|
+
);
|
|
1089
|
+
this.log("daemon socket closed; bridge stays alive for reconnect");
|
|
1090
|
+
this.detachSocketReader();
|
|
1091
|
+
this.activeSocket = null;
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
socket.on("error", (err) => {
|
|
1095
|
+
this.log(`socket error: ${String(err)}`);
|
|
1096
|
+
logInitializeDebug(this.log, this.env, `daemon socket error: ${String(err)}`);
|
|
1097
|
+
if (this.activeSocket === socket) {
|
|
1098
|
+
this.detachSocketReader();
|
|
1099
|
+
this.activeSocket = null;
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
detachSocketReader() {
|
|
1104
|
+
if (this.socketLineReader) {
|
|
1105
|
+
this.socketLineReader.close();
|
|
1106
|
+
this.socketLineReader = null;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
};
|
|
1110
|
+
async function runBridgeProxy(options) {
|
|
1111
|
+
ensureBaseDir();
|
|
1112
|
+
const stdin = options.stdin ?? process.stdin;
|
|
1113
|
+
const stdout = options.stdout ?? process.stdout;
|
|
1114
|
+
const log = options.log ?? defaultLog;
|
|
1115
|
+
const lineReader = options.lineReader ?? new StdioLineReader(stdin);
|
|
1116
|
+
const router = new BridgeDaemonRouter({ ...options, stdout, lineReader });
|
|
1117
|
+
while (true) {
|
|
1118
|
+
const line = await lineReader.readLine();
|
|
1119
|
+
if (line === null) {
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
const trimmed = line.trim();
|
|
1123
|
+
if (trimmed === "") {
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
let message;
|
|
1127
|
+
try {
|
|
1128
|
+
message = JSON.parse(trimmed);
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
throw new Error(`[memoraone-mcp] Invalid JSON-RPC on stdin: ${String(err)}`);
|
|
1131
|
+
}
|
|
1132
|
+
if (message.method === "initialize") {
|
|
1133
|
+
log("resolve binding from initialize request before daemon connect");
|
|
1134
|
+
const params = message.params ?? {};
|
|
1135
|
+
await router.ensureDaemonForInitialize(params);
|
|
1136
|
+
await router.forwardInitializeToDaemon(trimmed);
|
|
1137
|
+
await router.replayDeferredClientMessages();
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
await router.writeToDaemon(trimmed);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/setupIdeFiles.ts
|
|
1145
|
+
var fs7 = __toESM(require("fs/promises"), 1);
|
|
1146
|
+
var os4 = __toESM(require("os"), 1);
|
|
1147
|
+
var path9 = __toESM(require("path"), 1);
|
|
1148
|
+
|
|
1149
|
+
// src/cleanup.ts
|
|
1150
|
+
var fs5 = __toESM(require("fs/promises"), 1);
|
|
1151
|
+
var path7 = __toESM(require("path"), 1);
|
|
1152
|
+
var readline3 = __toESM(require("readline/promises"), 1);
|
|
1153
|
+
var import_node_child_process3 = require("child_process");
|
|
1154
|
+
var import_node_util2 = require("util");
|
|
1155
|
+
var import_node_process = require("process");
|
|
1156
|
+
|
|
1157
|
+
// src/cursorGlobalMcpConfig.ts
|
|
1158
|
+
var fs4 = __toESM(require("fs/promises"), 1);
|
|
1159
|
+
var os2 = __toESM(require("os"), 1);
|
|
1160
|
+
var path6 = __toESM(require("path"), 1);
|
|
1161
|
+
var import_node_child_process2 = require("child_process");
|
|
1162
|
+
var import_node_util = require("util");
|
|
1163
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
|
|
1164
|
+
var MEMORAONE_PROD_API_URL = "https://api.memoraone.com";
|
|
1165
|
+
var MEMORAONE_LOCAL_API_URL = "http://localhost:3001";
|
|
1166
|
+
var MEMORAONE_STAGING_API_URL_PREFIX = "https://memora-api-staging-";
|
|
1167
|
+
function buildMemoraoneCursorMcpServer(npxPath, workspaceRoot) {
|
|
1168
|
+
const env = {
|
|
1169
|
+
MEMORAONE_API_URL: MEMORAONE_PROD_API_URL,
|
|
1170
|
+
MEMORAONE_IDE_TYPE: "cursor"
|
|
1171
|
+
};
|
|
1172
|
+
if (workspaceRoot !== void 0) {
|
|
1173
|
+
env[MEMORAONE_WORKSPACE_ROOT_ENV] = path6.resolve(workspaceRoot);
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
command: npxPath,
|
|
1177
|
+
args: ["-y", "@memoraone/mcp@latest"],
|
|
1178
|
+
env
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
async function pathExists(filePath) {
|
|
1182
|
+
try {
|
|
1183
|
+
await fs4.access(filePath);
|
|
1184
|
+
return true;
|
|
1185
|
+
} catch {
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
function stripLeadingLineComments(text) {
|
|
1190
|
+
return text.split("\n").filter((line) => !/^\s*\/\//.test(line)).join("\n");
|
|
1191
|
+
}
|
|
1192
|
+
function getKnownCursorGlobalMcpConfigCandidates(homeDir) {
|
|
1193
|
+
return [path6.join(homeDir, ".cursor", "mcp.json")];
|
|
1194
|
+
}
|
|
1195
|
+
async function detectCursorGlobalMcpConfig(options) {
|
|
1196
|
+
if (options?.explicitPath) {
|
|
1197
|
+
return { ok: true, path: options.explicitPath, detectedExisting: await pathExists(options.explicitPath) };
|
|
1198
|
+
}
|
|
1199
|
+
const homeDir = options?.homeDir ?? os2.homedir();
|
|
1200
|
+
const candidates = getKnownCursorGlobalMcpConfigCandidates(homeDir);
|
|
1201
|
+
const existing = [];
|
|
1202
|
+
for (const candidate of candidates) {
|
|
1203
|
+
if (await pathExists(candidate)) existing.push(candidate);
|
|
1204
|
+
}
|
|
1205
|
+
if (existing.length > 1) {
|
|
1206
|
+
return {
|
|
1207
|
+
ok: false,
|
|
1208
|
+
error: "[setup-ide-files] Multiple Cursor global MCP config paths found. Specify one explicitly.",
|
|
1209
|
+
candidates: existing
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
if (existing.length === 1) {
|
|
1213
|
+
return { ok: true, path: existing[0], detectedExisting: true };
|
|
1214
|
+
}
|
|
1215
|
+
const defaultPath = candidates[0];
|
|
1216
|
+
if (!defaultPath) {
|
|
1217
|
+
return {
|
|
1218
|
+
ok: false,
|
|
1219
|
+
error: "[setup-ide-files] No known Cursor global MCP config path.",
|
|
1220
|
+
candidates: []
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
return { ok: true, path: defaultPath, detectedExisting: false };
|
|
1224
|
+
}
|
|
1225
|
+
function formatBackupTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
1226
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1227
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
1228
|
+
}
|
|
1229
|
+
async function isWorkingNpx(npxPath) {
|
|
1230
|
+
try {
|
|
1231
|
+
if (!await pathExists(npxPath)) return false;
|
|
1232
|
+
if (process.platform !== "win32") {
|
|
1233
|
+
try {
|
|
1234
|
+
await fs4.access(npxPath, fs4.constants.X_OK);
|
|
1235
|
+
} catch {
|
|
1236
|
+
return false;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
await execFileAsync(npxPath, ["--version"], { timeout: 1e4 });
|
|
1240
|
+
return true;
|
|
1241
|
+
} catch {
|
|
1242
|
+
return false;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
async function resolveNpxPath() {
|
|
1246
|
+
const npxName = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
1247
|
+
const candidates = [];
|
|
1248
|
+
if (process.platform === "darwin") {
|
|
1249
|
+
candidates.push("/opt/homebrew/bin/npx", "/usr/local/bin/npx");
|
|
1250
|
+
} else if (process.platform === "linux") {
|
|
1251
|
+
candidates.push("/usr/local/bin/npx");
|
|
1252
|
+
}
|
|
1253
|
+
const pathSep = process.platform === "win32" ? ";" : ":";
|
|
1254
|
+
for (const dir of (process.env.PATH ?? "").split(pathSep)) {
|
|
1255
|
+
if (!dir) continue;
|
|
1256
|
+
candidates.push(path6.join(dir, npxName));
|
|
1257
|
+
}
|
|
1258
|
+
try {
|
|
1259
|
+
const lookupCmd = process.platform === "win32" ? "where" : "which";
|
|
1260
|
+
const { stdout } = await execFileAsync(lookupCmd, [npxName], { timeout: 5e3 });
|
|
1261
|
+
const first = stdout.trim().split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
1262
|
+
if (first) candidates.unshift(first);
|
|
1263
|
+
} catch {
|
|
1264
|
+
}
|
|
1265
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1266
|
+
for (const candidate of candidates) {
|
|
1267
|
+
const abs = path6.isAbsolute(candidate) ? candidate : path6.resolve(candidate);
|
|
1268
|
+
const key = process.platform === "win32" ? abs.toLowerCase() : abs;
|
|
1269
|
+
if (seen.has(key)) continue;
|
|
1270
|
+
seen.add(key);
|
|
1271
|
+
if (await isWorkingNpx(abs)) return abs;
|
|
1272
|
+
}
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1275
|
+
function mergeCursorRepoMcpConfigObject(existing, npxPath, repoRoot) {
|
|
1276
|
+
const base = existing && typeof existing === "object" ? { ...existing } : { mcpServers: {} };
|
|
1277
|
+
const mcpServers = typeof base.mcpServers === "object" && base.mcpServers !== null && !Array.isArray(base.mcpServers) ? { ...base.mcpServers } : {};
|
|
1278
|
+
mcpServers.memoraone = buildMemoraoneCursorMcpServer(npxPath, repoRoot);
|
|
1279
|
+
return { ...base, mcpServers };
|
|
1280
|
+
}
|
|
1281
|
+
function isMemoraoneManagedApiUrl(url) {
|
|
1282
|
+
if (typeof url !== "string" || url.length === 0) return false;
|
|
1283
|
+
if (url === MEMORAONE_PROD_API_URL) return true;
|
|
1284
|
+
if (url === MEMORAONE_LOCAL_API_URL) return true;
|
|
1285
|
+
return url.startsWith(MEMORAONE_STAGING_API_URL_PREFIX);
|
|
1286
|
+
}
|
|
1287
|
+
function isManagedMemoraoneCursorServer(server) {
|
|
1288
|
+
if (!server || typeof server !== "object") return false;
|
|
1289
|
+
const s = server;
|
|
1290
|
+
if (!Array.isArray(s.args) || s.args.length !== 2) return false;
|
|
1291
|
+
if (s.args[0] !== "-y" || s.args[1] !== "@memoraone/mcp@latest") return false;
|
|
1292
|
+
const env = s.env;
|
|
1293
|
+
if (!env || typeof env !== "object") return false;
|
|
1294
|
+
return memoraoneEnvMatchesManagedCleanupShape(env);
|
|
1295
|
+
}
|
|
1296
|
+
function cursorConfigHasManagedMemoraone(parsed) {
|
|
1297
|
+
if (!parsed || typeof parsed !== "object") return false;
|
|
1298
|
+
const mcpServers = parsed.mcpServers;
|
|
1299
|
+
if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) return false;
|
|
1300
|
+
return isManagedMemoraoneCursorServer(mcpServers.memoraone);
|
|
1301
|
+
}
|
|
1302
|
+
function memoraoneEnvMatchesManagedCleanupShape(env) {
|
|
1303
|
+
return env.MEMORAONE_IDE_TYPE === "cursor" && isMemoraoneManagedApiUrl(env.MEMORAONE_API_URL);
|
|
1304
|
+
}
|
|
1305
|
+
function getCursorRepoMcpConfigPath(repoRoot) {
|
|
1306
|
+
return path6.join(repoRoot, ".cursor", "mcp.json");
|
|
1307
|
+
}
|
|
1308
|
+
async function readCursorMcpConfigObject(configPath) {
|
|
1309
|
+
try {
|
|
1310
|
+
const raw = await fs4.readFile(configPath, "utf8");
|
|
1311
|
+
return JSON.parse(stripLeadingLineComments(raw));
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
1314
|
+
if (code === "ENOENT") return null;
|
|
1315
|
+
throw err;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
async function removeMemoraoneFromCursorGlobalConfig(options) {
|
|
1319
|
+
const { configPath, dryRun } = options;
|
|
1320
|
+
const parsed = await readCursorMcpConfigObject(configPath);
|
|
1321
|
+
if (!parsed || !cursorConfigHasManagedMemoraone(parsed)) {
|
|
1322
|
+
return { changed: false };
|
|
1323
|
+
}
|
|
1324
|
+
if (dryRun) {
|
|
1325
|
+
return { changed: true, backupPath: `${configPath}.backup-<timestamp>` };
|
|
1326
|
+
}
|
|
1327
|
+
const backupPath = `${configPath}.backup-${formatBackupTimestamp()}`;
|
|
1328
|
+
await fs4.copyFile(configPath, backupPath);
|
|
1329
|
+
const mcpServers = typeof parsed.mcpServers === "object" && parsed.mcpServers !== null && !Array.isArray(parsed.mcpServers) ? { ...parsed.mcpServers } : {};
|
|
1330
|
+
delete mcpServers.memoraone;
|
|
1331
|
+
const hasOtherServers = Object.keys(mcpServers).length > 0;
|
|
1332
|
+
if (!hasOtherServers) {
|
|
1333
|
+
await fs4.unlink(configPath);
|
|
1334
|
+
return { changed: true, backupPath };
|
|
1335
|
+
}
|
|
1336
|
+
const next = { ...parsed, mcpServers };
|
|
1337
|
+
await fs4.mkdir(path6.dirname(configPath), { recursive: true });
|
|
1338
|
+
await fs4.writeFile(configPath, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
1339
|
+
return { changed: true, backupPath };
|
|
1340
|
+
}
|
|
1341
|
+
async function auditCursorMcpConfig(options) {
|
|
1342
|
+
const repoRoot = path6.resolve(options?.repoRoot ?? process.cwd());
|
|
1343
|
+
const repoConfigPath = getCursorRepoMcpConfigPath(repoRoot);
|
|
1344
|
+
const globalDetection = await detectCursorGlobalMcpConfig({
|
|
1345
|
+
homeDir: options?.homeDir,
|
|
1346
|
+
explicitPath: options?.explicitGlobalPath
|
|
1347
|
+
});
|
|
1348
|
+
const globalConfigPath = globalDetection.ok ? globalDetection.path : getKnownCursorGlobalMcpConfigCandidates(
|
|
1349
|
+
options?.homeDir ?? os2.homedir()
|
|
1350
|
+
)[0];
|
|
1351
|
+
let repoHasManagedMemoraone = false;
|
|
1352
|
+
try {
|
|
1353
|
+
const repoParsed = await readCursorMcpConfigObject(repoConfigPath);
|
|
1354
|
+
repoHasManagedMemoraone = cursorConfigHasManagedMemoraone(repoParsed);
|
|
1355
|
+
} catch {
|
|
1356
|
+
repoHasManagedMemoraone = false;
|
|
1357
|
+
}
|
|
1358
|
+
let globalHasManagedMemoraone = false;
|
|
1359
|
+
if (globalDetection.ok) {
|
|
1360
|
+
try {
|
|
1361
|
+
const globalParsed = await readCursorMcpConfigObject(globalConfigPath);
|
|
1362
|
+
globalHasManagedMemoraone = cursorConfigHasManagedMemoraone(globalParsed);
|
|
1363
|
+
} catch {
|
|
1364
|
+
globalHasManagedMemoraone = false;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return {
|
|
1368
|
+
repoConfigPath,
|
|
1369
|
+
repoHasManagedMemoraone,
|
|
1370
|
+
globalConfigPath,
|
|
1371
|
+
globalHasManagedMemoraone,
|
|
1372
|
+
conflict: repoHasManagedMemoraone && globalHasManagedMemoraone
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
function logCursorMcpConfigAudit(prefix, audit) {
|
|
1376
|
+
console.log(`${prefix} Cursor MCP config audit:`);
|
|
1377
|
+
console.log(
|
|
1378
|
+
`${prefix} repo ${audit.repoConfigPath}: managed memoraone=${audit.repoHasManagedMemoraone}`
|
|
1379
|
+
);
|
|
1380
|
+
console.log(
|
|
1381
|
+
`${prefix} global ${audit.globalConfigPath}: managed memoraone=${audit.globalHasManagedMemoraone}`
|
|
1382
|
+
);
|
|
1383
|
+
if (audit.conflict) {
|
|
1384
|
+
console.warn(
|
|
1385
|
+
`${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.`
|
|
1386
|
+
);
|
|
1387
|
+
} else if (audit.globalHasManagedMemoraone && !audit.repoHasManagedMemoraone) {
|
|
1388
|
+
console.warn(
|
|
1389
|
+
`${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.`
|
|
1390
|
+
);
|
|
1391
|
+
} else if (audit.repoHasManagedMemoraone && !audit.globalHasManagedMemoraone) {
|
|
1392
|
+
console.log(
|
|
1393
|
+
`${prefix} Cursor MCP is repo-scoped (.cursor/mcp.json) with no global memoraone entry (recommended for multi-repo windows).`
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
function logCursorMcpCliSummary(info, dryRun) {
|
|
1398
|
+
const { repoConfigPath, repoOutcome, npxPath, repoBackupPath, globalConfigPath, globalMemoraoneRemoved, globalBackupPath } = info;
|
|
1399
|
+
console.log(`[setup-ide-files] Cursor repo MCP config: ${repoConfigPath}`);
|
|
1400
|
+
console.log(`[setup-ide-files] Resolved npx: ${npxPath}`);
|
|
1401
|
+
if (repoBackupPath) {
|
|
1402
|
+
console.log(`[setup-ide-files] Cursor repo MCP config backup: ${repoBackupPath}`);
|
|
1403
|
+
}
|
|
1404
|
+
if (repoOutcome === "created") {
|
|
1405
|
+
console.log(
|
|
1406
|
+
dryRun ? `[setup-ide-files] Cursor repo MCP config would be created: ${repoConfigPath}` : `[setup-ide-files] Cursor repo MCP config created: ${repoConfigPath}`
|
|
1407
|
+
);
|
|
1408
|
+
} else if (repoOutcome === "updated") {
|
|
1409
|
+
console.log(
|
|
1410
|
+
dryRun ? `[setup-ide-files] Cursor repo MCP config would be updated: ${repoConfigPath}` : `[setup-ide-files] Cursor repo MCP config updated: ${repoConfigPath}`
|
|
1411
|
+
);
|
|
1412
|
+
} else if (repoOutcome === "skipped") {
|
|
1413
|
+
console.log(`[setup-ide-files] Cursor repo MCP config unchanged: ${repoConfigPath}`);
|
|
1414
|
+
}
|
|
1415
|
+
if (globalMemoraoneRemoved && globalConfigPath) {
|
|
1416
|
+
console.log(
|
|
1417
|
+
dryRun ? `[setup-ide-files] Would remove memoraone from Cursor global MCP config: ${globalConfigPath}` : `[setup-ide-files] Removed memoraone from Cursor global MCP config: ${globalConfigPath}`
|
|
1418
|
+
);
|
|
1419
|
+
if (globalBackupPath) {
|
|
1420
|
+
console.log(`[setup-ide-files] Cursor global MCP config backup: ${globalBackupPath}`);
|
|
1421
|
+
}
|
|
1422
|
+
} else if (globalConfigPath) {
|
|
1423
|
+
console.log(
|
|
1424
|
+
`[setup-ide-files] Cursor global MCP config unchanged (no managed memoraone to remove): ${globalConfigPath}`
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
console.log(
|
|
1428
|
+
"[setup-ide-files] Each Cursor window uses this repo\u2019s .cursor/mcp.json (separate MCP process per repo)."
|
|
1429
|
+
);
|
|
1430
|
+
console.log(
|
|
1431
|
+
"[setup-ide-files] Fully quit Cursor and reopen this repo for MCP changes to take effect."
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// src/cleanup.ts
|
|
1436
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
|
|
1437
|
+
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;
|
|
1438
|
+
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;
|
|
1439
|
+
var MEMORAONE_MCP_COMMAND_RE = /memoraone-mcp|memoraOne-mcp|@memoraone\/mcp/;
|
|
1440
|
+
var CLEANUP_PROJECT_ID_REQUIRED_ERROR = "Provide --project-id <id> or run from a folder containing memoraone.m1.";
|
|
1441
|
+
function isMemoraoneMcpCommandLine(commandLine) {
|
|
1442
|
+
return MEMORAONE_MCP_COMMAND_RE.test(commandLine);
|
|
1443
|
+
}
|
|
1444
|
+
function parseDaemonProjectIdFromCommandLine(commandLine) {
|
|
1445
|
+
if (!commandLine.includes("--daemon")) {
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
const match = commandLine.match(DAEMON_PROJECT_ID_RE);
|
|
1449
|
+
return match ? match[1].toLowerCase() : null;
|
|
1450
|
+
}
|
|
1451
|
+
function parseDaemonIdeFromCommandLine(commandLine) {
|
|
1452
|
+
if (!commandLine.includes("--daemon")) {
|
|
1453
|
+
return void 0;
|
|
1454
|
+
}
|
|
1455
|
+
return parseIdeTypeFromCommandLine(commandLine);
|
|
1456
|
+
}
|
|
1457
|
+
function parseDaemonProcessLines(lines) {
|
|
1458
|
+
const processes = [];
|
|
1459
|
+
for (const line of lines) {
|
|
1460
|
+
const trimmed = line.trim();
|
|
1461
|
+
if (!trimmed) continue;
|
|
1462
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
1463
|
+
if (spaceIdx <= 0) continue;
|
|
1464
|
+
const pid = Number.parseInt(trimmed.slice(0, spaceIdx), 10);
|
|
1465
|
+
if (!Number.isFinite(pid) || pid <= 0) continue;
|
|
1466
|
+
const command = trimmed.slice(spaceIdx + 1);
|
|
1467
|
+
if (!isMemoraoneMcpCommandLine(command)) continue;
|
|
1468
|
+
const projectId = parseDaemonProjectIdFromCommandLine(command);
|
|
1469
|
+
if (projectId === null) continue;
|
|
1470
|
+
const ide = parseDaemonIdeFromCommandLine(command);
|
|
1471
|
+
processes.push({ pid, command, projectId, ...ide !== void 0 ? { ide } : {} });
|
|
1472
|
+
}
|
|
1473
|
+
return processes;
|
|
1474
|
+
}
|
|
1475
|
+
function normalizeCleanupProjectId(projectId) {
|
|
1476
|
+
const trimmed = projectId.trim();
|
|
1477
|
+
if (!PROJECT_ID_RE.test(trimmed)) {
|
|
1478
|
+
return { error: `Invalid project id: ${projectId}` };
|
|
1479
|
+
}
|
|
1480
|
+
return trimmed.toLowerCase();
|
|
1481
|
+
}
|
|
1482
|
+
async function defaultListDaemonProcesses() {
|
|
1483
|
+
const { stdout } = await execFileAsync2("ps", ["-eo", "pid=,args="], {
|
|
1484
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1485
|
+
});
|
|
1486
|
+
return parseDaemonProcessLines(stdout.split("\n"));
|
|
1487
|
+
}
|
|
1488
|
+
async function defaultListSocketPaths(projectId) {
|
|
1489
|
+
const baseDir = getMcpBaseDir();
|
|
1490
|
+
let entries;
|
|
1491
|
+
try {
|
|
1492
|
+
entries = await fs5.readdir(baseDir);
|
|
1493
|
+
} catch (err) {
|
|
1494
|
+
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
1495
|
+
if (code === "ENOENT") {
|
|
1496
|
+
return [];
|
|
1497
|
+
}
|
|
1498
|
+
throw err;
|
|
1499
|
+
}
|
|
1500
|
+
const paths = [];
|
|
1501
|
+
for (const name of entries) {
|
|
1502
|
+
if (!name.endsWith(".sock") || !isMemoraoneSocketFilename(name)) {
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
const socketPath = path7.join(baseDir, name);
|
|
1506
|
+
if (projectId === null) {
|
|
1507
|
+
paths.push(socketPath);
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
const normalizedProjectId = projectId.trim().toLowerCase();
|
|
1511
|
+
if (isLegacySocketFilename(name) && isSocketFilenameForProject(name, normalizedProjectId)) {
|
|
1512
|
+
paths.push(socketPath);
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
if (isHashSocketFilename(name)) {
|
|
1516
|
+
const record = readBindingSidecarRecord(socketPath);
|
|
1517
|
+
if (record?.projectId.trim().toLowerCase() === normalizedProjectId) {
|
|
1518
|
+
paths.push(socketPath);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
return paths.sort();
|
|
1523
|
+
}
|
|
1524
|
+
async function filterSocketPathsByIde(socketPaths, projectId, ide) {
|
|
1525
|
+
if (ide === void 0) return socketPaths;
|
|
1526
|
+
const normalizedProjectId = projectId.trim().toLowerCase();
|
|
1527
|
+
const filtered = [];
|
|
1528
|
+
for (const socketPath of socketPaths) {
|
|
1529
|
+
const basename4 = path7.basename(socketPath);
|
|
1530
|
+
if (isLegacySocketFilename(basename4)) {
|
|
1531
|
+
if (isSocketFilenameForProjectAndIde(basename4, normalizedProjectId, ide)) {
|
|
1532
|
+
filtered.push(socketPath);
|
|
1533
|
+
}
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
if (isHashSocketFilename(basename4)) {
|
|
1537
|
+
const record = readBindingSidecarRecord(socketPath);
|
|
1538
|
+
if (record?.projectId.trim().toLowerCase() === normalizedProjectId && record.ideType === ide) {
|
|
1539
|
+
filtered.push(socketPath);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return filtered;
|
|
1544
|
+
}
|
|
1545
|
+
async function defaultKillProcess(pid) {
|
|
1546
|
+
process.kill(pid, "SIGTERM");
|
|
1547
|
+
}
|
|
1548
|
+
async function defaultRemoveSocket(socketPath) {
|
|
1549
|
+
await fs5.unlink(socketPath);
|
|
1550
|
+
try {
|
|
1551
|
+
await fs5.unlink(bindingSidecarPath(socketPath));
|
|
1552
|
+
} catch {
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
async function defaultConfirm(message) {
|
|
1556
|
+
if (!import_node_process.stdin.isTTY) {
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
const rl = readline3.createInterface({ input: import_node_process.stdin, output: import_node_process.stdout });
|
|
1560
|
+
try {
|
|
1561
|
+
const answer = await rl.question(`${message} [y/N] `);
|
|
1562
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1563
|
+
} finally {
|
|
1564
|
+
rl.close();
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
async function resolveCleanupTarget(cwd) {
|
|
1568
|
+
try {
|
|
1569
|
+
const binding = await resolveAuthoritativeBinding([path7.resolve(cwd)]);
|
|
1570
|
+
return {
|
|
1571
|
+
workspaceRoot: binding.workspaceRoot,
|
|
1572
|
+
m1Path: binding.m1Path,
|
|
1573
|
+
projectId: binding.projectId
|
|
1574
|
+
};
|
|
1575
|
+
} catch {
|
|
1576
|
+
return { error: CLEANUP_PROJECT_ID_REQUIRED_ERROR };
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
function filterProcessesForScope(processes, projectId) {
|
|
1580
|
+
if (projectId === null) {
|
|
1581
|
+
return { matching: processes, skipped: [] };
|
|
1582
|
+
}
|
|
1583
|
+
const normalized = projectId.toLowerCase();
|
|
1584
|
+
const matching = [];
|
|
1585
|
+
const skipped = [];
|
|
1586
|
+
for (const proc of processes) {
|
|
1587
|
+
if (proc.projectId === normalized) {
|
|
1588
|
+
matching.push(proc);
|
|
1589
|
+
} else {
|
|
1590
|
+
skipped.push(proc);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
return { matching, skipped };
|
|
433
1594
|
}
|
|
434
1595
|
function logPrefix(dryRun) {
|
|
435
1596
|
return dryRun ? "[cleanup][dry-run]" : "[cleanup]";
|
|
436
1597
|
}
|
|
437
|
-
function logReconnectNotice(prefix, ide) {
|
|
1598
|
+
function logReconnectNotice(opts, prefix, ide) {
|
|
1599
|
+
if (opts.quiet) return;
|
|
438
1600
|
if (ide) {
|
|
439
1601
|
console.log(
|
|
440
1602
|
`${prefix} Note: Valid ${ide} connections for this project may disconnect temporarily; they should reconnect automatically. Stale connections will remain cleared.`
|
|
@@ -445,6 +1607,16 @@ function logReconnectNotice(prefix, ide) {
|
|
|
445
1607
|
);
|
|
446
1608
|
}
|
|
447
1609
|
}
|
|
1610
|
+
function cleanupLog(opts, message) {
|
|
1611
|
+
if (!opts.quiet) {
|
|
1612
|
+
console.log(message);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
function cleanupWarn(opts, message) {
|
|
1616
|
+
if (!opts.quiet) {
|
|
1617
|
+
console.warn(message);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
448
1620
|
async function runCleanup(opts) {
|
|
449
1621
|
const listProcesses = opts.listProcesses ?? defaultListDaemonProcesses;
|
|
450
1622
|
const listSocketPaths = opts.listSocketPaths ?? defaultListSocketPaths;
|
|
@@ -465,8 +1637,9 @@ async function runCleanup(opts) {
|
|
|
465
1637
|
error: "Cannot combine --all-projects with --project-id."
|
|
466
1638
|
};
|
|
467
1639
|
}
|
|
468
|
-
|
|
469
|
-
|
|
1640
|
+
cleanupLog(opts, `${prefix} Mode: all projects (--all-projects)`);
|
|
1641
|
+
cleanupWarn(
|
|
1642
|
+
opts,
|
|
470
1643
|
`${prefix} WARNING: This stops every MemoraOne MCP daemon and removes all project sockets under ${getMcpBaseDir()}.`
|
|
471
1644
|
);
|
|
472
1645
|
} else if (opts.projectId) {
|
|
@@ -475,9 +1648,9 @@ async function runCleanup(opts) {
|
|
|
475
1648
|
return { exitCode: 1, killedPids: [], removedSockets: [], skippedProcesses: [], error: normalized.error };
|
|
476
1649
|
}
|
|
477
1650
|
targetProjectId = normalized;
|
|
478
|
-
|
|
1651
|
+
cleanupLog(opts, `${prefix} Project id: ${targetProjectId}`);
|
|
479
1652
|
if (opts.ide) {
|
|
480
|
-
|
|
1653
|
+
cleanupLog(opts, `${prefix} IDE filter: ${opts.ide}`);
|
|
481
1654
|
}
|
|
482
1655
|
} else {
|
|
483
1656
|
const target = await resolveCleanupTarget(opts.cwd);
|
|
@@ -487,15 +1660,23 @@ async function runCleanup(opts) {
|
|
|
487
1660
|
targetProjectId = target.projectId;
|
|
488
1661
|
workspaceRoot = target.workspaceRoot;
|
|
489
1662
|
m1Path = target.m1Path;
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
1663
|
+
cleanupLog(opts, `${prefix} Workspace root: ${workspaceRoot}`);
|
|
1664
|
+
cleanupLog(opts, `${prefix} memoraone.m1: ${m1Path}`);
|
|
1665
|
+
cleanupLog(opts, `${prefix} Project id: ${targetProjectId}`);
|
|
493
1666
|
if (opts.ide) {
|
|
494
|
-
|
|
1667
|
+
cleanupLog(opts, `${prefix} IDE filter: ${opts.ide}`);
|
|
1668
|
+
}
|
|
1669
|
+
if (workspaceRoot && (!opts.ide || opts.ide === "cursor")) {
|
|
1670
|
+
try {
|
|
1671
|
+
const cursorAudit = await auditCursorMcpConfig({ repoRoot: workspaceRoot });
|
|
1672
|
+
logCursorMcpConfigAudit(prefix, cursorAudit);
|
|
1673
|
+
} catch (err) {
|
|
1674
|
+
cleanupWarn(opts, `${prefix} Cursor MCP config audit failed: ${String(err)}`);
|
|
1675
|
+
}
|
|
495
1676
|
}
|
|
496
1677
|
}
|
|
497
1678
|
if (targetProjectId !== null || opts.ide) {
|
|
498
|
-
logReconnectNotice(prefix, opts.ide);
|
|
1679
|
+
logReconnectNotice(opts, prefix, opts.ide);
|
|
499
1680
|
}
|
|
500
1681
|
const allDaemonProcesses = await listProcesses();
|
|
501
1682
|
const { matching: projectProcesses, skipped: skippedProcesses } = filterProcessesForScope(
|
|
@@ -511,392 +1692,198 @@ async function runCleanup(opts) {
|
|
|
511
1692
|
processesToStop.push(proc);
|
|
512
1693
|
} else if (proc.ide === void 0) {
|
|
513
1694
|
ideSkippedProcesses.push(proc);
|
|
514
|
-
|
|
1695
|
+
cleanupLog(
|
|
1696
|
+
opts,
|
|
515
1697
|
`${prefix} Skipped daemon pid=${proc.pid} because IDE could not be safely determined.`
|
|
516
1698
|
);
|
|
517
1699
|
} else {
|
|
518
1700
|
ideSkippedProcesses.push(proc);
|
|
519
|
-
|
|
1701
|
+
cleanupLog(
|
|
1702
|
+
opts,
|
|
520
1703
|
`${prefix} Skipped daemon pid=${proc.pid} (IDE ${proc.ide} does not match filter ${opts.ide}).`
|
|
521
1704
|
);
|
|
522
1705
|
}
|
|
523
1706
|
}
|
|
524
1707
|
}
|
|
525
1708
|
const allSocketPaths = await listSocketPaths(targetProjectId);
|
|
526
|
-
const socketPaths = targetProjectId === null ? allSocketPaths : filterSocketPathsByIde(allSocketPaths, targetProjectId, opts.ide);
|
|
1709
|
+
const socketPaths = targetProjectId === null ? allSocketPaths : await filterSocketPathsByIde(allSocketPaths, targetProjectId, opts.ide);
|
|
527
1710
|
if (opts.allProjects) {
|
|
528
1711
|
const projectIds = /* @__PURE__ */ new Set();
|
|
529
1712
|
for (const proc of processesToStop) {
|
|
530
1713
|
projectIds.add(proc.projectId);
|
|
531
1714
|
}
|
|
532
1715
|
for (const socketPath of socketPaths) {
|
|
533
|
-
const id = extractProjectIdFromSocketFilename(
|
|
534
|
-
if (id)
|
|
1716
|
+
const id = extractProjectIdFromSocketFilename(path7.basename(socketPath));
|
|
1717
|
+
if (id) {
|
|
1718
|
+
projectIds.add(id);
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
const record = readBindingSidecarRecord(socketPath);
|
|
1722
|
+
if (record) {
|
|
1723
|
+
projectIds.add(record.projectId.trim().toLowerCase());
|
|
1724
|
+
}
|
|
535
1725
|
}
|
|
536
|
-
|
|
1726
|
+
cleanupLog(
|
|
1727
|
+
opts,
|
|
1728
|
+
`${prefix} Projects affected: ${projectIds.size ? [...projectIds].sort().join(", ") : "(none found)"}`
|
|
1729
|
+
);
|
|
537
1730
|
}
|
|
538
1731
|
if (processesToStop.length) {
|
|
539
|
-
|
|
1732
|
+
cleanupLog(opts, `${prefix} Daemon processes to stop:`);
|
|
540
1733
|
for (const proc of processesToStop) {
|
|
541
1734
|
const ideLabel = proc.ide ? ` ide=${proc.ide}` : "";
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
} else if (ideSkippedProcesses.length) {
|
|
545
|
-
|
|
546
|
-
} else {
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
if (socketPaths.length) {
|
|
550
|
-
console.log(`${prefix} Sockets to remove:`);
|
|
551
|
-
for (const socketPath of socketPaths) {
|
|
552
|
-
console.log(`${prefix} ${socketPath}`);
|
|
553
|
-
}
|
|
554
|
-
} else {
|
|
555
|
-
console.log(`${prefix} No matching sockets found under ${getMcpBaseDir()}.`);
|
|
556
|
-
}
|
|
557
|
-
if (skippedProcesses.length) {
|
|
558
|
-
console.log(`${prefix} Skipped unrelated daemon processes:`);
|
|
559
|
-
for (const proc of skippedProcesses) {
|
|
560
|
-
console.log(`${prefix} pid=${proc.pid} project=${proc.projectId}`);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
if (opts.allProjects && !opts.dryRun) {
|
|
564
|
-
const ok = opts.assumeYes ? true : await confirm(`${prefix} Proceed with cleanup for ALL projects?`);
|
|
565
|
-
if (!ok) {
|
|
566
|
-
console.log(`${prefix} Aborted.`);
|
|
567
|
-
return {
|
|
568
|
-
exitCode: 1,
|
|
569
|
-
workspaceRoot,
|
|
570
|
-
m1Path,
|
|
571
|
-
projectId: targetProjectId ?? void 0,
|
|
572
|
-
killedPids: [],
|
|
573
|
-
removedSockets: [],
|
|
574
|
-
skippedProcesses: [...skippedProcesses, ...ideSkippedProcesses],
|
|
575
|
-
error: opts.assumeYes ? void 0 : "Aborted (--all requires --yes in non-interactive mode)"
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
const killedPids = [];
|
|
580
|
-
const removedSockets = [];
|
|
581
|
-
if (opts.dryRun) {
|
|
582
|
-
console.log(`${prefix} Dry run complete \u2014 no processes stopped, no sockets removed.`);
|
|
583
|
-
return {
|
|
584
|
-
exitCode: 0,
|
|
585
|
-
workspaceRoot,
|
|
586
|
-
m1Path,
|
|
587
|
-
projectId: targetProjectId ?? void 0,
|
|
588
|
-
killedPids: processesToStop.map((p) => p.pid),
|
|
589
|
-
removedSockets: socketPaths,
|
|
590
|
-
skippedProcesses: [...skippedProcesses, ...ideSkippedProcesses]
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
for (const proc of processesToStop) {
|
|
594
|
-
try {
|
|
595
|
-
await killProcess(proc.pid);
|
|
596
|
-
killedPids.push(proc.pid);
|
|
597
|
-
console.log(`${prefix} Stopped daemon pid=${proc.pid} project=${proc.projectId}`);
|
|
598
|
-
} catch (err) {
|
|
599
|
-
console.warn(`${prefix} Could not stop pid=${proc.pid}: ${String(err)}`);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
if (killedPids.length) {
|
|
603
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
604
|
-
}
|
|
605
|
-
for (const socketPath of socketPaths) {
|
|
606
|
-
try {
|
|
607
|
-
await removeSocket(socketPath);
|
|
608
|
-
removedSockets.push(socketPath);
|
|
609
|
-
console.log(`${prefix} Removed socket ${socketPath}`);
|
|
610
|
-
} catch (err) {
|
|
611
|
-
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
612
|
-
if (code !== "ENOENT") {
|
|
613
|
-
console.warn(`${prefix} Could not remove socket ${socketPath}: ${String(err)}`);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
console.log(`${prefix} Done. stopped=${killedPids.length} socketsRemoved=${removedSockets.length}`);
|
|
618
|
-
return {
|
|
619
|
-
exitCode: 0,
|
|
620
|
-
workspaceRoot,
|
|
621
|
-
m1Path,
|
|
622
|
-
projectId: targetProjectId ?? void 0,
|
|
623
|
-
killedPids,
|
|
624
|
-
removedSockets,
|
|
625
|
-
skippedProcesses: [...skippedProcesses, ...ideSkippedProcesses]
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
function parseCleanupFlags(argv) {
|
|
629
|
-
let dryRun = false;
|
|
630
|
-
let allProjects = false;
|
|
631
|
-
let assumeYes = false;
|
|
632
|
-
let projectId;
|
|
633
|
-
let ide;
|
|
634
|
-
let invalidIde;
|
|
635
|
-
const unknown = [];
|
|
636
|
-
for (let i = 0; i < argv.length; i++) {
|
|
637
|
-
const arg = argv[i];
|
|
638
|
-
if (arg === "--dry-run") dryRun = true;
|
|
639
|
-
else if (arg === "--all-projects" || arg === "--all") allProjects = true;
|
|
640
|
-
else if (arg === "--yes" || arg === "-y") assumeYes = true;
|
|
641
|
-
else if (arg === "--project-id") {
|
|
642
|
-
if (i + 1 >= argv.length) {
|
|
643
|
-
unknown.push("--project-id (missing value)");
|
|
644
|
-
} else {
|
|
645
|
-
projectId = argv[++i];
|
|
646
|
-
}
|
|
647
|
-
} else if (arg === "--ide") {
|
|
648
|
-
if (i + 1 >= argv.length) {
|
|
649
|
-
unknown.push("--ide (missing value)");
|
|
650
|
-
} else {
|
|
651
|
-
const value = argv[++i];
|
|
652
|
-
if (IDE_TYPES.includes(value)) {
|
|
653
|
-
ide = value;
|
|
654
|
-
} else {
|
|
655
|
-
invalidIde = value;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
} else if (arg.startsWith("-")) unknown.push(arg);
|
|
659
|
-
else unknown.push(arg);
|
|
660
|
-
}
|
|
661
|
-
return { dryRun, allProjects, assumeYes, projectId, ide, invalidIde, unknown };
|
|
662
|
-
}
|
|
663
|
-
async function cliCleanup(argv) {
|
|
664
|
-
const { dryRun, allProjects, assumeYes, projectId, ide, invalidIde, unknown } = parseCleanupFlags(argv);
|
|
665
|
-
if (invalidIde) {
|
|
666
|
-
console.error(
|
|
667
|
-
`[cleanup] Invalid --ide value: ${invalidIde}. Expected one of: ${IDE_TYPES.join(", ")}.`
|
|
668
|
-
);
|
|
669
|
-
return 1;
|
|
670
|
-
}
|
|
671
|
-
if (unknown.length) {
|
|
672
|
-
console.error(`[cleanup] Unknown option(s): ${unknown.join(", ")}`);
|
|
673
|
-
return 1;
|
|
674
|
-
}
|
|
675
|
-
const result = await runCleanup({
|
|
676
|
-
cwd: process.cwd(),
|
|
677
|
-
dryRun,
|
|
678
|
-
allProjects,
|
|
679
|
-
assumeYes,
|
|
680
|
-
projectId,
|
|
681
|
-
ide
|
|
682
|
-
});
|
|
683
|
-
if (result.error) {
|
|
684
|
-
console.error(`[cleanup] ${result.error}`);
|
|
685
|
-
}
|
|
686
|
-
return result.exitCode;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// src/cursorGlobalMcpConfig.ts
|
|
690
|
-
var fs4 = __toESM(require("fs/promises"), 1);
|
|
691
|
-
var os2 = __toESM(require("os"), 1);
|
|
692
|
-
var path4 = __toESM(require("path"), 1);
|
|
693
|
-
var import_node_child_process2 = require("child_process");
|
|
694
|
-
var import_node_util2 = require("util");
|
|
695
|
-
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
696
|
-
function buildMemoraoneCursorMcpServer(npxPath) {
|
|
697
|
-
return {
|
|
698
|
-
command: npxPath,
|
|
699
|
-
args: ["-y", "@memoraone/mcp@latest"],
|
|
700
|
-
env: {
|
|
701
|
-
MEMORAONE_API_URL: "https://api.memoraone.com",
|
|
702
|
-
MEMORAONE_IDE_TYPE: "cursor"
|
|
703
|
-
}
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
async function pathExists(filePath) {
|
|
707
|
-
try {
|
|
708
|
-
await fs4.access(filePath);
|
|
709
|
-
return true;
|
|
710
|
-
} catch {
|
|
711
|
-
return false;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
function stripLeadingLineComments(text) {
|
|
715
|
-
return text.split("\n").filter((line) => !/^\s*\/\//.test(line)).join("\n");
|
|
716
|
-
}
|
|
717
|
-
function getKnownCursorGlobalMcpConfigCandidates(homeDir) {
|
|
718
|
-
return [path4.join(homeDir, ".cursor", "mcp.json")];
|
|
719
|
-
}
|
|
720
|
-
async function detectCursorGlobalMcpConfig(options) {
|
|
721
|
-
if (options?.explicitPath) {
|
|
722
|
-
return { ok: true, path: options.explicitPath, detectedExisting: await pathExists(options.explicitPath) };
|
|
723
|
-
}
|
|
724
|
-
const homeDir = options?.homeDir ?? os2.homedir();
|
|
725
|
-
const candidates = getKnownCursorGlobalMcpConfigCandidates(homeDir);
|
|
726
|
-
const existing = [];
|
|
727
|
-
for (const candidate of candidates) {
|
|
728
|
-
if (await pathExists(candidate)) existing.push(candidate);
|
|
729
|
-
}
|
|
730
|
-
if (existing.length > 1) {
|
|
731
|
-
return {
|
|
732
|
-
ok: false,
|
|
733
|
-
error: "[setup-ide-files] Multiple Cursor global MCP config paths found. Specify one explicitly.",
|
|
734
|
-
candidates: existing
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
if (existing.length === 1) {
|
|
738
|
-
return { ok: true, path: existing[0], detectedExisting: true };
|
|
739
|
-
}
|
|
740
|
-
const defaultPath = candidates[0];
|
|
741
|
-
if (!defaultPath) {
|
|
742
|
-
return {
|
|
743
|
-
ok: false,
|
|
744
|
-
error: "[setup-ide-files] No known Cursor global MCP config path.",
|
|
745
|
-
candidates: []
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
return { ok: true, path: defaultPath, detectedExisting: false };
|
|
749
|
-
}
|
|
750
|
-
function formatBackupTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
751
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
752
|
-
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
753
|
-
}
|
|
754
|
-
async function isWorkingNpx(npxPath) {
|
|
755
|
-
try {
|
|
756
|
-
if (!await pathExists(npxPath)) return false;
|
|
757
|
-
if (process.platform !== "win32") {
|
|
758
|
-
try {
|
|
759
|
-
await fs4.access(npxPath, fs4.constants.X_OK);
|
|
760
|
-
} catch {
|
|
761
|
-
return false;
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
await execFileAsync2(npxPath, ["--version"], { timeout: 1e4 });
|
|
765
|
-
return true;
|
|
766
|
-
} catch {
|
|
767
|
-
return false;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
async function resolveNpxPath() {
|
|
771
|
-
const npxName = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
772
|
-
const candidates = [];
|
|
773
|
-
if (process.platform === "darwin") {
|
|
774
|
-
candidates.push("/opt/homebrew/bin/npx", "/usr/local/bin/npx");
|
|
775
|
-
} else if (process.platform === "linux") {
|
|
776
|
-
candidates.push("/usr/local/bin/npx");
|
|
777
|
-
}
|
|
778
|
-
const pathSep = process.platform === "win32" ? ";" : ":";
|
|
779
|
-
for (const dir of (process.env.PATH ?? "").split(pathSep)) {
|
|
780
|
-
if (!dir) continue;
|
|
781
|
-
candidates.push(path4.join(dir, npxName));
|
|
782
|
-
}
|
|
783
|
-
try {
|
|
784
|
-
const lookupCmd = process.platform === "win32" ? "where" : "which";
|
|
785
|
-
const { stdout } = await execFileAsync2(lookupCmd, [npxName], { timeout: 5e3 });
|
|
786
|
-
const first = stdout.trim().split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
787
|
-
if (first) candidates.unshift(first);
|
|
788
|
-
} catch {
|
|
789
|
-
}
|
|
790
|
-
const seen = /* @__PURE__ */ new Set();
|
|
791
|
-
for (const candidate of candidates) {
|
|
792
|
-
const abs = path4.isAbsolute(candidate) ? candidate : path4.resolve(candidate);
|
|
793
|
-
const key = process.platform === "win32" ? abs.toLowerCase() : abs;
|
|
794
|
-
if (seen.has(key)) continue;
|
|
795
|
-
seen.add(key);
|
|
796
|
-
if (await isWorkingNpx(abs)) return abs;
|
|
797
|
-
}
|
|
798
|
-
return null;
|
|
799
|
-
}
|
|
800
|
-
function mergeCursorGlobalMcpConfigObject(existing, npxPath) {
|
|
801
|
-
const base = existing && typeof existing === "object" ? { ...existing } : { mcpServers: {} };
|
|
802
|
-
const mcpServers = typeof base.mcpServers === "object" && base.mcpServers !== null && !Array.isArray(base.mcpServers) ? { ...base.mcpServers } : {};
|
|
803
|
-
mcpServers.memoraone = buildMemoraoneCursorMcpServer(npxPath);
|
|
804
|
-
return { ...base, mcpServers };
|
|
805
|
-
}
|
|
806
|
-
function memoraoneServerMatches(server, npxPath) {
|
|
807
|
-
if (!server || typeof server !== "object") return false;
|
|
808
|
-
const s = server;
|
|
809
|
-
if (s.command !== npxPath) return false;
|
|
810
|
-
if (!Array.isArray(s.args) || s.args.length !== 2) return false;
|
|
811
|
-
if (s.args[0] !== "-y" || s.args[1] !== "@memoraone/mcp@latest") return false;
|
|
812
|
-
const env = s.env;
|
|
813
|
-
if (!env || typeof env !== "object") return false;
|
|
814
|
-
const e = env;
|
|
815
|
-
return e.MEMORAONE_API_URL === "https://api.memoraone.com" && e.MEMORAONE_IDE_TYPE === "cursor";
|
|
816
|
-
}
|
|
817
|
-
function validateCursorGlobalMcpConfig(parsed, npxPath) {
|
|
818
|
-
if (!parsed || typeof parsed !== "object") {
|
|
819
|
-
throw new Error("[setup-ide-files] Cursor global MCP config must be a JSON object.");
|
|
1735
|
+
cleanupLog(opts, `${prefix} pid=${proc.pid} project=${proc.projectId}${ideLabel}`);
|
|
1736
|
+
}
|
|
1737
|
+
} else if (ideSkippedProcesses.length) {
|
|
1738
|
+
cleanupLog(opts, `${prefix} No matching daemon processes for IDE filter ${opts.ide}.`);
|
|
1739
|
+
} else {
|
|
1740
|
+
cleanupLog(opts, `${prefix} No matching daemon processes found.`);
|
|
820
1741
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1742
|
+
if (socketPaths.length) {
|
|
1743
|
+
cleanupLog(opts, `${prefix} Sockets to remove:`);
|
|
1744
|
+
for (const socketPath of socketPaths) {
|
|
1745
|
+
cleanupLog(opts, `${prefix} ${socketPath}`);
|
|
1746
|
+
}
|
|
1747
|
+
} else {
|
|
1748
|
+
cleanupLog(opts, `${prefix} No matching sockets found under ${getMcpBaseDir()}.`);
|
|
824
1749
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1750
|
+
if (skippedProcesses.length) {
|
|
1751
|
+
cleanupLog(opts, `${prefix} Skipped unrelated daemon processes:`);
|
|
1752
|
+
for (const proc of skippedProcesses) {
|
|
1753
|
+
cleanupLog(opts, `${prefix} pid=${proc.pid} project=${proc.projectId}`);
|
|
1754
|
+
}
|
|
830
1755
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1756
|
+
if (opts.allProjects && !opts.dryRun) {
|
|
1757
|
+
const ok = opts.assumeYes ? true : await confirm(`${prefix} Proceed with cleanup for ALL projects?`);
|
|
1758
|
+
if (!ok) {
|
|
1759
|
+
cleanupLog(opts, `${prefix} Aborted.`);
|
|
1760
|
+
return {
|
|
1761
|
+
exitCode: 1,
|
|
1762
|
+
workspaceRoot,
|
|
1763
|
+
m1Path,
|
|
1764
|
+
projectId: targetProjectId ?? void 0,
|
|
1765
|
+
killedPids: [],
|
|
1766
|
+
removedSockets: [],
|
|
1767
|
+
skippedProcesses: [...skippedProcesses, ...ideSkippedProcesses],
|
|
1768
|
+
error: opts.assumeYes ? void 0 : "Aborted (--all requires --yes in non-interactive mode)"
|
|
1769
|
+
};
|
|
844
1770
|
}
|
|
845
1771
|
}
|
|
846
|
-
const
|
|
847
|
-
const
|
|
848
|
-
if (
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1772
|
+
const killedPids = [];
|
|
1773
|
+
const removedSockets = [];
|
|
1774
|
+
if (opts.dryRun) {
|
|
1775
|
+
cleanupLog(opts, `${prefix} Dry run complete \u2014 no processes stopped, no sockets removed.`);
|
|
1776
|
+
return {
|
|
1777
|
+
exitCode: 0,
|
|
1778
|
+
workspaceRoot,
|
|
1779
|
+
m1Path,
|
|
1780
|
+
projectId: targetProjectId ?? void 0,
|
|
1781
|
+
killedPids: processesToStop.map((p) => p.pid),
|
|
1782
|
+
removedSockets: socketPaths,
|
|
1783
|
+
skippedProcesses: [...skippedProcesses, ...ideSkippedProcesses]
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
for (const proc of processesToStop) {
|
|
1787
|
+
try {
|
|
1788
|
+
await killProcess(proc.pid);
|
|
1789
|
+
killedPids.push(proc.pid);
|
|
1790
|
+
cleanupLog(opts, `${prefix} Stopped daemon pid=${proc.pid} project=${proc.projectId}`);
|
|
1791
|
+
} catch (err) {
|
|
1792
|
+
cleanupWarn(opts, `${prefix} Could not stop pid=${proc.pid}: ${String(err)}`);
|
|
852
1793
|
}
|
|
853
1794
|
}
|
|
854
|
-
if (
|
|
855
|
-
|
|
1795
|
+
if (killedPids.length) {
|
|
1796
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
856
1797
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
}
|
|
869
|
-
function logCursorGlobalMcpCliSummary(info, dryRun) {
|
|
870
|
-
const { configPath, npxPath, backupPath, outcome } = info;
|
|
871
|
-
console.log(`[setup-ide-files] Cursor global MCP config: ${configPath}`);
|
|
872
|
-
console.log(`[setup-ide-files] Resolved npx: ${npxPath}`);
|
|
873
|
-
if (backupPath) {
|
|
874
|
-
console.log(`[setup-ide-files] Cursor global MCP config backup: ${backupPath}`);
|
|
1798
|
+
for (const socketPath of socketPaths) {
|
|
1799
|
+
try {
|
|
1800
|
+
await removeSocket(socketPath);
|
|
1801
|
+
removedSockets.push(socketPath);
|
|
1802
|
+
cleanupLog(opts, `${prefix} Removed socket ${socketPath}`);
|
|
1803
|
+
} catch (err) {
|
|
1804
|
+
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
1805
|
+
if (code !== "ENOENT") {
|
|
1806
|
+
cleanupWarn(opts, `${prefix} Could not remove socket ${socketPath}: ${String(err)}`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
875
1809
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1810
|
+
cleanupLog(opts, `${prefix} Done. stopped=${killedPids.length} socketsRemoved=${removedSockets.length}`);
|
|
1811
|
+
return {
|
|
1812
|
+
exitCode: 0,
|
|
1813
|
+
workspaceRoot,
|
|
1814
|
+
m1Path,
|
|
1815
|
+
projectId: targetProjectId ?? void 0,
|
|
1816
|
+
killedPids,
|
|
1817
|
+
removedSockets,
|
|
1818
|
+
skippedProcesses: [...skippedProcesses, ...ideSkippedProcesses]
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
function parseCleanupFlags(argv) {
|
|
1822
|
+
let dryRun = false;
|
|
1823
|
+
let allProjects = false;
|
|
1824
|
+
let assumeYes = false;
|
|
1825
|
+
let projectId;
|
|
1826
|
+
let ide;
|
|
1827
|
+
let invalidIde;
|
|
1828
|
+
const unknown = [];
|
|
1829
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1830
|
+
const arg = argv[i];
|
|
1831
|
+
if (arg === "--dry-run") dryRun = true;
|
|
1832
|
+
else if (arg === "--all-projects" || arg === "--all") allProjects = true;
|
|
1833
|
+
else if (arg === "--yes" || arg === "-y") assumeYes = true;
|
|
1834
|
+
else if (arg === "--project-id") {
|
|
1835
|
+
if (i + 1 >= argv.length) {
|
|
1836
|
+
unknown.push("--project-id (missing value)");
|
|
1837
|
+
} else {
|
|
1838
|
+
projectId = argv[++i];
|
|
1839
|
+
}
|
|
1840
|
+
} else if (arg === "--ide") {
|
|
1841
|
+
if (i + 1 >= argv.length) {
|
|
1842
|
+
unknown.push("--ide (missing value)");
|
|
1843
|
+
} else {
|
|
1844
|
+
const value = argv[++i];
|
|
1845
|
+
if (IDE_TYPES.includes(value)) {
|
|
1846
|
+
ide = value;
|
|
1847
|
+
} else {
|
|
1848
|
+
invalidIde = value;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
} else if (arg.startsWith("-")) unknown.push(arg);
|
|
1852
|
+
else unknown.push(arg);
|
|
1853
|
+
}
|
|
1854
|
+
return { dryRun, allProjects, assumeYes, projectId, ide, invalidIde, unknown };
|
|
1855
|
+
}
|
|
1856
|
+
async function cliCleanup(argv) {
|
|
1857
|
+
const { dryRun, allProjects, assumeYes, projectId, ide, invalidIde, unknown } = parseCleanupFlags(argv);
|
|
1858
|
+
if (invalidIde) {
|
|
1859
|
+
console.error(
|
|
1860
|
+
`[cleanup] Invalid --ide value: ${invalidIde}. Expected one of: ${IDE_TYPES.join(", ")}.`
|
|
883
1861
|
);
|
|
884
|
-
|
|
885
|
-
console.log(`[setup-ide-files] Cursor global MCP config unchanged: ${configPath}`);
|
|
1862
|
+
return 1;
|
|
886
1863
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1864
|
+
if (unknown.length) {
|
|
1865
|
+
console.error(`[cleanup] Unknown option(s): ${unknown.join(", ")}`);
|
|
1866
|
+
return 1;
|
|
1867
|
+
}
|
|
1868
|
+
const result = await runCleanup({
|
|
1869
|
+
cwd: process.cwd(),
|
|
1870
|
+
dryRun,
|
|
1871
|
+
allProjects,
|
|
1872
|
+
assumeYes,
|
|
1873
|
+
projectId,
|
|
1874
|
+
ide
|
|
1875
|
+
});
|
|
1876
|
+
if (result.error) {
|
|
1877
|
+
console.error(`[cleanup] ${result.error}`);
|
|
1878
|
+
}
|
|
1879
|
+
return result.exitCode;
|
|
893
1880
|
}
|
|
894
1881
|
|
|
895
1882
|
// src/jetbrainsMcpConfig.ts
|
|
896
|
-
var
|
|
1883
|
+
var fs6 = __toESM(require("fs/promises"), 1);
|
|
897
1884
|
var os3 = __toESM(require("os"), 1);
|
|
898
|
-
var
|
|
899
|
-
var
|
|
1885
|
+
var path8 = __toESM(require("path"), 1);
|
|
1886
|
+
var import_node_child_process4 = require("child_process");
|
|
900
1887
|
|
|
901
1888
|
// src/configUtils.ts
|
|
902
1889
|
var DEV_API_URL = "http://localhost:3001";
|
|
@@ -914,7 +1901,7 @@ function stripLeadingLineComments2(text) {
|
|
|
914
1901
|
}
|
|
915
1902
|
async function pathExists2(filePath) {
|
|
916
1903
|
try {
|
|
917
|
-
await
|
|
1904
|
+
await fs6.access(filePath);
|
|
918
1905
|
return true;
|
|
919
1906
|
} catch {
|
|
920
1907
|
return false;
|
|
@@ -925,12 +1912,12 @@ function formatJetBrainsBackupTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
|
925
1912
|
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
926
1913
|
}
|
|
927
1914
|
function getJetBrainsGlobalMcpConfigPath(homeDir) {
|
|
928
|
-
return
|
|
1915
|
+
return path8.join(homeDir, ".ai", "mcp", "mcp.json");
|
|
929
1916
|
}
|
|
930
1917
|
function getJetBrainsProjectMcpConfigPaths(repoRoot) {
|
|
931
1918
|
return [
|
|
932
|
-
{ kind: "project-ai", path:
|
|
933
|
-
{ kind: "project-ij", path:
|
|
1919
|
+
{ kind: "project-ai", path: path8.join(repoRoot, ".ai", "mcp", "mcp.json") },
|
|
1920
|
+
{ kind: "project-ij", path: path8.join(repoRoot, ".ij", "mcp", "mcp.json") }
|
|
934
1921
|
];
|
|
935
1922
|
}
|
|
936
1923
|
function getKnownJetBrainsMcpConfigLocations(homeDir, repoRoot) {
|
|
@@ -941,7 +1928,7 @@ function getKnownJetBrainsMcpConfigLocations(homeDir, repoRoot) {
|
|
|
941
1928
|
}
|
|
942
1929
|
async function isZeroByteConfigFile(filePath) {
|
|
943
1930
|
if (!await pathExists2(filePath)) return false;
|
|
944
|
-
const stat2 = await
|
|
1931
|
+
const stat2 = await fs6.stat(filePath);
|
|
945
1932
|
return stat2.size === 0;
|
|
946
1933
|
}
|
|
947
1934
|
function buildMemoraoneJetBrainsMcpServer(options) {
|
|
@@ -965,7 +1952,7 @@ function mergeJetBrainsMcpConfigObject(existing, memoraone) {
|
|
|
965
1952
|
mcpServers.memoraone = memoraone;
|
|
966
1953
|
return { ...base, mcpServers };
|
|
967
1954
|
}
|
|
968
|
-
function
|
|
1955
|
+
function memoraoneServerMatches(server, expected) {
|
|
969
1956
|
if (!server || typeof server !== "object") return false;
|
|
970
1957
|
const s = server;
|
|
971
1958
|
if (s.command !== expected.command) return false;
|
|
@@ -993,20 +1980,20 @@ function validateJetBrainsMcpConfig(parsed, expected) {
|
|
|
993
1980
|
throw new Error("[setup-ide-files] JetBrains MCP config missing mcpServers object.");
|
|
994
1981
|
}
|
|
995
1982
|
const memoraone = mcpServers.memoraone;
|
|
996
|
-
if (!
|
|
1983
|
+
if (!memoraoneServerMatches(memoraone, expected)) {
|
|
997
1984
|
throw new Error(
|
|
998
1985
|
"[setup-ide-files] JetBrains MCP config mcpServers.memoraone is missing or invalid."
|
|
999
1986
|
);
|
|
1000
1987
|
}
|
|
1001
1988
|
}
|
|
1002
1989
|
async function readJsonConfig(filePath) {
|
|
1003
|
-
const raw = await
|
|
1990
|
+
const raw = await fs6.readFile(filePath, "utf8");
|
|
1004
1991
|
if (raw.trim() === "") return null;
|
|
1005
1992
|
return JSON.parse(stripLeadingLineComments2(raw));
|
|
1006
1993
|
}
|
|
1007
1994
|
async function backupConfigFile(filePath) {
|
|
1008
1995
|
const backupPath = `${filePath}.bak-${formatJetBrainsBackupTimestamp()}`;
|
|
1009
|
-
await
|
|
1996
|
+
await fs6.copyFile(filePath, backupPath);
|
|
1010
1997
|
return backupPath;
|
|
1011
1998
|
}
|
|
1012
1999
|
async function repairZeroByteConfigFile(filePath, dryRun) {
|
|
@@ -1017,7 +2004,7 @@ async function repairZeroByteConfigFile(filePath, dryRun) {
|
|
|
1017
2004
|
return { repaired: true, backupPath: `${filePath}.bak-<timestamp>` };
|
|
1018
2005
|
}
|
|
1019
2006
|
const backupPath = await backupConfigFile(filePath);
|
|
1020
|
-
await
|
|
2007
|
+
await fs6.unlink(filePath);
|
|
1021
2008
|
return { repaired: true, backupPath };
|
|
1022
2009
|
}
|
|
1023
2010
|
function configHasMemoraone(parsed) {
|
|
@@ -1048,24 +2035,24 @@ async function removeMemoraoneFromProjectConfig(options) {
|
|
|
1048
2035
|
delete mcpServers.memoraone;
|
|
1049
2036
|
const hasOtherServers = Object.keys(mcpServers).length > 0;
|
|
1050
2037
|
if (!hasOtherServers) {
|
|
1051
|
-
await
|
|
2038
|
+
await fs6.unlink(configPath);
|
|
1052
2039
|
return { changed: true, backupPath };
|
|
1053
2040
|
}
|
|
1054
2041
|
const next = { ...parsed, mcpServers };
|
|
1055
|
-
await
|
|
1056
|
-
await
|
|
2042
|
+
await fs6.mkdir(path8.dirname(configPath), { recursive: true });
|
|
2043
|
+
await fs6.writeFile(configPath, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
1057
2044
|
return { changed: true, backupPath };
|
|
1058
2045
|
}
|
|
1059
2046
|
async function resolveLocalCliPathAsync() {
|
|
1060
|
-
const here = process.argv[1] ?
|
|
2047
|
+
const here = process.argv[1] ? path8.dirname(path8.resolve(process.argv[1])) : process.cwd();
|
|
1061
2048
|
const candidates = [
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
2049
|
+
path8.join(here, "cli.cjs"),
|
|
2050
|
+
path8.join(here, "..", "dist", "cli.cjs"),
|
|
2051
|
+
path8.join(here, "..", "..", "dist", "cli.cjs")
|
|
1065
2052
|
];
|
|
1066
2053
|
for (const candidate of candidates) {
|
|
1067
2054
|
if (await pathExists2(candidate)) {
|
|
1068
|
-
return
|
|
2055
|
+
return path8.resolve(candidate);
|
|
1069
2056
|
}
|
|
1070
2057
|
}
|
|
1071
2058
|
return null;
|
|
@@ -1107,7 +2094,7 @@ async function buildJetBrainsMemoraoneServer(options) {
|
|
|
1107
2094
|
async function verifyJetBrainsMcpHandshake(options) {
|
|
1108
2095
|
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
1109
2096
|
const { server } = options;
|
|
1110
|
-
return new Promise((
|
|
2097
|
+
return new Promise((resolve8) => {
|
|
1111
2098
|
let settled = false;
|
|
1112
2099
|
const finish = (ok, detail) => {
|
|
1113
2100
|
if (settled) return;
|
|
@@ -1117,9 +2104,9 @@ async function verifyJetBrainsMcpHandshake(options) {
|
|
|
1117
2104
|
child.kill();
|
|
1118
2105
|
} catch {
|
|
1119
2106
|
}
|
|
1120
|
-
|
|
2107
|
+
resolve8({ ok, detail });
|
|
1121
2108
|
};
|
|
1122
|
-
const child = (0,
|
|
2109
|
+
const child = (0, import_node_child_process4.spawn)(server.command, [...server.args], {
|
|
1123
2110
|
env: { ...process.env, ...server.env },
|
|
1124
2111
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1125
2112
|
});
|
|
@@ -1186,7 +2173,7 @@ async function verifyJetBrainsMcpHandshake(options) {
|
|
|
1186
2173
|
async function setupJetBrainsMcpConfig(options) {
|
|
1187
2174
|
const homeDir = options.homeDir ?? os3.homedir();
|
|
1188
2175
|
const globalPath = options.globalConfigPath ?? getJetBrainsGlobalMcpConfigPath(homeDir);
|
|
1189
|
-
const m1Path =
|
|
2176
|
+
const m1Path = path8.join(path8.resolve(options.repoRoot), "memoraone.m1");
|
|
1190
2177
|
const repairActions = [];
|
|
1191
2178
|
const allLocations = getKnownJetBrainsMcpConfigLocations(homeDir, options.repoRoot);
|
|
1192
2179
|
for (const location of allLocations) {
|
|
@@ -1241,7 +2228,7 @@ async function setupJetBrainsMcpConfig(options) {
|
|
|
1241
2228
|
path: globalPath,
|
|
1242
2229
|
backupPath: backupPath2
|
|
1243
2230
|
});
|
|
1244
|
-
await
|
|
2231
|
+
await fs6.unlink(globalPath);
|
|
1245
2232
|
existing = null;
|
|
1246
2233
|
}
|
|
1247
2234
|
}
|
|
@@ -1250,7 +2237,7 @@ async function setupJetBrainsMcpConfig(options) {
|
|
|
1250
2237
|
const body = JSON.stringify(merged, null, 2) + "\n";
|
|
1251
2238
|
if (existed && existing) {
|
|
1252
2239
|
const currentMemoraone = existing.mcpServers && typeof existing.mcpServers === "object" && !Array.isArray(existing.mcpServers) ? existing.mcpServers.memoraone : void 0;
|
|
1253
|
-
if (
|
|
2240
|
+
if (memoraoneServerMatches(currentMemoraone, memoraone)) {
|
|
1254
2241
|
repairActions.push({ type: "wrote-global-config", path: globalPath, outcome: "skipped" });
|
|
1255
2242
|
return { outcome: "skipped", repairActions, memoraone };
|
|
1256
2243
|
}
|
|
@@ -1264,9 +2251,9 @@ async function setupJetBrainsMcpConfig(options) {
|
|
|
1264
2251
|
if (existed) {
|
|
1265
2252
|
backupPath = await backupConfigFile(globalPath);
|
|
1266
2253
|
}
|
|
1267
|
-
await
|
|
1268
|
-
await
|
|
1269
|
-
const verifyRaw = await
|
|
2254
|
+
await fs6.mkdir(path8.dirname(globalPath), { recursive: true });
|
|
2255
|
+
await fs6.writeFile(globalPath, body, "utf8");
|
|
2256
|
+
const verifyRaw = await fs6.readFile(globalPath, "utf8");
|
|
1270
2257
|
const verifyParsed = JSON.parse(stripLeadingLineComments2(verifyRaw));
|
|
1271
2258
|
validateJetBrainsMcpConfig(verifyParsed, memoraone);
|
|
1272
2259
|
const outcome = existed ? "updated" : "created";
|
|
@@ -1336,15 +2323,15 @@ function buildMemoraoneMcpServer(ideType, command = "npx") {
|
|
|
1336
2323
|
};
|
|
1337
2324
|
}
|
|
1338
2325
|
function assertUnderRepoRoot(repoRoot, absPath) {
|
|
1339
|
-
const normRoot =
|
|
1340
|
-
const normPath =
|
|
1341
|
-
if (normPath !==
|
|
2326
|
+
const normRoot = path9.resolve(repoRoot) + path9.sep;
|
|
2327
|
+
const normPath = path9.resolve(absPath);
|
|
2328
|
+
if (normPath !== path9.resolve(repoRoot) && !normPath.startsWith(normRoot)) {
|
|
1342
2329
|
throw new Error(`[setup-ide-files] Refusing to write outside repo root: ${absPath}`);
|
|
1343
2330
|
}
|
|
1344
2331
|
}
|
|
1345
2332
|
async function pathExists3(filePath) {
|
|
1346
2333
|
try {
|
|
1347
|
-
await
|
|
2334
|
+
await fs7.access(filePath);
|
|
1348
2335
|
return true;
|
|
1349
2336
|
} catch {
|
|
1350
2337
|
return false;
|
|
@@ -1365,12 +2352,12 @@ ${GITIGNORE_MEMORAONE_ENTRY}
|
|
|
1365
2352
|
}
|
|
1366
2353
|
async function ensureGitignoreMemoraone(repoRoot, opts) {
|
|
1367
2354
|
if (opts.noGitignore) return "skipped";
|
|
1368
|
-
const abs =
|
|
2355
|
+
const abs = path9.join(repoRoot, ".gitignore");
|
|
1369
2356
|
assertUnderRepoRoot(repoRoot, abs);
|
|
1370
2357
|
let prior = "";
|
|
1371
2358
|
let existed = false;
|
|
1372
2359
|
try {
|
|
1373
|
-
prior = await
|
|
2360
|
+
prior = await fs7.readFile(abs, "utf8");
|
|
1374
2361
|
existed = true;
|
|
1375
2362
|
} catch (err) {
|
|
1376
2363
|
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
@@ -1381,22 +2368,22 @@ async function ensureGitignoreMemoraone(repoRoot, opts) {
|
|
|
1381
2368
|
const separator = existed && prior.length > 0 ? prior.endsWith("\n") ? "\n" : "\n\n" : "";
|
|
1382
2369
|
const next = (existed ? prior : "") + separator + block;
|
|
1383
2370
|
if (opts.dryRun) return existed ? "updated" : "created";
|
|
1384
|
-
await
|
|
2371
|
+
await fs7.writeFile(abs, next, "utf8");
|
|
1385
2372
|
return existed ? "updated" : "created";
|
|
1386
2373
|
}
|
|
1387
2374
|
async function findRepoRoot(startDir) {
|
|
1388
|
-
let current =
|
|
1389
|
-
const root =
|
|
2375
|
+
let current = path9.resolve(startDir);
|
|
2376
|
+
const root = path9.parse(current).root;
|
|
1390
2377
|
while (true) {
|
|
1391
|
-
const gitPath =
|
|
1392
|
-
const m1Path =
|
|
2378
|
+
const gitPath = path9.join(current, ".git");
|
|
2379
|
+
const m1Path = path9.join(current, "memoraone.m1");
|
|
1393
2380
|
if (await pathExists3(gitPath) || await pathExists3(m1Path)) {
|
|
1394
2381
|
return current;
|
|
1395
2382
|
}
|
|
1396
2383
|
if (current === root) {
|
|
1397
2384
|
return null;
|
|
1398
2385
|
}
|
|
1399
|
-
current =
|
|
2386
|
+
current = path9.dirname(current);
|
|
1400
2387
|
}
|
|
1401
2388
|
}
|
|
1402
2389
|
function stripLeadingLineComments3(text) {
|
|
@@ -1407,7 +2394,7 @@ function cursorRuleBody() {
|
|
|
1407
2394
|
|
|
1408
2395
|
## MemoraOne MCP (IDE agent only)
|
|
1409
2396
|
|
|
1410
|
-
This repository uses **MemoraOne** via the MCP server named **
|
|
2397
|
+
This repository uses **MemoraOne** via the MCP server named **memoraone** (repo-level .cursor/mcp.json). This guidance applies to the **IDE coding agent** only \u2014 not the MemoraOne Studio runtime path.
|
|
1411
2398
|
|
|
1412
2399
|
### Tools
|
|
1413
2400
|
|
|
@@ -1449,43 +2436,47 @@ function buildVscodeMcpJsonBody(existing) {
|
|
|
1449
2436
|
const merged = { ...base, servers };
|
|
1450
2437
|
return mcpJsonHeader() + JSON.stringify(merged, null, 2) + "\n";
|
|
1451
2438
|
}
|
|
2439
|
+
function buildCursorMcpJsonBody(existing, npxPath, repoRoot) {
|
|
2440
|
+
const merged = mergeCursorRepoMcpConfigObject(existing, npxPath, repoRoot);
|
|
2441
|
+
return mcpJsonHeader() + JSON.stringify(merged, null, 2) + "\n";
|
|
2442
|
+
}
|
|
1452
2443
|
async function writeManagedMarkdown(repoRoot, relPath, fullContent, opts) {
|
|
1453
|
-
const abs =
|
|
2444
|
+
const abs = path9.join(repoRoot, relPath);
|
|
1454
2445
|
assertUnderRepoRoot(repoRoot, abs);
|
|
1455
2446
|
let prior = "";
|
|
1456
2447
|
let existed = false;
|
|
1457
2448
|
try {
|
|
1458
|
-
prior = await
|
|
2449
|
+
prior = await fs7.readFile(abs, "utf8");
|
|
1459
2450
|
existed = true;
|
|
1460
2451
|
} catch (err) {
|
|
1461
2452
|
if (err?.code !== "ENOENT") throw err;
|
|
1462
2453
|
}
|
|
1463
2454
|
if (!existed) {
|
|
1464
2455
|
if (opts.dryRun) return "created";
|
|
1465
|
-
await
|
|
1466
|
-
await
|
|
2456
|
+
await fs7.mkdir(path9.dirname(abs), { recursive: true });
|
|
2457
|
+
await fs7.writeFile(abs, fullContent, "utf8");
|
|
1467
2458
|
return "created";
|
|
1468
2459
|
}
|
|
1469
2460
|
if (prior.includes(MANAGED_MARKER)) {
|
|
1470
2461
|
if (prior === fullContent) return "skipped";
|
|
1471
2462
|
if (opts.dryRun) return "updated";
|
|
1472
|
-
await
|
|
1473
|
-
await
|
|
2463
|
+
await fs7.mkdir(path9.dirname(abs), { recursive: true });
|
|
2464
|
+
await fs7.writeFile(abs, fullContent, "utf8");
|
|
1474
2465
|
return "updated";
|
|
1475
2466
|
}
|
|
1476
2467
|
if (!opts.force) return "skipped-untracked";
|
|
1477
2468
|
if (opts.dryRun) return "updated";
|
|
1478
|
-
await
|
|
1479
|
-
await
|
|
2469
|
+
await fs7.mkdir(path9.dirname(abs), { recursive: true });
|
|
2470
|
+
await fs7.writeFile(abs, fullContent, "utf8");
|
|
1480
2471
|
return "updated";
|
|
1481
2472
|
}
|
|
1482
2473
|
async function writeIdeMcpJson(repoRoot, relPath, buildBody, opts) {
|
|
1483
|
-
const abs =
|
|
2474
|
+
const abs = path9.join(repoRoot, relPath);
|
|
1484
2475
|
assertUnderRepoRoot(repoRoot, abs);
|
|
1485
2476
|
let raw = "";
|
|
1486
2477
|
let existed = false;
|
|
1487
2478
|
try {
|
|
1488
|
-
raw = await
|
|
2479
|
+
raw = await fs7.readFile(abs, "utf8");
|
|
1489
2480
|
existed = true;
|
|
1490
2481
|
} catch (err) {
|
|
1491
2482
|
if (err?.code !== "ENOENT") throw err;
|
|
@@ -1493,8 +2484,8 @@ async function writeIdeMcpJson(repoRoot, relPath, buildBody, opts) {
|
|
|
1493
2484
|
if (!existed) {
|
|
1494
2485
|
const body = buildBody(null);
|
|
1495
2486
|
if (opts.dryRun) return "created";
|
|
1496
|
-
await
|
|
1497
|
-
await
|
|
2487
|
+
await fs7.mkdir(path9.dirname(abs), { recursive: true });
|
|
2488
|
+
await fs7.writeFile(abs, body, "utf8");
|
|
1498
2489
|
return "created";
|
|
1499
2490
|
}
|
|
1500
2491
|
const managed = raw.includes(MANAGED_MARKER);
|
|
@@ -1509,8 +2500,8 @@ async function writeIdeMcpJson(repoRoot, relPath, buildBody, opts) {
|
|
|
1509
2500
|
const next = buildBody(parsed);
|
|
1510
2501
|
if (managed && next === raw) return "skipped";
|
|
1511
2502
|
if (opts.dryRun) return "updated";
|
|
1512
|
-
await
|
|
1513
|
-
await
|
|
2503
|
+
await fs7.mkdir(path9.dirname(abs), { recursive: true });
|
|
2504
|
+
await fs7.writeFile(abs, next, "utf8");
|
|
1514
2505
|
return "updated";
|
|
1515
2506
|
}
|
|
1516
2507
|
function parseSetupIdeFlags(argv) {
|
|
@@ -1567,9 +2558,144 @@ function summarizeOutcomes(outcomes) {
|
|
|
1567
2558
|
}
|
|
1568
2559
|
console.log(lines.join("\n"));
|
|
1569
2560
|
}
|
|
2561
|
+
function ideTypesFromSetupTargets(targets) {
|
|
2562
|
+
const ides = [];
|
|
2563
|
+
if (targets.cursor) ides.push("cursor");
|
|
2564
|
+
if (targets.vscode) ides.push("copilot-vscode");
|
|
2565
|
+
if (targets.jetbrains) ides.push("jetbrains");
|
|
2566
|
+
return ides;
|
|
2567
|
+
}
|
|
2568
|
+
function setupTargetsAllIdes(targets) {
|
|
2569
|
+
return targets.cursor && targets.vscode && targets.jetbrains;
|
|
2570
|
+
}
|
|
2571
|
+
function aggregateCleanupResults(results) {
|
|
2572
|
+
const killedPids = /* @__PURE__ */ new Set();
|
|
2573
|
+
const removedSockets = /* @__PURE__ */ new Set();
|
|
2574
|
+
const skippedUnrelated = /* @__PURE__ */ new Map();
|
|
2575
|
+
let foundDaemonCount = 0;
|
|
2576
|
+
let error;
|
|
2577
|
+
for (const result of results) {
|
|
2578
|
+
if (result.error) error = result.error;
|
|
2579
|
+
for (const pid of result.killedPids) {
|
|
2580
|
+
killedPids.add(pid);
|
|
2581
|
+
foundDaemonCount += 1;
|
|
2582
|
+
}
|
|
2583
|
+
for (const socketPath of result.removedSockets) {
|
|
2584
|
+
removedSockets.add(socketPath);
|
|
2585
|
+
}
|
|
2586
|
+
for (const proc of result.skippedProcesses) {
|
|
2587
|
+
if (proc.projectId !== result.projectId) {
|
|
2588
|
+
skippedUnrelated.set(proc.pid, proc);
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
return {
|
|
2593
|
+
foundDaemonCount,
|
|
2594
|
+
stoppedDaemonCount: killedPids.size,
|
|
2595
|
+
removedSocketCount: removedSockets.size,
|
|
2596
|
+
skippedUnrelatedDaemonCount: skippedUnrelated.size,
|
|
2597
|
+
error
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
function logSetupIdeCleanupSummary(cleanup) {
|
|
2601
|
+
if (cleanup.skipped) return;
|
|
2602
|
+
console.log(`[setup-ide-files] Project id: ${cleanup.projectId}`);
|
|
2603
|
+
if (cleanup.foundDaemonCount > 0) {
|
|
2604
|
+
console.log(`[setup-ide-files] Found ${cleanup.foundDaemonCount} stale daemon(s)`);
|
|
2605
|
+
if (cleanup.dryRun) {
|
|
2606
|
+
console.log(`[setup-ide-files] Would stop ${cleanup.foundDaemonCount} stale daemon(s)`);
|
|
2607
|
+
} else if (cleanup.stoppedDaemonCount > 0) {
|
|
2608
|
+
console.log(`[setup-ide-files] Stopped ${cleanup.stoppedDaemonCount} stale daemon(s)`);
|
|
2609
|
+
}
|
|
2610
|
+
} else {
|
|
2611
|
+
console.log("[setup-ide-files] No stale daemons found for this project and IDE target(s).");
|
|
2612
|
+
}
|
|
2613
|
+
if (cleanup.removedSocketCount > 0) {
|
|
2614
|
+
if (cleanup.dryRun) {
|
|
2615
|
+
console.log(`[setup-ide-files] Would remove ${cleanup.removedSocketCount} stale socket(s)`);
|
|
2616
|
+
} else {
|
|
2617
|
+
console.log(`[setup-ide-files] Removed ${cleanup.removedSocketCount} stale socket(s)`);
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
if (cleanup.skippedUnrelatedDaemonCount > 0) {
|
|
2621
|
+
console.log(
|
|
2622
|
+
`[setup-ide-files] Skipped ${cleanup.skippedUnrelatedDaemonCount} unrelated project daemon(s)`
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
async function runSetupIdeDaemonCleanup(opts) {
|
|
2627
|
+
const ides = ideTypesFromSetupTargets(opts.targets);
|
|
2628
|
+
if (ides.length === 0) {
|
|
2629
|
+
return {
|
|
2630
|
+
skipped: true,
|
|
2631
|
+
skipReason: "no-targets",
|
|
2632
|
+
foundDaemonCount: 0,
|
|
2633
|
+
stoppedDaemonCount: 0,
|
|
2634
|
+
removedSocketCount: 0,
|
|
2635
|
+
skippedUnrelatedDaemonCount: 0,
|
|
2636
|
+
dryRun: opts.dryRun
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
const target = await resolveCleanupTarget(opts.repoRoot);
|
|
2640
|
+
if ("error" in target) {
|
|
2641
|
+
return {
|
|
2642
|
+
skipped: true,
|
|
2643
|
+
skipReason: "no-m1",
|
|
2644
|
+
foundDaemonCount: 0,
|
|
2645
|
+
stoppedDaemonCount: 0,
|
|
2646
|
+
removedSocketCount: 0,
|
|
2647
|
+
skippedUnrelatedDaemonCount: 0,
|
|
2648
|
+
dryRun: opts.dryRun
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
const baseCleanupOpts = {
|
|
2652
|
+
cwd: opts.repoRoot,
|
|
2653
|
+
dryRun: opts.dryRun,
|
|
2654
|
+
allProjects: false,
|
|
2655
|
+
assumeYes: true,
|
|
2656
|
+
projectId: target.projectId,
|
|
2657
|
+
quiet: true,
|
|
2658
|
+
listProcesses: opts.listProcesses,
|
|
2659
|
+
listSocketPaths: opts.listSocketPaths,
|
|
2660
|
+
killProcess: opts.killProcess,
|
|
2661
|
+
removeSocket: opts.removeSocket
|
|
2662
|
+
};
|
|
2663
|
+
const results = [];
|
|
2664
|
+
if (setupTargetsAllIdes(opts.targets)) {
|
|
2665
|
+
results.push(await runCleanup(baseCleanupOpts));
|
|
2666
|
+
} else {
|
|
2667
|
+
for (const ide of ides) {
|
|
2668
|
+
results.push(await runCleanup({ ...baseCleanupOpts, ide }));
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
const aggregated = aggregateCleanupResults(results);
|
|
2672
|
+
const exitError = results.find((r) => r.exitCode !== 0)?.error ?? aggregated.error;
|
|
2673
|
+
return {
|
|
2674
|
+
skipped: false,
|
|
2675
|
+
projectId: target.projectId,
|
|
2676
|
+
foundDaemonCount: aggregated.foundDaemonCount,
|
|
2677
|
+
stoppedDaemonCount: opts.dryRun ? 0 : aggregated.stoppedDaemonCount,
|
|
2678
|
+
removedSocketCount: aggregated.removedSocketCount,
|
|
2679
|
+
skippedUnrelatedDaemonCount: aggregated.skippedUnrelatedDaemonCount,
|
|
2680
|
+
dryRun: opts.dryRun,
|
|
2681
|
+
error: exitError
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
function restartIdeInstruction(targets) {
|
|
2685
|
+
const names = [];
|
|
2686
|
+
if (targets.cursor) names.push("Cursor");
|
|
2687
|
+
if (targets.vscode) names.push("VS Code");
|
|
2688
|
+
if (targets.jetbrains) names.push("JetBrains IDE");
|
|
2689
|
+
if (names.length === 0) return "Fully quit your IDE and reopen this repo for MCP changes to take effect.";
|
|
2690
|
+
if (names.length === 1) {
|
|
2691
|
+
return `Fully quit ${names[0]} and reopen this repo for MCP changes to take effect.`;
|
|
2692
|
+
}
|
|
2693
|
+
const last = names.pop();
|
|
2694
|
+
return `Fully quit ${names.join(", ")} and ${last}, then reopen this repo for MCP changes to take effect.`;
|
|
2695
|
+
}
|
|
1570
2696
|
async function runSetupIdeFiles(o) {
|
|
1571
2697
|
const outcomes = {};
|
|
1572
|
-
let
|
|
2698
|
+
let cursorMcp;
|
|
1573
2699
|
let jetbrainsMcp;
|
|
1574
2700
|
const repoRoot = await findRepoRoot(o.cwd);
|
|
1575
2701
|
if (!repoRoot) {
|
|
@@ -1580,6 +2706,27 @@ async function runSetupIdeFiles(o) {
|
|
|
1580
2706
|
error: "[setup-ide-files] No repo root found (looked for .git or memoraone.m1)."
|
|
1581
2707
|
};
|
|
1582
2708
|
}
|
|
2709
|
+
let daemonCleanup;
|
|
2710
|
+
if (!o.skipDaemonCleanup) {
|
|
2711
|
+
daemonCleanup = await runSetupIdeDaemonCleanup({
|
|
2712
|
+
repoRoot,
|
|
2713
|
+
targets: o.targets,
|
|
2714
|
+
dryRun: o.dryRun,
|
|
2715
|
+
listProcesses: o.listDaemonProcesses,
|
|
2716
|
+
listSocketPaths: o.listCleanupSocketPaths,
|
|
2717
|
+
killProcess: o.killDaemonProcess,
|
|
2718
|
+
removeSocket: o.removeCleanupSocket
|
|
2719
|
+
});
|
|
2720
|
+
if (daemonCleanup.error && !daemonCleanup.skipped) {
|
|
2721
|
+
return {
|
|
2722
|
+
exitCode: 1,
|
|
2723
|
+
repoRoot,
|
|
2724
|
+
outcomes,
|
|
2725
|
+
daemonCleanup,
|
|
2726
|
+
error: `[setup-ide-files] Daemon cleanup failed: ${daemonCleanup.error}`
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
1583
2730
|
outcomes[".gitignore"] = await ensureGitignoreMemoraone(repoRoot, {
|
|
1584
2731
|
dryRun: o.dryRun,
|
|
1585
2732
|
noGitignore: o.noGitignore ?? false
|
|
@@ -1590,19 +2737,6 @@ description: MemoraOne MCP \u2014 IDE agent instructions
|
|
|
1590
2737
|
|
|
1591
2738
|
` + cursorRuleBody();
|
|
1592
2739
|
if (o.targets.cursor) {
|
|
1593
|
-
const detection = await detectCursorGlobalMcpConfig({
|
|
1594
|
-
homeDir: o.homeDir,
|
|
1595
|
-
explicitPath: o.cursorGlobalMcpConfigPath
|
|
1596
|
-
});
|
|
1597
|
-
if (!detection.ok) {
|
|
1598
|
-
return {
|
|
1599
|
-
exitCode: 1,
|
|
1600
|
-
repoRoot,
|
|
1601
|
-
outcomes,
|
|
1602
|
-
error: `${detection.error}
|
|
1603
|
-
${detection.candidates.join("\n ")}`
|
|
1604
|
-
};
|
|
1605
|
-
}
|
|
1606
2740
|
let npxPath;
|
|
1607
2741
|
if (o.npxPathOverride !== void 0) {
|
|
1608
2742
|
npxPath = o.npxPathOverride;
|
|
@@ -1614,7 +2748,7 @@ description: MemoraOne MCP \u2014 IDE agent instructions
|
|
|
1614
2748
|
exitCode: 1,
|
|
1615
2749
|
repoRoot,
|
|
1616
2750
|
outcomes,
|
|
1617
|
-
error: "[setup-ide-files] Could not resolve a working npx executable. Install Node.js/npm or ensure npx is on PATH before configuring Cursor
|
|
2751
|
+
error: "[setup-ide-files] Could not resolve a working npx executable. Install Node.js/npm or ensure npx is on PATH before configuring Cursor MCP."
|
|
1618
2752
|
};
|
|
1619
2753
|
}
|
|
1620
2754
|
outcomes[".cursor/rules/memoraone-mcp.mdc"] = await writeManagedMarkdown(
|
|
@@ -1623,29 +2757,58 @@ description: MemoraOne MCP \u2014 IDE agent instructions
|
|
|
1623
2757
|
cursorContent,
|
|
1624
2758
|
{ force: o.force, dryRun: o.dryRun }
|
|
1625
2759
|
);
|
|
2760
|
+
outcomes[".cursor/mcp.json"] = await writeIdeMcpJson(
|
|
2761
|
+
repoRoot,
|
|
2762
|
+
".cursor/mcp.json",
|
|
2763
|
+
(existing) => buildCursorMcpJsonBody(existing, npxPath, repoRoot),
|
|
2764
|
+
{ force: o.force, dryRun: o.dryRun }
|
|
2765
|
+
);
|
|
2766
|
+
const repoConfigPath = getCursorRepoMcpConfigPath(repoRoot);
|
|
2767
|
+
const repoOutcome = outcomes[".cursor/mcp.json"] ?? "skipped";
|
|
2768
|
+
let globalConfigPath;
|
|
2769
|
+
let globalMemoraoneRemoved = false;
|
|
2770
|
+
let globalBackupPath;
|
|
2771
|
+
const globalDetection = await detectCursorGlobalMcpConfig({
|
|
2772
|
+
homeDir: o.homeDir,
|
|
2773
|
+
explicitPath: o.cursorGlobalMcpConfigPath
|
|
2774
|
+
});
|
|
2775
|
+
if (!globalDetection.ok) {
|
|
2776
|
+
return {
|
|
2777
|
+
exitCode: 1,
|
|
2778
|
+
repoRoot,
|
|
2779
|
+
outcomes,
|
|
2780
|
+
error: `${globalDetection.error}
|
|
2781
|
+
${globalDetection.candidates.join("\n ")}`
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
globalConfigPath = globalDetection.path;
|
|
1626
2785
|
try {
|
|
1627
|
-
const
|
|
1628
|
-
configPath:
|
|
1629
|
-
npxPath,
|
|
2786
|
+
const removal = await removeMemoraoneFromCursorGlobalConfig({
|
|
2787
|
+
configPath: globalDetection.path,
|
|
1630
2788
|
dryRun: o.dryRun
|
|
1631
2789
|
});
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
};
|
|
1638
|
-
outcomes[`cursor-global:${detection.path}`] = globalSetup.outcome;
|
|
2790
|
+
if (removal.changed) {
|
|
2791
|
+
globalMemoraoneRemoved = true;
|
|
2792
|
+
globalBackupPath = removal.backupPath;
|
|
2793
|
+
outcomes[`cursor-global-removed:${globalDetection.path}`] = o.dryRun ? "updated" : "updated";
|
|
2794
|
+
}
|
|
1639
2795
|
} catch (err) {
|
|
1640
2796
|
const message = err instanceof Error ? err.message : String(err);
|
|
1641
2797
|
return {
|
|
1642
2798
|
exitCode: 1,
|
|
1643
2799
|
repoRoot,
|
|
1644
2800
|
outcomes,
|
|
1645
|
-
cursorGlobalMcp: { configPath: detection.path, outcome: "skipped", npxPath },
|
|
1646
2801
|
error: message
|
|
1647
2802
|
};
|
|
1648
2803
|
}
|
|
2804
|
+
cursorMcp = {
|
|
2805
|
+
repoConfigPath,
|
|
2806
|
+
repoOutcome,
|
|
2807
|
+
npxPath,
|
|
2808
|
+
globalConfigPath,
|
|
2809
|
+
globalMemoraoneRemoved,
|
|
2810
|
+
globalBackupPath
|
|
2811
|
+
};
|
|
1649
2812
|
}
|
|
1650
2813
|
if (o.targets.vscode) {
|
|
1651
2814
|
outcomes[".vscode/mcp.json"] = await writeIdeMcpJson(
|
|
@@ -1701,12 +2864,12 @@ description: MemoraOne MCP \u2014 IDE agent instructions
|
|
|
1701
2864
|
exitCode: 1,
|
|
1702
2865
|
repoRoot,
|
|
1703
2866
|
outcomes,
|
|
1704
|
-
|
|
2867
|
+
cursorMcp,
|
|
1705
2868
|
error: message
|
|
1706
2869
|
};
|
|
1707
2870
|
}
|
|
1708
2871
|
}
|
|
1709
|
-
return { exitCode: 0, repoRoot, outcomes,
|
|
2872
|
+
return { exitCode: 0, repoRoot, outcomes, cursorMcp, jetbrainsMcp, daemonCleanup };
|
|
1710
2873
|
}
|
|
1711
2874
|
async function cliSetupIdeFiles(argv) {
|
|
1712
2875
|
const { targets, force, dryRun, noGitignore, cleanup, devMode, repair, unknown } = parseSetupIdeFlags(argv);
|
|
@@ -1734,8 +2897,11 @@ async function cliSetupIdeFiles(argv) {
|
|
|
1734
2897
|
if (result.repoRoot) {
|
|
1735
2898
|
console.log(`[setup-ide-files] Repo root: ${result.repoRoot}`);
|
|
1736
2899
|
}
|
|
1737
|
-
if (
|
|
1738
|
-
|
|
2900
|
+
if (result.daemonCleanup && !result.daemonCleanup.skipped) {
|
|
2901
|
+
logSetupIdeCleanupSummary(result.daemonCleanup);
|
|
2902
|
+
}
|
|
2903
|
+
if (targets.cursor && result.cursorMcp) {
|
|
2904
|
+
logCursorMcpCliSummary(result.cursorMcp, dryRun);
|
|
1739
2905
|
}
|
|
1740
2906
|
if (targets.jetbrains && result.jetbrainsMcp) {
|
|
1741
2907
|
logJetBrainsMcpCliSummary(result.jetbrainsMcp, dryRun);
|
|
@@ -1743,9 +2909,13 @@ async function cliSetupIdeFiles(argv) {
|
|
|
1743
2909
|
summarizeOutcomes(result.outcomes);
|
|
1744
2910
|
if (dryRun) {
|
|
1745
2911
|
console.log("[setup-ide-files] Dry run: no files written.");
|
|
2912
|
+
if (result.daemonCleanup && !result.daemonCleanup.skipped) {
|
|
2913
|
+
console.log("[setup-ide-files] Dry run: no daemons stopped, no sockets removed.");
|
|
2914
|
+
}
|
|
1746
2915
|
}
|
|
2916
|
+
console.log(`[setup-ide-files] ${restartIdeInstruction(targets)}`);
|
|
1747
2917
|
if (cleanup) {
|
|
1748
|
-
console.log("[setup-ide-files] Running project
|
|
2918
|
+
console.log("[setup-ide-files] Running additional full-project cleanup (--cleanup)...");
|
|
1749
2919
|
const cleanupResult = await runCleanup({
|
|
1750
2920
|
cwd: process.cwd(),
|
|
1751
2921
|
dryRun,
|
|
@@ -1756,13 +2926,6 @@ async function cliSetupIdeFiles(argv) {
|
|
|
1756
2926
|
console.error(`[setup-ide-files] cleanup failed: ${cleanupResult.error}`);
|
|
1757
2927
|
return cleanupResult.exitCode;
|
|
1758
2928
|
}
|
|
1759
|
-
} else {
|
|
1760
|
-
console.log(
|
|
1761
|
-
"[setup-ide-files] If Studio shows stale connections, run: npx -y @memoraone/mcp@latest cleanup --project-id <projectId>"
|
|
1762
|
-
);
|
|
1763
|
-
console.log(
|
|
1764
|
-
"[setup-ide-files] From this repo (uses memoraone.m1): npx -y @memoraone/mcp@latest cleanup"
|
|
1765
|
-
);
|
|
1766
2929
|
}
|
|
1767
2930
|
return 0;
|
|
1768
2931
|
}
|
|
@@ -1799,92 +2962,9 @@ if (args[0] === "cleanup") {
|
|
|
1799
2962
|
process.exit(1);
|
|
1800
2963
|
});
|
|
1801
2964
|
} else {
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
const parts = [];
|
|
1805
|
-
if (raw !== void 0 && raw.trim() !== "") {
|
|
1806
|
-
for (const p of raw.split(path7.delimiter).map((s) => s.trim()).filter(Boolean)) {
|
|
1807
|
-
parts.push(path7.resolve(p));
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
parts.push(process.cwd());
|
|
1811
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1812
|
-
const deduped = [];
|
|
1813
|
-
for (const p of parts) {
|
|
1814
|
-
if (!seen.has(p)) {
|
|
1815
|
-
seen.add(p);
|
|
1816
|
-
deduped.push(p);
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
return deduped;
|
|
1820
|
-
}, connectWithRetry = function(socketPath) {
|
|
1821
|
-
return new Promise((resolve7, reject) => {
|
|
1822
|
-
const tryConnect = (attempt) => {
|
|
1823
|
-
const socket = net.connect(socketPath, () => {
|
|
1824
|
-
resolve7(socket);
|
|
1825
|
-
});
|
|
1826
|
-
socket.on("error", (err) => {
|
|
1827
|
-
if (attempt >= MAX_RETRIES) {
|
|
1828
|
-
reject(err);
|
|
1829
|
-
return;
|
|
1830
|
-
}
|
|
1831
|
-
log(`connect attempt ${attempt + 1} failed, retrying in ${RETRY_DELAY_MS}ms: ${String(err)}`);
|
|
1832
|
-
setTimeout(() => tryConnect(attempt + 1), RETRY_DELAY_MS);
|
|
1833
|
-
});
|
|
1834
|
-
};
|
|
1835
|
-
tryConnect(0);
|
|
1836
|
-
});
|
|
1837
|
-
};
|
|
1838
|
-
const log = (msg) => {
|
|
1839
|
-
process.stderr.write(`[memoraone-mcp][bridge] ${msg}
|
|
2965
|
+
runBridgeProxy({ cliPath: process.argv[1] }).catch((err) => {
|
|
2966
|
+
process.stderr.write(`[memoraone-mcp][bridge] fatal: ${String(err)}
|
|
1840
2967
|
`);
|
|
1841
|
-
};
|
|
1842
|
-
const MAX_RETRIES = 5;
|
|
1843
|
-
const RETRY_DELAY_MS = 200;
|
|
1844
|
-
async function resolveBinding() {
|
|
1845
|
-
return resolveAuthoritativeBinding(getWorkspaceRootCandidates());
|
|
1846
|
-
}
|
|
1847
|
-
async function runBridge() {
|
|
1848
|
-
ensureBaseDir();
|
|
1849
|
-
const binding = await resolveBinding();
|
|
1850
|
-
const socketPath = getDaemonSocketPath(binding.projectId);
|
|
1851
|
-
const environmentLog = binding.environment !== void 0 ? ` environment=${binding.environment}` : "";
|
|
1852
|
-
log(
|
|
1853
|
-
`authoritative binding project=${binding.projectId} workspace=${binding.workspaceRoot} m1=${binding.m1Path} source=${binding.bindingSource} apiKeySource=${binding.apiKeySource}${environmentLog}`
|
|
1854
|
-
);
|
|
1855
|
-
let socket;
|
|
1856
|
-
try {
|
|
1857
|
-
socket = await connectWithRetry(socketPath);
|
|
1858
|
-
} catch {
|
|
1859
|
-
log("daemon not running, spawning...");
|
|
1860
|
-
const child = (0, import_node_child_process4.spawn)(
|
|
1861
|
-
process.execPath,
|
|
1862
|
-
buildDaemonSpawnArgs(process.argv[1], binding.projectId),
|
|
1863
|
-
{
|
|
1864
|
-
detached: true,
|
|
1865
|
-
stdio: "ignore",
|
|
1866
|
-
env: {
|
|
1867
|
-
...process.env,
|
|
1868
|
-
MEMORAONE_DAEMON_BINDING_B64: encodeResolvedBinding(binding)
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
);
|
|
1872
|
-
child.unref();
|
|
1873
|
-
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
1874
|
-
socket = await connectWithRetry(socketPath);
|
|
1875
|
-
}
|
|
1876
|
-
log("bridge connected");
|
|
1877
|
-
log("forwarding active");
|
|
1878
|
-
process.stdin.pipe(socket);
|
|
1879
|
-
socket.pipe(process.stdout);
|
|
1880
|
-
socket.on("close", () => process.exit(0));
|
|
1881
|
-
socket.on("error", (err) => {
|
|
1882
|
-
log(`socket error: ${String(err)}`);
|
|
1883
|
-
process.exit(1);
|
|
1884
|
-
});
|
|
1885
|
-
}
|
|
1886
|
-
runBridge().catch((err) => {
|
|
1887
|
-
log(`fatal: ${String(err)}`);
|
|
1888
2968
|
process.exit(1);
|
|
1889
2969
|
});
|
|
1890
2970
|
}
|