@memoraone/mcp 0.1.34 → 0.1.36
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 +3267 -1585
- package/dist/daemon.cjs +1803 -1112
- package/dist/index.cjs +1773 -959
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -132,6 +132,593 @@ var config2 = {
|
|
|
132
132
|
|
|
133
133
|
// src/client/memoraClient.ts
|
|
134
134
|
var crypto = __toESM(require("crypto"), 1);
|
|
135
|
+
|
|
136
|
+
// src/localState/installationCredentials.ts
|
|
137
|
+
var import_node_crypto2 = require("crypto");
|
|
138
|
+
|
|
139
|
+
// src/localState/repositoryBindingId.ts
|
|
140
|
+
var import_node_crypto = require("crypto");
|
|
141
|
+
var MRB_PREFIX = "mrb_";
|
|
142
|
+
var MRB_RE = /^mrb_[A-Za-z0-9_-]{43}$/;
|
|
143
|
+
function generateRepositoryBindingId(random = () => (0, import_node_crypto.randomBytes)(32)) {
|
|
144
|
+
const bytes = random();
|
|
145
|
+
if (bytes.length !== 32) {
|
|
146
|
+
throw new Error("[memoraone-mcp] repository binding id requires exactly 32 random bytes");
|
|
147
|
+
}
|
|
148
|
+
return `${MRB_PREFIX}${bytes.toString("base64url")}`;
|
|
149
|
+
}
|
|
150
|
+
function isRepositoryBindingId(value) {
|
|
151
|
+
return MRB_RE.test(value);
|
|
152
|
+
}
|
|
153
|
+
function assertRepositoryBindingId(value) {
|
|
154
|
+
if (!isRepositoryBindingId(value)) {
|
|
155
|
+
throw new Error(`[memoraone-mcp] Invalid repository_binding_id: ${value}`);
|
|
156
|
+
}
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/localState/keyringStore.ts
|
|
161
|
+
var KEYRING_SERVICE = "MemoraOne Local MCP";
|
|
162
|
+
function keyringAccountForBinding(repositoryBindingId) {
|
|
163
|
+
return `binding:${assertRepositoryBindingId(repositoryBindingId)}`;
|
|
164
|
+
}
|
|
165
|
+
var KeyringUnavailableError = class extends Error {
|
|
166
|
+
constructor(message, cause) {
|
|
167
|
+
super(message);
|
|
168
|
+
this.name = "KeyringUnavailableError";
|
|
169
|
+
if (cause !== void 0) {
|
|
170
|
+
this.cause = cause;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
var KeyringOperationError = class extends Error {
|
|
175
|
+
constructor(message, cause) {
|
|
176
|
+
super(message);
|
|
177
|
+
this.name = "KeyringOperationError";
|
|
178
|
+
if (cause !== void 0) {
|
|
179
|
+
this.cause = cause;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
var cachedModule;
|
|
184
|
+
var loadError;
|
|
185
|
+
async function loadKeyringModule(loader = defaultKeyringLoader) {
|
|
186
|
+
if (cachedModule) {
|
|
187
|
+
return cachedModule;
|
|
188
|
+
}
|
|
189
|
+
if (cachedModule === null) {
|
|
190
|
+
throw new KeyringUnavailableError(
|
|
191
|
+
"[memoraone-mcp] OS keyring unavailable; cannot store or load credentials. No plaintext fallback.",
|
|
192
|
+
loadError
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
cachedModule = await loader();
|
|
197
|
+
return cachedModule;
|
|
198
|
+
} catch (err) {
|
|
199
|
+
cachedModule = null;
|
|
200
|
+
loadError = err;
|
|
201
|
+
throw new KeyringUnavailableError(
|
|
202
|
+
"[memoraone-mcp] OS keyring unavailable; cannot store or load credentials. No plaintext fallback.",
|
|
203
|
+
err
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function defaultKeyringLoader() {
|
|
208
|
+
const mod = await import("@napi-rs/keyring");
|
|
209
|
+
if (!mod?.Entry) {
|
|
210
|
+
throw new Error("Entry export missing from @napi-rs/keyring");
|
|
211
|
+
}
|
|
212
|
+
return mod;
|
|
213
|
+
}
|
|
214
|
+
async function keyringSetPassword(repositoryBindingId, password, options = {}) {
|
|
215
|
+
const mod = await loadKeyringModule(options.loader);
|
|
216
|
+
const account = keyringAccountForBinding(repositoryBindingId);
|
|
217
|
+
try {
|
|
218
|
+
const entry = new mod.Entry(KEYRING_SERVICE, account);
|
|
219
|
+
entry.setPassword(password);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (err instanceof KeyringUnavailableError) throw err;
|
|
222
|
+
throw new KeyringOperationError(
|
|
223
|
+
`[memoraone-mcp] Failed to write credentials to OS keyring for ${account}`,
|
|
224
|
+
err
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function keyringGetPassword(repositoryBindingId, options = {}) {
|
|
229
|
+
const mod = await loadKeyringModule(options.loader);
|
|
230
|
+
const account = keyringAccountForBinding(repositoryBindingId);
|
|
231
|
+
try {
|
|
232
|
+
const entry = new mod.Entry(KEYRING_SERVICE, account);
|
|
233
|
+
return entry.getPassword();
|
|
234
|
+
} catch (err) {
|
|
235
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
236
|
+
if (/NoEntry|not found|no entry/i.test(message)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
if (err instanceof KeyringUnavailableError) throw err;
|
|
240
|
+
throw new KeyringOperationError(
|
|
241
|
+
`[memoraone-mcp] Failed to read credentials from OS keyring for ${account}`,
|
|
242
|
+
err
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/localState/installationCredentials.ts
|
|
248
|
+
function parsePayload(raw, repositoryBindingId) {
|
|
249
|
+
let parsed2;
|
|
250
|
+
try {
|
|
251
|
+
parsed2 = JSON.parse(raw);
|
|
252
|
+
} catch {
|
|
253
|
+
throw new Error("[memoraone-mcp] Corrupt keyring credential payload");
|
|
254
|
+
}
|
|
255
|
+
if (!parsed2 || typeof parsed2 !== "object" || Array.isArray(parsed2)) {
|
|
256
|
+
throw new Error("[memoraone-mcp] Corrupt keyring credential payload");
|
|
257
|
+
}
|
|
258
|
+
const obj = parsed2;
|
|
259
|
+
const id = assertRepositoryBindingId(String(obj.repositoryBindingId ?? repositoryBindingId));
|
|
260
|
+
if (id !== repositoryBindingId) {
|
|
261
|
+
throw new Error("[memoraone-mcp] Keyring credential payload binding id mismatch");
|
|
262
|
+
}
|
|
263
|
+
const payload = {
|
|
264
|
+
repositoryBindingId: id,
|
|
265
|
+
updatedAt: typeof obj.updatedAt === "string" ? obj.updatedAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
266
|
+
};
|
|
267
|
+
const optionalString = (key) => {
|
|
268
|
+
const value = obj[key];
|
|
269
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
270
|
+
payload[key] = value;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
optionalString("installationPublicId");
|
|
274
|
+
optionalString("projectId");
|
|
275
|
+
optionalString("accessToken");
|
|
276
|
+
optionalString("refreshToken");
|
|
277
|
+
optionalString("accessTokenExpiresAt");
|
|
278
|
+
optionalString("refreshTokenExpiresAt");
|
|
279
|
+
optionalString("clientRedeemKey");
|
|
280
|
+
optionalString("clientRefreshKey");
|
|
281
|
+
return payload;
|
|
282
|
+
}
|
|
283
|
+
async function readInstallationCredentials(repositoryBindingId, options = {}) {
|
|
284
|
+
const id = assertRepositoryBindingId(repositoryBindingId);
|
|
285
|
+
const raw = await keyringGetPassword(id, options);
|
|
286
|
+
if (raw == null || raw.trim() === "") {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
return parsePayload(raw, id);
|
|
290
|
+
}
|
|
291
|
+
async function writeInstallationCredentials(payload, options = {}) {
|
|
292
|
+
const id = assertRepositoryBindingId(payload.repositoryBindingId);
|
|
293
|
+
const next = {
|
|
294
|
+
...payload,
|
|
295
|
+
repositoryBindingId: id,
|
|
296
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
297
|
+
};
|
|
298
|
+
await keyringSetPassword(id, JSON.stringify(next), options);
|
|
299
|
+
return next;
|
|
300
|
+
}
|
|
301
|
+
async function updateInstallationCredentials(repositoryBindingId, patch, options = {}) {
|
|
302
|
+
const id = assertRepositoryBindingId(repositoryBindingId);
|
|
303
|
+
const existing = await readInstallationCredentials(id, options) ?? {
|
|
304
|
+
repositoryBindingId: id,
|
|
305
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
306
|
+
};
|
|
307
|
+
const merged = {
|
|
308
|
+
...existing,
|
|
309
|
+
...Object.fromEntries(
|
|
310
|
+
Object.entries(patch).filter(([, value]) => value !== void 0)
|
|
311
|
+
),
|
|
312
|
+
repositoryBindingId: id
|
|
313
|
+
};
|
|
314
|
+
for (const key of options.clearKeys ?? []) {
|
|
315
|
+
if (key !== "repositoryBindingId" && key !== "updatedAt") {
|
|
316
|
+
delete merged[key];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return writeInstallationCredentials(merged, options);
|
|
320
|
+
}
|
|
321
|
+
async function ensureClientRefreshKey(repositoryBindingId, options = {}) {
|
|
322
|
+
const existing = await readInstallationCredentials(repositoryBindingId, options);
|
|
323
|
+
if (existing?.clientRefreshKey) {
|
|
324
|
+
return { payload: existing, clientRefreshKey: existing.clientRefreshKey, created: false };
|
|
325
|
+
}
|
|
326
|
+
const clientRefreshKey = (0, import_node_crypto2.randomUUID)();
|
|
327
|
+
const payload = await updateInstallationCredentials(
|
|
328
|
+
repositoryBindingId,
|
|
329
|
+
{ clientRefreshKey },
|
|
330
|
+
options
|
|
331
|
+
);
|
|
332
|
+
return { payload, clientRefreshKey, created: true };
|
|
333
|
+
}
|
|
334
|
+
async function clearClientRefreshKey(repositoryBindingId, options = {}) {
|
|
335
|
+
const existing = await readInstallationCredentials(repositoryBindingId, options);
|
|
336
|
+
if (!existing?.clientRefreshKey) return;
|
|
337
|
+
await updateInstallationCredentials(repositoryBindingId, {}, {
|
|
338
|
+
...options,
|
|
339
|
+
clearKeys: ["clientRefreshKey"]
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function hasUsableAccessToken(payload) {
|
|
343
|
+
return Boolean(payload?.accessToken && payload.accessToken.startsWith("mia_"));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/localState/localLocks.ts
|
|
347
|
+
var fs3 = __toESM(require("fs/promises"), 1);
|
|
348
|
+
|
|
349
|
+
// src/localState/atomicFs.ts
|
|
350
|
+
var fs2 = __toESM(require("fs/promises"), 1);
|
|
351
|
+
var path2 = __toESM(require("path"), 1);
|
|
352
|
+
var import_node_crypto3 = require("crypto");
|
|
353
|
+
var STATE_DIR_MODE = 448;
|
|
354
|
+
var STATE_FILE_MODE = 384;
|
|
355
|
+
async function ensurePrivateDir(dirPath) {
|
|
356
|
+
await fs2.mkdir(dirPath, { recursive: true, mode: STATE_DIR_MODE });
|
|
357
|
+
try {
|
|
358
|
+
await fs2.chmod(dirPath, STATE_DIR_MODE);
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async function writeFileAtomic(filePath, content, options = {}) {
|
|
363
|
+
const mode = options.mode ?? STATE_FILE_MODE;
|
|
364
|
+
const dir = path2.dirname(filePath);
|
|
365
|
+
await ensurePrivateDir(dir);
|
|
366
|
+
const tmpPath = path2.join(
|
|
367
|
+
dir,
|
|
368
|
+
`.${path2.basename(filePath)}.${process.pid}.${(0, import_node_crypto3.randomBytes)(8).toString("hex")}.tmp`
|
|
369
|
+
);
|
|
370
|
+
try {
|
|
371
|
+
await fs2.writeFile(tmpPath, content, { encoding: "utf8", mode });
|
|
372
|
+
await fs2.rename(tmpPath, filePath);
|
|
373
|
+
try {
|
|
374
|
+
await fs2.chmod(filePath, mode);
|
|
375
|
+
} catch {
|
|
376
|
+
}
|
|
377
|
+
} catch (err) {
|
|
378
|
+
try {
|
|
379
|
+
await fs2.unlink(tmpPath);
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
throw err;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function readJsonFile(filePath) {
|
|
386
|
+
try {
|
|
387
|
+
const raw = await fs2.readFile(filePath, "utf8");
|
|
388
|
+
return JSON.parse(raw);
|
|
389
|
+
} catch (err) {
|
|
390
|
+
if (err?.code === "ENOENT") {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
if (err instanceof SyntaxError) {
|
|
394
|
+
throw new Error(`[memoraone-mcp] Corrupt JSON at ${filePath}`);
|
|
395
|
+
}
|
|
396
|
+
throw err;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async function writeJsonAtomic(filePath, value, options = {}) {
|
|
400
|
+
await writeFileAtomic(filePath, `${JSON.stringify(value, null, 2)}
|
|
401
|
+
`, options);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/localState/statePaths.ts
|
|
405
|
+
var os = __toESM(require("os"), 1);
|
|
406
|
+
var path3 = __toESM(require("path"), 1);
|
|
407
|
+
var MEMORAONE_STATE_DIRNAME = ".memoraone";
|
|
408
|
+
function getMemoraoneStateDir(homeDir = os.homedir()) {
|
|
409
|
+
return path3.join(homeDir, MEMORAONE_STATE_DIRNAME);
|
|
410
|
+
}
|
|
411
|
+
function getPathIndexPath(homeDir = os.homedir()) {
|
|
412
|
+
return path3.join(getMemoraoneStateDir(homeDir), "path-index.json");
|
|
413
|
+
}
|
|
414
|
+
function getBindingsDir(homeDir = os.homedir()) {
|
|
415
|
+
return path3.join(getMemoraoneStateDir(homeDir), "bindings");
|
|
416
|
+
}
|
|
417
|
+
function getBindingFilePath(repositoryBindingId, homeDir = os.homedir()) {
|
|
418
|
+
return path3.join(getBindingsDir(homeDir), `${repositoryBindingId}.json`);
|
|
419
|
+
}
|
|
420
|
+
function getLocksDir(homeDir = os.homedir()) {
|
|
421
|
+
return path3.join(getMemoraoneStateDir(homeDir), "locks");
|
|
422
|
+
}
|
|
423
|
+
function getLockPath(lockName, homeDir = os.homedir()) {
|
|
424
|
+
return path3.join(getLocksDir(homeDir), `${lockName}.lock`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/localState/localLocks.ts
|
|
428
|
+
async function acquireLocalLock(lockName, options = {}) {
|
|
429
|
+
const homeDir = options.homeDir;
|
|
430
|
+
const maxRetries = options.maxRetries ?? 40;
|
|
431
|
+
const retryDelayMs = options.retryDelayMs ?? 50;
|
|
432
|
+
const maxLockAgeMs = options.maxLockAgeMs ?? 15e3;
|
|
433
|
+
const lockPath = getLockPath(lockName, homeDir);
|
|
434
|
+
await ensurePrivateDir(getLocksDir(homeDir));
|
|
435
|
+
let retries = 0;
|
|
436
|
+
while (retries <= maxRetries) {
|
|
437
|
+
try {
|
|
438
|
+
try {
|
|
439
|
+
const stat3 = await fs3.stat(lockPath);
|
|
440
|
+
if (Date.now() - stat3.mtimeMs > maxLockAgeMs) {
|
|
441
|
+
await fs3.unlink(lockPath);
|
|
442
|
+
}
|
|
443
|
+
} catch (err) {
|
|
444
|
+
if (err?.code !== "ENOENT") {
|
|
445
|
+
throw err;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const fd = await fs3.open(lockPath, "wx");
|
|
449
|
+
await fd.writeFile(
|
|
450
|
+
JSON.stringify({
|
|
451
|
+
pid: process.pid,
|
|
452
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
453
|
+
}),
|
|
454
|
+
"utf8"
|
|
455
|
+
);
|
|
456
|
+
await fd.close();
|
|
457
|
+
return async () => {
|
|
458
|
+
try {
|
|
459
|
+
await fs3.unlink(lockPath);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
if (err?.code !== "ENOENT") {
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
} catch (err) {
|
|
466
|
+
if (err?.code === "EEXIST") {
|
|
467
|
+
retries += 1;
|
|
468
|
+
if (retries > maxRetries) {
|
|
469
|
+
throw new Error(
|
|
470
|
+
`[memoraone-mcp] Failed to acquire lock ${lockName} after ${maxRetries} retries`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
await new Promise((resolve9) => setTimeout(resolve9, retryDelayMs));
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
throw err;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
throw new Error(`[memoraone-mcp] Failed to acquire lock ${lockName}`);
|
|
480
|
+
}
|
|
481
|
+
async function withLocalLock(lockName, fn, options = {}) {
|
|
482
|
+
const release = await acquireLocalLock(lockName, options);
|
|
483
|
+
try {
|
|
484
|
+
return await fn();
|
|
485
|
+
} finally {
|
|
486
|
+
await release();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/localState/localConnectClient.ts
|
|
491
|
+
async function requestJson(baseUrl, method, path14, options = {}) {
|
|
492
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
493
|
+
const url = `${baseUrl.replace(/\/+$/, "")}${path14.startsWith("/") ? path14 : `/${path14}`}`;
|
|
494
|
+
const res = await fetchImpl(url, {
|
|
495
|
+
method,
|
|
496
|
+
headers: {
|
|
497
|
+
"content-type": "application/json",
|
|
498
|
+
...options.headers ?? {}
|
|
499
|
+
},
|
|
500
|
+
body: method === "GET" ? void 0 : JSON.stringify(options.body ?? {})
|
|
501
|
+
});
|
|
502
|
+
const text = await res.text();
|
|
503
|
+
let json = null;
|
|
504
|
+
if (text) {
|
|
505
|
+
try {
|
|
506
|
+
json = JSON.parse(text);
|
|
507
|
+
} catch {
|
|
508
|
+
json = text;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (!res.ok && !options.acceptStatuses?.includes(res.status)) {
|
|
512
|
+
throw new MemoraOneHttpError(res.status, res.statusText, json);
|
|
513
|
+
}
|
|
514
|
+
return { status: res.status, statusText: res.statusText, ok: res.ok, json };
|
|
515
|
+
}
|
|
516
|
+
async function refreshLocalMcpToken(apiUrl, body, options = {}) {
|
|
517
|
+
const res = await requestJson(apiUrl, "POST", "/v1/local-mcp/token/refresh", {
|
|
518
|
+
body,
|
|
519
|
+
fetchImpl: options.fetchImpl
|
|
520
|
+
});
|
|
521
|
+
const data = res.json;
|
|
522
|
+
if (typeof data?.access_token !== "string") {
|
|
523
|
+
throw new Error("[memoraone-mcp] Invalid refresh response");
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
access_token: data.access_token,
|
|
527
|
+
refresh_token: typeof data.refresh_token === "string" ? data.refresh_token : void 0,
|
|
528
|
+
access_token_expires_at: typeof data.access_token_expires_at === "string" ? data.access_token_expires_at : null,
|
|
529
|
+
refresh_token_expires_at: typeof data.refresh_token_expires_at === "string" ? data.refresh_token_expires_at : null,
|
|
530
|
+
installation_public_id: typeof data.installation_public_id === "string" ? data.installation_public_id : void 0,
|
|
531
|
+
project_id: typeof data.project_id === "string" ? data.project_id : void 0,
|
|
532
|
+
repository_binding_id: typeof data.repository_binding_id === "string" ? data.repository_binding_id : void 0
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/localState/tokenRefreshCoordinator.ts
|
|
537
|
+
var ReconnectRequiredError = class extends Error {
|
|
538
|
+
constructor(message) {
|
|
539
|
+
super(message);
|
|
540
|
+
this.name = "ReconnectRequiredError";
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
async function refreshInstallationAccessToken(options) {
|
|
544
|
+
const { repositoryBindingId, apiUrl, homeDir } = options;
|
|
545
|
+
return withLocalLock(
|
|
546
|
+
`refresh-${repositoryBindingId}`,
|
|
547
|
+
async () => {
|
|
548
|
+
const latest = await readInstallationCredentials(repositoryBindingId, options);
|
|
549
|
+
if (latest?.accessToken?.startsWith("mia_")) {
|
|
550
|
+
const expiresAt = latest.accessTokenExpiresAt ? Date.parse(latest.accessTokenExpiresAt) : NaN;
|
|
551
|
+
if (!Number.isFinite(expiresAt) || expiresAt - Date.now() > 3e4) {
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (!latest?.refreshToken?.startsWith("mir_")) {
|
|
555
|
+
throw new ReconnectRequiredError(
|
|
556
|
+
"[memoraone-mcp] Installation credentials missing refresh token. Run: memoraone-mcp connect <code>"
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
const { clientRefreshKey } = await ensureClientRefreshKey(repositoryBindingId, options);
|
|
560
|
+
try {
|
|
561
|
+
const refreshed = await refreshLocalMcpToken(
|
|
562
|
+
apiUrl,
|
|
563
|
+
{
|
|
564
|
+
refresh_token: latest.refreshToken,
|
|
565
|
+
client_refresh_key: clientRefreshKey
|
|
566
|
+
},
|
|
567
|
+
{ fetchImpl: options.fetchImpl }
|
|
568
|
+
);
|
|
569
|
+
await updateInstallationCredentials(
|
|
570
|
+
repositoryBindingId,
|
|
571
|
+
{
|
|
572
|
+
accessToken: refreshed.access_token,
|
|
573
|
+
refreshToken: refreshed.refresh_token ?? latest.refreshToken,
|
|
574
|
+
accessTokenExpiresAt: refreshed.access_token_expires_at ?? void 0,
|
|
575
|
+
refreshTokenExpiresAt: refreshed.refresh_token_expires_at ?? void 0,
|
|
576
|
+
installationPublicId: refreshed.installation_public_id ?? latest.installationPublicId,
|
|
577
|
+
projectId: refreshed.project_id ?? latest.projectId,
|
|
578
|
+
clientRefreshKey: void 0
|
|
579
|
+
},
|
|
580
|
+
options
|
|
581
|
+
);
|
|
582
|
+
await clearClientRefreshKey(repositoryBindingId, options);
|
|
583
|
+
return refreshed.access_token;
|
|
584
|
+
} catch (err) {
|
|
585
|
+
if (err instanceof MemoraOneHttpError && (err.status === 401 || err.status === 403)) {
|
|
586
|
+
throw new ReconnectRequiredError(
|
|
587
|
+
"[memoraone-mcp] Installation revoked or refresh rejected. Run: memoraone-mcp connect <code>"
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
throw err;
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
{ homeDir }
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/localState/bindingStore.ts
|
|
598
|
+
var fs4 = __toESM(require("fs/promises"), 1);
|
|
599
|
+
var path4 = __toESM(require("path"), 1);
|
|
600
|
+
|
|
601
|
+
// src/localState/bindingRecord.ts
|
|
602
|
+
var BINDING_RECORD_VERSION = 1;
|
|
603
|
+
var SECRET_KEYS = [
|
|
604
|
+
"accessToken",
|
|
605
|
+
"refreshToken",
|
|
606
|
+
"access_token",
|
|
607
|
+
"refresh_token",
|
|
608
|
+
"apiKey",
|
|
609
|
+
"api_key",
|
|
610
|
+
"MEMORAONE_API_KEY",
|
|
611
|
+
"clientRedeemKey",
|
|
612
|
+
"client_redeem_key",
|
|
613
|
+
"clientRefreshKey",
|
|
614
|
+
"client_refresh_key",
|
|
615
|
+
"code",
|
|
616
|
+
"connectCode",
|
|
617
|
+
"connect_code"
|
|
618
|
+
];
|
|
619
|
+
function assertNoSecretsInBindingRecord(record) {
|
|
620
|
+
for (const key of SECRET_KEYS) {
|
|
621
|
+
if (key in record && record[key] != null && record[key] !== "") {
|
|
622
|
+
throw new Error(`[memoraone-mcp] Binding record must not contain secret field: ${key}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function parseBindingRecord(raw) {
|
|
627
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
628
|
+
throw new Error("[memoraone-mcp] Corrupt binding record");
|
|
629
|
+
}
|
|
630
|
+
const obj = raw;
|
|
631
|
+
assertNoSecretsInBindingRecord(obj);
|
|
632
|
+
if (obj.v !== BINDING_RECORD_VERSION) {
|
|
633
|
+
throw new Error(`[memoraone-mcp] Unsupported binding record version: ${String(obj.v)}`);
|
|
634
|
+
}
|
|
635
|
+
const repositoryBindingId = assertRepositoryBindingId(String(obj.repositoryBindingId ?? ""));
|
|
636
|
+
const workspaceRoot = typeof obj.workspaceRoot === "string" ? obj.workspaceRoot : "";
|
|
637
|
+
if (!workspaceRoot) {
|
|
638
|
+
throw new Error("[memoraone-mcp] Binding record missing workspaceRoot");
|
|
639
|
+
}
|
|
640
|
+
const fsId = obj.filesystemIdentity;
|
|
641
|
+
if (!fsId || typeof fsId !== "object" || Array.isArray(fsId)) {
|
|
642
|
+
throw new Error("[memoraone-mcp] Binding record missing filesystemIdentity");
|
|
643
|
+
}
|
|
644
|
+
const identity = fsId;
|
|
645
|
+
const birthtimeMs = Number(identity.birthtimeMs);
|
|
646
|
+
if (typeof identity.platform !== "string" || typeof identity.deviceId !== "string" || typeof identity.inode !== "string" || !Number.isFinite(birthtimeMs) || birthtimeMs <= 0) {
|
|
647
|
+
throw new Error("[memoraone-mcp] Binding record has invalid filesystemIdentity");
|
|
648
|
+
}
|
|
649
|
+
const status = obj.status;
|
|
650
|
+
if (status !== "connected" && status !== "reconnect_required" && status !== "pending") {
|
|
651
|
+
throw new Error("[memoraone-mcp] Binding record has invalid status");
|
|
652
|
+
}
|
|
653
|
+
const record = {
|
|
654
|
+
v: BINDING_RECORD_VERSION,
|
|
655
|
+
repositoryBindingId,
|
|
656
|
+
workspaceRoot,
|
|
657
|
+
filesystemIdentity: {
|
|
658
|
+
platform: identity.platform,
|
|
659
|
+
deviceId: identity.deviceId,
|
|
660
|
+
inode: identity.inode,
|
|
661
|
+
birthtimeMs
|
|
662
|
+
},
|
|
663
|
+
rootFingerprint: typeof obj.rootFingerprint === "string" ? obj.rootFingerprint : "",
|
|
664
|
+
displayName: typeof obj.displayName === "string" ? obj.displayName : "",
|
|
665
|
+
environment: typeof obj.environment === "string" ? obj.environment : "local",
|
|
666
|
+
normalizedGitRemote: obj.normalizedGitRemote === null || typeof obj.normalizedGitRemote === "string" ? obj.normalizedGitRemote : null,
|
|
667
|
+
status,
|
|
668
|
+
createdAt: typeof obj.createdAt === "string" ? obj.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
669
|
+
updatedAt: typeof obj.updatedAt === "string" ? obj.updatedAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
670
|
+
};
|
|
671
|
+
if (typeof obj.apiUrl === "string" && obj.apiUrl.trim()) {
|
|
672
|
+
record.apiUrl = obj.apiUrl.trim().replace(/\/+$/, "");
|
|
673
|
+
}
|
|
674
|
+
if (typeof obj.installationPublicId === "string" && obj.installationPublicId) {
|
|
675
|
+
record.installationPublicId = obj.installationPublicId;
|
|
676
|
+
}
|
|
677
|
+
if (typeof obj.projectId === "string" && obj.projectId) {
|
|
678
|
+
record.projectId = obj.projectId;
|
|
679
|
+
}
|
|
680
|
+
if (obj.packageVersion === null || typeof obj.packageVersion === "string") {
|
|
681
|
+
record.packageVersion = obj.packageVersion;
|
|
682
|
+
}
|
|
683
|
+
if (obj.ideType === null || typeof obj.ideType === "string") {
|
|
684
|
+
record.ideType = obj.ideType;
|
|
685
|
+
}
|
|
686
|
+
return record;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/localState/bindingStore.ts
|
|
690
|
+
async function ensureBindingsDir(homeDir) {
|
|
691
|
+
const dir = getBindingsDir(homeDir);
|
|
692
|
+
await ensurePrivateDir(dir);
|
|
693
|
+
return dir;
|
|
694
|
+
}
|
|
695
|
+
async function readBindingRecord(repositoryBindingId, homeDir) {
|
|
696
|
+
const id = assertRepositoryBindingId(repositoryBindingId);
|
|
697
|
+
const filePath = getBindingFilePath(id, homeDir);
|
|
698
|
+
const raw = await readJsonFile(filePath);
|
|
699
|
+
if (raw == null) {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
return parseBindingRecord(raw);
|
|
703
|
+
}
|
|
704
|
+
async function writeBindingRecord(record, homeDir) {
|
|
705
|
+
assertNoSecretsInBindingRecord(record);
|
|
706
|
+
const id = assertRepositoryBindingId(record.repositoryBindingId);
|
|
707
|
+
await ensureBindingsDir(homeDir);
|
|
708
|
+
const next = {
|
|
709
|
+
...record,
|
|
710
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
711
|
+
};
|
|
712
|
+
await withLocalLock(
|
|
713
|
+
`binding-${id}`,
|
|
714
|
+
async () => {
|
|
715
|
+
await writeJsonAtomic(getBindingFilePath(id, homeDir), next);
|
|
716
|
+
},
|
|
717
|
+
{ homeDir }
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/client/memoraClient.ts
|
|
135
722
|
var PROJECT_ID_HEADER = "x-project-id";
|
|
136
723
|
var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
137
724
|
var parseBooleanFlag2 = (value) => {
|
|
@@ -142,7 +729,7 @@ var parseBooleanFlag2 = (value) => {
|
|
|
142
729
|
return ["1", "true", "yes", "on"].includes(normalized);
|
|
143
730
|
};
|
|
144
731
|
var debugEnabled = parseBooleanFlag2(process.env.MEMORAONE_DEV_MODE);
|
|
145
|
-
async function
|
|
732
|
+
async function requestJson2(url, method, headers, body) {
|
|
146
733
|
const res = await fetch(url, {
|
|
147
734
|
method,
|
|
148
735
|
headers,
|
|
@@ -165,13 +752,20 @@ var MemoraOneHttpError = class extends Error {
|
|
|
165
752
|
}
|
|
166
753
|
};
|
|
167
754
|
var MemoraClient = class {
|
|
168
|
-
constructor(cfg,
|
|
169
|
-
if (!uuidRegex.test(projectId)) {
|
|
755
|
+
constructor(cfg, auth) {
|
|
756
|
+
if (!uuidRegex.test(auth.projectId)) {
|
|
170
757
|
throw new Error("[memoraone-mcp] Invalid project_id for MemoraClient");
|
|
171
758
|
}
|
|
172
759
|
this.baseUrl = cfg.apiUrl;
|
|
173
|
-
this.
|
|
174
|
-
this.
|
|
760
|
+
this.projectId = auth.projectId;
|
|
761
|
+
this.repositoryBindingId = auth.repositoryBindingId;
|
|
762
|
+
this.credentialOptions = auth.credentialOptions;
|
|
763
|
+
this.homeDir = auth.homeDir;
|
|
764
|
+
this.getAccessTokenOverride = auth.getAccessToken;
|
|
765
|
+
this.refreshAccessTokenOverride = auth.refreshAccessToken;
|
|
766
|
+
}
|
|
767
|
+
getRepositoryBindingId() {
|
|
768
|
+
return this.repositoryBindingId;
|
|
175
769
|
}
|
|
176
770
|
resolveProjectId() {
|
|
177
771
|
const projectId = this.projectId?.trim();
|
|
@@ -183,40 +777,71 @@ var MemoraClient = class {
|
|
|
183
777
|
}
|
|
184
778
|
return projectId;
|
|
185
779
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
780
|
+
async resolveAccessToken() {
|
|
781
|
+
if (this.getAccessTokenOverride) {
|
|
782
|
+
return this.getAccessTokenOverride();
|
|
783
|
+
}
|
|
784
|
+
const creds = await readInstallationCredentials(
|
|
785
|
+
this.repositoryBindingId,
|
|
786
|
+
this.credentialOptions
|
|
787
|
+
);
|
|
788
|
+
const token = creds?.accessToken?.trim();
|
|
789
|
+
if (!token || !token.startsWith("mia_")) {
|
|
790
|
+
throw new ReconnectRequiredError(
|
|
791
|
+
"[memoraone-mcp] Missing installation access token. Run: memoraone-mcp connect <code>"
|
|
792
|
+
);
|
|
190
793
|
}
|
|
191
|
-
return
|
|
794
|
+
return token;
|
|
192
795
|
}
|
|
193
|
-
|
|
796
|
+
async refreshAccessToken() {
|
|
797
|
+
if (this.refreshAccessTokenOverride) {
|
|
798
|
+
return this.refreshAccessTokenOverride();
|
|
799
|
+
}
|
|
800
|
+
return refreshInstallationAccessToken({
|
|
801
|
+
apiUrl: this.baseUrl,
|
|
802
|
+
repositoryBindingId: this.repositoryBindingId,
|
|
803
|
+
homeDir: this.homeDir,
|
|
804
|
+
...this.credentialOptions
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
async markReconnectRequired() {
|
|
808
|
+
try {
|
|
809
|
+
const record = await readBindingRecord(this.repositoryBindingId, this.homeDir);
|
|
810
|
+
if (record) {
|
|
811
|
+
await writeBindingRecord(
|
|
812
|
+
{ ...record, status: "reconnect_required", updatedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
813
|
+
this.homeDir
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
} catch {
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
async buildHeaders(options) {
|
|
194
820
|
const projectId = this.resolveProjectId();
|
|
195
|
-
const
|
|
821
|
+
const accessToken = await this.resolveAccessToken();
|
|
196
822
|
return {
|
|
197
823
|
"content-type": "application/json",
|
|
198
|
-
|
|
824
|
+
Authorization: `Bearer ${accessToken}`,
|
|
199
825
|
[PROJECT_ID_HEADER]: projectId,
|
|
200
826
|
...options?.headers ?? {}
|
|
201
827
|
};
|
|
202
828
|
}
|
|
203
|
-
async
|
|
204
|
-
console.error(
|
|
205
|
-
`[memoraone-mcp][info] MemoraClient.post ENTER path=${path9}`
|
|
206
|
-
);
|
|
829
|
+
async perform(method, path14, body, options, retried = false) {
|
|
207
830
|
const nonce = crypto.randomBytes(8).toString("hex");
|
|
208
|
-
const url = `${this.baseUrl}${
|
|
831
|
+
const url = `${this.baseUrl}${path14.startsWith("/") ? path14 : `/${path14}`}`;
|
|
209
832
|
this.resolveProjectId();
|
|
210
833
|
console.error(
|
|
211
|
-
`[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_fetch method
|
|
834
|
+
`[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_fetch method=${method} url=${url}`
|
|
212
835
|
);
|
|
213
|
-
const
|
|
836
|
+
const headers = await this.buildHeaders(options);
|
|
837
|
+
delete headers["x-api-key"];
|
|
838
|
+
const res = await requestJson2(url, method, headers, body);
|
|
214
839
|
if (debugEnabled && options?.log !== false) {
|
|
215
840
|
const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
|
|
216
841
|
console.error(
|
|
217
842
|
`[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_response_log`
|
|
218
843
|
);
|
|
219
|
-
const line = `[memoraone-mcp][info] http response method
|
|
844
|
+
const line = `[memoraone-mcp][info] http response method=${method} url=${url} status=${res.status} body=${snippet}`;
|
|
220
845
|
console.error(line);
|
|
221
846
|
console.error(
|
|
222
847
|
`[memoraone-mcp][info] requestJson nonce=${nonce} stage=after_response_log`
|
|
@@ -227,157 +852,458 @@ var MemoraClient = class {
|
|
|
227
852
|
if (accepted) {
|
|
228
853
|
return res.text ? JSON.parse(res.text) : null;
|
|
229
854
|
}
|
|
855
|
+
if ((res.status === 401 || res.status === 403) && !options?.skipAuthRefresh && !retried) {
|
|
856
|
+
try {
|
|
857
|
+
await this.refreshAccessToken();
|
|
858
|
+
return this.perform(method, path14, body, options, true);
|
|
859
|
+
} catch (err) {
|
|
860
|
+
if (err instanceof ReconnectRequiredError) {
|
|
861
|
+
await this.markReconnectRequired();
|
|
862
|
+
}
|
|
863
|
+
throw err;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (res.status === 401 || res.status === 403) {
|
|
867
|
+
await this.markReconnectRequired();
|
|
868
|
+
}
|
|
230
869
|
const quiet = options?.quietHttpStatuses?.includes(res.status);
|
|
231
870
|
if (!quiet) {
|
|
232
871
|
const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
|
|
233
872
|
process.stderr.write(
|
|
234
|
-
`[memoraone-mcp][error] http error method
|
|
873
|
+
`[memoraone-mcp][error] http error method=${method} url=${url} status=${res.status} body=${snippet}
|
|
235
874
|
`
|
|
236
875
|
);
|
|
237
876
|
}
|
|
238
|
-
throw new MemoraOneHttpError(res.status, res.statusText, res.text);
|
|
877
|
+
throw new MemoraOneHttpError(res.status, res.statusText, res.text);
|
|
878
|
+
}
|
|
879
|
+
return res.text ? JSON.parse(res.text) : null;
|
|
880
|
+
}
|
|
881
|
+
async post(path14, body, options) {
|
|
882
|
+
console.error(`[memoraone-mcp][info] MemoraClient.post ENTER path=${path14}`);
|
|
883
|
+
const result = await this.perform("POST", path14, body, options);
|
|
884
|
+
console.error(`[memoraone-mcp][info] MemoraClient.post EXIT path=${path14}`);
|
|
885
|
+
return result;
|
|
886
|
+
}
|
|
887
|
+
async get(path14, options) {
|
|
888
|
+
return this.perform("GET", path14, void 0, options);
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
var memoraClient_default = MemoraClient;
|
|
892
|
+
|
|
893
|
+
// src/projectBinding.ts
|
|
894
|
+
var fs7 = __toESM(require("fs/promises"), 1);
|
|
895
|
+
var path8 = __toESM(require("path"), 1);
|
|
896
|
+
|
|
897
|
+
// src/localState/resolveLocalBinding.ts
|
|
898
|
+
var fs6 = __toESM(require("fs/promises"), 1);
|
|
899
|
+
var path7 = __toESM(require("path"), 1);
|
|
900
|
+
|
|
901
|
+
// src/localState/pathIndex.ts
|
|
902
|
+
var path6 = __toESM(require("path"), 1);
|
|
903
|
+
|
|
904
|
+
// src/localState/rootFilesystemIdentity.ts
|
|
905
|
+
var fs5 = __toESM(require("fs/promises"), 1);
|
|
906
|
+
var os2 = __toESM(require("os"), 1);
|
|
907
|
+
var path5 = __toESM(require("path"), 1);
|
|
908
|
+
var import_node_child_process = require("child_process");
|
|
909
|
+
var import_node_util = require("util");
|
|
910
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
911
|
+
function filesystemIdentityKey(identity) {
|
|
912
|
+
return [
|
|
913
|
+
identity.platform,
|
|
914
|
+
identity.deviceId,
|
|
915
|
+
identity.inode,
|
|
916
|
+
String(identity.birthtimeMs)
|
|
917
|
+
].join("|");
|
|
918
|
+
}
|
|
919
|
+
function identitiesMatch(a, b) {
|
|
920
|
+
return filesystemIdentityKey(a) === filesystemIdentityKey(b);
|
|
921
|
+
}
|
|
922
|
+
async function readDarwinDeviceId() {
|
|
923
|
+
const { stdout } = await execFileAsync("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
|
|
924
|
+
const match = stdout.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
925
|
+
if (!match?.[1]) {
|
|
926
|
+
throw new Error("[memoraone-mcp] Unable to read macOS IOPlatformUUID");
|
|
927
|
+
}
|
|
928
|
+
return match[1].trim();
|
|
929
|
+
}
|
|
930
|
+
async function readLinuxDeviceId() {
|
|
931
|
+
try {
|
|
932
|
+
const content = await fs5.readFile("/etc/machine-id", "utf8");
|
|
933
|
+
const id = content.trim();
|
|
934
|
+
if (id) return id;
|
|
935
|
+
} catch {
|
|
936
|
+
}
|
|
937
|
+
try {
|
|
938
|
+
const content = await fs5.readFile("/var/lib/dbus/machine-id", "utf8");
|
|
939
|
+
const id = content.trim();
|
|
940
|
+
if (id) return id;
|
|
941
|
+
} catch {
|
|
942
|
+
}
|
|
943
|
+
throw new Error("[memoraone-mcp] Unable to read Linux machine-id");
|
|
944
|
+
}
|
|
945
|
+
async function readWindowsDeviceId() {
|
|
946
|
+
const { stdout } = await execFileAsync("reg", [
|
|
947
|
+
"query",
|
|
948
|
+
"HKLM\\SOFTWARE\\Microsoft\\Cryptography",
|
|
949
|
+
"/v",
|
|
950
|
+
"MachineGuid"
|
|
951
|
+
]);
|
|
952
|
+
const match = stdout.match(/MachineGuid\s+REG_SZ\s+(.+)/i);
|
|
953
|
+
if (!match?.[1]) {
|
|
954
|
+
throw new Error("[memoraone-mcp] Unable to read Windows MachineGuid");
|
|
955
|
+
}
|
|
956
|
+
return match[1].trim();
|
|
957
|
+
}
|
|
958
|
+
async function resolveDeviceId(platform2 = os2.platform()) {
|
|
959
|
+
if (platform2 === "darwin") return readDarwinDeviceId();
|
|
960
|
+
if (platform2 === "linux") return readLinuxDeviceId();
|
|
961
|
+
if (platform2 === "win32") return readWindowsDeviceId();
|
|
962
|
+
try {
|
|
963
|
+
const { stdout } = await execFileAsync("hostid", []);
|
|
964
|
+
const id = stdout.trim();
|
|
965
|
+
if (id) return id;
|
|
966
|
+
} catch {
|
|
967
|
+
}
|
|
968
|
+
throw new Error(`[memoraone-mcp] Unsupported platform for device ID: ${platform2}`);
|
|
969
|
+
}
|
|
970
|
+
async function captureRootFilesystemIdentity(rootPath, deps = {}) {
|
|
971
|
+
const resolved = path5.resolve(rootPath);
|
|
972
|
+
const platform2 = deps.platform ?? os2.platform();
|
|
973
|
+
const statRoot = deps.statRoot ?? (async (p) => {
|
|
974
|
+
const st2 = await fs5.stat(p);
|
|
975
|
+
return {
|
|
976
|
+
ino: st2.ino,
|
|
977
|
+
dev: st2.dev,
|
|
978
|
+
birthtimeMs: st2.birthtimeMs,
|
|
979
|
+
isDirectory: () => st2.isDirectory()
|
|
980
|
+
};
|
|
981
|
+
});
|
|
982
|
+
const readDeviceId = deps.readDeviceId ?? (() => resolveDeviceId(platform2));
|
|
983
|
+
const st = await statRoot(resolved);
|
|
984
|
+
if (!st.isDirectory()) {
|
|
985
|
+
throw new Error(`[memoraone-mcp] Workspace root is not a directory: ${resolved}`);
|
|
986
|
+
}
|
|
987
|
+
const birthtimeMs = Number(st.birthtimeMs);
|
|
988
|
+
if (!Number.isFinite(birthtimeMs) || birthtimeMs <= 0) {
|
|
989
|
+
throw new Error(
|
|
990
|
+
"[memoraone-mcp] Root birth time unavailable; cannot bind this working tree. Reconnect required."
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
const inode = typeof st.ino === "bigint" ? st.ino.toString() : String(st.ino);
|
|
994
|
+
const deviceId = await readDeviceId();
|
|
995
|
+
if (!deviceId || !inode) {
|
|
996
|
+
throw new Error(
|
|
997
|
+
"[memoraone-mcp] Ambiguous filesystem identity; cannot bind this working tree. Reconnect required."
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
return {
|
|
1001
|
+
platform: platform2,
|
|
1002
|
+
deviceId,
|
|
1003
|
+
inode,
|
|
1004
|
+
birthtimeMs
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/localState/pathIndex.ts
|
|
1009
|
+
var PATH_INDEX_VERSION = 1;
|
|
1010
|
+
function emptyIndex() {
|
|
1011
|
+
return {
|
|
1012
|
+
v: PATH_INDEX_VERSION,
|
|
1013
|
+
byPath: {},
|
|
1014
|
+
byIdentity: {},
|
|
1015
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
function parsePathIndex(raw) {
|
|
1019
|
+
if (raw == null) {
|
|
1020
|
+
return emptyIndex();
|
|
1021
|
+
}
|
|
1022
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1023
|
+
throw new Error("[memoraone-mcp] Corrupt path-index.json");
|
|
1024
|
+
}
|
|
1025
|
+
const obj = raw;
|
|
1026
|
+
if (obj.v !== PATH_INDEX_VERSION) {
|
|
1027
|
+
throw new Error(`[memoraone-mcp] Unsupported path-index version: ${String(obj.v)}`);
|
|
1028
|
+
}
|
|
1029
|
+
const byPath = obj.byPath && typeof obj.byPath === "object" && !Array.isArray(obj.byPath) ? obj.byPath : {};
|
|
1030
|
+
const byIdentity = obj.byIdentity && typeof obj.byIdentity === "object" && !Array.isArray(obj.byIdentity) ? obj.byIdentity : {};
|
|
1031
|
+
return {
|
|
1032
|
+
v: PATH_INDEX_VERSION,
|
|
1033
|
+
byPath: { ...byPath },
|
|
1034
|
+
byIdentity: { ...byIdentity },
|
|
1035
|
+
updatedAt: typeof obj.updatedAt === "string" ? obj.updatedAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
async function loadPathIndex(homeDir) {
|
|
1039
|
+
const filePath = getPathIndexPath(homeDir);
|
|
1040
|
+
try {
|
|
1041
|
+
const raw = await readJsonFile(filePath);
|
|
1042
|
+
return parsePathIndex(raw);
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
if (err instanceof Error && err.message.includes("Corrupt")) {
|
|
1045
|
+
throw err;
|
|
1046
|
+
}
|
|
1047
|
+
throw err;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
async function savePathIndex(index, homeDir) {
|
|
1051
|
+
const next = {
|
|
1052
|
+
...index,
|
|
1053
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1054
|
+
};
|
|
1055
|
+
await writeJsonAtomic(getPathIndexPath(homeDir), next);
|
|
1056
|
+
}
|
|
1057
|
+
function lookupPathIndex(index, workspaceRoot, identity) {
|
|
1058
|
+
const resolved = path6.resolve(workspaceRoot);
|
|
1059
|
+
const byPathId = index.byPath[resolved];
|
|
1060
|
+
if (byPathId) {
|
|
1061
|
+
return { kind: "path", repositoryBindingId: assertRepositoryBindingId(byPathId) };
|
|
1062
|
+
}
|
|
1063
|
+
const identityKey = filesystemIdentityKey(identity);
|
|
1064
|
+
const byIdentityId = index.byIdentity[identityKey];
|
|
1065
|
+
if (!byIdentityId) {
|
|
1066
|
+
return { kind: "none" };
|
|
1067
|
+
}
|
|
1068
|
+
const previousPath = Object.entries(index.byPath).find(([, id]) => id === byIdentityId)?.[0];
|
|
1069
|
+
if (!previousPath) {
|
|
1070
|
+
return {
|
|
1071
|
+
kind: "identity-rename",
|
|
1072
|
+
repositoryBindingId: assertRepositoryBindingId(byIdentityId),
|
|
1073
|
+
previousPath: resolved
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
return {
|
|
1077
|
+
kind: "identity-rename",
|
|
1078
|
+
repositoryBindingId: assertRepositoryBindingId(byIdentityId),
|
|
1079
|
+
previousPath
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
async function upsertPathIndexEntry(options) {
|
|
1083
|
+
const repositoryBindingId = assertRepositoryBindingId(options.repositoryBindingId);
|
|
1084
|
+
const resolved = path6.resolve(options.workspaceRoot);
|
|
1085
|
+
const identityKey = filesystemIdentityKey(options.identity);
|
|
1086
|
+
return withLocalLock(
|
|
1087
|
+
"path-index",
|
|
1088
|
+
async () => {
|
|
1089
|
+
const index = await loadPathIndex(options.homeDir);
|
|
1090
|
+
if (options.previousPath && path6.resolve(options.previousPath) !== resolved) {
|
|
1091
|
+
delete index.byPath[path6.resolve(options.previousPath)];
|
|
1092
|
+
}
|
|
1093
|
+
for (const [p, id] of Object.entries(index.byPath)) {
|
|
1094
|
+
if (id === repositoryBindingId && p !== resolved) {
|
|
1095
|
+
delete index.byPath[p];
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
for (const [key, id] of Object.entries(index.byIdentity)) {
|
|
1099
|
+
if (id === repositoryBindingId && key !== identityKey) {
|
|
1100
|
+
delete index.byIdentity[key];
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
index.byPath[resolved] = repositoryBindingId;
|
|
1104
|
+
index.byIdentity[identityKey] = repositoryBindingId;
|
|
1105
|
+
await savePathIndex(index, options.homeDir);
|
|
1106
|
+
return index;
|
|
1107
|
+
},
|
|
1108
|
+
{ homeDir: options.homeDir }
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
function identityMatchesStored(stored, current) {
|
|
1112
|
+
return identitiesMatch(stored, current);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// src/localState/resolveLocalBinding.ts
|
|
1116
|
+
async function detectLegacyM1Warning(workspaceRoot) {
|
|
1117
|
+
const candidate = path7.join(path7.resolve(workspaceRoot), CANONICAL_M1_FILENAME);
|
|
1118
|
+
try {
|
|
1119
|
+
await fs6.access(candidate);
|
|
1120
|
+
return candidate;
|
|
1121
|
+
} catch {
|
|
1122
|
+
return void 0;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
async function ensureRepositoryBindingForRoot(workspaceRoot, options = {}) {
|
|
1126
|
+
const resolved = path7.resolve(workspaceRoot);
|
|
1127
|
+
const identity = await captureRootFilesystemIdentity(resolved, options.identityDeps);
|
|
1128
|
+
const legacyM1WarningPath = await detectLegacyM1Warning(resolved);
|
|
1129
|
+
const index = await loadPathIndex(options.homeDir);
|
|
1130
|
+
const lookup = lookupPathIndex(index, resolved, identity);
|
|
1131
|
+
if (lookup.kind === "path") {
|
|
1132
|
+
const record = await readBindingRecord(lookup.repositoryBindingId, options.homeDir);
|
|
1133
|
+
if (record && identityMatchesStored(record.filesystemIdentity, identity)) {
|
|
1134
|
+
if (path7.resolve(record.workspaceRoot) !== resolved) {
|
|
1135
|
+
const updated = {
|
|
1136
|
+
...record,
|
|
1137
|
+
workspaceRoot: resolved,
|
|
1138
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1139
|
+
};
|
|
1140
|
+
await writeBindingRecord(updated, options.homeDir);
|
|
1141
|
+
await upsertPathIndexEntry({
|
|
1142
|
+
repositoryBindingId: record.repositoryBindingId,
|
|
1143
|
+
workspaceRoot: resolved,
|
|
1144
|
+
identity,
|
|
1145
|
+
homeDir: options.homeDir,
|
|
1146
|
+
previousPath: record.workspaceRoot
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
repositoryBindingId: record.repositoryBindingId,
|
|
1151
|
+
identity,
|
|
1152
|
+
created: false,
|
|
1153
|
+
legacyM1WarningPath
|
|
1154
|
+
};
|
|
239
1155
|
}
|
|
240
|
-
console.error(
|
|
241
|
-
`[memoraone-mcp][info] MemoraClient.post EXIT path=${path9}`
|
|
242
|
-
);
|
|
243
|
-
return res.text ? JSON.parse(res.text) : null;
|
|
244
1156
|
}
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
`
|
|
269
|
-
);
|
|
270
|
-
throw new MemoraOneHttpError(res.status, res.statusText, res.text);
|
|
1157
|
+
if (lookup.kind === "identity-rename") {
|
|
1158
|
+
const record = await readBindingRecord(lookup.repositoryBindingId, options.homeDir);
|
|
1159
|
+
if (record && identityMatchesStored(record.filesystemIdentity, identity)) {
|
|
1160
|
+
const updated = {
|
|
1161
|
+
...record,
|
|
1162
|
+
workspaceRoot: resolved,
|
|
1163
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1164
|
+
};
|
|
1165
|
+
await writeBindingRecord(updated, options.homeDir);
|
|
1166
|
+
await upsertPathIndexEntry({
|
|
1167
|
+
repositoryBindingId: record.repositoryBindingId,
|
|
1168
|
+
workspaceRoot: resolved,
|
|
1169
|
+
identity,
|
|
1170
|
+
homeDir: options.homeDir,
|
|
1171
|
+
previousPath: lookup.previousPath
|
|
1172
|
+
});
|
|
1173
|
+
return {
|
|
1174
|
+
repositoryBindingId: record.repositoryBindingId,
|
|
1175
|
+
identity,
|
|
1176
|
+
created: false,
|
|
1177
|
+
renamedFrom: lookup.previousPath,
|
|
1178
|
+
legacyM1WarningPath
|
|
1179
|
+
};
|
|
271
1180
|
}
|
|
272
|
-
return res.text ? JSON.parse(res.text) : null;
|
|
273
1181
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return a.projectId.trim().toLowerCase() === b.projectId.trim().toLowerCase() && path2.resolve(a.workspaceRoot) === path2.resolve(b.workspaceRoot) && path2.resolve(a.m1Path) === path2.resolve(b.m1Path);
|
|
1182
|
+
if (!options.createIfMissing) {
|
|
1183
|
+
throw new ReconnectRequiredError(
|
|
1184
|
+
`[memoraone-mcp] No local binding for workspace ${resolved}. Run: memoraone-mcp connect <code>`
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
const repositoryBindingId = generateRepositoryBindingId();
|
|
1188
|
+
return {
|
|
1189
|
+
repositoryBindingId,
|
|
1190
|
+
identity,
|
|
1191
|
+
created: true,
|
|
1192
|
+
legacyM1WarningPath
|
|
1193
|
+
};
|
|
287
1194
|
}
|
|
288
|
-
function
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
1195
|
+
async function resolveLocalBinding(workspaceRoot, options = {}) {
|
|
1196
|
+
const resolved = path7.resolve(workspaceRoot);
|
|
1197
|
+
const { repositoryBindingId, legacyM1WarningPath } = await ensureRepositoryBindingForRoot(
|
|
1198
|
+
resolved,
|
|
1199
|
+
{ ...options, createIfMissing: false }
|
|
1200
|
+
);
|
|
1201
|
+
const record = await readBindingRecord(repositoryBindingId, options.homeDir);
|
|
1202
|
+
if (!record) {
|
|
1203
|
+
throw new ReconnectRequiredError(
|
|
1204
|
+
`[memoraone-mcp] Binding metadata missing for ${repositoryBindingId}. Run: memoraone-mcp connect <code>`
|
|
295
1205
|
);
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
"
|
|
1206
|
+
}
|
|
1207
|
+
const identity = await captureRootFilesystemIdentity(resolved, options.identityDeps);
|
|
1208
|
+
if (!identityMatchesStored(record.filesystemIdentity, identity)) {
|
|
1209
|
+
throw new ReconnectRequiredError(
|
|
1210
|
+
"[memoraone-mcp] Workspace filesystem identity changed. Run: memoraone-mcp connect <code>"
|
|
301
1211
|
);
|
|
302
|
-
|
|
303
|
-
|
|
1212
|
+
}
|
|
1213
|
+
if (record.status === "reconnect_required") {
|
|
1214
|
+
throw new ReconnectRequiredError(
|
|
1215
|
+
"[memoraone-mcp] Installation requires reconnect. Run: memoraone-mcp connect <code>"
|
|
304
1216
|
);
|
|
305
|
-
return lines.join("\n");
|
|
306
1217
|
}
|
|
307
|
-
|
|
308
|
-
|
|
1218
|
+
const creds = await readInstallationCredentials(
|
|
1219
|
+
repositoryBindingId,
|
|
1220
|
+
options.credentialOptions
|
|
309
1221
|
);
|
|
310
|
-
|
|
1222
|
+
if (!hasUsableAccessToken(creds) || !creds?.refreshToken) {
|
|
1223
|
+
throw new ReconnectRequiredError(
|
|
1224
|
+
"[memoraone-mcp] Installation credentials missing. Run: memoraone-mcp connect <code>"
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
const projectId = record.projectId ?? creds.projectId;
|
|
1228
|
+
if (!projectId) {
|
|
1229
|
+
throw new ReconnectRequiredError(
|
|
1230
|
+
"[memoraone-mcp] Binding missing project id. Run: memoraone-mcp connect <code>"
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
return {
|
|
1234
|
+
repositoryBindingId,
|
|
1235
|
+
projectId,
|
|
1236
|
+
workspaceRoot: resolved,
|
|
1237
|
+
installationPublicId: record.installationPublicId ?? creds.installationPublicId,
|
|
1238
|
+
environment: record.environment,
|
|
1239
|
+
bindingSource: "local-binding",
|
|
1240
|
+
status: record.status,
|
|
1241
|
+
legacyM1WarningPath
|
|
1242
|
+
};
|
|
311
1243
|
}
|
|
312
1244
|
|
|
313
1245
|
// src/projectBinding.ts
|
|
314
|
-
var
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
1246
|
+
var CANONICAL_M1_FILENAME = "memoraone.m1";
|
|
1247
|
+
function toResolvedBinding(local) {
|
|
1248
|
+
return {
|
|
1249
|
+
repositoryBindingId: local.repositoryBindingId,
|
|
1250
|
+
projectId: local.projectId,
|
|
1251
|
+
workspaceRoot: local.workspaceRoot,
|
|
1252
|
+
installationPublicId: local.installationPublicId,
|
|
1253
|
+
environment: local.environment,
|
|
1254
|
+
bindingSource: "local-binding",
|
|
1255
|
+
status: local.status,
|
|
1256
|
+
legacyM1WarningPath: local.legacyM1WarningPath
|
|
1257
|
+
};
|
|
323
1258
|
}
|
|
324
|
-
function
|
|
325
|
-
|
|
1259
|
+
async function warnLegacyM1IfPresent(workspaceRoot) {
|
|
1260
|
+
const candidate = path8.join(path8.resolve(workspaceRoot), CANONICAL_M1_FILENAME);
|
|
326
1261
|
try {
|
|
327
|
-
|
|
1262
|
+
await fs7.access(candidate);
|
|
1263
|
+
process.stderr.write(
|
|
1264
|
+
`[memoraone-mcp] warning: ignoring legacy ${CANONICAL_M1_FILENAME} (not used for credentials or binding)
|
|
1265
|
+
`
|
|
1266
|
+
);
|
|
1267
|
+
return candidate;
|
|
328
1268
|
} catch {
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
const projectId = parsed2?.projectId ?? parsed2?.project_id;
|
|
332
|
-
if (!projectId || typeof projectId !== "string") {
|
|
333
|
-
throw new Error(`[memoraone-mcp] memoraone.m1 missing projectId at ${markerPath}`);
|
|
334
|
-
}
|
|
335
|
-
if (!uuidRegex2.test(projectId.trim())) {
|
|
336
|
-
throw new Error(`[memoraone-mcp] memoraone.m1 projectId is not a UUID at ${markerPath}`);
|
|
1269
|
+
return void 0;
|
|
337
1270
|
}
|
|
338
|
-
const apiKeyRaw = parsed2?.MEMORAONE_API_KEY ?? parsed2?.api_key;
|
|
339
|
-
const apiKey = apiKeyRaw !== void 0 && apiKeyRaw !== null && typeof apiKeyRaw === "string" && apiKeyRaw.trim() !== "" ? apiKeyRaw.trim() : null;
|
|
340
|
-
const environment = normalizeEnvironment(parsed2?.environment);
|
|
341
|
-
return environment === void 0 ? { projectId: projectId.trim(), apiKey } : { projectId: projectId.trim(), apiKey, environment };
|
|
342
1271
|
}
|
|
343
|
-
async function
|
|
344
|
-
const
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
try {
|
|
350
|
-
const content = await fs2.readFile(markerPath, "utf8");
|
|
351
|
-
const { projectId, apiKey, environment } = parseAndValidateM1(content, markerPath);
|
|
352
|
-
return environment === void 0 ? { projectId, apiKey, foundAt: markerPath } : { projectId, apiKey, environment, foundAt: markerPath };
|
|
353
|
-
} catch (err) {
|
|
354
|
-
if (err?.code === "ENOENT") {
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
357
|
-
throw err;
|
|
1272
|
+
async function resolveAuthoritativeBinding(workspaceRoot, _options = {}) {
|
|
1273
|
+
const candidates = normalizeWorkspaceSearchRoots(workspaceRoot);
|
|
1274
|
+
if (candidates.length === 0) {
|
|
1275
|
+
throw new ReconnectRequiredError(
|
|
1276
|
+
"[memoraone-mcp] Could not resolve workspace root. Open a connected repository folder.\nRun: memoraone-mcp connect <code>"
|
|
1277
|
+
);
|
|
358
1278
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
while (true) {
|
|
363
|
-
const markerPath = path3.join(current, "memoraone.m1");
|
|
1279
|
+
const bindings = [];
|
|
1280
|
+
for (const root of candidates) {
|
|
1281
|
+
await warnLegacyM1IfPresent(root);
|
|
364
1282
|
try {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
const repoRoot = path3.dirname(markerPath);
|
|
368
|
-
return environment === void 0 ? { projectId, apiKey, repoRoot, markerPath } : { projectId, apiKey, environment, repoRoot, markerPath };
|
|
1283
|
+
const local = await resolveLocalBinding(root);
|
|
1284
|
+
bindings.push(toResolvedBinding(local));
|
|
369
1285
|
} catch (err) {
|
|
370
|
-
if (err
|
|
371
|
-
|
|
1286
|
+
if (err instanceof ReconnectRequiredError) {
|
|
1287
|
+
continue;
|
|
372
1288
|
}
|
|
1289
|
+
throw err;
|
|
373
1290
|
}
|
|
374
|
-
const parent = path3.dirname(current);
|
|
375
|
-
if (parent === current) {
|
|
376
|
-
break;
|
|
377
|
-
}
|
|
378
|
-
current = parent;
|
|
379
1291
|
}
|
|
380
|
-
|
|
1292
|
+
if (bindings.length === 0) {
|
|
1293
|
+
throw new ReconnectRequiredError(
|
|
1294
|
+
"[memoraone-mcp] No local MemoraOne binding for this workspace.\nRun: memoraone-mcp connect <code>"
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
const distinctIds = new Set(bindings.map((b) => b.repositoryBindingId));
|
|
1298
|
+
if (distinctIds.size > 1) {
|
|
1299
|
+
const lines = bindings.map(
|
|
1300
|
+
(b) => ` - workspace=${b.workspaceRoot} binding=${b.repositoryBindingId} project=${b.projectId}`
|
|
1301
|
+
);
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
"[memoraone-mcp] Ambiguous workspace binding: multiple open roots map to different repository bindings.\n" + lines.join("\n") + "\nOpen one repo per window."
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
return bindings[0];
|
|
381
1307
|
}
|
|
382
1308
|
function normalizeWorkspaceSearchRoots(workspaceRoot) {
|
|
383
1309
|
if (workspaceRoot === void 0) {
|
|
@@ -387,14 +1313,10 @@ function normalizeWorkspaceSearchRoots(workspaceRoot) {
|
|
|
387
1313
|
const seen = /* @__PURE__ */ new Set();
|
|
388
1314
|
const out = [];
|
|
389
1315
|
for (const raw of list) {
|
|
390
|
-
if (raw === void 0)
|
|
391
|
-
continue;
|
|
392
|
-
}
|
|
1316
|
+
if (raw === void 0) continue;
|
|
393
1317
|
const trimmed = String(raw).trim();
|
|
394
|
-
if (trimmed === "")
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
const resolved = path3.resolve(trimmed);
|
|
1318
|
+
if (trimmed === "") continue;
|
|
1319
|
+
const resolved = path8.resolve(trimmed);
|
|
398
1320
|
if (!seen.has(resolved)) {
|
|
399
1321
|
seen.add(resolved);
|
|
400
1322
|
out.push(resolved);
|
|
@@ -402,77 +1324,109 @@ function normalizeWorkspaceSearchRoots(workspaceRoot) {
|
|
|
402
1324
|
}
|
|
403
1325
|
return out;
|
|
404
1326
|
}
|
|
405
|
-
function
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
1327
|
+
function bindingRelevantValuesMatch(a, b) {
|
|
1328
|
+
const envA = a.environment ?? void 0;
|
|
1329
|
+
const envB = b.environment ?? void 0;
|
|
1330
|
+
return a.repositoryBindingId === b.repositoryBindingId && a.projectId.trim().toLowerCase() === b.projectId.trim().toLowerCase() && path8.resolve(a.workspaceRoot) === path8.resolve(b.workspaceRoot) && (a.installationPublicId ?? void 0) === (b.installationPublicId ?? void 0) && envA === envB && a.status === b.status;
|
|
1331
|
+
}
|
|
1332
|
+
async function reconcileResolvedBindingWithDisk(cached) {
|
|
1333
|
+
const repositoryBindingId = assertRepositoryBindingId(cached.repositoryBindingId);
|
|
1334
|
+
const record = await readBindingRecord(repositoryBindingId);
|
|
1335
|
+
if (!record) {
|
|
1336
|
+
throw new ReconnectRequiredError(
|
|
1337
|
+
`[memoraone-mcp] Cached binding missing for ${repositoryBindingId}. Run: memoraone-mcp connect <code>`
|
|
1338
|
+
);
|
|
413
1339
|
}
|
|
414
|
-
|
|
415
|
-
|
|
1340
|
+
const workspaceRoot = path8.resolve(record.workspaceRoot);
|
|
1341
|
+
const identity = await captureRootFilesystemIdentity(workspaceRoot);
|
|
1342
|
+
if (!identitiesMatch(record.filesystemIdentity, identity)) {
|
|
1343
|
+
throw new ReconnectRequiredError(
|
|
1344
|
+
"[memoraone-mcp] Workspace filesystem identity changed. Run: memoraone-mcp connect <code>"
|
|
1345
|
+
);
|
|
416
1346
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (respectExplicitM1Path) {
|
|
422
|
-
const explicitBinding = await resolveProjectIdFromExplicitM1Path();
|
|
423
|
-
if (explicitBinding) {
|
|
424
|
-
const resolved = resolveApiKeyWithSource(explicitBinding.apiKey);
|
|
425
|
-
return {
|
|
426
|
-
projectId: explicitBinding.projectId,
|
|
427
|
-
workspaceRoot: path3.dirname(explicitBinding.foundAt),
|
|
428
|
-
m1Path: explicitBinding.foundAt,
|
|
429
|
-
apiKey: resolved.apiKey,
|
|
430
|
-
...explicitBinding.environment !== void 0 ? { environment: explicitBinding.environment } : {},
|
|
431
|
-
bindingSource: "explicit-m1-path",
|
|
432
|
-
apiKeySource: resolved.apiKeySource
|
|
433
|
-
};
|
|
434
|
-
}
|
|
1347
|
+
if (record.status === "reconnect_required" || !record.projectId) {
|
|
1348
|
+
throw new ReconnectRequiredError(
|
|
1349
|
+
"[memoraone-mcp] Installation requires reconnect. Run: memoraone-mcp connect <code>"
|
|
1350
|
+
);
|
|
435
1351
|
}
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
1352
|
+
const fresh = {
|
|
1353
|
+
repositoryBindingId,
|
|
1354
|
+
projectId: record.projectId,
|
|
1355
|
+
workspaceRoot,
|
|
1356
|
+
installationPublicId: record.installationPublicId,
|
|
1357
|
+
environment: record.environment,
|
|
1358
|
+
bindingSource: "local-binding",
|
|
1359
|
+
status: record.status
|
|
1360
|
+
};
|
|
1361
|
+
if (bindingRelevantValuesMatch(cached, fresh)) {
|
|
1362
|
+
return { binding: fresh, cacheRefreshed: false };
|
|
439
1363
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
1364
|
+
return { binding: fresh, cacheRefreshed: true };
|
|
1365
|
+
}
|
|
1366
|
+
function encodeResolvedBinding(binding) {
|
|
1367
|
+
const payload = {
|
|
1368
|
+
repositoryBindingId: binding.repositoryBindingId,
|
|
1369
|
+
projectId: binding.projectId,
|
|
1370
|
+
workspaceRoot: binding.workspaceRoot,
|
|
1371
|
+
installationPublicId: binding.installationPublicId,
|
|
1372
|
+
environment: binding.environment,
|
|
1373
|
+
bindingSource: binding.bindingSource,
|
|
1374
|
+
status: binding.status
|
|
1375
|
+
};
|
|
1376
|
+
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// src/initializeBinding.ts
|
|
1380
|
+
var path11 = __toESM(require("path"), 1);
|
|
1381
|
+
var import_node_url = require("url");
|
|
1382
|
+
|
|
1383
|
+
// src/bindingIdentity.ts
|
|
1384
|
+
var crypto2 = __toESM(require("crypto"), 1);
|
|
1385
|
+
var path9 = __toESM(require("path"), 1);
|
|
1386
|
+
var BINDING_SOCKET_HASH_LENGTH = 16;
|
|
1387
|
+
function hashBindingIdentity(repositoryBindingId, workspaceRoot, ideType) {
|
|
1388
|
+
const input = [
|
|
1389
|
+
repositoryBindingId.trim(),
|
|
1390
|
+
path9.resolve(workspaceRoot),
|
|
1391
|
+
ideType
|
|
1392
|
+
].join("|");
|
|
1393
|
+
return crypto2.createHash("sha256").update(input).digest("hex").slice(0, BINDING_SOCKET_HASH_LENGTH);
|
|
1394
|
+
}
|
|
1395
|
+
function bindingsMatch(a, b) {
|
|
1396
|
+
const envA = a.environment ?? void 0;
|
|
1397
|
+
const envB = b.environment ?? void 0;
|
|
1398
|
+
return a.repositoryBindingId === b.repositoryBindingId && a.projectId.trim().toLowerCase() === b.projectId.trim().toLowerCase() && path9.resolve(a.workspaceRoot) === path9.resolve(b.workspaceRoot) && (a.installationPublicId ?? void 0) === (b.installationPublicId ?? void 0) && envA === envB && a.status === b.status;
|
|
1399
|
+
}
|
|
1400
|
+
function formatMissingInitializeWorkspaceError(options) {
|
|
1401
|
+
const lines = [
|
|
1402
|
+
"[memoraone-mcp] Could not resolve workspace from MCP initialize params."
|
|
1403
|
+
];
|
|
1404
|
+
if (options?.rootsListAttempted) {
|
|
1405
|
+
lines.push(
|
|
1406
|
+
"Cursor initialize lacked workspaceFolders/rootUri; roots/list was attempted but returned no usable repo root."
|
|
1407
|
+
);
|
|
1408
|
+
if (options.rootsListUris && options.rootsListUris.length > 0) {
|
|
1409
|
+
lines.push(`roots/list URIs: ${options.rootsListUris.join(", ")}`);
|
|
454
1410
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
|
|
458
|
-
}
|
|
459
|
-
const distinctProjectIds = new Set(bindings.map((b) => b.projectId));
|
|
460
|
-
if (distinctProjectIds.size > 1) {
|
|
461
|
-
const lines = bindings.map(
|
|
462
|
-
(b) => ` - workspace=${b.workspaceRoot} project=${b.projectId} m1=${b.m1Path}`
|
|
1411
|
+
lines.push(
|
|
1412
|
+
"Global Cursor MCP cannot safely bind per-window repos without a workspace signal from Cursor (initialize roots or roots/list)."
|
|
463
1413
|
);
|
|
464
|
-
|
|
465
|
-
"
|
|
1414
|
+
lines.push(
|
|
1415
|
+
"Reload MCP in this Cursor window, or ensure this repo has a managed .cursor/mcp.json from setup-ide-files --cursor."
|
|
466
1416
|
);
|
|
1417
|
+
return lines.join("\n");
|
|
467
1418
|
}
|
|
468
|
-
|
|
1419
|
+
lines.push(
|
|
1420
|
+
"Reload MCP in this Cursor window so initialize includes workspaceFolders, rootUri, or a usable roots/list response for this repo."
|
|
1421
|
+
);
|
|
1422
|
+
return lines.join("\n");
|
|
469
1423
|
}
|
|
470
1424
|
|
|
471
1425
|
// src/socketPaths.ts
|
|
472
|
-
var
|
|
473
|
-
var
|
|
474
|
-
var
|
|
475
|
-
var BASE_DIR = process.env.MEMORAONE_MCP_LOCK_DIR ||
|
|
1426
|
+
var os3 = __toESM(require("os"), 1);
|
|
1427
|
+
var path10 = __toESM(require("path"), 1);
|
|
1428
|
+
var fs8 = __toESM(require("fs"), 1);
|
|
1429
|
+
var BASE_DIR = process.env.MEMORAONE_MCP_LOCK_DIR || path10.join(os3.homedir(), ".memoraone-mcp");
|
|
476
1430
|
var HASH_SOCKET_FILENAME_RE = new RegExp(
|
|
477
1431
|
`^mcp-[0-9a-f]{${BINDING_SOCKET_HASH_LENGTH}}\\.sock$`,
|
|
478
1432
|
"i"
|
|
@@ -488,6 +1442,17 @@ function parseIdeType(value) {
|
|
|
488
1442
|
function resolveIdeTypeFromEnv(env2 = process.env) {
|
|
489
1443
|
return parseIdeType(env2.MEMORAONE_IDE_TYPE);
|
|
490
1444
|
}
|
|
1445
|
+
function resolveBindingIdeType(env2 = process.env) {
|
|
1446
|
+
return resolveIdeTypeFromEnv(env2) ?? "";
|
|
1447
|
+
}
|
|
1448
|
+
function getBindingSocketFilename(binding, env2 = process.env) {
|
|
1449
|
+
const ideType = resolveBindingIdeType(env2);
|
|
1450
|
+
const hash = hashBindingIdentity(binding.repositoryBindingId, binding.workspaceRoot, ideType);
|
|
1451
|
+
return `mcp-${hash}.sock`;
|
|
1452
|
+
}
|
|
1453
|
+
function getBindingSocketPath(binding, env2 = process.env) {
|
|
1454
|
+
return path10.join(BASE_DIR, getBindingSocketFilename(binding, env2));
|
|
1455
|
+
}
|
|
491
1456
|
|
|
492
1457
|
// src/initializeBinding.ts
|
|
493
1458
|
var MEMORAONE_WORKSPACE_ROOT_ENV = "MEMORAONE_WORKSPACE_ROOT";
|
|
@@ -508,8 +1473,8 @@ function getEnvWorkspaceRootCandidates() {
|
|
|
508
1473
|
const raw = process.env.WORKSPACE_FOLDER_PATHS;
|
|
509
1474
|
const parts = [];
|
|
510
1475
|
if (raw !== void 0 && raw.trim() !== "") {
|
|
511
|
-
for (const p of raw.split(
|
|
512
|
-
parts.push(
|
|
1476
|
+
for (const p of raw.split(path11.delimiter).map((s) => s.trim()).filter(Boolean)) {
|
|
1477
|
+
parts.push(path11.resolve(p));
|
|
513
1478
|
}
|
|
514
1479
|
}
|
|
515
1480
|
parts.push(process.cwd());
|
|
@@ -533,7 +1498,7 @@ function extractWorkspaceRootsFromInitialize(params) {
|
|
|
533
1498
|
if (uri === void 0 || uri.trim() === "") {
|
|
534
1499
|
return;
|
|
535
1500
|
}
|
|
536
|
-
const resolved =
|
|
1501
|
+
const resolved = path11.resolve(uriToPath(uri));
|
|
537
1502
|
if (!seen.has(resolved)) {
|
|
538
1503
|
seen.add(resolved);
|
|
539
1504
|
roots.push(resolved);
|
|
@@ -555,11 +1520,11 @@ function getRepoScopedWorkspaceHint(env2 = process.env) {
|
|
|
555
1520
|
if (raw === void 0 || raw.trim() === "") {
|
|
556
1521
|
return null;
|
|
557
1522
|
}
|
|
558
|
-
return
|
|
1523
|
+
return path11.resolve(raw.trim());
|
|
559
1524
|
}
|
|
560
1525
|
function formatWorkspaceAmbiguityError(bindings) {
|
|
561
1526
|
const lines = bindings.map(
|
|
562
|
-
(b) => ` - workspace=${b.workspaceRoot}
|
|
1527
|
+
(b) => ` - workspace=${b.workspaceRoot} binding=${b.repositoryBindingId} project=${b.projectId}`
|
|
563
1528
|
);
|
|
564
1529
|
return "[memoraone-mcp] Ambiguous workspace binding: multiple open roots map to different MemoraOne projects.\n" + lines.join("\n") + `
|
|
565
1530
|
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.`;
|
|
@@ -567,7 +1532,7 @@ Open one repo per Cursor window, or ensure this repo's managed .cursor/mcp.json
|
|
|
567
1532
|
function formatRepoHintInitializeMismatchError(repoHintRoot, initializeBinding) {
|
|
568
1533
|
return `[memoraone-mcp] Repo-scoped workspace hint conflicts with MCP initialize workspace.
|
|
569
1534
|
${MEMORAONE_WORKSPACE_ROOT_ENV}: ${repoHintRoot}
|
|
570
|
-
initialize:
|
|
1535
|
+
initialize: binding=${initializeBinding.repositoryBindingId} project=${initializeBinding.projectId} workspace=${initializeBinding.workspaceRoot}
|
|
571
1536
|
Reload MCP in the Cursor window for this repo, or re-run setup-ide-files --cursor here if the hint is stale.`;
|
|
572
1537
|
}
|
|
573
1538
|
function formatRepoHintNotInRootsListError(repoHintRoot, rootsListPaths) {
|
|
@@ -578,13 +1543,16 @@ Reload MCP in the Cursor window for this repo, or re-run setup-ide-files --curso
|
|
|
578
1543
|
}
|
|
579
1544
|
function formatBindingMismatchError(daemonHint, sessionBinding) {
|
|
580
1545
|
return `[memoraone-mcp] Project binding mismatch between daemon and MCP initialize workspace.
|
|
581
|
-
daemon:
|
|
582
|
-
initialize:
|
|
583
|
-
Reconnect or reload the MCP server in this IDE window so the bridge can bind to the correct project
|
|
1546
|
+
daemon: binding=${daemonHint.repositoryBindingId} project=${daemonHint.projectId} workspace=${daemonHint.workspaceRoot}
|
|
1547
|
+
initialize: binding=${sessionBinding.repositoryBindingId} project=${sessionBinding.projectId} workspace=${sessionBinding.workspaceRoot}
|
|
1548
|
+
Reconnect or reload the MCP server in this IDE window so the bridge can bind to the correct project.
|
|
1549
|
+
If this workspace is not connected, run: memoraone-mcp connect <code>`;
|
|
584
1550
|
}
|
|
585
1551
|
async function resolveBindingFromWorkspaceRoots(workspaceRoots, options = {}) {
|
|
586
1552
|
if (workspaceRoots.length === 0) {
|
|
587
|
-
throw new Error(
|
|
1553
|
+
throw new Error(
|
|
1554
|
+
"[memoraone-mcp] No local MemoraOne binding for this workspace.\nRun: memoraone-mcp connect <code>"
|
|
1555
|
+
);
|
|
588
1556
|
}
|
|
589
1557
|
const bindings = [];
|
|
590
1558
|
for (const root of workspaceRoots) {
|
|
@@ -595,14 +1563,16 @@ async function resolveBindingFromWorkspaceRoots(workspaceRoots, options = {}) {
|
|
|
595
1563
|
})
|
|
596
1564
|
);
|
|
597
1565
|
} catch (err) {
|
|
598
|
-
if (err instanceof Error && err.message.includes("
|
|
1566
|
+
if (err instanceof Error && (err.message.includes("No local MemoraOne binding") || err.message.includes("memoraone-mcp connect") || err.name === "ReconnectRequiredError")) {
|
|
599
1567
|
continue;
|
|
600
1568
|
}
|
|
601
1569
|
throw err;
|
|
602
1570
|
}
|
|
603
1571
|
}
|
|
604
1572
|
if (bindings.length === 0) {
|
|
605
|
-
throw new Error(
|
|
1573
|
+
throw new Error(
|
|
1574
|
+
"[memoraone-mcp] No local MemoraOne binding for this workspace.\nRun: memoraone-mcp connect <code>"
|
|
1575
|
+
);
|
|
606
1576
|
}
|
|
607
1577
|
const distinctProjectIds = new Set(bindings.map((b) => b.projectId));
|
|
608
1578
|
if (distinctProjectIds.size > 1) {
|
|
@@ -633,108 +1603,67 @@ async function resolveBindingFromInitializeParams(params, options = {}) {
|
|
|
633
1603
|
const rootsListUris = options.rootsListUris ?? [];
|
|
634
1604
|
const rootsListPaths = rootsListUris.map((uri) => uriToPath(uri)).filter(Boolean);
|
|
635
1605
|
if (rootsListPaths.length > 1 && repoHint !== null) {
|
|
636
|
-
const hintResolved =
|
|
637
|
-
const matchingRoot = rootsListPaths.find((root) =>
|
|
1606
|
+
const hintResolved = path11.resolve(repoHint);
|
|
1607
|
+
const matchingRoot = rootsListPaths.find((root) => path11.resolve(root) === hintResolved);
|
|
638
1608
|
if (!matchingRoot) {
|
|
639
1609
|
throw new Error(formatRepoHintNotInRootsListError(repoHint, rootsListPaths));
|
|
640
1610
|
}
|
|
641
|
-
return resolveAuthoritativeBinding(repoHint, { respectExplicitM1Path: false });
|
|
642
|
-
}
|
|
643
|
-
if (rootsListPaths.length > 0) {
|
|
644
|
-
return resolveBindingFromWorkspaceRoots(rootsListPaths, resolveOpts);
|
|
645
|
-
}
|
|
646
|
-
if (repoHint !== null) {
|
|
647
|
-
return resolveAuthoritativeBinding(repoHint, { respectExplicitM1Path: false });
|
|
648
|
-
}
|
|
649
|
-
if (options.allowEnvWorkspaceFallback === false) {
|
|
650
|
-
throw new Error(
|
|
651
|
-
formatMissingInitializeWorkspaceError({
|
|
652
|
-
rootsListAttempted: options.rootsListAttempted === true,
|
|
653
|
-
rootsListUris
|
|
654
|
-
})
|
|
655
|
-
);
|
|
656
|
-
}
|
|
657
|
-
const fallbackRoots = options.fallbackWorkspaceRoots ?? getEnvWorkspaceRootCandidates();
|
|
658
|
-
return resolveAuthoritativeBinding(fallbackRoots, {
|
|
659
|
-
respectExplicitM1Path: options.respectExplicitM1Path
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// src/
|
|
664
|
-
var
|
|
665
|
-
var
|
|
666
|
-
function
|
|
667
|
-
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
try {
|
|
689
|
-
if (repoPath === void 0 || repoPath === null || String(repoPath).trim() === "") {
|
|
690
|
-
process.stderr.write(
|
|
691
|
-
`${LOG_PREFIX} skipped: empty repoPath (cannot register workspace)
|
|
692
|
-
`
|
|
693
|
-
);
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
const normalizedRepoPath = path6.resolve(String(repoPath));
|
|
697
|
-
const body = buildRepoSourcePayload(normalizedRepoPath, ideType);
|
|
698
|
-
const primaryPath = `/v1/projects/${projectId}/sources`;
|
|
699
|
-
const alternatePath = `/v1/projects/${projectId}/sources/register`;
|
|
700
|
-
process.stderr.write(
|
|
701
|
-
`${LOG_PREFIX} registering projectId=${projectId} path=${normalizedRepoPath} ideType=${ideType ?? "(none)"}
|
|
702
|
-
`
|
|
703
|
-
);
|
|
704
|
-
try {
|
|
705
|
-
await client.post(primaryPath, body, {
|
|
706
|
-
acceptStatuses: [409],
|
|
707
|
-
log: false,
|
|
708
|
-
quietHttpStatuses: [404, 405, 501]
|
|
709
|
-
});
|
|
710
|
-
process.stderr.write(`${LOG_PREFIX} ok: POST ${primaryPath}
|
|
711
|
-
`);
|
|
712
|
-
return;
|
|
713
|
-
} catch (err) {
|
|
714
|
-
if (err instanceof MemoraOneHttpError && (err.status === 404 || err.status === 405 || err.status === 501)) {
|
|
715
|
-
process.stderr.write(
|
|
716
|
-
`${LOG_PREFIX} primary route returned ${err.status}; retrying POST ${alternatePath}
|
|
717
|
-
`
|
|
718
|
-
);
|
|
719
|
-
await client.post(alternatePath, body, { acceptStatuses: [409], log: false });
|
|
720
|
-
process.stderr.write(`${LOG_PREFIX} ok: POST ${alternatePath}
|
|
721
|
-
`);
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
throw err;
|
|
725
|
-
}
|
|
726
|
-
} catch (err) {
|
|
727
|
-
const msg = String(err?.message ?? err);
|
|
728
|
-
process.stderr.write(`${LOG_PREFIX} failed: ${msg}
|
|
729
|
-
`);
|
|
730
|
-
if (err instanceof MemoraOneHttpError) {
|
|
731
|
-
const bodyStr = typeof err.body === "string" ? err.body : JSON.stringify(err.body ?? null);
|
|
732
|
-
process.stderr.write(
|
|
733
|
-
`${LOG_PREFIX} http status=${err.status} body=${bodyStr.length > 500 ? bodyStr.slice(0, 500) + "..." : bodyStr}
|
|
734
|
-
`
|
|
735
|
-
);
|
|
736
|
-
}
|
|
1611
|
+
return resolveAuthoritativeBinding(repoHint, { respectExplicitM1Path: false });
|
|
1612
|
+
}
|
|
1613
|
+
if (rootsListPaths.length > 0) {
|
|
1614
|
+
return resolveBindingFromWorkspaceRoots(rootsListPaths, resolveOpts);
|
|
1615
|
+
}
|
|
1616
|
+
if (repoHint !== null) {
|
|
1617
|
+
return resolveAuthoritativeBinding(repoHint, { respectExplicitM1Path: false });
|
|
1618
|
+
}
|
|
1619
|
+
if (options.allowEnvWorkspaceFallback === false) {
|
|
1620
|
+
throw new Error(
|
|
1621
|
+
formatMissingInitializeWorkspaceError({
|
|
1622
|
+
rootsListAttempted: options.rootsListAttempted === true,
|
|
1623
|
+
rootsListUris
|
|
1624
|
+
})
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
const fallbackRoots = options.fallbackWorkspaceRoots ?? getEnvWorkspaceRootCandidates();
|
|
1628
|
+
return resolveAuthoritativeBinding(fallbackRoots, {
|
|
1629
|
+
respectExplicitM1Path: options.respectExplicitM1Path
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// src/bindingSidecar.ts
|
|
1634
|
+
var fs9 = __toESM(require("fs"), 1);
|
|
1635
|
+
var path12 = __toESM(require("path"), 1);
|
|
1636
|
+
function bindingSidecarPath(socketPath) {
|
|
1637
|
+
if (socketPath.endsWith(".sock")) {
|
|
1638
|
+
return `${socketPath.slice(0, -".sock".length)}.binding.json`;
|
|
1639
|
+
}
|
|
1640
|
+
return `${socketPath}.binding.json`;
|
|
1641
|
+
}
|
|
1642
|
+
function writeBindingSidecar(socketPath, binding, ideType = resolveBindingIdeType()) {
|
|
1643
|
+
const payload = encodeResolvedBinding(binding);
|
|
1644
|
+
if (/accessToken|refreshToken|apiKey|mia_|mir_|sk_/.test(payload)) {
|
|
1645
|
+
throw new Error("[memoraone-mcp] Refusing to write sidecar containing secrets");
|
|
1646
|
+
}
|
|
1647
|
+
const record = {
|
|
1648
|
+
v: 3,
|
|
1649
|
+
...ideType ? { ideType } : {},
|
|
1650
|
+
repositoryBindingId: binding.repositoryBindingId,
|
|
1651
|
+
projectId: binding.projectId,
|
|
1652
|
+
workspaceRoot: binding.workspaceRoot,
|
|
1653
|
+
binding: payload
|
|
1654
|
+
};
|
|
1655
|
+
const text = JSON.stringify(record);
|
|
1656
|
+
if (/["']?(accessToken|refreshToken|apiKey|clientRedeemKey|clientRefreshKey)["']?\s*:/.test(text)) {
|
|
1657
|
+
throw new Error("[memoraone-mcp] Refusing to write sidecar containing secrets");
|
|
737
1658
|
}
|
|
1659
|
+
fs9.writeFileSync(bindingSidecarPath(socketPath), text, "utf8");
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// src/bridgeClientRoots.ts
|
|
1663
|
+
var readline = __toESM(require("readline"), 1);
|
|
1664
|
+
var TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
1665
|
+
function isInitializeDebugEnabled(env2 = process.env) {
|
|
1666
|
+
return TRUTHY.has(String(env2.MEMORAONE_DEBUG_INIT ?? "").trim().toLowerCase());
|
|
738
1667
|
}
|
|
739
1668
|
|
|
740
1669
|
// src/tools/postEvent.ts
|
|
@@ -838,21 +1767,11 @@ var logCommandShape = {
|
|
|
838
1767
|
stats: import_v410.z.record(import_v410.z.string(), import_v410.z.any()).optional()
|
|
839
1768
|
};
|
|
840
1769
|
|
|
841
|
-
// src/tools/listProjects.ts
|
|
842
|
-
var listProjectsShape = {};
|
|
843
|
-
|
|
844
|
-
// src/tools/setProject.ts
|
|
845
|
-
var import_v411 = require("zod/v4");
|
|
846
|
-
var setProjectShape = {
|
|
847
|
-
projectKey: import_v411.z.string().min(1).optional(),
|
|
848
|
-
projectId: import_v411.z.string().min(1).optional()
|
|
849
|
-
};
|
|
850
|
-
|
|
851
1770
|
// src/tools/bindingStatus.ts
|
|
852
1771
|
var bindingStatusShape = {};
|
|
853
1772
|
|
|
854
1773
|
// src/tools/handlers/postEvent.ts
|
|
855
|
-
var
|
|
1774
|
+
var import_v411 = require("zod/v4");
|
|
856
1775
|
var crypto4 = __toESM(require("crypto"), 1);
|
|
857
1776
|
|
|
858
1777
|
// src/runContext.ts
|
|
@@ -885,9 +1804,6 @@ function getBoundProjectId() {
|
|
|
885
1804
|
function setBoundProjectId(id) {
|
|
886
1805
|
getSessionContext().boundProjectId = id;
|
|
887
1806
|
}
|
|
888
|
-
function setBoundApiKey(key) {
|
|
889
|
-
getSessionContext().boundApiKey = key;
|
|
890
|
-
}
|
|
891
1807
|
function getCurrentRunId() {
|
|
892
1808
|
return getSessionContext().currentRunId;
|
|
893
1809
|
}
|
|
@@ -900,9 +1816,6 @@ function getCurrentProjectId() {
|
|
|
900
1816
|
function setCurrentProjectId(id) {
|
|
901
1817
|
getSessionContext().currentProjectId = id;
|
|
902
1818
|
}
|
|
903
|
-
function setCurrentApiKey(key) {
|
|
904
|
-
getSessionContext().currentApiKey = key;
|
|
905
|
-
}
|
|
906
1819
|
function resolveRunId(passed) {
|
|
907
1820
|
if (passed) {
|
|
908
1821
|
return passed;
|
|
@@ -914,14 +1827,14 @@ function generateRunId() {
|
|
|
914
1827
|
}
|
|
915
1828
|
|
|
916
1829
|
// src/tools/handlers/postEvent.ts
|
|
917
|
-
var postEventInputSchema =
|
|
918
|
-
kind:
|
|
919
|
-
actor:
|
|
920
|
-
identifier:
|
|
921
|
-
id:
|
|
1830
|
+
var postEventInputSchema = import_v411.z.object({
|
|
1831
|
+
kind: import_v411.z.string().min(1),
|
|
1832
|
+
actor: import_v411.z.object({
|
|
1833
|
+
identifier: import_v411.z.string().min(1),
|
|
1834
|
+
id: import_v411.z.string().min(1).optional()
|
|
922
1835
|
}),
|
|
923
|
-
content:
|
|
924
|
-
metadata:
|
|
1836
|
+
content: import_v411.z.record(import_v411.z.string(), import_v411.z.any()),
|
|
1837
|
+
metadata: import_v411.z.record(import_v411.z.string(), import_v411.z.any()).optional()
|
|
925
1838
|
});
|
|
926
1839
|
function buildPostEventContentFields(content) {
|
|
927
1840
|
if (typeof content.message === "string") {
|
|
@@ -957,7 +1870,9 @@ async function handlePostEvent(client, args) {
|
|
|
957
1870
|
const parsed2 = postEventInputSchema.parse(args ?? {});
|
|
958
1871
|
const projectKey = getCurrentProjectId();
|
|
959
1872
|
if (!projectKey) {
|
|
960
|
-
throw new Error(
|
|
1873
|
+
throw new Error(
|
|
1874
|
+
"No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
|
|
1875
|
+
);
|
|
961
1876
|
}
|
|
962
1877
|
const content = parsed2.content ?? {};
|
|
963
1878
|
const { message, new_value } = buildPostEventContentFields(content);
|
|
@@ -991,16 +1906,18 @@ async function handlePostEvent(client, args) {
|
|
|
991
1906
|
}
|
|
992
1907
|
|
|
993
1908
|
// src/tools/handlers/createFact.ts
|
|
994
|
-
var
|
|
995
|
-
var createFactInputSchema =
|
|
996
|
-
content:
|
|
997
|
-
metadata:
|
|
1909
|
+
var import_v412 = require("zod/v4");
|
|
1910
|
+
var createFactInputSchema = import_v412.z.object({
|
|
1911
|
+
content: import_v412.z.string().min(1),
|
|
1912
|
+
metadata: import_v412.z.record(import_v412.z.string(), import_v412.z.any()).optional()
|
|
998
1913
|
});
|
|
999
1914
|
async function handleCreateFact(client, args) {
|
|
1000
1915
|
const parsed2 = createFactInputSchema.parse(args ?? {});
|
|
1001
1916
|
const projectKey = getCurrentProjectId();
|
|
1002
1917
|
if (!projectKey) {
|
|
1003
|
-
throw new Error(
|
|
1918
|
+
throw new Error(
|
|
1919
|
+
"No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
|
|
1920
|
+
);
|
|
1004
1921
|
}
|
|
1005
1922
|
const content = parsed2.content.trim();
|
|
1006
1923
|
if (!content) {
|
|
@@ -1037,13 +1954,13 @@ async function handleCreateFact(client, args) {
|
|
|
1037
1954
|
}
|
|
1038
1955
|
|
|
1039
1956
|
// src/tools/handlers/addPersonalContext.ts
|
|
1040
|
-
var
|
|
1041
|
-
var addPersonalContextInputSchema =
|
|
1042
|
-
content:
|
|
1043
|
-
category:
|
|
1044
|
-
tags:
|
|
1045
|
-
scope_type:
|
|
1046
|
-
scope_id:
|
|
1957
|
+
var import_v413 = require("zod/v4");
|
|
1958
|
+
var addPersonalContextInputSchema = import_v413.z.object({
|
|
1959
|
+
content: import_v413.z.string().min(1),
|
|
1960
|
+
category: import_v413.z.string().optional(),
|
|
1961
|
+
tags: import_v413.z.array(import_v413.z.string()).optional(),
|
|
1962
|
+
scope_type: import_v413.z.enum(["general", "project"]).optional(),
|
|
1963
|
+
scope_id: import_v413.z.string().optional()
|
|
1047
1964
|
});
|
|
1048
1965
|
async function handleAddPersonalContext(client, args) {
|
|
1049
1966
|
const parsed2 = addPersonalContextInputSchema.parse(args ?? {});
|
|
@@ -1079,12 +1996,12 @@ async function handleAddPersonalContext(client, args) {
|
|
|
1079
1996
|
}
|
|
1080
1997
|
|
|
1081
1998
|
// src/tools/handlers/getPersonalContext.ts
|
|
1082
|
-
var
|
|
1083
|
-
var getPersonalContextInputSchema =
|
|
1084
|
-
query:
|
|
1085
|
-
scope_type:
|
|
1086
|
-
scope_id:
|
|
1087
|
-
limit:
|
|
1999
|
+
var import_v414 = require("zod/v4");
|
|
2000
|
+
var getPersonalContextInputSchema = import_v414.z.object({
|
|
2001
|
+
query: import_v414.z.string().optional(),
|
|
2002
|
+
scope_type: import_v414.z.enum(["general", "project"]).optional(),
|
|
2003
|
+
scope_id: import_v414.z.string().optional(),
|
|
2004
|
+
limit: import_v414.z.number().int().positive().optional()
|
|
1088
2005
|
});
|
|
1089
2006
|
function buildPersonalContextPath(parsed2) {
|
|
1090
2007
|
const params = new URLSearchParams();
|
|
@@ -1105,9 +2022,9 @@ function buildPersonalContextPath(parsed2) {
|
|
|
1105
2022
|
}
|
|
1106
2023
|
async function handleGetPersonalContext(client, args) {
|
|
1107
2024
|
const parsed2 = getPersonalContextInputSchema.parse(args ?? {});
|
|
1108
|
-
const
|
|
2025
|
+
const path14 = buildPersonalContextPath(parsed2);
|
|
1109
2026
|
try {
|
|
1110
|
-
const result = await client.get(
|
|
2027
|
+
const result = await client.get(path14);
|
|
1111
2028
|
return { ok: true, result };
|
|
1112
2029
|
} catch (err) {
|
|
1113
2030
|
if (err instanceof MemoraOneHttpError) {
|
|
@@ -1121,13 +2038,13 @@ async function handleGetPersonalContext(client, args) {
|
|
|
1121
2038
|
}
|
|
1122
2039
|
|
|
1123
2040
|
// src/tools/handlers/askWithMemory.ts
|
|
1124
|
-
var
|
|
1125
|
-
var askWithMemoryInputSchema =
|
|
1126
|
-
question:
|
|
1127
|
-
code_context:
|
|
1128
|
-
file_path:
|
|
1129
|
-
selected_text:
|
|
1130
|
-
language:
|
|
2041
|
+
var import_v415 = require("zod/v4");
|
|
2042
|
+
var askWithMemoryInputSchema = import_v415.z.object({
|
|
2043
|
+
question: import_v415.z.string().min(1),
|
|
2044
|
+
code_context: import_v415.z.object({
|
|
2045
|
+
file_path: import_v415.z.string().optional(),
|
|
2046
|
+
selected_text: import_v415.z.string().optional(),
|
|
2047
|
+
language: import_v415.z.string().optional()
|
|
1131
2048
|
}).optional()
|
|
1132
2049
|
});
|
|
1133
2050
|
function isAskWithMemoryResponse(value) {
|
|
@@ -1137,7 +2054,9 @@ async function handleAskWithMemory(client, args) {
|
|
|
1137
2054
|
const parsed2 = askWithMemoryInputSchema.parse(args ?? {});
|
|
1138
2055
|
const projectKey = getCurrentProjectId();
|
|
1139
2056
|
if (!projectKey) {
|
|
1140
|
-
throw new Error(
|
|
2057
|
+
throw new Error(
|
|
2058
|
+
"No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
|
|
2059
|
+
);
|
|
1141
2060
|
}
|
|
1142
2061
|
const payload = {
|
|
1143
2062
|
question: parsed2.question,
|
|
@@ -1161,19 +2080,21 @@ async function handleAskWithMemory(client, args) {
|
|
|
1161
2080
|
}
|
|
1162
2081
|
|
|
1163
2082
|
// src/tools/handlers/logIntent.ts
|
|
1164
|
-
var
|
|
1165
|
-
var logIntentInputSchema =
|
|
1166
|
-
intent:
|
|
1167
|
-
message:
|
|
1168
|
-
context:
|
|
1169
|
-
intent_source:
|
|
1170
|
-
run_id:
|
|
2083
|
+
var import_v416 = require("zod/v4");
|
|
2084
|
+
var logIntentInputSchema = import_v416.z.object({
|
|
2085
|
+
intent: import_v416.z.enum(["task", "decision"]),
|
|
2086
|
+
message: import_v416.z.string().min(1),
|
|
2087
|
+
context: import_v416.z.record(import_v416.z.string(), import_v416.z.any()).optional(),
|
|
2088
|
+
intent_source: import_v416.z.string().optional().default("cursor_chat"),
|
|
2089
|
+
run_id: import_v416.z.string().min(1).optional()
|
|
1171
2090
|
});
|
|
1172
2091
|
async function handleLogIntent(client, args) {
|
|
1173
2092
|
const parsed2 = logIntentInputSchema.parse(args ?? {});
|
|
1174
2093
|
const projectKey = getCurrentProjectId();
|
|
1175
2094
|
if (!projectKey) {
|
|
1176
|
-
throw new Error(
|
|
2095
|
+
throw new Error(
|
|
2096
|
+
"No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
|
|
2097
|
+
);
|
|
1177
2098
|
}
|
|
1178
2099
|
const intent = parsed2.intent;
|
|
1179
2100
|
const message = parsed2.message.trim();
|
|
@@ -1207,525 +2128,188 @@ async function handleLogIntent(client, args) {
|
|
|
1207
2128
|
}
|
|
1208
2129
|
|
|
1209
2130
|
// src/tools/handlers/logChangeSummary.ts
|
|
1210
|
-
var
|
|
1211
|
-
var logChangeSummaryInputSchema =
|
|
1212
|
-
summary:
|
|
1213
|
-
scope:
|
|
1214
|
-
files:
|
|
1215
|
-
stats:
|
|
1216
|
-
files:
|
|
1217
|
-
add:
|
|
1218
|
-
del:
|
|
2131
|
+
var import_v417 = require("zod/v4");
|
|
2132
|
+
var logChangeSummaryInputSchema = import_v417.z.object({
|
|
2133
|
+
summary: import_v417.z.string().min(1),
|
|
2134
|
+
scope: import_v417.z.string().min(1).optional(),
|
|
2135
|
+
files: import_v417.z.array(import_v417.z.string().min(1)).optional(),
|
|
2136
|
+
stats: import_v417.z.object({
|
|
2137
|
+
files: import_v417.z.number().int().nonnegative().optional(),
|
|
2138
|
+
add: import_v417.z.number().int().nonnegative().optional(),
|
|
2139
|
+
del: import_v417.z.number().int().nonnegative().optional()
|
|
1219
2140
|
}).optional(),
|
|
1220
|
-
commit:
|
|
1221
|
-
run_id:
|
|
2141
|
+
commit: import_v417.z.string().min(1).optional(),
|
|
2142
|
+
run_id: import_v417.z.string().min(1).optional()
|
|
1222
2143
|
});
|
|
1223
2144
|
async function handleLogChangeSummary(client, args) {
|
|
1224
2145
|
const parsed2 = logChangeSummaryInputSchema.parse(args ?? {});
|
|
1225
|
-
const projectKey = getCurrentProjectId();
|
|
1226
|
-
if (!projectKey) {
|
|
1227
|
-
throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
|
|
1228
|
-
}
|
|
1229
|
-
const { summary, scope, files, stats, commit } = parsed2;
|
|
1230
|
-
const message = summary.startsWith("CHANGE:") ? summary : `CHANGE: ${scope ?? "code"} \u2014 ${summary}`;
|
|
1231
|
-
const run_id = resolveRunId(parsed2.run_id);
|
|
1232
|
-
const body = {
|
|
1233
|
-
kind: "note",
|
|
1234
|
-
concept: "concept:change_summary",
|
|
1235
|
-
actor: { type: config2.agentType, name: config2.agentName },
|
|
1236
|
-
message,
|
|
1237
|
-
projectKey,
|
|
1238
|
-
metadata: {
|
|
1239
|
-
source: config2.source,
|
|
1240
|
-
purpose: "change_summary",
|
|
1241
|
-
tool: "memora_log_change_summary",
|
|
1242
|
-
...scope ? { scope } : {},
|
|
1243
|
-
...files ? { files } : {},
|
|
1244
|
-
...stats ? { stats } : {},
|
|
1245
|
-
...commit ? { commit } : {},
|
|
1246
|
-
...run_id ? { run_id } : {}
|
|
1247
|
-
}
|
|
1248
|
-
};
|
|
1249
|
-
await client.post("/timeline/events", body);
|
|
1250
|
-
return { ok: true };
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
// src/tools/handlers/logToolResult.ts
|
|
1254
|
-
var import_v419 = require("zod/v4");
|
|
1255
|
-
var logToolResultInputSchema = import_v419.z.object({
|
|
1256
|
-
tool: import_v419.z.string().min(1),
|
|
1257
|
-
status: import_v419.z.enum(["ok", "error", "partial"]),
|
|
1258
|
-
summary: import_v419.z.string().min(1),
|
|
1259
|
-
run_id: import_v419.z.string().min(1).optional(),
|
|
1260
|
-
duration_ms: import_v419.z.number().int().nonnegative().optional(),
|
|
1261
|
-
error_code: import_v419.z.string().min(1).optional(),
|
|
1262
|
-
error_message: import_v419.z.string().min(1).optional(),
|
|
1263
|
-
error_kind: import_v419.z.enum(["infra", "logic", "auth", "rate_limit", "validation", "unknown"]).optional(),
|
|
1264
|
-
stats: import_v419.z.record(import_v419.z.string(), import_v419.z.any()).optional()
|
|
1265
|
-
});
|
|
1266
|
-
async function handleLogToolResult(client, args) {
|
|
1267
|
-
const parsed2 = logToolResultInputSchema.parse(args ?? {});
|
|
1268
|
-
const projectKey = getCurrentProjectId();
|
|
1269
|
-
if (!projectKey) {
|
|
1270
|
-
throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
|
|
1271
|
-
}
|
|
1272
|
-
const { tool, status, summary, duration_ms, error_code, error_message, error_kind, stats } = parsed2;
|
|
1273
|
-
const message = summary.startsWith("RESULT:") ? summary : `RESULT: ${tool} \u2014 ${status} \u2014 ${summary}`;
|
|
1274
|
-
const run_id = resolveRunId(parsed2.run_id);
|
|
1275
|
-
const body = {
|
|
1276
|
-
kind: "note",
|
|
1277
|
-
concept: "concept:tool_result",
|
|
1278
|
-
actor: { type: config2.agentType, name: config2.agentName },
|
|
1279
|
-
message,
|
|
1280
|
-
projectKey,
|
|
1281
|
-
metadata: {
|
|
1282
|
-
source: config2.source,
|
|
1283
|
-
purpose: "tool_result",
|
|
1284
|
-
tool: "memora_log_tool_result",
|
|
1285
|
-
tool_name: tool,
|
|
1286
|
-
status,
|
|
1287
|
-
...run_id ? { run_id } : {},
|
|
1288
|
-
...duration_ms ? { duration_ms } : {},
|
|
1289
|
-
...error_code ? { error_code } : {},
|
|
1290
|
-
...error_message ? { error_message } : {},
|
|
1291
|
-
...error_kind ? { error_kind } : {},
|
|
1292
|
-
...stats ? { stats } : {}
|
|
1293
|
-
}
|
|
1294
|
-
};
|
|
1295
|
-
await client.post("/timeline/events", body);
|
|
1296
|
-
return { ok: true };
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
// src/tools/handlers/logCommand.ts
|
|
1300
|
-
var import_v420 = require("zod/v4");
|
|
1301
|
-
var logCommandInputSchema = import_v420.z.object({
|
|
1302
|
-
cmd: import_v420.z.string().min(1),
|
|
1303
|
-
summary: import_v420.z.string().min(1),
|
|
1304
|
-
cwd: import_v420.z.string().min(1).optional(),
|
|
1305
|
-
exit_code: import_v420.z.number().int().optional(),
|
|
1306
|
-
duration_ms: import_v420.z.number().int().nonnegative().optional(),
|
|
1307
|
-
run_id: import_v420.z.string().min(1).optional(),
|
|
1308
|
-
stats: import_v420.z.record(import_v420.z.string(), import_v420.z.any()).optional()
|
|
1309
|
-
});
|
|
1310
|
-
async function handleLogCommand(client, args) {
|
|
1311
|
-
const parsed2 = logCommandInputSchema.parse(args ?? {});
|
|
1312
|
-
const projectKey = getCurrentProjectId();
|
|
1313
|
-
if (!projectKey) {
|
|
1314
|
-
throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
|
|
1315
|
-
}
|
|
1316
|
-
const { cmd, summary, cwd: cwd2, exit_code, duration_ms, stats } = parsed2;
|
|
1317
|
-
const message = summary.startsWith("COMMAND:") ? summary : `COMMAND: ${cmd} \u2014 ${summary}`;
|
|
1318
|
-
const run_id = resolveRunId(parsed2.run_id);
|
|
1319
|
-
const body = {
|
|
1320
|
-
kind: "note",
|
|
1321
|
-
concept: "concept:command",
|
|
1322
|
-
actor: { type: config2.agentType, name: config2.agentName },
|
|
1323
|
-
message,
|
|
1324
|
-
projectKey,
|
|
1325
|
-
metadata: {
|
|
1326
|
-
source: config2.source,
|
|
1327
|
-
purpose: "command",
|
|
1328
|
-
tool: "memora_log_command",
|
|
1329
|
-
cmd,
|
|
1330
|
-
...cwd2 ? { cwd: cwd2 } : {},
|
|
1331
|
-
...exit_code !== void 0 ? { exit_code } : {},
|
|
1332
|
-
...duration_ms !== void 0 ? { duration_ms } : {},
|
|
1333
|
-
...run_id ? { run_id } : {},
|
|
1334
|
-
...stats ? { stats } : {}
|
|
1335
|
-
}
|
|
1336
|
-
};
|
|
1337
|
-
await client.post("/timeline/events", body);
|
|
1338
|
-
return { ok: true };
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
// src/tools/handlers/listProjects.ts
|
|
1342
|
-
async function handleListProjects(client) {
|
|
1343
|
-
const res = await client.get("/v1/projects");
|
|
1344
|
-
return res ?? { items: [] };
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
// src/tools/handlers/setProject.ts
|
|
1348
|
-
var import_v421 = require("zod/v4");
|
|
1349
|
-
|
|
1350
|
-
// src/repoFingerprint.ts
|
|
1351
|
-
var fs4 = __toESM(require("fs"), 1);
|
|
1352
|
-
var path7 = __toESM(require("path"), 1);
|
|
1353
|
-
var crypto5 = __toESM(require("crypto"), 1);
|
|
1354
|
-
var parseBooleanFlag3 = (value) => {
|
|
1355
|
-
if (!value) {
|
|
1356
|
-
return false;
|
|
1357
|
-
}
|
|
1358
|
-
const normalized = value.trim().toLowerCase();
|
|
1359
|
-
return ["1", "true", "yes", "on"].includes(normalized);
|
|
1360
|
-
};
|
|
1361
|
-
var debugEnabled2 = parseBooleanFlag3(process.env.MEMORAONE_DEV_MODE);
|
|
1362
|
-
var debugLog = (message) => {
|
|
1363
|
-
if (!debugEnabled2) {
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
process.stderr.write(`[memoraone-mcp][debug] ${message}
|
|
1367
|
-
`);
|
|
1368
|
-
};
|
|
1369
|
-
var normalizeRemoteUrl = (remoteUrl) => {
|
|
1370
|
-
let normalized = remoteUrl.trim();
|
|
1371
|
-
normalized = normalized.replace(/^[a-z]+:\/\//i, "");
|
|
1372
|
-
normalized = normalized.replace(/^git@([^:]+):/i, "$1/");
|
|
1373
|
-
normalized = normalized.replace(/\.git$/i, "");
|
|
1374
|
-
normalized = normalized.replace(/\/+$/, "");
|
|
1375
|
-
return normalized.toLowerCase();
|
|
1376
|
-
};
|
|
1377
|
-
var sha256 = (value) => {
|
|
1378
|
-
return crypto5.createHash("sha256").update(value).digest("hex");
|
|
1379
|
-
};
|
|
1380
|
-
var resolveGitDir = (gitPath) => {
|
|
1381
|
-
try {
|
|
1382
|
-
const stat2 = fs4.statSync(gitPath);
|
|
1383
|
-
if (stat2.isDirectory()) {
|
|
1384
|
-
return gitPath;
|
|
1385
|
-
}
|
|
1386
|
-
if (stat2.isFile()) {
|
|
1387
|
-
const content = fs4.readFileSync(gitPath, "utf8");
|
|
1388
|
-
const match = content.match(/^gitdir:\s*(.+)$/m);
|
|
1389
|
-
if (match) {
|
|
1390
|
-
const gitDir = match[1].trim();
|
|
1391
|
-
return path7.resolve(path7.dirname(gitPath), gitDir);
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
} catch {
|
|
1395
|
-
return null;
|
|
1396
|
-
}
|
|
1397
|
-
return null;
|
|
1398
|
-
};
|
|
1399
|
-
var findGitRoot = (start) => {
|
|
1400
|
-
let current = path7.resolve(start);
|
|
1401
|
-
while (true) {
|
|
1402
|
-
const gitPath = path7.join(current, ".git");
|
|
1403
|
-
if (fs4.existsSync(gitPath)) {
|
|
1404
|
-
const gitDir = resolveGitDir(gitPath);
|
|
1405
|
-
if (gitDir) {
|
|
1406
|
-
return { gitRoot: current, gitDir };
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
const parent = path7.dirname(current);
|
|
1410
|
-
if (parent === current) {
|
|
1411
|
-
break;
|
|
1412
|
-
}
|
|
1413
|
-
current = parent;
|
|
1414
|
-
}
|
|
1415
|
-
return null;
|
|
1416
|
-
};
|
|
1417
|
-
var readOriginRemote = (gitDir) => {
|
|
1418
|
-
const configPath = path7.join(gitDir, "config");
|
|
1419
|
-
try {
|
|
1420
|
-
const content = fs4.readFileSync(configPath, "utf8");
|
|
1421
|
-
const lines = content.split(/\r?\n/);
|
|
1422
|
-
let inOrigin = false;
|
|
1423
|
-
for (const line of lines) {
|
|
1424
|
-
const sectionMatch = line.match(/^\s*\[(.+)]\s*$/);
|
|
1425
|
-
if (sectionMatch) {
|
|
1426
|
-
inOrigin = sectionMatch[1].trim() === 'remote "origin"';
|
|
1427
|
-
continue;
|
|
1428
|
-
}
|
|
1429
|
-
if (inOrigin) {
|
|
1430
|
-
const urlMatch = line.match(/^\s*url\s*=\s*(.+)\s*$/);
|
|
1431
|
-
if (urlMatch) {
|
|
1432
|
-
return urlMatch[1].trim();
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
} catch {
|
|
1437
|
-
return null;
|
|
1438
|
-
}
|
|
1439
|
-
return null;
|
|
1440
|
-
};
|
|
1441
|
-
function resolveRepoFingerprint(cwd2) {
|
|
1442
|
-
const found = findGitRoot(cwd2);
|
|
1443
|
-
if (!found) {
|
|
1444
|
-
const fallbackPath = path7.resolve(cwd2);
|
|
1445
|
-
const fingerprint2 = sha256(fallbackPath);
|
|
1446
|
-
debugLog(`repo fingerprint=${fingerprint2} source=path-fallback`);
|
|
1447
|
-
return {
|
|
1448
|
-
fingerprint: fingerprint2,
|
|
1449
|
-
gitRoot: fallbackPath,
|
|
1450
|
-
source: "path-fallback"
|
|
1451
|
-
};
|
|
1452
|
-
}
|
|
1453
|
-
const { gitRoot, gitDir } = found;
|
|
1454
|
-
const remoteUrl = readOriginRemote(gitDir);
|
|
1455
|
-
if (remoteUrl) {
|
|
1456
|
-
const normalized = normalizeRemoteUrl(remoteUrl);
|
|
1457
|
-
const fingerprint2 = sha256(normalized);
|
|
1458
|
-
debugLog(`repo fingerprint=${fingerprint2} source=git-remote`);
|
|
1459
|
-
return {
|
|
1460
|
-
fingerprint: fingerprint2,
|
|
1461
|
-
gitRoot,
|
|
1462
|
-
remoteUrl,
|
|
1463
|
-
source: "git-remote"
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
|
-
const fingerprint = sha256(path7.resolve(gitRoot));
|
|
1467
|
-
debugLog(`repo fingerprint=${fingerprint} source=path-fallback`);
|
|
1468
|
-
return {
|
|
1469
|
-
fingerprint,
|
|
1470
|
-
gitRoot,
|
|
1471
|
-
source: "path-fallback"
|
|
1472
|
-
};
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
// src/workspaceMap.ts
|
|
1476
|
-
var fs5 = __toESM(require("fs/promises"), 1);
|
|
1477
|
-
var path8 = __toESM(require("path"), 1);
|
|
1478
|
-
var import_node_os = __toESM(require("os"), 1);
|
|
1479
|
-
var parseBooleanFlag4 = (value) => {
|
|
1480
|
-
if (!value) {
|
|
1481
|
-
return false;
|
|
1482
|
-
}
|
|
1483
|
-
const normalized = value.trim().toLowerCase();
|
|
1484
|
-
return ["1", "true", "yes", "on"].includes(normalized);
|
|
1485
|
-
};
|
|
1486
|
-
var debugEnabled3 = parseBooleanFlag4(process.env.MEMORAONE_DEV_MODE);
|
|
1487
|
-
var debugLog2 = (message) => {
|
|
1488
|
-
if (!debugEnabled3) {
|
|
1489
|
-
return;
|
|
1490
|
-
}
|
|
1491
|
-
process.stderr.write(`[memoraone-mcp][debug] ${message}
|
|
1492
|
-
`);
|
|
1493
|
-
};
|
|
1494
|
-
var fingerprintRegex = /^[0-9a-f]{64}$/i;
|
|
1495
|
-
function getWorkspaceMapPath() {
|
|
1496
|
-
return path8.join(import_node_os.default.homedir(), ".memoraone", "workspaces.json");
|
|
1497
|
-
}
|
|
1498
|
-
var ensureWorkspaceDir = async () => {
|
|
1499
|
-
const dir = path8.dirname(getWorkspaceMapPath());
|
|
1500
|
-
await fs5.mkdir(dir, { recursive: true });
|
|
1501
|
-
};
|
|
1502
|
-
async function acquireWorkspaceMapLock() {
|
|
1503
|
-
const filePath = getWorkspaceMapPath();
|
|
1504
|
-
const lockPath = `${filePath}.lock`;
|
|
1505
|
-
const maxRetries = 10;
|
|
1506
|
-
const retryDelayMs = 50;
|
|
1507
|
-
const maxLockAgeMs = 5e3;
|
|
1508
|
-
await ensureWorkspaceDir();
|
|
1509
|
-
let lockAcquired = false;
|
|
1510
|
-
let retries = 0;
|
|
1511
|
-
while (!lockAcquired && retries < maxRetries) {
|
|
1512
|
-
try {
|
|
1513
|
-
try {
|
|
1514
|
-
const stat2 = await fs5.stat(lockPath);
|
|
1515
|
-
const ageMs = Date.now() - stat2.mtimeMs;
|
|
1516
|
-
if (ageMs > maxLockAgeMs) {
|
|
1517
|
-
await fs5.unlink(lockPath);
|
|
1518
|
-
debugLog2(`removed stale workspace map lock (age: ${ageMs}ms)`);
|
|
1519
|
-
}
|
|
1520
|
-
} catch (err) {
|
|
1521
|
-
if (err?.code !== "ENOENT") {
|
|
1522
|
-
throw err;
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
const fd = await fs5.open(lockPath, "wx");
|
|
1526
|
-
await fd.close();
|
|
1527
|
-
lockAcquired = true;
|
|
1528
|
-
} catch (err) {
|
|
1529
|
-
if (err?.code === "EEXIST") {
|
|
1530
|
-
retries++;
|
|
1531
|
-
if (retries < maxRetries) {
|
|
1532
|
-
await new Promise((resolve7) => setTimeout(resolve7, retryDelayMs));
|
|
1533
|
-
continue;
|
|
1534
|
-
}
|
|
1535
|
-
throw new Error(
|
|
1536
|
-
`[memoraone-mcp] Failed to acquire workspace map lock after ${maxRetries} retries`
|
|
1537
|
-
);
|
|
1538
|
-
}
|
|
1539
|
-
throw err;
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
return async () => {
|
|
1543
|
-
try {
|
|
1544
|
-
await fs5.unlink(lockPath);
|
|
1545
|
-
} catch (err) {
|
|
1546
|
-
if (err?.code !== "ENOENT") {
|
|
1547
|
-
debugLog2(`failed to release workspace map lock: ${String(err)}`);
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
};
|
|
1551
|
-
}
|
|
1552
|
-
var validateWorkspaceMap = (map, filePath) => {
|
|
1553
|
-
if (!map || typeof map !== "object" || Array.isArray(map)) {
|
|
2146
|
+
const projectKey = getCurrentProjectId();
|
|
2147
|
+
if (!projectKey) {
|
|
1554
2148
|
throw new Error(
|
|
1555
|
-
|
|
2149
|
+
"No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
|
|
1556
2150
|
);
|
|
1557
2151
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
}
|
|
1577
|
-
const projectKey = entry.projectKey ?? entry.project_id;
|
|
1578
|
-
if (!projectKey || !projectKey.trim()) {
|
|
1579
|
-
throw new Error(
|
|
1580
|
-
`[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
|
|
1581
|
-
);
|
|
1582
|
-
}
|
|
1583
|
-
const source = entry.source;
|
|
1584
|
-
if (source !== void 0 && typeof source !== "string") {
|
|
1585
|
-
throw new Error(
|
|
1586
|
-
`[memoraone-mcp] Invalid workspace source in ${filePath}`
|
|
1587
|
-
);
|
|
1588
|
-
}
|
|
1589
|
-
const linkedAt = entry.linked_at;
|
|
1590
|
-
if (linkedAt !== void 0 && typeof linkedAt !== "string") {
|
|
1591
|
-
throw new Error(
|
|
1592
|
-
`[memoraone-mcp] Invalid workspace linked_at in ${filePath}`
|
|
1593
|
-
);
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
};
|
|
1597
|
-
async function readWorkspaceMap() {
|
|
1598
|
-
const filePath = getWorkspaceMapPath();
|
|
1599
|
-
try {
|
|
1600
|
-
const content = await fs5.readFile(filePath, "utf8");
|
|
1601
|
-
const parsed2 = JSON.parse(content);
|
|
1602
|
-
validateWorkspaceMap(parsed2, filePath);
|
|
1603
|
-
const typed = parsed2;
|
|
1604
|
-
let migrated = false;
|
|
1605
|
-
const normalized = {};
|
|
1606
|
-
for (const [fingerprint, entry] of Object.entries(typed)) {
|
|
1607
|
-
if (typeof entry === "string") {
|
|
1608
|
-
normalized[fingerprint] = entry;
|
|
1609
|
-
continue;
|
|
1610
|
-
}
|
|
1611
|
-
const projectKey = entry.projectKey ?? entry.project_id ?? "";
|
|
1612
|
-
if (entry.project_id && !entry.projectKey) {
|
|
1613
|
-
migrated = true;
|
|
1614
|
-
}
|
|
1615
|
-
normalized[fingerprint] = {
|
|
1616
|
-
...projectKey ? { projectKey } : {},
|
|
1617
|
-
...entry.source ? { source: entry.source } : {},
|
|
1618
|
-
...entry.linked_at ? { linked_at: entry.linked_at } : {}
|
|
1619
|
-
};
|
|
1620
|
-
}
|
|
1621
|
-
debugLog2(
|
|
1622
|
-
`workspace map loaded path=${filePath} entries=${Object.keys(normalized).length}`
|
|
1623
|
-
);
|
|
1624
|
-
return { map: normalized, needsMigration: migrated };
|
|
1625
|
-
} catch (err) {
|
|
1626
|
-
if (err?.code === "ENOENT") {
|
|
1627
|
-
const emptyMap = {};
|
|
1628
|
-
debugLog2(`workspace map loaded path=${filePath} entries=0`);
|
|
1629
|
-
return { map: emptyMap, needsMigration: false };
|
|
1630
|
-
}
|
|
1631
|
-
if (err instanceof SyntaxError) {
|
|
1632
|
-
throw new Error(
|
|
1633
|
-
`[memoraone-mcp] Failed to parse workspace map at ${filePath}`
|
|
1634
|
-
);
|
|
2152
|
+
const { summary, scope, files, stats, commit } = parsed2;
|
|
2153
|
+
const message = summary.startsWith("CHANGE:") ? summary : `CHANGE: ${scope ?? "code"} \u2014 ${summary}`;
|
|
2154
|
+
const run_id = resolveRunId(parsed2.run_id);
|
|
2155
|
+
const body = {
|
|
2156
|
+
kind: "note",
|
|
2157
|
+
concept: "concept:change_summary",
|
|
2158
|
+
actor: { type: config2.agentType, name: config2.agentName },
|
|
2159
|
+
message,
|
|
2160
|
+
projectKey,
|
|
2161
|
+
metadata: {
|
|
2162
|
+
source: config2.source,
|
|
2163
|
+
purpose: "change_summary",
|
|
2164
|
+
tool: "memora_log_change_summary",
|
|
2165
|
+
...scope ? { scope } : {},
|
|
2166
|
+
...files ? { files } : {},
|
|
2167
|
+
...stats ? { stats } : {},
|
|
2168
|
+
...commit ? { commit } : {},
|
|
2169
|
+
...run_id ? { run_id } : {}
|
|
1635
2170
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
}
|
|
1639
|
-
async function writeWorkspaceMap(map) {
|
|
1640
|
-
const filePath = getWorkspaceMapPath();
|
|
1641
|
-
validateWorkspaceMap(map, filePath);
|
|
1642
|
-
await ensureWorkspaceDir();
|
|
1643
|
-
const tempPath = `${filePath}.tmp`;
|
|
1644
|
-
const content = JSON.stringify(map, null, 2);
|
|
1645
|
-
await fs5.writeFile(tempPath, content, "utf8");
|
|
1646
|
-
await fs5.rename(tempPath, filePath);
|
|
2171
|
+
};
|
|
2172
|
+
await client.post("/timeline/events", body);
|
|
2173
|
+
return { ok: true };
|
|
1647
2174
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
debugLog2(
|
|
1669
|
-
`workspace map set fingerprint=${fingerprint} projectKey=${projectKey}`
|
|
2175
|
+
|
|
2176
|
+
// src/tools/handlers/logToolResult.ts
|
|
2177
|
+
var import_v418 = require("zod/v4");
|
|
2178
|
+
var logToolResultInputSchema = import_v418.z.object({
|
|
2179
|
+
tool: import_v418.z.string().min(1),
|
|
2180
|
+
status: import_v418.z.enum(["ok", "error", "partial"]),
|
|
2181
|
+
summary: import_v418.z.string().min(1),
|
|
2182
|
+
run_id: import_v418.z.string().min(1).optional(),
|
|
2183
|
+
duration_ms: import_v418.z.number().int().nonnegative().optional(),
|
|
2184
|
+
error_code: import_v418.z.string().min(1).optional(),
|
|
2185
|
+
error_message: import_v418.z.string().min(1).optional(),
|
|
2186
|
+
error_kind: import_v418.z.enum(["infra", "logic", "auth", "rate_limit", "validation", "unknown"]).optional(),
|
|
2187
|
+
stats: import_v418.z.record(import_v418.z.string(), import_v418.z.any()).optional()
|
|
2188
|
+
});
|
|
2189
|
+
async function handleLogToolResult(client, args) {
|
|
2190
|
+
const parsed2 = logToolResultInputSchema.parse(args ?? {});
|
|
2191
|
+
const projectKey = getCurrentProjectId();
|
|
2192
|
+
if (!projectKey) {
|
|
2193
|
+
throw new Error(
|
|
2194
|
+
"No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
|
|
1670
2195
|
);
|
|
1671
|
-
} finally {
|
|
1672
|
-
await releaseLock();
|
|
1673
2196
|
}
|
|
2197
|
+
const { tool, status, summary, duration_ms, error_code, error_message, error_kind, stats } = parsed2;
|
|
2198
|
+
const message = summary.startsWith("RESULT:") ? summary : `RESULT: ${tool} \u2014 ${status} \u2014 ${summary}`;
|
|
2199
|
+
const run_id = resolveRunId(parsed2.run_id);
|
|
2200
|
+
const body = {
|
|
2201
|
+
kind: "note",
|
|
2202
|
+
concept: "concept:tool_result",
|
|
2203
|
+
actor: { type: config2.agentType, name: config2.agentName },
|
|
2204
|
+
message,
|
|
2205
|
+
projectKey,
|
|
2206
|
+
metadata: {
|
|
2207
|
+
source: config2.source,
|
|
2208
|
+
purpose: "tool_result",
|
|
2209
|
+
tool: "memora_log_tool_result",
|
|
2210
|
+
tool_name: tool,
|
|
2211
|
+
status,
|
|
2212
|
+
...run_id ? { run_id } : {},
|
|
2213
|
+
...duration_ms ? { duration_ms } : {},
|
|
2214
|
+
...error_code ? { error_code } : {},
|
|
2215
|
+
...error_message ? { error_message } : {},
|
|
2216
|
+
...error_kind ? { error_kind } : {},
|
|
2217
|
+
...stats ? { stats } : {}
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
2220
|
+
await client.post("/timeline/events", body);
|
|
2221
|
+
return { ok: true };
|
|
1674
2222
|
}
|
|
1675
2223
|
|
|
1676
|
-
// src/tools/handlers/
|
|
1677
|
-
var
|
|
1678
|
-
|
|
1679
|
-
|
|
2224
|
+
// src/tools/handlers/logCommand.ts
|
|
2225
|
+
var import_v419 = require("zod/v4");
|
|
2226
|
+
var logCommandInputSchema = import_v419.z.object({
|
|
2227
|
+
cmd: import_v419.z.string().min(1),
|
|
2228
|
+
summary: import_v419.z.string().min(1),
|
|
2229
|
+
cwd: import_v419.z.string().min(1).optional(),
|
|
2230
|
+
exit_code: import_v419.z.number().int().optional(),
|
|
2231
|
+
duration_ms: import_v419.z.number().int().nonnegative().optional(),
|
|
2232
|
+
run_id: import_v419.z.string().min(1).optional(),
|
|
2233
|
+
stats: import_v419.z.record(import_v419.z.string(), import_v419.z.any()).optional()
|
|
1680
2234
|
});
|
|
1681
|
-
async function
|
|
1682
|
-
const parsed2 =
|
|
1683
|
-
const
|
|
1684
|
-
if (!
|
|
1685
|
-
throw new Error("projectKey is required");
|
|
1686
|
-
}
|
|
1687
|
-
const requested = resolvedProjectKey.trim();
|
|
1688
|
-
const bound = getBoundProjectId();
|
|
1689
|
-
if (bound !== null && requested !== bound) {
|
|
2235
|
+
async function handleLogCommand(client, args) {
|
|
2236
|
+
const parsed2 = logCommandInputSchema.parse(args ?? {});
|
|
2237
|
+
const projectKey = getCurrentProjectId();
|
|
2238
|
+
if (!projectKey) {
|
|
1690
2239
|
throw new Error(
|
|
1691
|
-
|
|
2240
|
+
"No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
|
|
1692
2241
|
);
|
|
1693
2242
|
}
|
|
1694
|
-
|
|
1695
|
-
const
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
2243
|
+
const { cmd, summary, cwd: cwd2, exit_code, duration_ms, stats } = parsed2;
|
|
2244
|
+
const message = summary.startsWith("COMMAND:") ? summary : `COMMAND: ${cmd} \u2014 ${summary}`;
|
|
2245
|
+
const run_id = resolveRunId(parsed2.run_id);
|
|
2246
|
+
const body = {
|
|
2247
|
+
kind: "note",
|
|
2248
|
+
concept: "concept:command",
|
|
2249
|
+
actor: { type: config2.agentType, name: config2.agentName },
|
|
2250
|
+
message,
|
|
2251
|
+
projectKey,
|
|
2252
|
+
metadata: {
|
|
2253
|
+
source: config2.source,
|
|
2254
|
+
purpose: "command",
|
|
2255
|
+
tool: "memora_log_command",
|
|
2256
|
+
cmd,
|
|
2257
|
+
...cwd2 ? { cwd: cwd2 } : {},
|
|
2258
|
+
...exit_code !== void 0 ? { exit_code } : {},
|
|
2259
|
+
...duration_ms !== void 0 ? { duration_ms } : {},
|
|
2260
|
+
...run_id ? { run_id } : {},
|
|
2261
|
+
...stats ? { stats } : {}
|
|
2262
|
+
}
|
|
2263
|
+
};
|
|
2264
|
+
await client.post("/timeline/events", body);
|
|
2265
|
+
return { ok: true };
|
|
1702
2266
|
}
|
|
1703
2267
|
|
|
1704
2268
|
// src/tools/handlers/bindingStatus.ts
|
|
1705
|
-
function buildBindingStatus(binding) {
|
|
2269
|
+
function buildBindingStatus(binding, options = {}) {
|
|
1706
2270
|
const status = {
|
|
2271
|
+
repositoryBindingId: binding.repositoryBindingId,
|
|
1707
2272
|
projectId: binding.projectId,
|
|
1708
2273
|
workspaceRoot: binding.workspaceRoot,
|
|
1709
|
-
m1Path: binding.m1Path,
|
|
1710
2274
|
bindingSource: binding.bindingSource,
|
|
1711
|
-
|
|
2275
|
+
status: binding.status,
|
|
2276
|
+
credentialSource: "keyring",
|
|
2277
|
+
cacheRefreshed: options.cacheRefreshed === true
|
|
1712
2278
|
};
|
|
1713
2279
|
if (binding.environment !== void 0) {
|
|
1714
2280
|
status.environment = binding.environment;
|
|
1715
2281
|
}
|
|
2282
|
+
if (binding.installationPublicId !== void 0) {
|
|
2283
|
+
status.installationPublicId = binding.installationPublicId;
|
|
2284
|
+
}
|
|
1716
2285
|
return status;
|
|
1717
2286
|
}
|
|
1718
|
-
function handleBindingStatus(binding) {
|
|
2287
|
+
function handleBindingStatus(binding, options = {}) {
|
|
1719
2288
|
if (!binding) {
|
|
1720
2289
|
throw new Error("[memoraone-mcp] Binding status unavailable (not initialized)");
|
|
1721
2290
|
}
|
|
1722
|
-
return buildBindingStatus(binding);
|
|
2291
|
+
return buildBindingStatus(binding, options);
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// src/heartbeat.ts
|
|
2295
|
+
var crypto5 = __toESM(require("crypto"), 1);
|
|
2296
|
+
var fs10 = __toESM(require("fs"), 1);
|
|
2297
|
+
var path13 = __toESM(require("path"), 1);
|
|
2298
|
+
|
|
2299
|
+
// src/localState/mcpSessionId.ts
|
|
2300
|
+
var import_node_crypto4 = require("crypto");
|
|
2301
|
+
var MCS_PREFIX = "mcs_";
|
|
2302
|
+
function generateMcpSessionId(random = () => (0, import_node_crypto4.randomBytes)(32)) {
|
|
2303
|
+
const bytes = random();
|
|
2304
|
+
if (bytes.length !== 32) {
|
|
2305
|
+
throw new Error("[memoraone-mcp] mcp session id requires exactly 32 random bytes");
|
|
2306
|
+
}
|
|
2307
|
+
return `${MCS_PREFIX}${bytes.toString("base64url")}`;
|
|
1723
2308
|
}
|
|
1724
2309
|
|
|
1725
2310
|
// src/heartbeat.ts
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
return crypto6.createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
|
|
2311
|
+
function fingerprintAccessToken(accessToken) {
|
|
2312
|
+
return crypto5.createHash("sha256").update(accessToken).digest("hex").slice(0, 12);
|
|
1729
2313
|
}
|
|
1730
2314
|
function isHeartbeatDebugEnabled() {
|
|
1731
2315
|
const value = String(process.env.MEMORAONE_DEBUG_HEARTBEAT ?? "").trim().toLowerCase();
|
|
@@ -1734,12 +2318,80 @@ function isHeartbeatDebugEnabled() {
|
|
|
1734
2318
|
function resolveHeartbeatIntervalMs() {
|
|
1735
2319
|
return Number.isFinite(config2.heartbeatIntervalMs) ? Math.max(1e3, config2.heartbeatIntervalMs) : 3e4;
|
|
1736
2320
|
}
|
|
2321
|
+
function redactSensitiveText(text) {
|
|
2322
|
+
return text.replace(/mcs_[A-Za-z0-9_-]+/g, "mcs_[redacted]").replace(/mia_[A-Za-z0-9_-]+/g, "mia_[redacted]").replace(/mir_[A-Za-z0-9_-]+/g, "mir_[redacted]").replace(/mcc_[A-Za-z0-9_-]+/g, "mcc_[redacted]").replace(/Bearer\s+\S+/gi, "Bearer [redacted]");
|
|
2323
|
+
}
|
|
2324
|
+
function resolvePackageVersion() {
|
|
2325
|
+
const fromEnv = process.env.npm_package_version?.trim();
|
|
2326
|
+
if (fromEnv) return fromEnv;
|
|
2327
|
+
const moduleDir = typeof __dirname !== "undefined" ? __dirname : void 0;
|
|
2328
|
+
const candidates = [
|
|
2329
|
+
...moduleDir ? [path13.join(moduleDir, "..", "package.json"), path13.join(moduleDir, "package.json")] : [],
|
|
2330
|
+
path13.join(process.cwd(), "package.json"),
|
|
2331
|
+
path13.join(process.cwd(), "packages", "mcp", "package.json")
|
|
2332
|
+
];
|
|
2333
|
+
for (const candidate of candidates) {
|
|
2334
|
+
try {
|
|
2335
|
+
const pkg = JSON.parse(fs10.readFileSync(candidate, "utf8"));
|
|
2336
|
+
if (typeof pkg.version === "string" && pkg.version.trim()) {
|
|
2337
|
+
return pkg.version.trim();
|
|
2338
|
+
}
|
|
2339
|
+
} catch {
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
return void 0;
|
|
2343
|
+
}
|
|
2344
|
+
function parseActiveFlag(payload) {
|
|
2345
|
+
if (!payload || typeof payload !== "object") return null;
|
|
2346
|
+
const active = payload.active;
|
|
2347
|
+
if (typeof active === "boolean") return active;
|
|
2348
|
+
return null;
|
|
2349
|
+
}
|
|
2350
|
+
async function announceIdeSession(client, ctx) {
|
|
2351
|
+
const body = {
|
|
2352
|
+
session_id: ctx.sessionId,
|
|
2353
|
+
ide_type: ctx.ideType
|
|
2354
|
+
};
|
|
2355
|
+
if (ctx.packageVersion) {
|
|
2356
|
+
body.package_version = ctx.packageVersion;
|
|
2357
|
+
}
|
|
2358
|
+
let lastErr;
|
|
2359
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2360
|
+
try {
|
|
2361
|
+
const raw = await client.post("/v1/local-mcp/session/announce", body, { log: false });
|
|
2362
|
+
const data = raw ?? {};
|
|
2363
|
+
if (data.ok !== true || typeof data.active !== "boolean" || typeof data.ide_type !== "string") {
|
|
2364
|
+
throw new Error("[memoraone-mcp] Invalid session announce response");
|
|
2365
|
+
}
|
|
2366
|
+
return {
|
|
2367
|
+
ok: true,
|
|
2368
|
+
active: data.active,
|
|
2369
|
+
ideType: data.ide_type,
|
|
2370
|
+
announcedAt: typeof data.announced_at === "string" ? data.announced_at : ""
|
|
2371
|
+
};
|
|
2372
|
+
} catch (err) {
|
|
2373
|
+
lastErr = err;
|
|
2374
|
+
if (err instanceof MemoraOneHttpError && (err.status === 404 || err.status === 405)) {
|
|
2375
|
+
throw new Error(
|
|
2376
|
+
"[memoraone-mcp] Session announce endpoint is not supported by this API (POST /v1/local-mcp/session/announce)"
|
|
2377
|
+
);
|
|
2378
|
+
}
|
|
2379
|
+
const msg = String(err);
|
|
2380
|
+
const transient = /fetch failed|ECONNRESET|ETIMEDOUT|ENOTFOUND|network|socket/i.test(msg) && !(err instanceof MemoraOneHttpError && err.status >= 400 && err.status < 500);
|
|
2381
|
+
if (!transient || attempt === 1) {
|
|
2382
|
+
break;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
const safe = redactSensitiveText(String(lastErr));
|
|
2387
|
+
throw new Error(`[memoraone-mcp] Session announce failed: ${safe}`);
|
|
2388
|
+
}
|
|
1737
2389
|
async function sendProjectHeartbeat(client, ctx) {
|
|
1738
2390
|
try {
|
|
1739
2391
|
const pid = ctx.projectId?.trim();
|
|
1740
2392
|
if (isHeartbeatDebugEnabled()) {
|
|
1741
2393
|
process.stderr.write(
|
|
1742
|
-
`[memoraone-mcp][diag] heartbeat projectId=${pid ?? "unknown"}
|
|
2394
|
+
`[memoraone-mcp][diag] heartbeat projectId=${pid ?? "unknown"} binding=${ctx.repositoryBindingId} credentialSource=${ctx.credentialSource ?? "keyring"} accessTokenFingerprint=${ctx.accessTokenFingerprint ?? "unknown"} ideType=${ctx.ideType ?? "unknown"}
|
|
1743
2395
|
`
|
|
1744
2396
|
);
|
|
1745
2397
|
}
|
|
@@ -1748,18 +2400,183 @@ async function sendProjectHeartbeat(client, ctx) {
|
|
|
1748
2400
|
}
|
|
1749
2401
|
const body = {};
|
|
1750
2402
|
if (ctx.ideType) body.ide_type = ctx.ideType;
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
"x-project-id": pid
|
|
1755
|
-
}
|
|
1756
|
-
});
|
|
2403
|
+
if (ctx.sessionId) body.session_id = ctx.sessionId;
|
|
2404
|
+
const raw = await client.post("/v1/local-mcp/heartbeat", body, { log: false });
|
|
2405
|
+
return { active: parseActiveFlag(raw) };
|
|
1757
2406
|
} catch (err) {
|
|
2407
|
+
if (err instanceof ReconnectRequiredError) {
|
|
2408
|
+
try {
|
|
2409
|
+
const record = await readBindingRecord(ctx.repositoryBindingId);
|
|
2410
|
+
if (record) {
|
|
2411
|
+
await writeBindingRecord({
|
|
2412
|
+
...record,
|
|
2413
|
+
status: "reconnect_required",
|
|
2414
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
} catch {
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
1758
2420
|
process.stderr.write(
|
|
1759
|
-
`[memoraone-mcp][info] heartbeat error (silent) ${String(err)}
|
|
2421
|
+
`[memoraone-mcp][info] heartbeat error (silent) ${redactSensitiveText(String(err))}
|
|
1760
2422
|
`
|
|
1761
2423
|
);
|
|
1762
|
-
|
|
2424
|
+
return { active: null };
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
function createDaemonHeartbeat(opts) {
|
|
2428
|
+
let interval = null;
|
|
2429
|
+
let client = null;
|
|
2430
|
+
let announced = false;
|
|
2431
|
+
let studioActive = null;
|
|
2432
|
+
let starting = false;
|
|
2433
|
+
let waitingForIdeType = false;
|
|
2434
|
+
const sessionId = opts.sessionId ?? generateMcpSessionId();
|
|
2435
|
+
const packageVersion = opts.packageVersion ?? resolvePackageVersion();
|
|
2436
|
+
const ctx = {
|
|
2437
|
+
projectId: opts.binding.projectId,
|
|
2438
|
+
repositoryBindingId: opts.binding.repositoryBindingId,
|
|
2439
|
+
ideType: opts.ideType,
|
|
2440
|
+
sessionId,
|
|
2441
|
+
credentialSource: "keyring",
|
|
2442
|
+
accessTokenFingerprint: null
|
|
2443
|
+
};
|
|
2444
|
+
const log = opts.onLog ?? ((msg) => {
|
|
2445
|
+
process.stderr.write(`[memoraone-mcp][daemon-heartbeat] ${redactSensitiveText(msg)}
|
|
2446
|
+
`);
|
|
2447
|
+
});
|
|
2448
|
+
const ensureClient = async () => {
|
|
2449
|
+
if (client) return client;
|
|
2450
|
+
const creds = await readInstallationCredentials(opts.binding.repositoryBindingId);
|
|
2451
|
+
if (!creds?.accessToken) {
|
|
2452
|
+
log("cannot start: no access token in keyring");
|
|
2453
|
+
return null;
|
|
2454
|
+
}
|
|
2455
|
+
ctx.accessTokenFingerprint = fingerprintAccessToken(creds.accessToken);
|
|
2456
|
+
client = opts.createClient?.(opts.binding) ?? new memoraClient_default(config2, {
|
|
2457
|
+
repositoryBindingId: opts.binding.repositoryBindingId,
|
|
2458
|
+
projectId: opts.binding.projectId
|
|
2459
|
+
});
|
|
2460
|
+
return client;
|
|
2461
|
+
};
|
|
2462
|
+
const ensureAnnounced = async (activeClient) => {
|
|
2463
|
+
if (announced) return true;
|
|
2464
|
+
if (!ctx.ideType) {
|
|
2465
|
+
waitingForIdeType = true;
|
|
2466
|
+
log("waiting for ide type before session announce");
|
|
2467
|
+
return false;
|
|
2468
|
+
}
|
|
2469
|
+
waitingForIdeType = false;
|
|
2470
|
+
try {
|
|
2471
|
+
const result = await announceIdeSession(activeClient, {
|
|
2472
|
+
sessionId,
|
|
2473
|
+
ideType: ctx.ideType,
|
|
2474
|
+
packageVersion
|
|
2475
|
+
});
|
|
2476
|
+
announced = true;
|
|
2477
|
+
studioActive = result.active;
|
|
2478
|
+
log(`session announced ideType=${ctx.ideType} active=${String(result.active)}`);
|
|
2479
|
+
return true;
|
|
2480
|
+
} catch (err) {
|
|
2481
|
+
studioActive = false;
|
|
2482
|
+
log(`session announce failed: ${redactSensitiveText(String(err))}`);
|
|
2483
|
+
return false;
|
|
2484
|
+
}
|
|
2485
|
+
};
|
|
2486
|
+
const applyHeartbeatOutcome = (outcome) => {
|
|
2487
|
+
if (outcome.active === null) return;
|
|
2488
|
+
if (outcome.active === false && studioActive !== false) {
|
|
2489
|
+
studioActive = false;
|
|
2490
|
+
log("session superseded (active=false); continuing to serve MCP without re-announce");
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2493
|
+
if (outcome.active === true) {
|
|
2494
|
+
studioActive = true;
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
const tick = async () => {
|
|
2498
|
+
if (!client) return;
|
|
2499
|
+
const outcome = await sendProjectHeartbeat(client, ctx);
|
|
2500
|
+
applyHeartbeatOutcome(outcome);
|
|
2501
|
+
};
|
|
2502
|
+
const beginInterval = () => {
|
|
2503
|
+
if (interval) return;
|
|
2504
|
+
const intervalMs = resolveHeartbeatIntervalMs();
|
|
2505
|
+
log(
|
|
2506
|
+
`daemon owns heartbeat for binding=${opts.binding.repositoryBindingId} project=${opts.binding.projectId} ideType=${ctx.ideType ?? "unknown"} interval=${intervalMs}ms`
|
|
2507
|
+
);
|
|
2508
|
+
void tick();
|
|
2509
|
+
interval = setInterval(() => {
|
|
2510
|
+
void tick();
|
|
2511
|
+
}, intervalMs);
|
|
2512
|
+
};
|
|
2513
|
+
const start = async () => {
|
|
2514
|
+
if (!config2.heartbeatEnabled) {
|
|
2515
|
+
log("disabled by config");
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
if (interval) {
|
|
2519
|
+
log("already running (skipped duplicate start)");
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
if (starting) {
|
|
2523
|
+
log("already starting (skipped duplicate start)");
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
starting = true;
|
|
2527
|
+
try {
|
|
2528
|
+
const activeClient = await ensureClient();
|
|
2529
|
+
if (!activeClient) return;
|
|
2530
|
+
const ok = await ensureAnnounced(activeClient);
|
|
2531
|
+
if (!ok) {
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
beginInterval();
|
|
2535
|
+
} finally {
|
|
2536
|
+
starting = false;
|
|
2537
|
+
if (waitingForIdeType && !announced && !interval && ctx.ideType && config2.heartbeatEnabled) {
|
|
2538
|
+
waitingForIdeType = false;
|
|
2539
|
+
void start();
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
};
|
|
2543
|
+
const stop = () => {
|
|
2544
|
+
if (interval) {
|
|
2545
|
+
clearInterval(interval);
|
|
2546
|
+
interval = null;
|
|
2547
|
+
}
|
|
2548
|
+
log(`daemon released heartbeat for binding=${opts.binding.repositoryBindingId}`);
|
|
2549
|
+
};
|
|
2550
|
+
const isRunning = () => interval !== null;
|
|
2551
|
+
const setIdeType = (ideType) => {
|
|
2552
|
+
const previous = ctx.ideType;
|
|
2553
|
+
if (previous === ideType) {
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
ctx.ideType = ideType;
|
|
2557
|
+
log(`daemon heartbeat ideType updated to ${ideType}`);
|
|
2558
|
+
if (!announced) {
|
|
2559
|
+
void start();
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
if (client && interval) {
|
|
2563
|
+
void tick();
|
|
2564
|
+
}
|
|
2565
|
+
};
|
|
2566
|
+
const getIdeType = () => ctx.ideType;
|
|
2567
|
+
const getSessionId = () => sessionId;
|
|
2568
|
+
const isStudioActive = () => studioActive;
|
|
2569
|
+
const hasAnnounced = () => announced;
|
|
2570
|
+
return {
|
|
2571
|
+
start,
|
|
2572
|
+
stop,
|
|
2573
|
+
isRunning,
|
|
2574
|
+
setIdeType,
|
|
2575
|
+
getIdeType,
|
|
2576
|
+
getSessionId,
|
|
2577
|
+
isStudioActive,
|
|
2578
|
+
hasAnnounced
|
|
2579
|
+
};
|
|
1763
2580
|
}
|
|
1764
2581
|
|
|
1765
2582
|
// src/ideType.ts
|
|
@@ -1888,8 +2705,8 @@ function registerToolWithWorklog(server, runtime, sessionContext, toolName, desc
|
|
|
1888
2705
|
async function main(opts = {}) {
|
|
1889
2706
|
let bindingReadyResolve = null;
|
|
1890
2707
|
let bindingReadyReject = null;
|
|
1891
|
-
const bindingReady = new Promise((
|
|
1892
|
-
bindingReadyResolve =
|
|
2708
|
+
const bindingReady = new Promise((resolve9, reject) => {
|
|
2709
|
+
bindingReadyResolve = resolve9;
|
|
1893
2710
|
bindingReadyReject = reject;
|
|
1894
2711
|
});
|
|
1895
2712
|
const devMode = Boolean(config2.devMode);
|
|
@@ -1898,9 +2715,11 @@ async function main(opts = {}) {
|
|
|
1898
2715
|
const runtime = {
|
|
1899
2716
|
client: null,
|
|
1900
2717
|
projectId: null,
|
|
1901
|
-
|
|
1902
|
-
|
|
2718
|
+
repositoryBindingId: null,
|
|
2719
|
+
credentialSource: null,
|
|
2720
|
+
accessTokenFingerprint: null,
|
|
1903
2721
|
authoritativeBinding: null,
|
|
2722
|
+
bindingCacheRefreshed: false,
|
|
1904
2723
|
ideType: void 0
|
|
1905
2724
|
};
|
|
1906
2725
|
let workspaceRoot;
|
|
@@ -1913,10 +2732,25 @@ async function main(opts = {}) {
|
|
|
1913
2732
|
if (initializeRoots.length === 0 && opts.daemonBindingHint) {
|
|
1914
2733
|
if (isInitializeDebugEnabled()) {
|
|
1915
2734
|
console.error(
|
|
1916
|
-
"[memoraone-mcp][init-debug] Workspace resolution strategy: daemonBindingHint (bridge pre-resolved)"
|
|
2735
|
+
"[memoraone-mcp][init-debug] Workspace resolution strategy: daemonBindingHint (bridge pre-resolved, reconciled from disk)"
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
const reconciled = await reconcileResolvedBindingWithDisk(opts.daemonBindingHint);
|
|
2739
|
+
runtime.bindingCacheRefreshed = reconciled.cacheRefreshed;
|
|
2740
|
+
if (reconciled.cacheRefreshed) {
|
|
2741
|
+
console.error(
|
|
2742
|
+
`[memoraone-mcp] refreshed stale cached binding ${reconciled.binding.repositoryBindingId}: project=${reconciled.binding.projectId}`
|
|
1917
2743
|
);
|
|
2744
|
+
try {
|
|
2745
|
+
const socketPath = getBindingSocketPath(opts.daemonBindingHint);
|
|
2746
|
+
writeBindingSidecar(socketPath, reconciled.binding, runtime.ideType ?? "");
|
|
2747
|
+
} catch (err) {
|
|
2748
|
+
console.error(
|
|
2749
|
+
`[memoraone-mcp] warning: could not rewrite binding sidecar after refresh: ${String(err)}`
|
|
2750
|
+
);
|
|
2751
|
+
}
|
|
1918
2752
|
}
|
|
1919
|
-
return
|
|
2753
|
+
return reconciled.binding;
|
|
1920
2754
|
}
|
|
1921
2755
|
let rootsListUris;
|
|
1922
2756
|
let rootsListAttempted = false;
|
|
@@ -2011,39 +2845,15 @@ async function main(opts = {}) {
|
|
|
2011
2845
|
})
|
|
2012
2846
|
);
|
|
2013
2847
|
registeredToolNames.push("memora_get_personal_context");
|
|
2014
|
-
server.tool(
|
|
2015
|
-
"memora_list_projects",
|
|
2016
|
-
"List projects available to the current API key",
|
|
2017
|
-
listProjectsShape,
|
|
2018
|
-
async () => runWithSessionContext(sessionContext, async () => {
|
|
2019
|
-
if (!runtime.client || !runtime.projectId) return notInitializedResult;
|
|
2020
|
-
const result = await handleListProjects(runtime.client);
|
|
2021
|
-
return {
|
|
2022
|
-
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
2023
|
-
};
|
|
2024
|
-
})
|
|
2025
|
-
);
|
|
2026
|
-
registeredToolNames.push("memora_list_projects");
|
|
2027
|
-
server.tool(
|
|
2028
|
-
"memora_set_project",
|
|
2029
|
-
"Set the current project key for subsequent tool calls",
|
|
2030
|
-
setProjectShape,
|
|
2031
|
-
async (args) => runWithSessionContext(sessionContext, async () => {
|
|
2032
|
-
if (!runtime.client || !runtime.projectId) return notInitializedResult;
|
|
2033
|
-
const result = await handleSetProject(args);
|
|
2034
|
-
return {
|
|
2035
|
-
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
2036
|
-
};
|
|
2037
|
-
})
|
|
2038
|
-
);
|
|
2039
|
-
registeredToolNames.push("memora_set_project");
|
|
2040
2848
|
server.tool(
|
|
2041
2849
|
"memora_status",
|
|
2042
2850
|
"Return non-secret project binding metadata for this MCP session",
|
|
2043
2851
|
bindingStatusShape,
|
|
2044
2852
|
async () => runWithSessionContext(sessionContext, async () => {
|
|
2045
2853
|
if (!runtime.authoritativeBinding) return notInitializedResult;
|
|
2046
|
-
const result = handleBindingStatus(runtime.authoritativeBinding
|
|
2854
|
+
const result = handleBindingStatus(runtime.authoritativeBinding, {
|
|
2855
|
+
cacheRefreshed: runtime.bindingCacheRefreshed
|
|
2856
|
+
});
|
|
2047
2857
|
return {
|
|
2048
2858
|
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
2049
2859
|
};
|
|
@@ -2165,72 +2975,79 @@ async function main(opts = {}) {
|
|
|
2165
2975
|
const debugAuth = ["1", "true", "yes", "on"].includes(
|
|
2166
2976
|
String(process.env.MEMORAONE_DEBUG_AUTH ?? "").trim().toLowerCase()
|
|
2167
2977
|
);
|
|
2168
|
-
const
|
|
2978
|
+
const debugLog = config2.devMode || debugAuth;
|
|
2169
2979
|
const binding = await resolveSessionBindingFromInitialize(params);
|
|
2170
|
-
if (opts.daemonBindingHint &&
|
|
2980
|
+
if (opts.daemonBindingHint && opts.daemonBindingHint.repositoryBindingId !== binding.repositoryBindingId) {
|
|
2171
2981
|
const errMsg = formatBindingMismatchError(opts.daemonBindingHint, binding);
|
|
2172
2982
|
console.error(`[memoraone-mcp][ERROR] ${errMsg}`);
|
|
2173
2983
|
bindingReadyReject?.(new Error(errMsg));
|
|
2174
2984
|
throw new Error(errMsg);
|
|
2175
2985
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2986
|
+
if (binding.status === "reconnect_required") {
|
|
2987
|
+
throw new ReconnectRequiredError(
|
|
2988
|
+
"[memoraone-mcp] Installation requires reconnect. Run: memoraone-mcp connect <code>"
|
|
2989
|
+
);
|
|
2990
|
+
}
|
|
2991
|
+
const creds = await readInstallationCredentials(binding.repositoryBindingId);
|
|
2992
|
+
if (!creds?.accessToken?.startsWith("mia_")) {
|
|
2993
|
+
throw new ReconnectRequiredError(
|
|
2994
|
+
"[memoraone-mcp] Missing installation credentials. Run: memoraone-mcp connect <code>"
|
|
2180
2995
|
);
|
|
2181
2996
|
}
|
|
2182
|
-
if (
|
|
2997
|
+
if (debugLog) {
|
|
2998
|
+
console.error("[memoraone-mcp][debug] Resolved installation credentials from OS keyring");
|
|
2999
|
+
}
|
|
3000
|
+
if (binding.legacyM1WarningPath) {
|
|
2183
3001
|
console.error(
|
|
2184
|
-
|
|
3002
|
+
`[memoraone-mcp] warning: ignoring legacy memoraone.m1 at ${binding.legacyM1WarningPath}`
|
|
2185
3003
|
);
|
|
2186
3004
|
}
|
|
2187
3005
|
const projectId = binding.projectId;
|
|
2188
3006
|
const existing = getBoundProjectId();
|
|
2189
3007
|
if (existing !== null && existing !== projectId) {
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
3008
|
+
if (runtime.bindingCacheRefreshed) {
|
|
3009
|
+
setBoundProjectId(projectId);
|
|
3010
|
+
console.error(
|
|
3011
|
+
`[memoraone-mcp] ${sessionLabel} rebound to project ${projectId} after local binding refresh (was ${existing})`
|
|
3012
|
+
);
|
|
3013
|
+
} else {
|
|
3014
|
+
const requestedRoot = binding.workspaceRoot ?? workspaceRoot ?? process.cwd();
|
|
3015
|
+
const action = "Open this repo in a separate window or configure a separate MCP server instance per root.";
|
|
3016
|
+
const errMsg = `[memoraone-mcp] This MCP process is already bound to project ${existing}. Open a new IDE window or start a separate MCP instance for a different project.`;
|
|
3017
|
+
console.error(
|
|
3018
|
+
`[memoraone-mcp][ERROR] Option A conflict: boundProjectId=${existing} requestedProjectId=${projectId} workspaceRoot=${requestedRoot}. ${action}`
|
|
3019
|
+
);
|
|
3020
|
+
bindingReadyReject?.(new Error(errMsg));
|
|
3021
|
+
setImmediate(() => process.exit(1));
|
|
3022
|
+
throw new Error(errMsg);
|
|
3023
|
+
}
|
|
2199
3024
|
}
|
|
2200
3025
|
if (existing === null) {
|
|
2201
3026
|
setBoundProjectId(projectId);
|
|
2202
|
-
setBoundApiKey(apiKeyToUse);
|
|
2203
3027
|
console.error(
|
|
2204
3028
|
`[memoraone-mcp] ${sessionLabel} bound to project ${projectId} (Option A: single-project binding)`
|
|
2205
3029
|
);
|
|
2206
3030
|
}
|
|
2207
3031
|
setCurrentProjectId(projectId);
|
|
2208
|
-
setCurrentApiKey(apiKeyToUse);
|
|
2209
3032
|
runtime.projectId = projectId;
|
|
2210
|
-
runtime.
|
|
2211
|
-
runtime.
|
|
3033
|
+
runtime.repositoryBindingId = binding.repositoryBindingId;
|
|
3034
|
+
runtime.credentialSource = "keyring";
|
|
3035
|
+
runtime.accessTokenFingerprint = fingerprintAccessToken(creds.accessToken);
|
|
2212
3036
|
runtime.authoritativeBinding = binding;
|
|
2213
|
-
runtime.client = new memoraClient_default(config2,
|
|
3037
|
+
runtime.client = new memoraClient_default(config2, {
|
|
3038
|
+
repositoryBindingId: binding.repositoryBindingId,
|
|
3039
|
+
projectId
|
|
3040
|
+
});
|
|
2214
3041
|
workspaceRoot = binding.workspaceRoot;
|
|
2215
|
-
const environmentLog = binding.environment !== void 0 ? ` environment=${binding.environment}` : "";
|
|
2216
|
-
process.stderr.write(
|
|
2217
|
-
`[memoraone-mcp] registering workspace source bindingSource=${binding.bindingSource} workspaceRoot=${workspaceRoot ?? "(unset)"} m1Path=${binding.m1Path}${environmentLog}
|
|
2218
|
-
`
|
|
2219
|
-
);
|
|
2220
|
-
await registerRepoSource(
|
|
2221
|
-
runtime.client,
|
|
2222
|
-
runtime.projectId,
|
|
2223
|
-
binding.workspaceRoot,
|
|
2224
|
-
runtime.ideType
|
|
2225
|
-
);
|
|
2226
3042
|
if (debugAuth) {
|
|
2227
3043
|
console.error("[memoraone-mcp][auth] repo root:", binding.workspaceRoot);
|
|
2228
3044
|
console.error("[memoraone-mcp][auth] project_id:", projectId);
|
|
2229
|
-
console.error("[memoraone-mcp][auth]
|
|
3045
|
+
console.error("[memoraone-mcp][auth] repository_binding_id:", binding.repositoryBindingId);
|
|
3046
|
+
console.error("[memoraone-mcp][auth] credential source: keyring");
|
|
2230
3047
|
}
|
|
2231
3048
|
const bindingEnvironmentLog = binding.environment !== void 0 ? ` environment=${binding.environment}` : "";
|
|
2232
3049
|
console.error(
|
|
2233
|
-
`[memoraone-mcp] ${sessionLabel} authoritative binding:
|
|
3050
|
+
`[memoraone-mcp] ${sessionLabel} authoritative binding: binding=${binding.repositoryBindingId} project=${binding.projectId} workspace=${binding.workspaceRoot} source=${binding.bindingSource}${bindingEnvironmentLog}`
|
|
2234
3051
|
);
|
|
2235
3052
|
bindingReadyResolve?.(runtime.client);
|
|
2236
3053
|
return server.server._oninitialize(request);
|
|
@@ -2246,37 +3063,34 @@ async function main(opts = {}) {
|
|
|
2246
3063
|
const transport = opts.transport ?? new import_stdio.StdioServerTransport();
|
|
2247
3064
|
await server.connect(transport);
|
|
2248
3065
|
const activeClient = await bindingReady;
|
|
2249
|
-
let
|
|
3066
|
+
let ownedHeartbeat = null;
|
|
2250
3067
|
const daemonSession = Boolean(opts.sessionSocket);
|
|
2251
3068
|
if (config2.heartbeatEnabled && daemonSession) {
|
|
2252
3069
|
console.error(
|
|
2253
3070
|
`[memoraone-mcp] ${sessionLabel} defers heartbeat to daemon for project ${runtime.projectId}`
|
|
2254
3071
|
);
|
|
2255
|
-
} else if (config2.heartbeatEnabled) {
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
3072
|
+
} else if (config2.heartbeatEnabled && runtime.authoritativeBinding) {
|
|
3073
|
+
ownedHeartbeat = createDaemonHeartbeat({
|
|
3074
|
+
binding: runtime.authoritativeBinding,
|
|
3075
|
+
ideType: runtime.ideType ?? config2.ideType,
|
|
3076
|
+
createClient: () => activeClient,
|
|
3077
|
+
onLog: (msg) => {
|
|
3078
|
+
console.error(`[memoraone-mcp][session-heartbeat] ${msg}`);
|
|
3079
|
+
}
|
|
3080
|
+
});
|
|
2263
3081
|
console.error(
|
|
2264
|
-
`[memoraone-mcp] ${sessionLabel} owns heartbeat for project ${runtime.projectId}
|
|
3082
|
+
`[memoraone-mcp] ${sessionLabel} owns heartbeat for project ${runtime.projectId}`
|
|
2265
3083
|
);
|
|
2266
|
-
await
|
|
2267
|
-
heartbeatInterval = setInterval(() => {
|
|
2268
|
-
sendProjectHeartbeat(activeClient, heartbeatCtx).catch(() => {
|
|
2269
|
-
});
|
|
2270
|
-
}, intervalMs);
|
|
3084
|
+
await ownedHeartbeat.start();
|
|
2271
3085
|
}
|
|
2272
3086
|
const onSigInt = () => shutdown("SIGINT");
|
|
2273
3087
|
const onSigTerm = () => shutdown("SIGTERM");
|
|
2274
3088
|
const shutdown = (signal, exitProcess = true) => {
|
|
2275
3089
|
process.off("SIGINT", onSigInt);
|
|
2276
3090
|
process.off("SIGTERM", onSigTerm);
|
|
2277
|
-
if (
|
|
2278
|
-
|
|
2279
|
-
|
|
3091
|
+
if (ownedHeartbeat?.isRunning()) {
|
|
3092
|
+
ownedHeartbeat.stop();
|
|
3093
|
+
ownedHeartbeat = null;
|
|
2280
3094
|
if (runtime.projectId) {
|
|
2281
3095
|
console.error(
|
|
2282
3096
|
`[memoraone-mcp] ${sessionLabel} released session heartbeat for project ${runtime.projectId}`
|
|
@@ -2296,10 +3110,10 @@ async function main(opts = {}) {
|
|
|
2296
3110
|
console.error("[memoraone-mcp] MCP server ready");
|
|
2297
3111
|
}
|
|
2298
3112
|
if (opts.sessionSocket) {
|
|
2299
|
-
await new Promise((
|
|
3113
|
+
await new Promise((resolve9) => {
|
|
2300
3114
|
opts.sessionSocket.once("close", () => {
|
|
2301
3115
|
shutdown("session closed", false);
|
|
2302
|
-
|
|
3116
|
+
resolve9();
|
|
2303
3117
|
});
|
|
2304
3118
|
});
|
|
2305
3119
|
}
|