@memoraone/mcp 0.1.29 → 0.1.31
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 +2060 -515
- 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.31",
|
|
34
34
|
type: "module",
|
|
35
35
|
main: "dist/index.cjs",
|
|
36
36
|
bin: {
|
|
@@ -66,17 +66,272 @@ var require_package = __commonJS({
|
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
// src/
|
|
70
|
-
var path6 = __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
|
+
|
|
74
|
+
// src/bindingIdentity.ts
|
|
75
|
+
var crypto = __toESM(require("crypto"), 1);
|
|
76
|
+
var path = __toESM(require("path"), 1);
|
|
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");
|
|
107
|
+
}
|
|
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");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/bindingSidecar.ts
|
|
115
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
116
|
+
var path4 = __toESM(require("path"), 1);
|
|
117
|
+
|
|
118
|
+
// src/projectBinding.ts
|
|
119
|
+
var fs = __toESM(require("fs/promises"), 1);
|
|
120
|
+
var path2 = __toESM(require("path"), 1);
|
|
121
|
+
var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
122
|
+
function normalizeEnvironment(raw) {
|
|
123
|
+
if (raw === void 0 || raw === null || typeof raw !== "string") {
|
|
124
|
+
return void 0;
|
|
125
|
+
}
|
|
126
|
+
const trimmed = raw.trim();
|
|
127
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
128
|
+
}
|
|
129
|
+
function parseAndValidateM1(content, markerPath) {
|
|
130
|
+
let parsed;
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(content);
|
|
133
|
+
} catch {
|
|
134
|
+
throw new Error(`[memoraone-mcp] Invalid memoraone.m1 JSON at ${markerPath}`);
|
|
135
|
+
}
|
|
136
|
+
const projectId = parsed?.projectId ?? parsed?.project_id;
|
|
137
|
+
if (!projectId || typeof projectId !== "string") {
|
|
138
|
+
throw new Error(`[memoraone-mcp] memoraone.m1 missing projectId at ${markerPath}`);
|
|
139
|
+
}
|
|
140
|
+
if (!uuidRegex.test(projectId.trim())) {
|
|
141
|
+
throw new Error(`[memoraone-mcp] memoraone.m1 projectId is not a UUID at ${markerPath}`);
|
|
142
|
+
}
|
|
143
|
+
const apiKeyRaw = parsed?.MEMORAONE_API_KEY ?? parsed?.api_key;
|
|
144
|
+
const apiKey = apiKeyRaw !== void 0 && apiKeyRaw !== null && typeof apiKeyRaw === "string" && apiKeyRaw.trim() !== "" ? apiKeyRaw.trim() : null;
|
|
145
|
+
const environment = normalizeEnvironment(parsed?.environment);
|
|
146
|
+
return environment === void 0 ? { projectId: projectId.trim(), apiKey } : { projectId: projectId.trim(), apiKey, environment };
|
|
147
|
+
}
|
|
148
|
+
async function resolveProjectIdFromExplicitM1Path() {
|
|
149
|
+
const raw = process.env.MEMORAONE_M1_PATH;
|
|
150
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const markerPath = path2.resolve(raw);
|
|
154
|
+
try {
|
|
155
|
+
const content = await fs.readFile(markerPath, "utf8");
|
|
156
|
+
const { projectId, apiKey, environment } = parseAndValidateM1(content, markerPath);
|
|
157
|
+
return environment === void 0 ? { projectId, apiKey, foundAt: markerPath } : { projectId, apiKey, environment, foundAt: markerPath };
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (err?.code === "ENOENT") {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function findM1WalkingUp(workspaceRoot) {
|
|
166
|
+
let current = path2.resolve(workspaceRoot);
|
|
167
|
+
while (true) {
|
|
168
|
+
const markerPath = path2.join(current, "memoraone.m1");
|
|
169
|
+
try {
|
|
170
|
+
const content = await fs.readFile(markerPath, "utf8");
|
|
171
|
+
const { projectId, apiKey, environment } = parseAndValidateM1(content, markerPath);
|
|
172
|
+
const repoRoot = path2.dirname(markerPath);
|
|
173
|
+
return environment === void 0 ? { projectId, apiKey, repoRoot, markerPath } : { projectId, apiKey, environment, repoRoot, markerPath };
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (err?.code !== "ENOENT") {
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const parent = path2.dirname(current);
|
|
180
|
+
if (parent === current) {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
current = parent;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
function normalizeWorkspaceSearchRoots(workspaceRoot) {
|
|
188
|
+
if (workspaceRoot === void 0) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const list = Array.isArray(workspaceRoot) ? workspaceRoot : [workspaceRoot];
|
|
192
|
+
const seen = /* @__PURE__ */ new Set();
|
|
193
|
+
const out = [];
|
|
194
|
+
for (const raw of list) {
|
|
195
|
+
if (raw === void 0) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const trimmed = String(raw).trim();
|
|
199
|
+
if (trimmed === "") {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const resolved = path2.resolve(trimmed);
|
|
203
|
+
if (!seen.has(resolved)) {
|
|
204
|
+
seen.add(resolved);
|
|
205
|
+
out.push(resolved);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
function resolveApiKeyWithSource(fileApiKey) {
|
|
211
|
+
const envApiKey = process.env.MEMORAONE_API_KEY?.trim();
|
|
212
|
+
if (envApiKey) {
|
|
213
|
+
return { apiKey: envApiKey, apiKeySource: "env" };
|
|
214
|
+
}
|
|
215
|
+
const aliasEnvApiKey = process.env.MEMORA_API_KEY?.trim();
|
|
216
|
+
if (aliasEnvApiKey) {
|
|
217
|
+
return { apiKey: aliasEnvApiKey, apiKeySource: "env" };
|
|
218
|
+
}
|
|
219
|
+
if (fileApiKey) {
|
|
220
|
+
return { apiKey: fileApiKey, apiKeySource: "memoraone.m1" };
|
|
221
|
+
}
|
|
222
|
+
return { apiKey: null, apiKeySource: "none" };
|
|
223
|
+
}
|
|
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
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const candidates = normalizeWorkspaceSearchRoots(workspaceRoot);
|
|
242
|
+
if (candidates.length === 0) {
|
|
243
|
+
throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
|
|
244
|
+
}
|
|
245
|
+
const bindings = [];
|
|
246
|
+
for (const root of candidates) {
|
|
247
|
+
const binding = await findM1WalkingUp(root);
|
|
248
|
+
if (binding) {
|
|
249
|
+
const resolved = resolveApiKeyWithSource(binding.apiKey);
|
|
250
|
+
bindings.push({
|
|
251
|
+
projectId: binding.projectId,
|
|
252
|
+
workspaceRoot: binding.repoRoot,
|
|
253
|
+
m1Path: binding.markerPath,
|
|
254
|
+
apiKey: resolved.apiKey,
|
|
255
|
+
...binding.environment !== void 0 ? { environment: binding.environment } : {},
|
|
256
|
+
bindingSource: "workspace-search",
|
|
257
|
+
apiKeySource: resolved.apiKeySource
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
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];
|
|
274
|
+
}
|
|
275
|
+
function encodeResolvedBinding(binding) {
|
|
276
|
+
return Buffer.from(JSON.stringify(binding), "utf8").toString("base64");
|
|
277
|
+
}
|
|
278
|
+
function decodeResolvedBinding(value) {
|
|
279
|
+
if (!value) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
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
|
+
};
|
|
322
|
+
}
|
|
73
323
|
|
|
74
324
|
// src/socketPaths.ts
|
|
75
325
|
var os = __toESM(require("os"), 1);
|
|
76
|
-
var
|
|
77
|
-
var
|
|
78
|
-
var BASE_DIR = process.env.MEMORAONE_MCP_LOCK_DIR ||
|
|
79
|
-
var
|
|
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;
|
|
80
335
|
function getMcpBaseDir() {
|
|
81
336
|
return BASE_DIR;
|
|
82
337
|
}
|
|
@@ -103,24 +358,33 @@ function buildDaemonSpawnArgs(scriptPath, projectId, env = process.env) {
|
|
|
103
358
|
}
|
|
104
359
|
return args2;
|
|
105
360
|
}
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
361
|
+
function resolveBindingIdeType(env = process.env) {
|
|
362
|
+
return resolveIdeTypeFromEnv(env) ?? "";
|
|
363
|
+
}
|
|
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`;
|
|
111
368
|
}
|
|
112
|
-
function
|
|
113
|
-
return
|
|
369
|
+
function getBindingSocketPath(binding, env = process.env) {
|
|
370
|
+
return path3.join(BASE_DIR, getBindingSocketFilename(binding, env));
|
|
114
371
|
}
|
|
115
372
|
function ensureBaseDir() {
|
|
116
|
-
|
|
373
|
+
fs2.mkdirSync(BASE_DIR, { recursive: true });
|
|
117
374
|
return BASE_DIR;
|
|
118
375
|
}
|
|
376
|
+
function isHashSocketFilename(filename) {
|
|
377
|
+
return HASH_SOCKET_FILENAME_RE.test(path3.basename(filename));
|
|
378
|
+
}
|
|
379
|
+
function isLegacySocketFilename(filename) {
|
|
380
|
+
return LEGACY_SOCKET_FILENAME_RE.test(path3.basename(filename));
|
|
381
|
+
}
|
|
119
382
|
function isMemoraoneSocketFilename(filename) {
|
|
120
|
-
|
|
383
|
+
const base = path3.basename(filename);
|
|
384
|
+
return HASH_SOCKET_FILENAME_RE.test(base) || LEGACY_SOCKET_PROJECT_ID_RE.test(base);
|
|
121
385
|
}
|
|
122
386
|
function extractProjectIdFromSocketFilename(filename) {
|
|
123
|
-
const match =
|
|
387
|
+
const match = path3.basename(filename).match(LEGACY_SOCKET_PROJECT_ID_RE);
|
|
124
388
|
return match ? match[1].toLowerCase() : null;
|
|
125
389
|
}
|
|
126
390
|
function isSocketFilenameForProject(filename, projectId) {
|
|
@@ -128,11 +392,11 @@ function isSocketFilenameForProject(filename, projectId) {
|
|
|
128
392
|
return extracted !== null && extracted === projectId.trim().toLowerCase();
|
|
129
393
|
}
|
|
130
394
|
function extractIdeTypeFromSocketFilename(filename) {
|
|
131
|
-
const match =
|
|
395
|
+
const match = path3.basename(filename).match(LEGACY_SOCKET_FILENAME_RE);
|
|
132
396
|
if (!match) return null;
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
135
|
-
if (IDE_TYPE_SET.has(
|
|
397
|
+
const ide = match[3];
|
|
398
|
+
if (ide === void 0) return "legacy";
|
|
399
|
+
if (IDE_TYPE_SET.has(ide)) return ide;
|
|
136
400
|
return null;
|
|
137
401
|
}
|
|
138
402
|
function isSocketFilenameForProjectAndIde(filename, projectId, ide) {
|
|
@@ -146,163 +410,1021 @@ function isSocketFilenameForProjectAndIde(filename, projectId, ide) {
|
|
|
146
410
|
return socketIde === ide;
|
|
147
411
|
}
|
|
148
412
|
|
|
149
|
-
// src/
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
413
|
+
// src/bindingSidecar.ts
|
|
414
|
+
function bindingSidecarPath(socketPath) {
|
|
415
|
+
if (socketPath.endsWith(".sock")) {
|
|
416
|
+
return `${socketPath.slice(0, -".sock".length)}.binding.json`;
|
|
417
|
+
}
|
|
418
|
+
return `${socketPath}.binding.json`;
|
|
419
|
+
}
|
|
420
|
+
function parseSidecarRecord(raw) {
|
|
421
|
+
try {
|
|
422
|
+
const parsed = JSON.parse(raw);
|
|
423
|
+
if (!parsed?.binding) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
const binding = decodeResolvedBinding(parsed.binding);
|
|
427
|
+
return {
|
|
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
|
|
434
|
+
};
|
|
435
|
+
} catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function readBindingSidecarRecord(socketPath) {
|
|
440
|
+
try {
|
|
441
|
+
const raw = fs3.readFileSync(bindingSidecarPath(socketPath), "utf8");
|
|
442
|
+
return parseSidecarRecord(raw);
|
|
443
|
+
} catch {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
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
|
+
function buildMemoraoneCursorMcpServer(npxPath, workspaceRoot) {
|
|
1165
|
+
const env = {
|
|
1166
|
+
MEMORAONE_API_URL: "https://api.memoraone.com",
|
|
1167
|
+
MEMORAONE_IDE_TYPE: "cursor"
|
|
1168
|
+
};
|
|
1169
|
+
if (workspaceRoot !== void 0) {
|
|
1170
|
+
env[MEMORAONE_WORKSPACE_ROOT_ENV] = path6.resolve(workspaceRoot);
|
|
1171
|
+
}
|
|
1172
|
+
return {
|
|
1173
|
+
command: npxPath,
|
|
1174
|
+
args: ["-y", "@memoraone/mcp@latest"],
|
|
1175
|
+
env
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
async function pathExists(filePath) {
|
|
1179
|
+
try {
|
|
1180
|
+
await fs4.access(filePath);
|
|
1181
|
+
return true;
|
|
1182
|
+
} catch {
|
|
1183
|
+
return false;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
function stripLeadingLineComments(text) {
|
|
1187
|
+
return text.split("\n").filter((line) => !/^\s*\/\//.test(line)).join("\n");
|
|
1188
|
+
}
|
|
1189
|
+
function getKnownCursorGlobalMcpConfigCandidates(homeDir) {
|
|
1190
|
+
return [path6.join(homeDir, ".cursor", "mcp.json")];
|
|
1191
|
+
}
|
|
1192
|
+
async function detectCursorGlobalMcpConfig(options) {
|
|
1193
|
+
if (options?.explicitPath) {
|
|
1194
|
+
return { ok: true, path: options.explicitPath, detectedExisting: await pathExists(options.explicitPath) };
|
|
1195
|
+
}
|
|
1196
|
+
const homeDir = options?.homeDir ?? os2.homedir();
|
|
1197
|
+
const candidates = getKnownCursorGlobalMcpConfigCandidates(homeDir);
|
|
1198
|
+
const existing = [];
|
|
1199
|
+
for (const candidate of candidates) {
|
|
1200
|
+
if (await pathExists(candidate)) existing.push(candidate);
|
|
1201
|
+
}
|
|
1202
|
+
if (existing.length > 1) {
|
|
1203
|
+
return {
|
|
1204
|
+
ok: false,
|
|
1205
|
+
error: "[setup-ide-files] Multiple Cursor global MCP config paths found. Specify one explicitly.",
|
|
1206
|
+
candidates: existing
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
if (existing.length === 1) {
|
|
1210
|
+
return { ok: true, path: existing[0], detectedExisting: true };
|
|
1211
|
+
}
|
|
1212
|
+
const defaultPath = candidates[0];
|
|
1213
|
+
if (!defaultPath) {
|
|
1214
|
+
return {
|
|
1215
|
+
ok: false,
|
|
1216
|
+
error: "[setup-ide-files] No known Cursor global MCP config path.",
|
|
1217
|
+
candidates: []
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
return { ok: true, path: defaultPath, detectedExisting: false };
|
|
1221
|
+
}
|
|
1222
|
+
function formatBackupTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
1223
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1224
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
1225
|
+
}
|
|
1226
|
+
async function isWorkingNpx(npxPath) {
|
|
1227
|
+
try {
|
|
1228
|
+
if (!await pathExists(npxPath)) return false;
|
|
1229
|
+
if (process.platform !== "win32") {
|
|
1230
|
+
try {
|
|
1231
|
+
await fs4.access(npxPath, fs4.constants.X_OK);
|
|
1232
|
+
} catch {
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
await execFileAsync(npxPath, ["--version"], { timeout: 1e4 });
|
|
1237
|
+
return true;
|
|
1238
|
+
} catch {
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
async function resolveNpxPath() {
|
|
1243
|
+
const npxName = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
1244
|
+
const candidates = [];
|
|
1245
|
+
if (process.platform === "darwin") {
|
|
1246
|
+
candidates.push("/opt/homebrew/bin/npx", "/usr/local/bin/npx");
|
|
1247
|
+
} else if (process.platform === "linux") {
|
|
1248
|
+
candidates.push("/usr/local/bin/npx");
|
|
1249
|
+
}
|
|
1250
|
+
const pathSep = process.platform === "win32" ? ";" : ":";
|
|
1251
|
+
for (const dir of (process.env.PATH ?? "").split(pathSep)) {
|
|
1252
|
+
if (!dir) continue;
|
|
1253
|
+
candidates.push(path6.join(dir, npxName));
|
|
156
1254
|
}
|
|
157
|
-
const trimmed = raw.trim();
|
|
158
|
-
return trimmed === "" ? void 0 : trimmed;
|
|
159
|
-
}
|
|
160
|
-
function parseAndValidateM1(content, markerPath) {
|
|
161
|
-
let parsed;
|
|
162
1255
|
try {
|
|
163
|
-
|
|
1256
|
+
const lookupCmd = process.platform === "win32" ? "where" : "which";
|
|
1257
|
+
const { stdout } = await execFileAsync(lookupCmd, [npxName], { timeout: 5e3 });
|
|
1258
|
+
const first = stdout.trim().split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
1259
|
+
if (first) candidates.unshift(first);
|
|
164
1260
|
} catch {
|
|
165
|
-
throw new Error(`[memoraone-mcp] Invalid memoraone.m1 JSON at ${markerPath}`);
|
|
166
|
-
}
|
|
167
|
-
const projectId = parsed?.projectId ?? parsed?.project_id;
|
|
168
|
-
if (!projectId || typeof projectId !== "string") {
|
|
169
|
-
throw new Error(`[memoraone-mcp] memoraone.m1 missing projectId at ${markerPath}`);
|
|
170
1261
|
}
|
|
171
|
-
|
|
172
|
-
|
|
1262
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1263
|
+
for (const candidate of candidates) {
|
|
1264
|
+
const abs = path6.isAbsolute(candidate) ? candidate : path6.resolve(candidate);
|
|
1265
|
+
const key = process.platform === "win32" ? abs.toLowerCase() : abs;
|
|
1266
|
+
if (seen.has(key)) continue;
|
|
1267
|
+
seen.add(key);
|
|
1268
|
+
if (await isWorkingNpx(abs)) return abs;
|
|
173
1269
|
}
|
|
174
|
-
|
|
175
|
-
const apiKey = apiKeyRaw !== void 0 && apiKeyRaw !== null && typeof apiKeyRaw === "string" && apiKeyRaw.trim() !== "" ? apiKeyRaw.trim() : null;
|
|
176
|
-
const environment = normalizeEnvironment(parsed?.environment);
|
|
177
|
-
return environment === void 0 ? { projectId: projectId.trim(), apiKey } : { projectId: projectId.trim(), apiKey, environment };
|
|
1270
|
+
return null;
|
|
178
1271
|
}
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
1272
|
+
function mergeCursorRepoMcpConfigObject(existing, npxPath, repoRoot) {
|
|
1273
|
+
const base = existing && typeof existing === "object" ? { ...existing } : { mcpServers: {} };
|
|
1274
|
+
const mcpServers = typeof base.mcpServers === "object" && base.mcpServers !== null && !Array.isArray(base.mcpServers) ? { ...base.mcpServers } : {};
|
|
1275
|
+
mcpServers.memoraone = buildMemoraoneCursorMcpServer(npxPath, repoRoot);
|
|
1276
|
+
return { ...base, mcpServers };
|
|
1277
|
+
}
|
|
1278
|
+
function isManagedMemoraoneCursorServer(server) {
|
|
1279
|
+
if (!server || typeof server !== "object") return false;
|
|
1280
|
+
const s = server;
|
|
1281
|
+
if (!Array.isArray(s.args) || s.args.length !== 2) return false;
|
|
1282
|
+
if (s.args[0] !== "-y" || s.args[1] !== "@memoraone/mcp@latest") return false;
|
|
1283
|
+
const env = s.env;
|
|
1284
|
+
if (!env || typeof env !== "object") return false;
|
|
1285
|
+
return memoraoneEnvMatchesBase(env);
|
|
1286
|
+
}
|
|
1287
|
+
function cursorConfigHasManagedMemoraone(parsed) {
|
|
1288
|
+
if (!parsed || typeof parsed !== "object") return false;
|
|
1289
|
+
const mcpServers = parsed.mcpServers;
|
|
1290
|
+
if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) return false;
|
|
1291
|
+
return isManagedMemoraoneCursorServer(mcpServers.memoraone);
|
|
1292
|
+
}
|
|
1293
|
+
function memoraoneEnvMatchesBase(env) {
|
|
1294
|
+
return env.MEMORAONE_API_URL === "https://api.memoraone.com" && env.MEMORAONE_IDE_TYPE === "cursor";
|
|
1295
|
+
}
|
|
1296
|
+
function getCursorRepoMcpConfigPath(repoRoot) {
|
|
1297
|
+
return path6.join(repoRoot, ".cursor", "mcp.json");
|
|
1298
|
+
}
|
|
1299
|
+
async function readCursorMcpConfigObject(configPath) {
|
|
185
1300
|
try {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
return environment === void 0 ? { projectId, apiKey, foundAt: markerPath } : { projectId, apiKey, environment, foundAt: markerPath };
|
|
1301
|
+
const raw = await fs4.readFile(configPath, "utf8");
|
|
1302
|
+
return JSON.parse(stripLeadingLineComments(raw));
|
|
189
1303
|
} catch (err) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
1304
|
+
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
1305
|
+
if (code === "ENOENT") return null;
|
|
193
1306
|
throw err;
|
|
194
1307
|
}
|
|
195
1308
|
}
|
|
196
|
-
async function
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const content = await fs2.readFile(markerPath, "utf8");
|
|
202
|
-
const { projectId, apiKey, environment } = parseAndValidateM1(content, markerPath);
|
|
203
|
-
const repoRoot = path2.dirname(markerPath);
|
|
204
|
-
return environment === void 0 ? { projectId, apiKey, repoRoot, markerPath } : { projectId, apiKey, environment, repoRoot, markerPath };
|
|
205
|
-
} catch (err) {
|
|
206
|
-
if (err?.code !== "ENOENT") {
|
|
207
|
-
throw err;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
const parent = path2.dirname(current);
|
|
211
|
-
if (parent === current) {
|
|
212
|
-
break;
|
|
213
|
-
}
|
|
214
|
-
current = parent;
|
|
1309
|
+
async function removeMemoraoneFromCursorGlobalConfig(options) {
|
|
1310
|
+
const { configPath, dryRun } = options;
|
|
1311
|
+
const parsed = await readCursorMcpConfigObject(configPath);
|
|
1312
|
+
if (!parsed || !cursorConfigHasManagedMemoraone(parsed)) {
|
|
1313
|
+
return { changed: false };
|
|
215
1314
|
}
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
1315
|
+
if (dryRun) {
|
|
1316
|
+
return { changed: true, backupPath: `${configPath}.backup-<timestamp>` };
|
|
1317
|
+
}
|
|
1318
|
+
const backupPath = `${configPath}.backup-${formatBackupTimestamp()}`;
|
|
1319
|
+
await fs4.copyFile(configPath, backupPath);
|
|
1320
|
+
const mcpServers = typeof parsed.mcpServers === "object" && parsed.mcpServers !== null && !Array.isArray(parsed.mcpServers) ? { ...parsed.mcpServers } : {};
|
|
1321
|
+
delete mcpServers.memoraone;
|
|
1322
|
+
const hasOtherServers = Object.keys(mcpServers).length > 0;
|
|
1323
|
+
if (!hasOtherServers) {
|
|
1324
|
+
await fs4.unlink(configPath);
|
|
1325
|
+
return { changed: true, backupPath };
|
|
1326
|
+
}
|
|
1327
|
+
const next = { ...parsed, mcpServers };
|
|
1328
|
+
await fs4.mkdir(path6.dirname(configPath), { recursive: true });
|
|
1329
|
+
await fs4.writeFile(configPath, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
1330
|
+
return { changed: true, backupPath };
|
|
1331
|
+
}
|
|
1332
|
+
async function auditCursorMcpConfig(options) {
|
|
1333
|
+
const repoRoot = path6.resolve(options?.repoRoot ?? process.cwd());
|
|
1334
|
+
const repoConfigPath = getCursorRepoMcpConfigPath(repoRoot);
|
|
1335
|
+
const globalDetection = await detectCursorGlobalMcpConfig({
|
|
1336
|
+
homeDir: options?.homeDir,
|
|
1337
|
+
explicitPath: options?.explicitGlobalPath
|
|
1338
|
+
});
|
|
1339
|
+
const globalConfigPath = globalDetection.ok ? globalDetection.path : getKnownCursorGlobalMcpConfigCandidates(
|
|
1340
|
+
options?.homeDir ?? os2.homedir()
|
|
1341
|
+
)[0];
|
|
1342
|
+
let repoHasManagedMemoraone = false;
|
|
1343
|
+
try {
|
|
1344
|
+
const repoParsed = await readCursorMcpConfigObject(repoConfigPath);
|
|
1345
|
+
repoHasManagedMemoraone = cursorConfigHasManagedMemoraone(repoParsed);
|
|
1346
|
+
} catch {
|
|
1347
|
+
repoHasManagedMemoraone = false;
|
|
221
1348
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const trimmed = String(raw).trim();
|
|
230
|
-
if (trimmed === "") {
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
const resolved = path2.resolve(trimmed);
|
|
234
|
-
if (!seen.has(resolved)) {
|
|
235
|
-
seen.add(resolved);
|
|
236
|
-
out.push(resolved);
|
|
1349
|
+
let globalHasManagedMemoraone = false;
|
|
1350
|
+
if (globalDetection.ok) {
|
|
1351
|
+
try {
|
|
1352
|
+
const globalParsed = await readCursorMcpConfigObject(globalConfigPath);
|
|
1353
|
+
globalHasManagedMemoraone = cursorConfigHasManagedMemoraone(globalParsed);
|
|
1354
|
+
} catch {
|
|
1355
|
+
globalHasManagedMemoraone = false;
|
|
237
1356
|
}
|
|
238
1357
|
}
|
|
239
|
-
return
|
|
1358
|
+
return {
|
|
1359
|
+
repoConfigPath,
|
|
1360
|
+
repoHasManagedMemoraone,
|
|
1361
|
+
globalConfigPath,
|
|
1362
|
+
globalHasManagedMemoraone,
|
|
1363
|
+
conflict: repoHasManagedMemoraone && globalHasManagedMemoraone
|
|
1364
|
+
};
|
|
240
1365
|
}
|
|
241
|
-
function
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
1366
|
+
function logCursorMcpConfigAudit(prefix, audit) {
|
|
1367
|
+
console.log(`${prefix} Cursor MCP config audit:`);
|
|
1368
|
+
console.log(
|
|
1369
|
+
`${prefix} repo ${audit.repoConfigPath}: managed memoraone=${audit.repoHasManagedMemoraone}`
|
|
1370
|
+
);
|
|
1371
|
+
console.log(
|
|
1372
|
+
`${prefix} global ${audit.globalConfigPath}: managed memoraone=${audit.globalHasManagedMemoraone}`
|
|
1373
|
+
);
|
|
1374
|
+
if (audit.conflict) {
|
|
1375
|
+
console.warn(
|
|
1376
|
+
`${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.`
|
|
1377
|
+
);
|
|
1378
|
+
} else if (audit.globalHasManagedMemoraone && !audit.repoHasManagedMemoraone) {
|
|
1379
|
+
console.warn(
|
|
1380
|
+
`${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.`
|
|
1381
|
+
);
|
|
1382
|
+
} else if (audit.repoHasManagedMemoraone && !audit.globalHasManagedMemoraone) {
|
|
1383
|
+
console.log(
|
|
1384
|
+
`${prefix} Cursor MCP is repo-scoped (.cursor/mcp.json) with no global memoraone entry (recommended for multi-repo windows).`
|
|
1385
|
+
);
|
|
252
1386
|
}
|
|
253
|
-
return { apiKey: null, apiKeySource: "none" };
|
|
254
1387
|
}
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
workspaceRoot: path2.dirname(explicitBinding.foundAt),
|
|
262
|
-
m1Path: explicitBinding.foundAt,
|
|
263
|
-
apiKey: resolved.apiKey,
|
|
264
|
-
...explicitBinding.environment !== void 0 ? { environment: explicitBinding.environment } : {},
|
|
265
|
-
bindingSource: "explicit-m1-path",
|
|
266
|
-
apiKeySource: resolved.apiKeySource
|
|
267
|
-
};
|
|
1388
|
+
function logCursorMcpCliSummary(info, dryRun) {
|
|
1389
|
+
const { repoConfigPath, repoOutcome, npxPath, repoBackupPath, globalConfigPath, globalMemoraoneRemoved, globalBackupPath } = info;
|
|
1390
|
+
console.log(`[setup-ide-files] Cursor repo MCP config: ${repoConfigPath}`);
|
|
1391
|
+
console.log(`[setup-ide-files] Resolved npx: ${npxPath}`);
|
|
1392
|
+
if (repoBackupPath) {
|
|
1393
|
+
console.log(`[setup-ide-files] Cursor repo MCP config backup: ${repoBackupPath}`);
|
|
268
1394
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
1395
|
+
if (repoOutcome === "created") {
|
|
1396
|
+
console.log(
|
|
1397
|
+
dryRun ? `[setup-ide-files] Cursor repo MCP config would be created: ${repoConfigPath}` : `[setup-ide-files] Cursor repo MCP config created: ${repoConfigPath}`
|
|
1398
|
+
);
|
|
1399
|
+
} else if (repoOutcome === "updated") {
|
|
1400
|
+
console.log(
|
|
1401
|
+
dryRun ? `[setup-ide-files] Cursor repo MCP config would be updated: ${repoConfigPath}` : `[setup-ide-files] Cursor repo MCP config updated: ${repoConfigPath}`
|
|
1402
|
+
);
|
|
1403
|
+
} else if (repoOutcome === "skipped") {
|
|
1404
|
+
console.log(`[setup-ide-files] Cursor repo MCP config unchanged: ${repoConfigPath}`);
|
|
272
1405
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
workspaceRoot: binding.repoRoot,
|
|
280
|
-
m1Path: binding.markerPath,
|
|
281
|
-
apiKey: resolved.apiKey,
|
|
282
|
-
...binding.environment !== void 0 ? { environment: binding.environment } : {},
|
|
283
|
-
bindingSource: "workspace-search",
|
|
284
|
-
apiKeySource: resolved.apiKeySource
|
|
285
|
-
};
|
|
1406
|
+
if (globalMemoraoneRemoved && globalConfigPath) {
|
|
1407
|
+
console.log(
|
|
1408
|
+
dryRun ? `[setup-ide-files] Would remove memoraone from Cursor global MCP config: ${globalConfigPath}` : `[setup-ide-files] Removed memoraone from Cursor global MCP config: ${globalConfigPath}`
|
|
1409
|
+
);
|
|
1410
|
+
if (globalBackupPath) {
|
|
1411
|
+
console.log(`[setup-ide-files] Cursor global MCP config backup: ${globalBackupPath}`);
|
|
286
1412
|
}
|
|
1413
|
+
} else if (globalConfigPath) {
|
|
1414
|
+
console.log(
|
|
1415
|
+
`[setup-ide-files] Cursor global MCP config unchanged (no managed memoraone to remove): ${globalConfigPath}`
|
|
1416
|
+
);
|
|
287
1417
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
1418
|
+
console.log(
|
|
1419
|
+
"[setup-ide-files] Each Cursor window uses this repo\u2019s .cursor/mcp.json (separate MCP process per repo)."
|
|
1420
|
+
);
|
|
1421
|
+
console.log(
|
|
1422
|
+
"[setup-ide-files] Fully quit Cursor and reopen this repo for MCP changes to take effect."
|
|
1423
|
+
);
|
|
292
1424
|
}
|
|
293
1425
|
|
|
294
|
-
// src/setupIdeFiles.ts
|
|
295
|
-
var fs5 = __toESM(require("fs/promises"), 1);
|
|
296
|
-
var path5 = __toESM(require("path"), 1);
|
|
297
|
-
|
|
298
1426
|
// src/cleanup.ts
|
|
299
|
-
var
|
|
300
|
-
var path3 = __toESM(require("path"), 1);
|
|
301
|
-
var readline = __toESM(require("readline/promises"), 1);
|
|
302
|
-
var import_node_child_process = require("child_process");
|
|
303
|
-
var import_node_util = require("util");
|
|
304
|
-
var import_node_process = require("process");
|
|
305
|
-
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
1427
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
|
|
306
1428
|
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;
|
|
307
1429
|
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;
|
|
308
1430
|
var MEMORAONE_MCP_COMMAND_RE = /memoraone-mcp|memoraOne-mcp|@memoraone\/mcp/;
|
|
@@ -349,7 +1471,7 @@ function normalizeCleanupProjectId(projectId) {
|
|
|
349
1471
|
return trimmed.toLowerCase();
|
|
350
1472
|
}
|
|
351
1473
|
async function defaultListDaemonProcesses() {
|
|
352
|
-
const { stdout } = await
|
|
1474
|
+
const { stdout } = await execFileAsync2("ps", ["-eo", "pid=,args="], {
|
|
353
1475
|
maxBuffer: 10 * 1024 * 1024
|
|
354
1476
|
});
|
|
355
1477
|
return parseDaemonProcessLines(stdout.split("\n"));
|
|
@@ -358,7 +1480,7 @@ async function defaultListSocketPaths(projectId) {
|
|
|
358
1480
|
const baseDir = getMcpBaseDir();
|
|
359
1481
|
let entries;
|
|
360
1482
|
try {
|
|
361
|
-
entries = await
|
|
1483
|
+
entries = await fs5.readdir(baseDir);
|
|
362
1484
|
} catch (err) {
|
|
363
1485
|
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
364
1486
|
if (code === "ENOENT") {
|
|
@@ -368,33 +1490,64 @@ async function defaultListSocketPaths(projectId) {
|
|
|
368
1490
|
}
|
|
369
1491
|
const paths = [];
|
|
370
1492
|
for (const name of entries) {
|
|
1493
|
+
if (!name.endsWith(".sock") || !isMemoraoneSocketFilename(name)) {
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
const socketPath = path7.join(baseDir, name);
|
|
371
1497
|
if (projectId === null) {
|
|
372
|
-
|
|
373
|
-
|
|
1498
|
+
paths.push(socketPath);
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
const normalizedProjectId = projectId.trim().toLowerCase();
|
|
1502
|
+
if (isLegacySocketFilename(name) && isSocketFilenameForProject(name, normalizedProjectId)) {
|
|
1503
|
+
paths.push(socketPath);
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
if (isHashSocketFilename(name)) {
|
|
1507
|
+
const record = readBindingSidecarRecord(socketPath);
|
|
1508
|
+
if (record?.projectId.trim().toLowerCase() === normalizedProjectId) {
|
|
1509
|
+
paths.push(socketPath);
|
|
374
1510
|
}
|
|
375
|
-
} else if (isSocketFilenameForProject(name, projectId)) {
|
|
376
|
-
paths.push(path3.join(baseDir, name));
|
|
377
1511
|
}
|
|
378
1512
|
}
|
|
379
1513
|
return paths.sort();
|
|
380
1514
|
}
|
|
381
|
-
function filterSocketPathsByIde(socketPaths, projectId, ide) {
|
|
1515
|
+
async function filterSocketPathsByIde(socketPaths, projectId, ide) {
|
|
382
1516
|
if (ide === void 0) return socketPaths;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
)
|
|
1517
|
+
const normalizedProjectId = projectId.trim().toLowerCase();
|
|
1518
|
+
const filtered = [];
|
|
1519
|
+
for (const socketPath of socketPaths) {
|
|
1520
|
+
const basename4 = path7.basename(socketPath);
|
|
1521
|
+
if (isLegacySocketFilename(basename4)) {
|
|
1522
|
+
if (isSocketFilenameForProjectAndIde(basename4, normalizedProjectId, ide)) {
|
|
1523
|
+
filtered.push(socketPath);
|
|
1524
|
+
}
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
if (isHashSocketFilename(basename4)) {
|
|
1528
|
+
const record = readBindingSidecarRecord(socketPath);
|
|
1529
|
+
if (record?.projectId.trim().toLowerCase() === normalizedProjectId && record.ideType === ide) {
|
|
1530
|
+
filtered.push(socketPath);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
return filtered;
|
|
386
1535
|
}
|
|
387
1536
|
async function defaultKillProcess(pid) {
|
|
388
1537
|
process.kill(pid, "SIGTERM");
|
|
389
1538
|
}
|
|
390
1539
|
async function defaultRemoveSocket(socketPath) {
|
|
391
|
-
await
|
|
1540
|
+
await fs5.unlink(socketPath);
|
|
1541
|
+
try {
|
|
1542
|
+
await fs5.unlink(bindingSidecarPath(socketPath));
|
|
1543
|
+
} catch {
|
|
1544
|
+
}
|
|
392
1545
|
}
|
|
393
1546
|
async function defaultConfirm(message) {
|
|
394
1547
|
if (!import_node_process.stdin.isTTY) {
|
|
395
1548
|
return false;
|
|
396
1549
|
}
|
|
397
|
-
const rl =
|
|
1550
|
+
const rl = readline3.createInterface({ input: import_node_process.stdin, output: import_node_process.stdout });
|
|
398
1551
|
try {
|
|
399
1552
|
const answer = await rl.question(`${message} [y/N] `);
|
|
400
1553
|
return /^y(es)?$/i.test(answer.trim());
|
|
@@ -404,7 +1557,7 @@ async function defaultConfirm(message) {
|
|
|
404
1557
|
}
|
|
405
1558
|
async function resolveCleanupTarget(cwd) {
|
|
406
1559
|
try {
|
|
407
|
-
const binding = await resolveAuthoritativeBinding([
|
|
1560
|
+
const binding = await resolveAuthoritativeBinding([path7.resolve(cwd)]);
|
|
408
1561
|
return {
|
|
409
1562
|
workspaceRoot: binding.workspaceRoot,
|
|
410
1563
|
m1Path: binding.m1Path,
|
|
@@ -433,7 +1586,8 @@ function filterProcessesForScope(processes, projectId) {
|
|
|
433
1586
|
function logPrefix(dryRun) {
|
|
434
1587
|
return dryRun ? "[cleanup][dry-run]" : "[cleanup]";
|
|
435
1588
|
}
|
|
436
|
-
function logReconnectNotice(prefix, ide) {
|
|
1589
|
+
function logReconnectNotice(opts, prefix, ide) {
|
|
1590
|
+
if (opts.quiet) return;
|
|
437
1591
|
if (ide) {
|
|
438
1592
|
console.log(
|
|
439
1593
|
`${prefix} Note: Valid ${ide} connections for this project may disconnect temporarily; they should reconnect automatically. Stale connections will remain cleared.`
|
|
@@ -444,6 +1598,16 @@ function logReconnectNotice(prefix, ide) {
|
|
|
444
1598
|
);
|
|
445
1599
|
}
|
|
446
1600
|
}
|
|
1601
|
+
function cleanupLog(opts, message) {
|
|
1602
|
+
if (!opts.quiet) {
|
|
1603
|
+
console.log(message);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
function cleanupWarn(opts, message) {
|
|
1607
|
+
if (!opts.quiet) {
|
|
1608
|
+
console.warn(message);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
447
1611
|
async function runCleanup(opts) {
|
|
448
1612
|
const listProcesses = opts.listProcesses ?? defaultListDaemonProcesses;
|
|
449
1613
|
const listSocketPaths = opts.listSocketPaths ?? defaultListSocketPaths;
|
|
@@ -464,8 +1628,9 @@ async function runCleanup(opts) {
|
|
|
464
1628
|
error: "Cannot combine --all-projects with --project-id."
|
|
465
1629
|
};
|
|
466
1630
|
}
|
|
467
|
-
|
|
468
|
-
|
|
1631
|
+
cleanupLog(opts, `${prefix} Mode: all projects (--all-projects)`);
|
|
1632
|
+
cleanupWarn(
|
|
1633
|
+
opts,
|
|
469
1634
|
`${prefix} WARNING: This stops every MemoraOne MCP daemon and removes all project sockets under ${getMcpBaseDir()}.`
|
|
470
1635
|
);
|
|
471
1636
|
} else if (opts.projectId) {
|
|
@@ -474,9 +1639,9 @@ async function runCleanup(opts) {
|
|
|
474
1639
|
return { exitCode: 1, killedPids: [], removedSockets: [], skippedProcesses: [], error: normalized.error };
|
|
475
1640
|
}
|
|
476
1641
|
targetProjectId = normalized;
|
|
477
|
-
|
|
1642
|
+
cleanupLog(opts, `${prefix} Project id: ${targetProjectId}`);
|
|
478
1643
|
if (opts.ide) {
|
|
479
|
-
|
|
1644
|
+
cleanupLog(opts, `${prefix} IDE filter: ${opts.ide}`);
|
|
480
1645
|
}
|
|
481
1646
|
} else {
|
|
482
1647
|
const target = await resolveCleanupTarget(opts.cwd);
|
|
@@ -486,15 +1651,23 @@ async function runCleanup(opts) {
|
|
|
486
1651
|
targetProjectId = target.projectId;
|
|
487
1652
|
workspaceRoot = target.workspaceRoot;
|
|
488
1653
|
m1Path = target.m1Path;
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
1654
|
+
cleanupLog(opts, `${prefix} Workspace root: ${workspaceRoot}`);
|
|
1655
|
+
cleanupLog(opts, `${prefix} memoraone.m1: ${m1Path}`);
|
|
1656
|
+
cleanupLog(opts, `${prefix} Project id: ${targetProjectId}`);
|
|
492
1657
|
if (opts.ide) {
|
|
493
|
-
|
|
1658
|
+
cleanupLog(opts, `${prefix} IDE filter: ${opts.ide}`);
|
|
1659
|
+
}
|
|
1660
|
+
if (workspaceRoot && (!opts.ide || opts.ide === "cursor")) {
|
|
1661
|
+
try {
|
|
1662
|
+
const cursorAudit = await auditCursorMcpConfig({ repoRoot: workspaceRoot });
|
|
1663
|
+
logCursorMcpConfigAudit(prefix, cursorAudit);
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
cleanupWarn(opts, `${prefix} Cursor MCP config audit failed: ${String(err)}`);
|
|
1666
|
+
}
|
|
494
1667
|
}
|
|
495
1668
|
}
|
|
496
1669
|
if (targetProjectId !== null || opts.ide) {
|
|
497
|
-
logReconnectNotice(prefix, opts.ide);
|
|
1670
|
+
logReconnectNotice(opts, prefix, opts.ide);
|
|
498
1671
|
}
|
|
499
1672
|
const allDaemonProcesses = await listProcesses();
|
|
500
1673
|
const { matching: projectProcesses, skipped: skippedProcesses } = filterProcessesForScope(
|
|
@@ -510,59 +1683,71 @@ async function runCleanup(opts) {
|
|
|
510
1683
|
processesToStop.push(proc);
|
|
511
1684
|
} else if (proc.ide === void 0) {
|
|
512
1685
|
ideSkippedProcesses.push(proc);
|
|
513
|
-
|
|
1686
|
+
cleanupLog(
|
|
1687
|
+
opts,
|
|
514
1688
|
`${prefix} Skipped daemon pid=${proc.pid} because IDE could not be safely determined.`
|
|
515
1689
|
);
|
|
516
1690
|
} else {
|
|
517
1691
|
ideSkippedProcesses.push(proc);
|
|
518
|
-
|
|
1692
|
+
cleanupLog(
|
|
1693
|
+
opts,
|
|
519
1694
|
`${prefix} Skipped daemon pid=${proc.pid} (IDE ${proc.ide} does not match filter ${opts.ide}).`
|
|
520
1695
|
);
|
|
521
1696
|
}
|
|
522
1697
|
}
|
|
523
1698
|
}
|
|
524
1699
|
const allSocketPaths = await listSocketPaths(targetProjectId);
|
|
525
|
-
const socketPaths = targetProjectId === null ? allSocketPaths : filterSocketPathsByIde(allSocketPaths, targetProjectId, opts.ide);
|
|
1700
|
+
const socketPaths = targetProjectId === null ? allSocketPaths : await filterSocketPathsByIde(allSocketPaths, targetProjectId, opts.ide);
|
|
526
1701
|
if (opts.allProjects) {
|
|
527
1702
|
const projectIds = /* @__PURE__ */ new Set();
|
|
528
1703
|
for (const proc of processesToStop) {
|
|
529
1704
|
projectIds.add(proc.projectId);
|
|
530
1705
|
}
|
|
531
1706
|
for (const socketPath of socketPaths) {
|
|
532
|
-
const id = extractProjectIdFromSocketFilename(
|
|
533
|
-
if (id)
|
|
1707
|
+
const id = extractProjectIdFromSocketFilename(path7.basename(socketPath));
|
|
1708
|
+
if (id) {
|
|
1709
|
+
projectIds.add(id);
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
const record = readBindingSidecarRecord(socketPath);
|
|
1713
|
+
if (record) {
|
|
1714
|
+
projectIds.add(record.projectId.trim().toLowerCase());
|
|
1715
|
+
}
|
|
534
1716
|
}
|
|
535
|
-
|
|
1717
|
+
cleanupLog(
|
|
1718
|
+
opts,
|
|
1719
|
+
`${prefix} Projects affected: ${projectIds.size ? [...projectIds].sort().join(", ") : "(none found)"}`
|
|
1720
|
+
);
|
|
536
1721
|
}
|
|
537
1722
|
if (processesToStop.length) {
|
|
538
|
-
|
|
1723
|
+
cleanupLog(opts, `${prefix} Daemon processes to stop:`);
|
|
539
1724
|
for (const proc of processesToStop) {
|
|
540
1725
|
const ideLabel = proc.ide ? ` ide=${proc.ide}` : "";
|
|
541
|
-
|
|
1726
|
+
cleanupLog(opts, `${prefix} pid=${proc.pid} project=${proc.projectId}${ideLabel}`);
|
|
542
1727
|
}
|
|
543
1728
|
} else if (ideSkippedProcesses.length) {
|
|
544
|
-
|
|
1729
|
+
cleanupLog(opts, `${prefix} No matching daemon processes for IDE filter ${opts.ide}.`);
|
|
545
1730
|
} else {
|
|
546
|
-
|
|
1731
|
+
cleanupLog(opts, `${prefix} No matching daemon processes found.`);
|
|
547
1732
|
}
|
|
548
1733
|
if (socketPaths.length) {
|
|
549
|
-
|
|
1734
|
+
cleanupLog(opts, `${prefix} Sockets to remove:`);
|
|
550
1735
|
for (const socketPath of socketPaths) {
|
|
551
|
-
|
|
1736
|
+
cleanupLog(opts, `${prefix} ${socketPath}`);
|
|
552
1737
|
}
|
|
553
1738
|
} else {
|
|
554
|
-
|
|
1739
|
+
cleanupLog(opts, `${prefix} No matching sockets found under ${getMcpBaseDir()}.`);
|
|
555
1740
|
}
|
|
556
1741
|
if (skippedProcesses.length) {
|
|
557
|
-
|
|
1742
|
+
cleanupLog(opts, `${prefix} Skipped unrelated daemon processes:`);
|
|
558
1743
|
for (const proc of skippedProcesses) {
|
|
559
|
-
|
|
1744
|
+
cleanupLog(opts, `${prefix} pid=${proc.pid} project=${proc.projectId}`);
|
|
560
1745
|
}
|
|
561
1746
|
}
|
|
562
1747
|
if (opts.allProjects && !opts.dryRun) {
|
|
563
1748
|
const ok = opts.assumeYes ? true : await confirm(`${prefix} Proceed with cleanup for ALL projects?`);
|
|
564
1749
|
if (!ok) {
|
|
565
|
-
|
|
1750
|
+
cleanupLog(opts, `${prefix} Aborted.`);
|
|
566
1751
|
return {
|
|
567
1752
|
exitCode: 1,
|
|
568
1753
|
workspaceRoot,
|
|
@@ -578,7 +1763,7 @@ async function runCleanup(opts) {
|
|
|
578
1763
|
const killedPids = [];
|
|
579
1764
|
const removedSockets = [];
|
|
580
1765
|
if (opts.dryRun) {
|
|
581
|
-
|
|
1766
|
+
cleanupLog(opts, `${prefix} Dry run complete \u2014 no processes stopped, no sockets removed.`);
|
|
582
1767
|
return {
|
|
583
1768
|
exitCode: 0,
|
|
584
1769
|
workspaceRoot,
|
|
@@ -593,9 +1778,9 @@ async function runCleanup(opts) {
|
|
|
593
1778
|
try {
|
|
594
1779
|
await killProcess(proc.pid);
|
|
595
1780
|
killedPids.push(proc.pid);
|
|
596
|
-
|
|
1781
|
+
cleanupLog(opts, `${prefix} Stopped daemon pid=${proc.pid} project=${proc.projectId}`);
|
|
597
1782
|
} catch (err) {
|
|
598
|
-
|
|
1783
|
+
cleanupWarn(opts, `${prefix} Could not stop pid=${proc.pid}: ${String(err)}`);
|
|
599
1784
|
}
|
|
600
1785
|
}
|
|
601
1786
|
if (killedPids.length) {
|
|
@@ -605,15 +1790,15 @@ async function runCleanup(opts) {
|
|
|
605
1790
|
try {
|
|
606
1791
|
await removeSocket(socketPath);
|
|
607
1792
|
removedSockets.push(socketPath);
|
|
608
|
-
|
|
1793
|
+
cleanupLog(opts, `${prefix} Removed socket ${socketPath}`);
|
|
609
1794
|
} catch (err) {
|
|
610
1795
|
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
611
1796
|
if (code !== "ENOENT") {
|
|
612
|
-
|
|
1797
|
+
cleanupWarn(opts, `${prefix} Could not remove socket ${socketPath}: ${String(err)}`);
|
|
613
1798
|
}
|
|
614
1799
|
}
|
|
615
1800
|
}
|
|
616
|
-
|
|
1801
|
+
cleanupLog(opts, `${prefix} Done. stopped=${killedPids.length} socketsRemoved=${removedSockets.length}`);
|
|
617
1802
|
return {
|
|
618
1803
|
exitCode: 0,
|
|
619
1804
|
workspaceRoot,
|
|
@@ -685,209 +1870,432 @@ async function cliCleanup(argv) {
|
|
|
685
1870
|
return result.exitCode;
|
|
686
1871
|
}
|
|
687
1872
|
|
|
688
|
-
// src/
|
|
689
|
-
var
|
|
690
|
-
var
|
|
691
|
-
var
|
|
692
|
-
var
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
await fs4.access(filePath);
|
|
708
|
-
return true;
|
|
709
|
-
} catch {
|
|
710
|
-
return false;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
function stripLeadingLineComments(text) {
|
|
714
|
-
return text.split("\n").filter((line) => !/^\s*\/\//.test(line)).join("\n");
|
|
715
|
-
}
|
|
716
|
-
function getKnownCursorGlobalMcpConfigCandidates(homeDir) {
|
|
717
|
-
return [path4.join(homeDir, ".cursor", "mcp.json")];
|
|
718
|
-
}
|
|
719
|
-
async function detectCursorGlobalMcpConfig(options) {
|
|
720
|
-
if (options?.explicitPath) {
|
|
721
|
-
return { ok: true, path: options.explicitPath, detectedExisting: await pathExists(options.explicitPath) };
|
|
722
|
-
}
|
|
723
|
-
const homeDir = options?.homeDir ?? os2.homedir();
|
|
724
|
-
const candidates = getKnownCursorGlobalMcpConfigCandidates(homeDir);
|
|
725
|
-
const existing = [];
|
|
726
|
-
for (const candidate of candidates) {
|
|
727
|
-
if (await pathExists(candidate)) existing.push(candidate);
|
|
728
|
-
}
|
|
729
|
-
if (existing.length > 1) {
|
|
730
|
-
return {
|
|
731
|
-
ok: false,
|
|
732
|
-
error: "[setup-ide-files] Multiple Cursor global MCP config paths found. Specify one explicitly.",
|
|
733
|
-
candidates: existing
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
if (existing.length === 1) {
|
|
737
|
-
return { ok: true, path: existing[0], detectedExisting: true };
|
|
738
|
-
}
|
|
739
|
-
const defaultPath = candidates[0];
|
|
740
|
-
if (!defaultPath) {
|
|
741
|
-
return {
|
|
742
|
-
ok: false,
|
|
743
|
-
error: "[setup-ide-files] No known Cursor global MCP config path.",
|
|
744
|
-
candidates: []
|
|
745
|
-
};
|
|
746
|
-
}
|
|
747
|
-
return { ok: true, path: defaultPath, detectedExisting: false };
|
|
748
|
-
}
|
|
749
|
-
function formatBackupTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
750
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
751
|
-
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
1873
|
+
// src/jetbrainsMcpConfig.ts
|
|
1874
|
+
var fs6 = __toESM(require("fs/promises"), 1);
|
|
1875
|
+
var os3 = __toESM(require("os"), 1);
|
|
1876
|
+
var path8 = __toESM(require("path"), 1);
|
|
1877
|
+
var import_node_child_process4 = require("child_process");
|
|
1878
|
+
|
|
1879
|
+
// src/configUtils.ts
|
|
1880
|
+
var DEV_API_URL = "http://localhost:3001";
|
|
1881
|
+
|
|
1882
|
+
// src/jetbrainsMcpConfig.ts
|
|
1883
|
+
var PROD_API_URL = "https://api.memoraone.com";
|
|
1884
|
+
var JETBRAINS_DEBUG_ENV_VARS = [
|
|
1885
|
+
"MEMORAONE_DEBUG_INIT",
|
|
1886
|
+
"MEMORAONE_DEBUG_MINIMAL_TOOLS",
|
|
1887
|
+
"MEMORAONE_DEBUG_MINIMAL_INITIALIZE",
|
|
1888
|
+
"MEMORAONE_DEBUG_DIRECT_STDIO"
|
|
1889
|
+
];
|
|
1890
|
+
function stripLeadingLineComments2(text) {
|
|
1891
|
+
return text.split("\n").filter((line) => !/^\s*\/\//.test(line)).join("\n");
|
|
752
1892
|
}
|
|
753
|
-
async function
|
|
1893
|
+
async function pathExists2(filePath) {
|
|
754
1894
|
try {
|
|
755
|
-
|
|
756
|
-
if (process.platform !== "win32") {
|
|
757
|
-
try {
|
|
758
|
-
await fs4.access(npxPath, fs4.constants.X_OK);
|
|
759
|
-
} catch {
|
|
760
|
-
return false;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
await execFileAsync2(npxPath, ["--version"], { timeout: 1e4 });
|
|
1895
|
+
await fs6.access(filePath);
|
|
764
1896
|
return true;
|
|
765
1897
|
} catch {
|
|
766
1898
|
return false;
|
|
767
1899
|
}
|
|
768
1900
|
}
|
|
769
|
-
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1901
|
+
function formatJetBrainsBackupTimestamp(d = /* @__PURE__ */ new Date()) {
|
|
1902
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1903
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
1904
|
+
}
|
|
1905
|
+
function getJetBrainsGlobalMcpConfigPath(homeDir) {
|
|
1906
|
+
return path8.join(homeDir, ".ai", "mcp", "mcp.json");
|
|
1907
|
+
}
|
|
1908
|
+
function getJetBrainsProjectMcpConfigPaths(repoRoot) {
|
|
1909
|
+
return [
|
|
1910
|
+
{ kind: "project-ai", path: path8.join(repoRoot, ".ai", "mcp", "mcp.json") },
|
|
1911
|
+
{ kind: "project-ij", path: path8.join(repoRoot, ".ij", "mcp", "mcp.json") }
|
|
1912
|
+
];
|
|
1913
|
+
}
|
|
1914
|
+
function getKnownJetBrainsMcpConfigLocations(homeDir, repoRoot) {
|
|
1915
|
+
return [
|
|
1916
|
+
{ kind: "global", path: getJetBrainsGlobalMcpConfigPath(homeDir) },
|
|
1917
|
+
...getJetBrainsProjectMcpConfigPaths(repoRoot)
|
|
1918
|
+
];
|
|
1919
|
+
}
|
|
1920
|
+
async function isZeroByteConfigFile(filePath) {
|
|
1921
|
+
if (!await pathExists2(filePath)) return false;
|
|
1922
|
+
const stat2 = await fs6.stat(filePath);
|
|
1923
|
+
return stat2.size === 0;
|
|
1924
|
+
}
|
|
1925
|
+
function buildMemoraoneJetBrainsMcpServer(options) {
|
|
1926
|
+
const env = {
|
|
1927
|
+
MEMORAONE_API_URL: options.devMode ? DEV_API_URL : PROD_API_URL,
|
|
1928
|
+
MEMORAONE_IDE_TYPE: "jetbrains",
|
|
1929
|
+
MEMORAONE_M1_PATH: options.m1Path
|
|
1930
|
+
};
|
|
1931
|
+
if (options.devMode) {
|
|
1932
|
+
env.MEMORAONE_DEV_MODE = "1";
|
|
796
1933
|
}
|
|
797
|
-
return
|
|
1934
|
+
return {
|
|
1935
|
+
command: options.command,
|
|
1936
|
+
args: options.args,
|
|
1937
|
+
env
|
|
1938
|
+
};
|
|
798
1939
|
}
|
|
799
|
-
function
|
|
1940
|
+
function mergeJetBrainsMcpConfigObject(existing, memoraone) {
|
|
800
1941
|
const base = existing && typeof existing === "object" ? { ...existing } : { mcpServers: {} };
|
|
801
1942
|
const mcpServers = typeof base.mcpServers === "object" && base.mcpServers !== null && !Array.isArray(base.mcpServers) ? { ...base.mcpServers } : {};
|
|
802
|
-
mcpServers.memoraone =
|
|
1943
|
+
mcpServers.memoraone = memoraone;
|
|
803
1944
|
return { ...base, mcpServers };
|
|
804
1945
|
}
|
|
805
|
-
function memoraoneServerMatches(server,
|
|
1946
|
+
function memoraoneServerMatches(server, expected) {
|
|
806
1947
|
if (!server || typeof server !== "object") return false;
|
|
807
1948
|
const s = server;
|
|
808
|
-
if (s.command !==
|
|
809
|
-
if (!Array.isArray(s.args) || s.args.length !==
|
|
810
|
-
|
|
1949
|
+
if (s.command !== expected.command) return false;
|
|
1950
|
+
if (!Array.isArray(s.args) || s.args.length !== expected.args.length) return false;
|
|
1951
|
+
for (let i = 0; i < expected.args.length; i += 1) {
|
|
1952
|
+
if (s.args[i] !== expected.args[i]) return false;
|
|
1953
|
+
}
|
|
811
1954
|
const env = s.env;
|
|
812
1955
|
if (!env || typeof env !== "object") return false;
|
|
813
1956
|
const e = env;
|
|
814
|
-
|
|
1957
|
+
for (const [key, value] of Object.entries(expected.env)) {
|
|
1958
|
+
if (e[key] !== value) return false;
|
|
1959
|
+
}
|
|
1960
|
+
for (const debugKey of JETBRAINS_DEBUG_ENV_VARS) {
|
|
1961
|
+
if (debugKey in e) return false;
|
|
1962
|
+
}
|
|
1963
|
+
return true;
|
|
815
1964
|
}
|
|
816
|
-
function
|
|
1965
|
+
function validateJetBrainsMcpConfig(parsed, expected) {
|
|
817
1966
|
if (!parsed || typeof parsed !== "object") {
|
|
818
|
-
throw new Error("[setup-ide-files]
|
|
1967
|
+
throw new Error("[setup-ide-files] JetBrains MCP config must be a JSON object.");
|
|
819
1968
|
}
|
|
820
1969
|
const mcpServers = parsed.mcpServers;
|
|
821
1970
|
if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
|
|
822
|
-
throw new Error("[setup-ide-files]
|
|
1971
|
+
throw new Error("[setup-ide-files] JetBrains MCP config missing mcpServers object.");
|
|
823
1972
|
}
|
|
824
1973
|
const memoraone = mcpServers.memoraone;
|
|
825
|
-
if (!memoraoneServerMatches(memoraone,
|
|
1974
|
+
if (!memoraoneServerMatches(memoraone, expected)) {
|
|
1975
|
+
throw new Error(
|
|
1976
|
+
"[setup-ide-files] JetBrains MCP config mcpServers.memoraone is missing or invalid."
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
async function readJsonConfig(filePath) {
|
|
1981
|
+
const raw = await fs6.readFile(filePath, "utf8");
|
|
1982
|
+
if (raw.trim() === "") return null;
|
|
1983
|
+
return JSON.parse(stripLeadingLineComments2(raw));
|
|
1984
|
+
}
|
|
1985
|
+
async function backupConfigFile(filePath) {
|
|
1986
|
+
const backupPath = `${filePath}.bak-${formatJetBrainsBackupTimestamp()}`;
|
|
1987
|
+
await fs6.copyFile(filePath, backupPath);
|
|
1988
|
+
return backupPath;
|
|
1989
|
+
}
|
|
1990
|
+
async function repairZeroByteConfigFile(filePath, dryRun) {
|
|
1991
|
+
if (!await isZeroByteConfigFile(filePath)) {
|
|
1992
|
+
return { repaired: false };
|
|
1993
|
+
}
|
|
1994
|
+
if (dryRun) {
|
|
1995
|
+
return { repaired: true, backupPath: `${filePath}.bak-<timestamp>` };
|
|
1996
|
+
}
|
|
1997
|
+
const backupPath = await backupConfigFile(filePath);
|
|
1998
|
+
await fs6.unlink(filePath);
|
|
1999
|
+
return { repaired: true, backupPath };
|
|
2000
|
+
}
|
|
2001
|
+
function configHasMemoraone(parsed) {
|
|
2002
|
+
if (!parsed) return false;
|
|
2003
|
+
const mcpServers = parsed.mcpServers;
|
|
2004
|
+
if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) return false;
|
|
2005
|
+
return Boolean(mcpServers.memoraone);
|
|
2006
|
+
}
|
|
2007
|
+
async function removeMemoraoneFromProjectConfig(options) {
|
|
2008
|
+
const { configPath, dryRun } = options;
|
|
2009
|
+
if (!await pathExists2(configPath)) {
|
|
2010
|
+
return { changed: false };
|
|
2011
|
+
}
|
|
2012
|
+
let parsed = null;
|
|
2013
|
+
try {
|
|
2014
|
+
parsed = await readJsonConfig(configPath);
|
|
2015
|
+
} catch {
|
|
2016
|
+
return { changed: false };
|
|
2017
|
+
}
|
|
2018
|
+
if (!configHasMemoraone(parsed)) {
|
|
2019
|
+
return { changed: false };
|
|
2020
|
+
}
|
|
2021
|
+
if (dryRun) {
|
|
2022
|
+
return { changed: true, backupPath: `${configPath}.bak-<timestamp>` };
|
|
2023
|
+
}
|
|
2024
|
+
const backupPath = await backupConfigFile(configPath);
|
|
2025
|
+
const mcpServers = parsed && typeof parsed.mcpServers === "object" && parsed.mcpServers !== null && !Array.isArray(parsed.mcpServers) ? { ...parsed.mcpServers } : {};
|
|
2026
|
+
delete mcpServers.memoraone;
|
|
2027
|
+
const hasOtherServers = Object.keys(mcpServers).length > 0;
|
|
2028
|
+
if (!hasOtherServers) {
|
|
2029
|
+
await fs6.unlink(configPath);
|
|
2030
|
+
return { changed: true, backupPath };
|
|
2031
|
+
}
|
|
2032
|
+
const next = { ...parsed, mcpServers };
|
|
2033
|
+
await fs6.mkdir(path8.dirname(configPath), { recursive: true });
|
|
2034
|
+
await fs6.writeFile(configPath, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
2035
|
+
return { changed: true, backupPath };
|
|
2036
|
+
}
|
|
2037
|
+
async function resolveLocalCliPathAsync() {
|
|
2038
|
+
const here = process.argv[1] ? path8.dirname(path8.resolve(process.argv[1])) : process.cwd();
|
|
2039
|
+
const candidates = [
|
|
2040
|
+
path8.join(here, "cli.cjs"),
|
|
2041
|
+
path8.join(here, "..", "dist", "cli.cjs"),
|
|
2042
|
+
path8.join(here, "..", "..", "dist", "cli.cjs")
|
|
2043
|
+
];
|
|
2044
|
+
for (const candidate of candidates) {
|
|
2045
|
+
if (await pathExists2(candidate)) {
|
|
2046
|
+
return path8.resolve(candidate);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
return null;
|
|
2050
|
+
}
|
|
2051
|
+
async function buildJetBrainsMemoraoneServer(options) {
|
|
2052
|
+
if (options.devMode) {
|
|
2053
|
+
let cliPath = options.cliPathOverride;
|
|
2054
|
+
if (cliPath === void 0) {
|
|
2055
|
+
cliPath = await resolveLocalCliPathAsync();
|
|
2056
|
+
}
|
|
2057
|
+
if (!cliPath) {
|
|
2058
|
+
throw new Error(
|
|
2059
|
+
"[setup-ide-files] Dev mode requires a built CLI at packages/mcp/dist/cli.cjs. Run pnpm build first."
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
return buildMemoraoneJetBrainsMcpServer({
|
|
2063
|
+
command: process.execPath,
|
|
2064
|
+
args: [cliPath],
|
|
2065
|
+
m1Path: options.m1Path,
|
|
2066
|
+
devMode: true
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
let npxPath = options.npxPathOverride;
|
|
2070
|
+
if (npxPath === void 0) {
|
|
2071
|
+
npxPath = await resolveNpxPath();
|
|
2072
|
+
}
|
|
2073
|
+
if (!npxPath) {
|
|
826
2074
|
throw new Error(
|
|
827
|
-
"[setup-ide-files]
|
|
2075
|
+
"[setup-ide-files] Could not resolve a working npx executable. Install Node.js/npm or ensure npx is on PATH before configuring JetBrains MCP."
|
|
828
2076
|
);
|
|
829
2077
|
}
|
|
2078
|
+
return buildMemoraoneJetBrainsMcpServer({
|
|
2079
|
+
command: npxPath,
|
|
2080
|
+
args: ["-y", "@memoraone/mcp@latest"],
|
|
2081
|
+
m1Path: options.m1Path,
|
|
2082
|
+
devMode: false
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
async function verifyJetBrainsMcpHandshake(options) {
|
|
2086
|
+
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
2087
|
+
const { server } = options;
|
|
2088
|
+
return new Promise((resolve8) => {
|
|
2089
|
+
let settled = false;
|
|
2090
|
+
const finish = (ok, detail) => {
|
|
2091
|
+
if (settled) return;
|
|
2092
|
+
settled = true;
|
|
2093
|
+
clearTimeout(timer);
|
|
2094
|
+
try {
|
|
2095
|
+
child.kill();
|
|
2096
|
+
} catch {
|
|
2097
|
+
}
|
|
2098
|
+
resolve8({ ok, detail });
|
|
2099
|
+
};
|
|
2100
|
+
const child = (0, import_node_child_process4.spawn)(server.command, [...server.args], {
|
|
2101
|
+
env: { ...process.env, ...server.env },
|
|
2102
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2103
|
+
});
|
|
2104
|
+
let buffer = "";
|
|
2105
|
+
let initializeOk = false;
|
|
2106
|
+
let toolsListOk = false;
|
|
2107
|
+
let nextId = 1;
|
|
2108
|
+
const send = (method, params) => {
|
|
2109
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", id: nextId, method, params }) + "\n";
|
|
2110
|
+
nextId += 1;
|
|
2111
|
+
child.stdin?.write(msg);
|
|
2112
|
+
};
|
|
2113
|
+
const timer = setTimeout(() => {
|
|
2114
|
+
finish(false, `handshake timed out after ${timeoutMs}ms`);
|
|
2115
|
+
}, timeoutMs);
|
|
2116
|
+
child.stdout?.on("data", (chunk) => {
|
|
2117
|
+
buffer += chunk.toString("utf8");
|
|
2118
|
+
const lines = buffer.split("\n");
|
|
2119
|
+
buffer = lines.pop() ?? "";
|
|
2120
|
+
for (const line of lines) {
|
|
2121
|
+
if (!line.trim()) continue;
|
|
2122
|
+
let msg;
|
|
2123
|
+
try {
|
|
2124
|
+
msg = JSON.parse(line);
|
|
2125
|
+
} catch {
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
if (msg.id === 1 && msg.result) {
|
|
2129
|
+
initializeOk = true;
|
|
2130
|
+
send("notifications/initialized", {});
|
|
2131
|
+
send("tools/list", {});
|
|
2132
|
+
}
|
|
2133
|
+
if (msg.id === 2 && msg.result) {
|
|
2134
|
+
toolsListOk = true;
|
|
2135
|
+
finish(true, "initialize OK; tools/list OK");
|
|
2136
|
+
}
|
|
2137
|
+
if (msg.error) {
|
|
2138
|
+
finish(false, `JSON-RPC error: ${JSON.stringify(msg.error)}`);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
});
|
|
2142
|
+
child.on("error", (err) => {
|
|
2143
|
+
finish(false, `spawn error: ${String(err)}`);
|
|
2144
|
+
});
|
|
2145
|
+
child.on("exit", (code) => {
|
|
2146
|
+
if (!settled) {
|
|
2147
|
+
if (initializeOk && toolsListOk) {
|
|
2148
|
+
finish(true, "initialize OK; tools/list OK");
|
|
2149
|
+
} else {
|
|
2150
|
+
finish(
|
|
2151
|
+
false,
|
|
2152
|
+
`process exited code=${code ?? "null"} (initialize=${initializeOk}, tools/list=${toolsListOk})`
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
send("initialize", {
|
|
2158
|
+
protocolVersion: "2024-11-05",
|
|
2159
|
+
capabilities: {},
|
|
2160
|
+
clientInfo: { name: "memoraone-setup", version: "1.0.0" }
|
|
2161
|
+
});
|
|
2162
|
+
});
|
|
830
2163
|
}
|
|
831
|
-
async function
|
|
832
|
-
const
|
|
833
|
-
const
|
|
2164
|
+
async function setupJetBrainsMcpConfig(options) {
|
|
2165
|
+
const homeDir = options.homeDir ?? os3.homedir();
|
|
2166
|
+
const globalPath = options.globalConfigPath ?? getJetBrainsGlobalMcpConfigPath(homeDir);
|
|
2167
|
+
const m1Path = path8.join(path8.resolve(options.repoRoot), "memoraone.m1");
|
|
2168
|
+
const repairActions = [];
|
|
2169
|
+
const allLocations = getKnownJetBrainsMcpConfigLocations(homeDir, options.repoRoot);
|
|
2170
|
+
for (const location of allLocations) {
|
|
2171
|
+
if (await pathExists2(location.path)) {
|
|
2172
|
+
repairActions.push({ type: "found-config", location });
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
for (const location of allLocations) {
|
|
2176
|
+
const zeroByte = await repairZeroByteConfigFile(location.path, options.dryRun);
|
|
2177
|
+
if (zeroByte.repaired) {
|
|
2178
|
+
repairActions.push({
|
|
2179
|
+
type: "repaired-zero-byte",
|
|
2180
|
+
path: location.path,
|
|
2181
|
+
backupPath: zeroByte.backupPath ?? `${location.path}.bak-<timestamp>`
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
const memoraone = await buildJetBrainsMemoraoneServer({
|
|
2186
|
+
m1Path,
|
|
2187
|
+
devMode: options.devMode,
|
|
2188
|
+
npxPathOverride: options.npxPathOverride,
|
|
2189
|
+
cliPathOverride: options.cliPathOverride
|
|
2190
|
+
});
|
|
2191
|
+
for (const location of getJetBrainsProjectMcpConfigPaths(options.repoRoot)) {
|
|
2192
|
+
const removal = await removeMemoraoneFromProjectConfig({
|
|
2193
|
+
configPath: location.path,
|
|
2194
|
+
dryRun: options.dryRun
|
|
2195
|
+
});
|
|
2196
|
+
if (removal.changed) {
|
|
2197
|
+
if (removal.backupPath) {
|
|
2198
|
+
repairActions.push({
|
|
2199
|
+
type: "backed-up-conflicting-project-config",
|
|
2200
|
+
path: location.path,
|
|
2201
|
+
backupPath: removal.backupPath
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
repairActions.push({ type: "removed-project-memoraone", path: location.path });
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
const existed = await pathExists2(globalPath);
|
|
834
2208
|
let existing = null;
|
|
835
2209
|
if (existed) {
|
|
836
|
-
const raw = await fs4.readFile(configPath, "utf8");
|
|
837
2210
|
try {
|
|
838
|
-
existing =
|
|
2211
|
+
existing = await readJsonConfig(globalPath);
|
|
839
2212
|
} catch {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
2213
|
+
if (options.dryRun) {
|
|
2214
|
+
existing = null;
|
|
2215
|
+
} else {
|
|
2216
|
+
const backupPath2 = await backupConfigFile(globalPath);
|
|
2217
|
+
repairActions.push({
|
|
2218
|
+
type: "repaired-zero-byte",
|
|
2219
|
+
path: globalPath,
|
|
2220
|
+
backupPath: backupPath2
|
|
2221
|
+
});
|
|
2222
|
+
await fs6.unlink(globalPath);
|
|
2223
|
+
existing = null;
|
|
2224
|
+
}
|
|
843
2225
|
}
|
|
844
2226
|
}
|
|
845
|
-
const merged =
|
|
2227
|
+
const merged = mergeJetBrainsMcpConfigObject(existing, memoraone);
|
|
846
2228
|
const body = JSON.stringify(merged, null, 2) + "\n";
|
|
847
|
-
if (existed) {
|
|
848
|
-
const currentMemoraone = existing && typeof existing.mcpServers === "object" &&
|
|
849
|
-
if (memoraoneServerMatches(currentMemoraone,
|
|
850
|
-
|
|
2229
|
+
if (existed && existing) {
|
|
2230
|
+
const currentMemoraone = existing.mcpServers && typeof existing.mcpServers === "object" && !Array.isArray(existing.mcpServers) ? existing.mcpServers.memoraone : void 0;
|
|
2231
|
+
if (memoraoneServerMatches(currentMemoraone, memoraone)) {
|
|
2232
|
+
repairActions.push({ type: "wrote-global-config", path: globalPath, outcome: "skipped" });
|
|
2233
|
+
return { outcome: "skipped", repairActions, memoraone };
|
|
851
2234
|
}
|
|
852
2235
|
}
|
|
853
|
-
if (dryRun) {
|
|
854
|
-
|
|
2236
|
+
if (options.dryRun) {
|
|
2237
|
+
const outcome2 = existed ? "updated" : "created";
|
|
2238
|
+
repairActions.push({ type: "wrote-global-config", path: globalPath, outcome: outcome2 });
|
|
2239
|
+
return { outcome: outcome2, repairActions, memoraone };
|
|
855
2240
|
}
|
|
856
2241
|
let backupPath;
|
|
857
2242
|
if (existed) {
|
|
858
|
-
backupPath =
|
|
859
|
-
|
|
860
|
-
}
|
|
861
|
-
await
|
|
862
|
-
await
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
2243
|
+
backupPath = await backupConfigFile(globalPath);
|
|
2244
|
+
}
|
|
2245
|
+
await fs6.mkdir(path8.dirname(globalPath), { recursive: true });
|
|
2246
|
+
await fs6.writeFile(globalPath, body, "utf8");
|
|
2247
|
+
const verifyRaw = await fs6.readFile(globalPath, "utf8");
|
|
2248
|
+
const verifyParsed = JSON.parse(stripLeadingLineComments2(verifyRaw));
|
|
2249
|
+
validateJetBrainsMcpConfig(verifyParsed, memoraone);
|
|
2250
|
+
const outcome = existed ? "updated" : "created";
|
|
2251
|
+
repairActions.push({ type: "wrote-global-config", path: globalPath, outcome });
|
|
2252
|
+
let verifyOk;
|
|
2253
|
+
let verifyDetail;
|
|
2254
|
+
if (options.verify !== false) {
|
|
2255
|
+
const verify = await verifyJetBrainsMcpHandshake({ server: memoraone });
|
|
2256
|
+
verifyOk = verify.ok;
|
|
2257
|
+
verifyDetail = verify.detail;
|
|
2258
|
+
repairActions.push({ type: "verify-handshake", ok: verify.ok, detail: verify.detail });
|
|
2259
|
+
}
|
|
2260
|
+
return { outcome, backupPath, repairActions, verifyOk, verifyDetail, memoraone };
|
|
2261
|
+
}
|
|
2262
|
+
function logJetBrainsMcpCliSummary(info, dryRun) {
|
|
2263
|
+
for (const action of info.repairActions) {
|
|
2264
|
+
if (action.type === "found-config") {
|
|
2265
|
+
console.log(`[setup-ide-files] Found JetBrains MCP config (${action.location.kind}): ${action.location.path}`);
|
|
2266
|
+
} else if (action.type === "repaired-zero-byte") {
|
|
2267
|
+
console.log(`[setup-ide-files] Repaired zero-byte MCP config: ${action.path}`);
|
|
2268
|
+
console.log(`[setup-ide-files] Backup: ${action.backupPath}`);
|
|
2269
|
+
} else if (action.type === "backed-up-conflicting-project-config") {
|
|
2270
|
+
console.log(`[setup-ide-files] Backed up conflicting project MCP config: ${action.path}`);
|
|
2271
|
+
console.log(`[setup-ide-files] Backup: ${action.backupPath}`);
|
|
2272
|
+
} else if (action.type === "removed-project-memoraone") {
|
|
2273
|
+
console.log(`[setup-ide-files] Removed project-scoped memoraone definition: ${action.path}`);
|
|
2274
|
+
} else if (action.type === "verify-handshake") {
|
|
2275
|
+
if (action.ok) {
|
|
2276
|
+
console.log(`[setup-ide-files] MCP handshake verification: ${action.detail}`);
|
|
2277
|
+
} else {
|
|
2278
|
+
console.log(`[setup-ide-files] MCP handshake verification skipped/failed: ${action.detail}`);
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
874
2281
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
);
|
|
883
|
-
} else if (outcome === "skipped") {
|
|
884
|
-
console.log(`[setup-ide-files] Cursor global MCP config unchanged: ${configPath}`);
|
|
2282
|
+
const prefix = dryRun ? "would be " : "";
|
|
2283
|
+
if (info.outcome === "created") {
|
|
2284
|
+
console.log(`[setup-ide-files] JetBrains global MCP config ${prefix}created: ${info.activeConfigPath}`);
|
|
2285
|
+
} else if (info.outcome === "updated") {
|
|
2286
|
+
console.log(`[setup-ide-files] JetBrains global MCP config ${prefix}updated: ${info.activeConfigPath}`);
|
|
2287
|
+
} else {
|
|
2288
|
+
console.log(`[setup-ide-files] JetBrains global MCP config unchanged: ${info.activeConfigPath}`);
|
|
885
2289
|
}
|
|
2290
|
+
if (info.backupPath) {
|
|
2291
|
+
console.log(`[setup-ide-files] JetBrains global MCP config backup: ${info.backupPath}`);
|
|
2292
|
+
}
|
|
2293
|
+
if (info.npxPath) {
|
|
2294
|
+
console.log(`[setup-ide-files] Resolved npx: ${info.npxPath}`);
|
|
2295
|
+
}
|
|
2296
|
+
console.log(`[setup-ide-files] Final active JetBrains MCP config: ${info.activeConfigPath}`);
|
|
886
2297
|
console.log(
|
|
887
|
-
"[setup-ide-files]
|
|
888
|
-
);
|
|
889
|
-
console.log(
|
|
890
|
-
"[setup-ide-files] Fully quit Cursor and reopen this repo for MCP changes to take effect."
|
|
2298
|
+
"[setup-ide-files] Fully quit JetBrains IDE and reopen this repo for MCP changes to take effect."
|
|
891
2299
|
);
|
|
892
2300
|
}
|
|
893
2301
|
|
|
@@ -906,15 +2314,15 @@ function buildMemoraoneMcpServer(ideType, command = "npx") {
|
|
|
906
2314
|
};
|
|
907
2315
|
}
|
|
908
2316
|
function assertUnderRepoRoot(repoRoot, absPath) {
|
|
909
|
-
const normRoot =
|
|
910
|
-
const normPath =
|
|
911
|
-
if (normPath !==
|
|
2317
|
+
const normRoot = path9.resolve(repoRoot) + path9.sep;
|
|
2318
|
+
const normPath = path9.resolve(absPath);
|
|
2319
|
+
if (normPath !== path9.resolve(repoRoot) && !normPath.startsWith(normRoot)) {
|
|
912
2320
|
throw new Error(`[setup-ide-files] Refusing to write outside repo root: ${absPath}`);
|
|
913
2321
|
}
|
|
914
2322
|
}
|
|
915
|
-
async function
|
|
2323
|
+
async function pathExists3(filePath) {
|
|
916
2324
|
try {
|
|
917
|
-
await
|
|
2325
|
+
await fs7.access(filePath);
|
|
918
2326
|
return true;
|
|
919
2327
|
} catch {
|
|
920
2328
|
return false;
|
|
@@ -935,12 +2343,12 @@ ${GITIGNORE_MEMORAONE_ENTRY}
|
|
|
935
2343
|
}
|
|
936
2344
|
async function ensureGitignoreMemoraone(repoRoot, opts) {
|
|
937
2345
|
if (opts.noGitignore) return "skipped";
|
|
938
|
-
const abs =
|
|
2346
|
+
const abs = path9.join(repoRoot, ".gitignore");
|
|
939
2347
|
assertUnderRepoRoot(repoRoot, abs);
|
|
940
2348
|
let prior = "";
|
|
941
2349
|
let existed = false;
|
|
942
2350
|
try {
|
|
943
|
-
prior = await
|
|
2351
|
+
prior = await fs7.readFile(abs, "utf8");
|
|
944
2352
|
existed = true;
|
|
945
2353
|
} catch (err) {
|
|
946
2354
|
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
@@ -951,25 +2359,25 @@ async function ensureGitignoreMemoraone(repoRoot, opts) {
|
|
|
951
2359
|
const separator = existed && prior.length > 0 ? prior.endsWith("\n") ? "\n" : "\n\n" : "";
|
|
952
2360
|
const next = (existed ? prior : "") + separator + block;
|
|
953
2361
|
if (opts.dryRun) return existed ? "updated" : "created";
|
|
954
|
-
await
|
|
2362
|
+
await fs7.writeFile(abs, next, "utf8");
|
|
955
2363
|
return existed ? "updated" : "created";
|
|
956
2364
|
}
|
|
957
2365
|
async function findRepoRoot(startDir) {
|
|
958
|
-
let current =
|
|
959
|
-
const root =
|
|
2366
|
+
let current = path9.resolve(startDir);
|
|
2367
|
+
const root = path9.parse(current).root;
|
|
960
2368
|
while (true) {
|
|
961
|
-
const gitPath =
|
|
962
|
-
const m1Path =
|
|
963
|
-
if (await
|
|
2369
|
+
const gitPath = path9.join(current, ".git");
|
|
2370
|
+
const m1Path = path9.join(current, "memoraone.m1");
|
|
2371
|
+
if (await pathExists3(gitPath) || await pathExists3(m1Path)) {
|
|
964
2372
|
return current;
|
|
965
2373
|
}
|
|
966
2374
|
if (current === root) {
|
|
967
2375
|
return null;
|
|
968
2376
|
}
|
|
969
|
-
current =
|
|
2377
|
+
current = path9.dirname(current);
|
|
970
2378
|
}
|
|
971
2379
|
}
|
|
972
|
-
function
|
|
2380
|
+
function stripLeadingLineComments3(text) {
|
|
973
2381
|
return text.split("\n").filter((line) => !/^\s*\/\//.test(line)).join("\n");
|
|
974
2382
|
}
|
|
975
2383
|
function cursorRuleBody() {
|
|
@@ -977,7 +2385,7 @@ function cursorRuleBody() {
|
|
|
977
2385
|
|
|
978
2386
|
## MemoraOne MCP (IDE agent only)
|
|
979
2387
|
|
|
980
|
-
This repository uses **MemoraOne** via the MCP server named **
|
|
2388
|
+
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.
|
|
981
2389
|
|
|
982
2390
|
### Tools
|
|
983
2391
|
|
|
@@ -1019,43 +2427,47 @@ function buildVscodeMcpJsonBody(existing) {
|
|
|
1019
2427
|
const merged = { ...base, servers };
|
|
1020
2428
|
return mcpJsonHeader() + JSON.stringify(merged, null, 2) + "\n";
|
|
1021
2429
|
}
|
|
2430
|
+
function buildCursorMcpJsonBody(existing, npxPath, repoRoot) {
|
|
2431
|
+
const merged = mergeCursorRepoMcpConfigObject(existing, npxPath, repoRoot);
|
|
2432
|
+
return mcpJsonHeader() + JSON.stringify(merged, null, 2) + "\n";
|
|
2433
|
+
}
|
|
1022
2434
|
async function writeManagedMarkdown(repoRoot, relPath, fullContent, opts) {
|
|
1023
|
-
const abs =
|
|
2435
|
+
const abs = path9.join(repoRoot, relPath);
|
|
1024
2436
|
assertUnderRepoRoot(repoRoot, abs);
|
|
1025
2437
|
let prior = "";
|
|
1026
2438
|
let existed = false;
|
|
1027
2439
|
try {
|
|
1028
|
-
prior = await
|
|
2440
|
+
prior = await fs7.readFile(abs, "utf8");
|
|
1029
2441
|
existed = true;
|
|
1030
2442
|
} catch (err) {
|
|
1031
2443
|
if (err?.code !== "ENOENT") throw err;
|
|
1032
2444
|
}
|
|
1033
2445
|
if (!existed) {
|
|
1034
2446
|
if (opts.dryRun) return "created";
|
|
1035
|
-
await
|
|
1036
|
-
await
|
|
2447
|
+
await fs7.mkdir(path9.dirname(abs), { recursive: true });
|
|
2448
|
+
await fs7.writeFile(abs, fullContent, "utf8");
|
|
1037
2449
|
return "created";
|
|
1038
2450
|
}
|
|
1039
2451
|
if (prior.includes(MANAGED_MARKER)) {
|
|
1040
2452
|
if (prior === fullContent) return "skipped";
|
|
1041
2453
|
if (opts.dryRun) return "updated";
|
|
1042
|
-
await
|
|
1043
|
-
await
|
|
2454
|
+
await fs7.mkdir(path9.dirname(abs), { recursive: true });
|
|
2455
|
+
await fs7.writeFile(abs, fullContent, "utf8");
|
|
1044
2456
|
return "updated";
|
|
1045
2457
|
}
|
|
1046
2458
|
if (!opts.force) return "skipped-untracked";
|
|
1047
2459
|
if (opts.dryRun) return "updated";
|
|
1048
|
-
await
|
|
1049
|
-
await
|
|
2460
|
+
await fs7.mkdir(path9.dirname(abs), { recursive: true });
|
|
2461
|
+
await fs7.writeFile(abs, fullContent, "utf8");
|
|
1050
2462
|
return "updated";
|
|
1051
2463
|
}
|
|
1052
2464
|
async function writeIdeMcpJson(repoRoot, relPath, buildBody, opts) {
|
|
1053
|
-
const abs =
|
|
2465
|
+
const abs = path9.join(repoRoot, relPath);
|
|
1054
2466
|
assertUnderRepoRoot(repoRoot, abs);
|
|
1055
2467
|
let raw = "";
|
|
1056
2468
|
let existed = false;
|
|
1057
2469
|
try {
|
|
1058
|
-
raw = await
|
|
2470
|
+
raw = await fs7.readFile(abs, "utf8");
|
|
1059
2471
|
existed = true;
|
|
1060
2472
|
} catch (err) {
|
|
1061
2473
|
if (err?.code !== "ENOENT") throw err;
|
|
@@ -1063,15 +2475,15 @@ async function writeIdeMcpJson(repoRoot, relPath, buildBody, opts) {
|
|
|
1063
2475
|
if (!existed) {
|
|
1064
2476
|
const body = buildBody(null);
|
|
1065
2477
|
if (opts.dryRun) return "created";
|
|
1066
|
-
await
|
|
1067
|
-
await
|
|
2478
|
+
await fs7.mkdir(path9.dirname(abs), { recursive: true });
|
|
2479
|
+
await fs7.writeFile(abs, body, "utf8");
|
|
1068
2480
|
return "created";
|
|
1069
2481
|
}
|
|
1070
2482
|
const managed = raw.includes(MANAGED_MARKER);
|
|
1071
2483
|
if (!managed && !opts.force) return "skipped-untracked";
|
|
1072
2484
|
let parsed = null;
|
|
1073
2485
|
try {
|
|
1074
|
-
parsed = JSON.parse(
|
|
2486
|
+
parsed = JSON.parse(stripLeadingLineComments3(raw));
|
|
1075
2487
|
} catch {
|
|
1076
2488
|
parsed = null;
|
|
1077
2489
|
}
|
|
@@ -1079,8 +2491,8 @@ async function writeIdeMcpJson(repoRoot, relPath, buildBody, opts) {
|
|
|
1079
2491
|
const next = buildBody(parsed);
|
|
1080
2492
|
if (managed && next === raw) return "skipped";
|
|
1081
2493
|
if (opts.dryRun) return "updated";
|
|
1082
|
-
await
|
|
1083
|
-
await
|
|
2494
|
+
await fs7.mkdir(path9.dirname(abs), { recursive: true });
|
|
2495
|
+
await fs7.writeFile(abs, next, "utf8");
|
|
1084
2496
|
return "updated";
|
|
1085
2497
|
}
|
|
1086
2498
|
function parseSetupIdeFlags(argv) {
|
|
@@ -1092,6 +2504,8 @@ function parseSetupIdeFlags(argv) {
|
|
|
1092
2504
|
let dryRun = false;
|
|
1093
2505
|
let noGitignore = false;
|
|
1094
2506
|
let cleanup = false;
|
|
2507
|
+
let devMode = false;
|
|
2508
|
+
let repair = false;
|
|
1095
2509
|
const unknown = [];
|
|
1096
2510
|
for (const a of argv) {
|
|
1097
2511
|
if (a === "--cursor") cursor = true;
|
|
@@ -1102,6 +2516,8 @@ function parseSetupIdeFlags(argv) {
|
|
|
1102
2516
|
else if (a === "--dry-run") dryRun = true;
|
|
1103
2517
|
else if (a === "--no-gitignore") noGitignore = true;
|
|
1104
2518
|
else if (a === "--cleanup") cleanup = true;
|
|
2519
|
+
else if (a === "--dev") devMode = true;
|
|
2520
|
+
else if (a === "--repair") repair = true;
|
|
1105
2521
|
else if (a.startsWith("-")) unknown.push(a);
|
|
1106
2522
|
}
|
|
1107
2523
|
const specific = cursor || vscode || jetbrains;
|
|
@@ -1111,7 +2527,7 @@ function parseSetupIdeFlags(argv) {
|
|
|
1111
2527
|
} else {
|
|
1112
2528
|
targets = { cursor, vscode, jetbrains };
|
|
1113
2529
|
}
|
|
1114
|
-
return { targets, force, dryRun, noGitignore, cleanup, unknown };
|
|
2530
|
+
return { targets, force, dryRun, noGitignore, cleanup, devMode, repair, unknown };
|
|
1115
2531
|
}
|
|
1116
2532
|
function summarizeOutcomes(outcomes) {
|
|
1117
2533
|
const created = [];
|
|
@@ -1133,9 +2549,145 @@ function summarizeOutcomes(outcomes) {
|
|
|
1133
2549
|
}
|
|
1134
2550
|
console.log(lines.join("\n"));
|
|
1135
2551
|
}
|
|
2552
|
+
function ideTypesFromSetupTargets(targets) {
|
|
2553
|
+
const ides = [];
|
|
2554
|
+
if (targets.cursor) ides.push("cursor");
|
|
2555
|
+
if (targets.vscode) ides.push("copilot-vscode");
|
|
2556
|
+
if (targets.jetbrains) ides.push("jetbrains");
|
|
2557
|
+
return ides;
|
|
2558
|
+
}
|
|
2559
|
+
function setupTargetsAllIdes(targets) {
|
|
2560
|
+
return targets.cursor && targets.vscode && targets.jetbrains;
|
|
2561
|
+
}
|
|
2562
|
+
function aggregateCleanupResults(results) {
|
|
2563
|
+
const killedPids = /* @__PURE__ */ new Set();
|
|
2564
|
+
const removedSockets = /* @__PURE__ */ new Set();
|
|
2565
|
+
const skippedUnrelated = /* @__PURE__ */ new Map();
|
|
2566
|
+
let foundDaemonCount = 0;
|
|
2567
|
+
let error;
|
|
2568
|
+
for (const result of results) {
|
|
2569
|
+
if (result.error) error = result.error;
|
|
2570
|
+
for (const pid of result.killedPids) {
|
|
2571
|
+
killedPids.add(pid);
|
|
2572
|
+
foundDaemonCount += 1;
|
|
2573
|
+
}
|
|
2574
|
+
for (const socketPath of result.removedSockets) {
|
|
2575
|
+
removedSockets.add(socketPath);
|
|
2576
|
+
}
|
|
2577
|
+
for (const proc of result.skippedProcesses) {
|
|
2578
|
+
if (proc.projectId !== result.projectId) {
|
|
2579
|
+
skippedUnrelated.set(proc.pid, proc);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return {
|
|
2584
|
+
foundDaemonCount,
|
|
2585
|
+
stoppedDaemonCount: killedPids.size,
|
|
2586
|
+
removedSocketCount: removedSockets.size,
|
|
2587
|
+
skippedUnrelatedDaemonCount: skippedUnrelated.size,
|
|
2588
|
+
error
|
|
2589
|
+
};
|
|
2590
|
+
}
|
|
2591
|
+
function logSetupIdeCleanupSummary(cleanup) {
|
|
2592
|
+
if (cleanup.skipped) return;
|
|
2593
|
+
console.log(`[setup-ide-files] Project id: ${cleanup.projectId}`);
|
|
2594
|
+
if (cleanup.foundDaemonCount > 0) {
|
|
2595
|
+
console.log(`[setup-ide-files] Found ${cleanup.foundDaemonCount} stale daemon(s)`);
|
|
2596
|
+
if (cleanup.dryRun) {
|
|
2597
|
+
console.log(`[setup-ide-files] Would stop ${cleanup.foundDaemonCount} stale daemon(s)`);
|
|
2598
|
+
} else if (cleanup.stoppedDaemonCount > 0) {
|
|
2599
|
+
console.log(`[setup-ide-files] Stopped ${cleanup.stoppedDaemonCount} stale daemon(s)`);
|
|
2600
|
+
}
|
|
2601
|
+
} else {
|
|
2602
|
+
console.log("[setup-ide-files] No stale daemons found for this project and IDE target(s).");
|
|
2603
|
+
}
|
|
2604
|
+
if (cleanup.removedSocketCount > 0) {
|
|
2605
|
+
if (cleanup.dryRun) {
|
|
2606
|
+
console.log(`[setup-ide-files] Would remove ${cleanup.removedSocketCount} stale socket(s)`);
|
|
2607
|
+
} else {
|
|
2608
|
+
console.log(`[setup-ide-files] Removed ${cleanup.removedSocketCount} stale socket(s)`);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
if (cleanup.skippedUnrelatedDaemonCount > 0) {
|
|
2612
|
+
console.log(
|
|
2613
|
+
`[setup-ide-files] Skipped ${cleanup.skippedUnrelatedDaemonCount} unrelated project daemon(s)`
|
|
2614
|
+
);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
async function runSetupIdeDaemonCleanup(opts) {
|
|
2618
|
+
const ides = ideTypesFromSetupTargets(opts.targets);
|
|
2619
|
+
if (ides.length === 0) {
|
|
2620
|
+
return {
|
|
2621
|
+
skipped: true,
|
|
2622
|
+
skipReason: "no-targets",
|
|
2623
|
+
foundDaemonCount: 0,
|
|
2624
|
+
stoppedDaemonCount: 0,
|
|
2625
|
+
removedSocketCount: 0,
|
|
2626
|
+
skippedUnrelatedDaemonCount: 0,
|
|
2627
|
+
dryRun: opts.dryRun
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
const target = await resolveCleanupTarget(opts.repoRoot);
|
|
2631
|
+
if ("error" in target) {
|
|
2632
|
+
return {
|
|
2633
|
+
skipped: true,
|
|
2634
|
+
skipReason: "no-m1",
|
|
2635
|
+
foundDaemonCount: 0,
|
|
2636
|
+
stoppedDaemonCount: 0,
|
|
2637
|
+
removedSocketCount: 0,
|
|
2638
|
+
skippedUnrelatedDaemonCount: 0,
|
|
2639
|
+
dryRun: opts.dryRun
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
const baseCleanupOpts = {
|
|
2643
|
+
cwd: opts.repoRoot,
|
|
2644
|
+
dryRun: opts.dryRun,
|
|
2645
|
+
allProjects: false,
|
|
2646
|
+
assumeYes: true,
|
|
2647
|
+
projectId: target.projectId,
|
|
2648
|
+
quiet: true,
|
|
2649
|
+
listProcesses: opts.listProcesses,
|
|
2650
|
+
listSocketPaths: opts.listSocketPaths,
|
|
2651
|
+
killProcess: opts.killProcess,
|
|
2652
|
+
removeSocket: opts.removeSocket
|
|
2653
|
+
};
|
|
2654
|
+
const results = [];
|
|
2655
|
+
if (setupTargetsAllIdes(opts.targets)) {
|
|
2656
|
+
results.push(await runCleanup(baseCleanupOpts));
|
|
2657
|
+
} else {
|
|
2658
|
+
for (const ide of ides) {
|
|
2659
|
+
results.push(await runCleanup({ ...baseCleanupOpts, ide }));
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
const aggregated = aggregateCleanupResults(results);
|
|
2663
|
+
const exitError = results.find((r) => r.exitCode !== 0)?.error ?? aggregated.error;
|
|
2664
|
+
return {
|
|
2665
|
+
skipped: false,
|
|
2666
|
+
projectId: target.projectId,
|
|
2667
|
+
foundDaemonCount: aggregated.foundDaemonCount,
|
|
2668
|
+
stoppedDaemonCount: opts.dryRun ? 0 : aggregated.stoppedDaemonCount,
|
|
2669
|
+
removedSocketCount: aggregated.removedSocketCount,
|
|
2670
|
+
skippedUnrelatedDaemonCount: aggregated.skippedUnrelatedDaemonCount,
|
|
2671
|
+
dryRun: opts.dryRun,
|
|
2672
|
+
error: exitError
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2675
|
+
function restartIdeInstruction(targets) {
|
|
2676
|
+
const names = [];
|
|
2677
|
+
if (targets.cursor) names.push("Cursor");
|
|
2678
|
+
if (targets.vscode) names.push("VS Code");
|
|
2679
|
+
if (targets.jetbrains) names.push("JetBrains IDE");
|
|
2680
|
+
if (names.length === 0) return "Fully quit your IDE and reopen this repo for MCP changes to take effect.";
|
|
2681
|
+
if (names.length === 1) {
|
|
2682
|
+
return `Fully quit ${names[0]} and reopen this repo for MCP changes to take effect.`;
|
|
2683
|
+
}
|
|
2684
|
+
const last = names.pop();
|
|
2685
|
+
return `Fully quit ${names.join(", ")} and ${last}, then reopen this repo for MCP changes to take effect.`;
|
|
2686
|
+
}
|
|
1136
2687
|
async function runSetupIdeFiles(o) {
|
|
1137
2688
|
const outcomes = {};
|
|
1138
|
-
let
|
|
2689
|
+
let cursorMcp;
|
|
2690
|
+
let jetbrainsMcp;
|
|
1139
2691
|
const repoRoot = await findRepoRoot(o.cwd);
|
|
1140
2692
|
if (!repoRoot) {
|
|
1141
2693
|
return {
|
|
@@ -1145,6 +2697,27 @@ async function runSetupIdeFiles(o) {
|
|
|
1145
2697
|
error: "[setup-ide-files] No repo root found (looked for .git or memoraone.m1)."
|
|
1146
2698
|
};
|
|
1147
2699
|
}
|
|
2700
|
+
let daemonCleanup;
|
|
2701
|
+
if (!o.skipDaemonCleanup) {
|
|
2702
|
+
daemonCleanup = await runSetupIdeDaemonCleanup({
|
|
2703
|
+
repoRoot,
|
|
2704
|
+
targets: o.targets,
|
|
2705
|
+
dryRun: o.dryRun,
|
|
2706
|
+
listProcesses: o.listDaemonProcesses,
|
|
2707
|
+
listSocketPaths: o.listCleanupSocketPaths,
|
|
2708
|
+
killProcess: o.killDaemonProcess,
|
|
2709
|
+
removeSocket: o.removeCleanupSocket
|
|
2710
|
+
});
|
|
2711
|
+
if (daemonCleanup.error && !daemonCleanup.skipped) {
|
|
2712
|
+
return {
|
|
2713
|
+
exitCode: 1,
|
|
2714
|
+
repoRoot,
|
|
2715
|
+
outcomes,
|
|
2716
|
+
daemonCleanup,
|
|
2717
|
+
error: `[setup-ide-files] Daemon cleanup failed: ${daemonCleanup.error}`
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
1148
2721
|
outcomes[".gitignore"] = await ensureGitignoreMemoraone(repoRoot, {
|
|
1149
2722
|
dryRun: o.dryRun,
|
|
1150
2723
|
noGitignore: o.noGitignore ?? false
|
|
@@ -1155,19 +2728,6 @@ description: MemoraOne MCP \u2014 IDE agent instructions
|
|
|
1155
2728
|
|
|
1156
2729
|
` + cursorRuleBody();
|
|
1157
2730
|
if (o.targets.cursor) {
|
|
1158
|
-
const detection = await detectCursorGlobalMcpConfig({
|
|
1159
|
-
homeDir: o.homeDir,
|
|
1160
|
-
explicitPath: o.cursorGlobalMcpConfigPath
|
|
1161
|
-
});
|
|
1162
|
-
if (!detection.ok) {
|
|
1163
|
-
return {
|
|
1164
|
-
exitCode: 1,
|
|
1165
|
-
repoRoot,
|
|
1166
|
-
outcomes,
|
|
1167
|
-
error: `${detection.error}
|
|
1168
|
-
${detection.candidates.join("\n ")}`
|
|
1169
|
-
};
|
|
1170
|
-
}
|
|
1171
2731
|
let npxPath;
|
|
1172
2732
|
if (o.npxPathOverride !== void 0) {
|
|
1173
2733
|
npxPath = o.npxPathOverride;
|
|
@@ -1179,7 +2739,7 @@ description: MemoraOne MCP \u2014 IDE agent instructions
|
|
|
1179
2739
|
exitCode: 1,
|
|
1180
2740
|
repoRoot,
|
|
1181
2741
|
outcomes,
|
|
1182
|
-
error: "[setup-ide-files] Could not resolve a working npx executable. Install Node.js/npm or ensure npx is on PATH before configuring Cursor
|
|
2742
|
+
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."
|
|
1183
2743
|
};
|
|
1184
2744
|
}
|
|
1185
2745
|
outcomes[".cursor/rules/memoraone-mcp.mdc"] = await writeManagedMarkdown(
|
|
@@ -1188,29 +2748,58 @@ description: MemoraOne MCP \u2014 IDE agent instructions
|
|
|
1188
2748
|
cursorContent,
|
|
1189
2749
|
{ force: o.force, dryRun: o.dryRun }
|
|
1190
2750
|
);
|
|
2751
|
+
outcomes[".cursor/mcp.json"] = await writeIdeMcpJson(
|
|
2752
|
+
repoRoot,
|
|
2753
|
+
".cursor/mcp.json",
|
|
2754
|
+
(existing) => buildCursorMcpJsonBody(existing, npxPath, repoRoot),
|
|
2755
|
+
{ force: o.force, dryRun: o.dryRun }
|
|
2756
|
+
);
|
|
2757
|
+
const repoConfigPath = getCursorRepoMcpConfigPath(repoRoot);
|
|
2758
|
+
const repoOutcome = outcomes[".cursor/mcp.json"] ?? "skipped";
|
|
2759
|
+
let globalConfigPath;
|
|
2760
|
+
let globalMemoraoneRemoved = false;
|
|
2761
|
+
let globalBackupPath;
|
|
2762
|
+
const globalDetection = await detectCursorGlobalMcpConfig({
|
|
2763
|
+
homeDir: o.homeDir,
|
|
2764
|
+
explicitPath: o.cursorGlobalMcpConfigPath
|
|
2765
|
+
});
|
|
2766
|
+
if (!globalDetection.ok) {
|
|
2767
|
+
return {
|
|
2768
|
+
exitCode: 1,
|
|
2769
|
+
repoRoot,
|
|
2770
|
+
outcomes,
|
|
2771
|
+
error: `${globalDetection.error}
|
|
2772
|
+
${globalDetection.candidates.join("\n ")}`
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
globalConfigPath = globalDetection.path;
|
|
1191
2776
|
try {
|
|
1192
|
-
const
|
|
1193
|
-
configPath:
|
|
1194
|
-
npxPath,
|
|
2777
|
+
const removal = await removeMemoraoneFromCursorGlobalConfig({
|
|
2778
|
+
configPath: globalDetection.path,
|
|
1195
2779
|
dryRun: o.dryRun
|
|
1196
2780
|
});
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
};
|
|
1203
|
-
outcomes[`cursor-global:${detection.path}`] = globalSetup.outcome;
|
|
2781
|
+
if (removal.changed) {
|
|
2782
|
+
globalMemoraoneRemoved = true;
|
|
2783
|
+
globalBackupPath = removal.backupPath;
|
|
2784
|
+
outcomes[`cursor-global-removed:${globalDetection.path}`] = o.dryRun ? "updated" : "updated";
|
|
2785
|
+
}
|
|
1204
2786
|
} catch (err) {
|
|
1205
2787
|
const message = err instanceof Error ? err.message : String(err);
|
|
1206
2788
|
return {
|
|
1207
2789
|
exitCode: 1,
|
|
1208
2790
|
repoRoot,
|
|
1209
2791
|
outcomes,
|
|
1210
|
-
cursorGlobalMcp: { configPath: detection.path, outcome: "skipped", npxPath },
|
|
1211
2792
|
error: message
|
|
1212
2793
|
};
|
|
1213
2794
|
}
|
|
2795
|
+
cursorMcp = {
|
|
2796
|
+
repoConfigPath,
|
|
2797
|
+
repoOutcome,
|
|
2798
|
+
npxPath,
|
|
2799
|
+
globalConfigPath,
|
|
2800
|
+
globalMemoraoneRemoved,
|
|
2801
|
+
globalBackupPath
|
|
2802
|
+
};
|
|
1214
2803
|
}
|
|
1215
2804
|
if (o.targets.vscode) {
|
|
1216
2805
|
outcomes[".vscode/mcp.json"] = await writeIdeMcpJson(
|
|
@@ -1236,11 +2825,45 @@ description: MemoraOne MCP \u2014 IDE agent instructions
|
|
|
1236
2825
|
copilotAndJetBrainsBody("MemoraOne MCP \u2014 JetBrains AI Assistant"),
|
|
1237
2826
|
{ force: o.force, dryRun: o.dryRun }
|
|
1238
2827
|
);
|
|
2828
|
+
try {
|
|
2829
|
+
const homeDir = o.jetbrainsHomeDir ?? os4.homedir();
|
|
2830
|
+
const activePath = o.jetbrainsGlobalMcpConfigPath ?? getJetBrainsGlobalMcpConfigPath(homeDir);
|
|
2831
|
+
const jetbrainsSetup = await setupJetBrainsMcpConfig({
|
|
2832
|
+
homeDir,
|
|
2833
|
+
repoRoot,
|
|
2834
|
+
globalConfigPath: activePath,
|
|
2835
|
+
dryRun: o.dryRun,
|
|
2836
|
+
devMode: o.devMode,
|
|
2837
|
+
repair: o.repair ?? false,
|
|
2838
|
+
verify: o.verifyHandshake ?? !o.dryRun,
|
|
2839
|
+
npxPathOverride: o.npxPathOverride,
|
|
2840
|
+
cliPathOverride: o.cliPathOverride
|
|
2841
|
+
});
|
|
2842
|
+
jetbrainsMcp = {
|
|
2843
|
+
activeConfigPath: activePath,
|
|
2844
|
+
outcome: jetbrainsSetup.outcome,
|
|
2845
|
+
npxPath: jetbrainsSetup.memoraone?.command,
|
|
2846
|
+
backupPath: jetbrainsSetup.backupPath,
|
|
2847
|
+
repairActions: jetbrainsSetup.repairActions,
|
|
2848
|
+
verifyOk: jetbrainsSetup.verifyOk,
|
|
2849
|
+
verifyDetail: jetbrainsSetup.verifyDetail
|
|
2850
|
+
};
|
|
2851
|
+
outcomes[`jetbrains-global:${activePath}`] = jetbrainsSetup.outcome;
|
|
2852
|
+
} catch (err) {
|
|
2853
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2854
|
+
return {
|
|
2855
|
+
exitCode: 1,
|
|
2856
|
+
repoRoot,
|
|
2857
|
+
outcomes,
|
|
2858
|
+
cursorMcp,
|
|
2859
|
+
error: message
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
1239
2862
|
}
|
|
1240
|
-
return { exitCode: 0, repoRoot, outcomes,
|
|
2863
|
+
return { exitCode: 0, repoRoot, outcomes, cursorMcp, jetbrainsMcp, daemonCleanup };
|
|
1241
2864
|
}
|
|
1242
2865
|
async function cliSetupIdeFiles(argv) {
|
|
1243
|
-
const { targets, force, dryRun, noGitignore, cleanup, unknown } = parseSetupIdeFlags(argv);
|
|
2866
|
+
const { targets, force, dryRun, noGitignore, cleanup, devMode, repair, unknown } = parseSetupIdeFlags(argv);
|
|
1244
2867
|
if (unknown.length) {
|
|
1245
2868
|
console.error(`[setup-ide-files] Unknown option(s): ${unknown.join(", ")}`);
|
|
1246
2869
|
return 1;
|
|
@@ -1250,7 +2873,9 @@ async function cliSetupIdeFiles(argv) {
|
|
|
1250
2873
|
targets,
|
|
1251
2874
|
force,
|
|
1252
2875
|
dryRun,
|
|
1253
|
-
noGitignore
|
|
2876
|
+
noGitignore,
|
|
2877
|
+
devMode,
|
|
2878
|
+
repair
|
|
1254
2879
|
});
|
|
1255
2880
|
if (result.error) {
|
|
1256
2881
|
console.error(result.error);
|
|
@@ -1263,15 +2888,25 @@ async function cliSetupIdeFiles(argv) {
|
|
|
1263
2888
|
if (result.repoRoot) {
|
|
1264
2889
|
console.log(`[setup-ide-files] Repo root: ${result.repoRoot}`);
|
|
1265
2890
|
}
|
|
1266
|
-
if (
|
|
1267
|
-
|
|
2891
|
+
if (result.daemonCleanup && !result.daemonCleanup.skipped) {
|
|
2892
|
+
logSetupIdeCleanupSummary(result.daemonCleanup);
|
|
2893
|
+
}
|
|
2894
|
+
if (targets.cursor && result.cursorMcp) {
|
|
2895
|
+
logCursorMcpCliSummary(result.cursorMcp, dryRun);
|
|
2896
|
+
}
|
|
2897
|
+
if (targets.jetbrains && result.jetbrainsMcp) {
|
|
2898
|
+
logJetBrainsMcpCliSummary(result.jetbrainsMcp, dryRun);
|
|
1268
2899
|
}
|
|
1269
2900
|
summarizeOutcomes(result.outcomes);
|
|
1270
2901
|
if (dryRun) {
|
|
1271
2902
|
console.log("[setup-ide-files] Dry run: no files written.");
|
|
2903
|
+
if (result.daemonCleanup && !result.daemonCleanup.skipped) {
|
|
2904
|
+
console.log("[setup-ide-files] Dry run: no daemons stopped, no sockets removed.");
|
|
2905
|
+
}
|
|
1272
2906
|
}
|
|
2907
|
+
console.log(`[setup-ide-files] ${restartIdeInstruction(targets)}`);
|
|
1273
2908
|
if (cleanup) {
|
|
1274
|
-
console.log("[setup-ide-files] Running project
|
|
2909
|
+
console.log("[setup-ide-files] Running additional full-project cleanup (--cleanup)...");
|
|
1275
2910
|
const cleanupResult = await runCleanup({
|
|
1276
2911
|
cwd: process.cwd(),
|
|
1277
2912
|
dryRun,
|
|
@@ -1282,13 +2917,6 @@ async function cliSetupIdeFiles(argv) {
|
|
|
1282
2917
|
console.error(`[setup-ide-files] cleanup failed: ${cleanupResult.error}`);
|
|
1283
2918
|
return cleanupResult.exitCode;
|
|
1284
2919
|
}
|
|
1285
|
-
} else {
|
|
1286
|
-
console.log(
|
|
1287
|
-
"[setup-ide-files] If Studio shows stale connections, run: npx -y @memoraone/mcp@latest cleanup --project-id <projectId>"
|
|
1288
|
-
);
|
|
1289
|
-
console.log(
|
|
1290
|
-
"[setup-ide-files] From this repo (uses memoraone.m1): npx -y @memoraone/mcp@latest cleanup"
|
|
1291
|
-
);
|
|
1292
2920
|
}
|
|
1293
2921
|
return 0;
|
|
1294
2922
|
}
|
|
@@ -1302,7 +2930,7 @@ if (args.includes("--version") || args.includes("-v")) {
|
|
|
1302
2930
|
}
|
|
1303
2931
|
if (args.includes("--help") || args.includes("-h")) {
|
|
1304
2932
|
console.log(
|
|
1305
|
-
"Usage: memoraone-mcp [--version] [--help] [--daemon --project-id <uuid> [--ide cursor|copilot-vscode|jetbrains]]\n memoraone-mcp setup-ide-files [--all|--cursor|--vscode|--jetbrains] [--force] [--dry-run] [--no-gitignore] [--cleanup]\n memoraone-mcp cleanup [--project-id <uuid>] [--ide cursor|copilot-vscode|jetbrains] [--dry-run] [--all-projects] [--yes]"
|
|
2933
|
+
"Usage: memoraone-mcp [--version] [--help] [--daemon --project-id <uuid> [--ide cursor|copilot-vscode|jetbrains]]\n memoraone-mcp setup-ide-files [--all|--cursor|--vscode|--jetbrains] [--force] [--dry-run] [--no-gitignore] [--cleanup] [--dev] [--repair]\n memoraone-mcp cleanup [--project-id <uuid>] [--ide cursor|copilot-vscode|jetbrains] [--dry-run] [--all-projects] [--yes]"
|
|
1306
2934
|
);
|
|
1307
2935
|
process.exit(0);
|
|
1308
2936
|
}
|
|
@@ -1325,92 +2953,9 @@ if (args[0] === "cleanup") {
|
|
|
1325
2953
|
process.exit(1);
|
|
1326
2954
|
});
|
|
1327
2955
|
} else {
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
const parts = [];
|
|
1331
|
-
if (raw !== void 0 && raw.trim() !== "") {
|
|
1332
|
-
for (const p of raw.split(path6.delimiter).map((s) => s.trim()).filter(Boolean)) {
|
|
1333
|
-
parts.push(path6.resolve(p));
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
parts.push(process.cwd());
|
|
1337
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1338
|
-
const deduped = [];
|
|
1339
|
-
for (const p of parts) {
|
|
1340
|
-
if (!seen.has(p)) {
|
|
1341
|
-
seen.add(p);
|
|
1342
|
-
deduped.push(p);
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
return deduped;
|
|
1346
|
-
}, connectWithRetry = function(socketPath) {
|
|
1347
|
-
return new Promise((resolve6, reject) => {
|
|
1348
|
-
const tryConnect = (attempt) => {
|
|
1349
|
-
const socket = net.connect(socketPath, () => {
|
|
1350
|
-
resolve6(socket);
|
|
1351
|
-
});
|
|
1352
|
-
socket.on("error", (err) => {
|
|
1353
|
-
if (attempt >= MAX_RETRIES) {
|
|
1354
|
-
reject(err);
|
|
1355
|
-
return;
|
|
1356
|
-
}
|
|
1357
|
-
log(`connect attempt ${attempt + 1} failed, retrying in ${RETRY_DELAY_MS}ms: ${String(err)}`);
|
|
1358
|
-
setTimeout(() => tryConnect(attempt + 1), RETRY_DELAY_MS);
|
|
1359
|
-
});
|
|
1360
|
-
};
|
|
1361
|
-
tryConnect(0);
|
|
1362
|
-
});
|
|
1363
|
-
};
|
|
1364
|
-
const log = (msg) => {
|
|
1365
|
-
process.stderr.write(`[memoraone-mcp][bridge] ${msg}
|
|
2956
|
+
runBridgeProxy({ cliPath: process.argv[1] }).catch((err) => {
|
|
2957
|
+
process.stderr.write(`[memoraone-mcp][bridge] fatal: ${String(err)}
|
|
1366
2958
|
`);
|
|
1367
|
-
};
|
|
1368
|
-
const MAX_RETRIES = 5;
|
|
1369
|
-
const RETRY_DELAY_MS = 200;
|
|
1370
|
-
async function resolveBinding() {
|
|
1371
|
-
return resolveAuthoritativeBinding(getWorkspaceRootCandidates());
|
|
1372
|
-
}
|
|
1373
|
-
async function runBridge() {
|
|
1374
|
-
ensureBaseDir();
|
|
1375
|
-
const binding = await resolveBinding();
|
|
1376
|
-
const socketPath = getDaemonSocketPath(binding.projectId);
|
|
1377
|
-
const environmentLog = binding.environment !== void 0 ? ` environment=${binding.environment}` : "";
|
|
1378
|
-
log(
|
|
1379
|
-
`authoritative binding project=${binding.projectId} workspace=${binding.workspaceRoot} m1=${binding.m1Path} source=${binding.bindingSource} apiKeySource=${binding.apiKeySource}${environmentLog}`
|
|
1380
|
-
);
|
|
1381
|
-
let socket;
|
|
1382
|
-
try {
|
|
1383
|
-
socket = await connectWithRetry(socketPath);
|
|
1384
|
-
} catch {
|
|
1385
|
-
log("daemon not running, spawning...");
|
|
1386
|
-
const child = (0, import_node_child_process3.spawn)(
|
|
1387
|
-
process.execPath,
|
|
1388
|
-
buildDaemonSpawnArgs(process.argv[1], binding.projectId),
|
|
1389
|
-
{
|
|
1390
|
-
detached: true,
|
|
1391
|
-
stdio: "ignore",
|
|
1392
|
-
env: {
|
|
1393
|
-
...process.env,
|
|
1394
|
-
MEMORAONE_DAEMON_BINDING_B64: encodeResolvedBinding(binding)
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
);
|
|
1398
|
-
child.unref();
|
|
1399
|
-
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
1400
|
-
socket = await connectWithRetry(socketPath);
|
|
1401
|
-
}
|
|
1402
|
-
log("bridge connected");
|
|
1403
|
-
log("forwarding active");
|
|
1404
|
-
process.stdin.pipe(socket);
|
|
1405
|
-
socket.pipe(process.stdout);
|
|
1406
|
-
socket.on("close", () => process.exit(0));
|
|
1407
|
-
socket.on("error", (err) => {
|
|
1408
|
-
log(`socket error: ${String(err)}`);
|
|
1409
|
-
process.exit(1);
|
|
1410
|
-
});
|
|
1411
|
-
}
|
|
1412
|
-
runBridge().catch((err) => {
|
|
1413
|
-
log(`fatal: ${String(err)}`);
|
|
1414
2959
|
process.exit(1);
|
|
1415
2960
|
});
|
|
1416
2961
|
}
|