@memoraone/mcp 0.1.35 → 0.1.37
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 +2330 -637
- package/dist/daemon.cjs +1730 -1114
- package/dist/index.cjs +1654 -959
- package/package.json +12 -10
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
|
+
);
|
|
793
|
+
}
|
|
794
|
+
return token;
|
|
795
|
+
}
|
|
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 {
|
|
190
817
|
}
|
|
191
|
-
return key;
|
|
192
818
|
}
|
|
193
|
-
buildHeaders(options) {
|
|
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=${path11}`
|
|
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,247 +852,511 @@ 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
877
|
throw new MemoraOneHttpError(res.status, res.statusText, res.text);
|
|
239
878
|
}
|
|
240
|
-
console.error(
|
|
241
|
-
`[memoraone-mcp][info] MemoraClient.post EXIT path=${path11}`
|
|
242
|
-
);
|
|
243
879
|
return res.text ? JSON.parse(res.text) : null;
|
|
244
880
|
}
|
|
245
|
-
async
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (debugEnabled && options?.log !== false) {
|
|
254
|
-
const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
|
|
255
|
-
console.error(
|
|
256
|
-
`[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_response_log`
|
|
257
|
-
);
|
|
258
|
-
const line = `[memoraone-mcp][info] http response method=GET url=${url} status=${res.status} body=${snippet}`;
|
|
259
|
-
console.error(line);
|
|
260
|
-
console.error(
|
|
261
|
-
`[memoraone-mcp][info] requestJson nonce=${nonce} stage=after_response_log`
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
if (!res.ok) {
|
|
265
|
-
const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
|
|
266
|
-
process.stderr.write(
|
|
267
|
-
`[memoraone-mcp][error] http error method=GET url=${url} status=${res.status} body=${snippet}
|
|
268
|
-
`
|
|
269
|
-
);
|
|
270
|
-
throw new MemoraOneHttpError(res.status, res.statusText, res.text);
|
|
271
|
-
}
|
|
272
|
-
return res.text ? JSON.parse(res.text) : null;
|
|
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);
|
|
273
889
|
}
|
|
274
890
|
};
|
|
275
891
|
var memoraClient_default = MemoraClient;
|
|
276
892
|
|
|
277
893
|
// src/projectBinding.ts
|
|
278
|
-
var
|
|
279
|
-
var
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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("|");
|
|
284
918
|
}
|
|
285
|
-
function
|
|
286
|
-
|
|
287
|
-
|
|
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");
|
|
288
927
|
}
|
|
289
|
-
|
|
290
|
-
return trimmed === "" ? void 0 : trimmed;
|
|
928
|
+
return match[1].trim();
|
|
291
929
|
}
|
|
292
|
-
function
|
|
293
|
-
|
|
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();
|
|
294
962
|
try {
|
|
295
|
-
|
|
963
|
+
const { stdout } = await execFileAsync("hostid", []);
|
|
964
|
+
const id = stdout.trim();
|
|
965
|
+
if (id) return id;
|
|
296
966
|
} catch {
|
|
297
|
-
throw new Error(`[memoraone-mcp] Invalid memoraone.m1 JSON at ${markerPath}`);
|
|
298
967
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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}`);
|
|
302
986
|
}
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
);
|
|
305
992
|
}
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
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
|
+
};
|
|
310
1006
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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();
|
|
315
1021
|
}
|
|
316
|
-
|
|
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);
|
|
317
1040
|
try {
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
return environment === void 0 ? { projectId, apiKey, foundAt: markerPath } : { projectId, apiKey, environment, foundAt: markerPath };
|
|
1041
|
+
const raw = await readJsonFile(filePath);
|
|
1042
|
+
return parsePathIndex(raw);
|
|
321
1043
|
} catch (err) {
|
|
322
|
-
if (err
|
|
323
|
-
|
|
1044
|
+
if (err instanceof Error && err.message.includes("Corrupt")) {
|
|
1045
|
+
throw err;
|
|
324
1046
|
}
|
|
325
1047
|
throw err;
|
|
326
1048
|
}
|
|
327
1049
|
}
|
|
328
|
-
async function
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
const parent = path2.dirname(current);
|
|
343
|
-
if (parent === current) {
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
current = parent;
|
|
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) };
|
|
347
1062
|
}
|
|
348
|
-
|
|
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
|
+
};
|
|
349
1081
|
}
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
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;
|
|
353
1123
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
+
};
|
|
364
1155
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
1156
|
+
}
|
|
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
|
+
};
|
|
369
1180
|
}
|
|
370
1181
|
}
|
|
371
|
-
|
|
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
|
+
};
|
|
372
1194
|
}
|
|
373
|
-
function
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
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>`
|
|
1205
|
+
);
|
|
377
1206
|
}
|
|
378
|
-
const
|
|
379
|
-
if (
|
|
380
|
-
|
|
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>"
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
if (record.status === "reconnect_required") {
|
|
1214
|
+
throw new ReconnectRequiredError(
|
|
1215
|
+
"[memoraone-mcp] Installation requires reconnect. Run: memoraone-mcp connect <code>"
|
|
1216
|
+
);
|
|
381
1217
|
}
|
|
382
|
-
|
|
383
|
-
|
|
1218
|
+
const creds = await readInstallationCredentials(
|
|
1219
|
+
repositoryBindingId,
|
|
1220
|
+
options.credentialOptions
|
|
1221
|
+
);
|
|
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
|
+
);
|
|
384
1232
|
}
|
|
385
|
-
return {
|
|
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
|
+
};
|
|
386
1243
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
1244
|
+
|
|
1245
|
+
// src/projectBinding.ts
|
|
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
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
async function warnLegacyM1IfPresent(workspaceRoot) {
|
|
1260
|
+
const candidate = path8.join(path8.resolve(workspaceRoot), CANONICAL_M1_FILENAME);
|
|
1261
|
+
try {
|
|
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;
|
|
1268
|
+
} catch {
|
|
1269
|
+
return void 0;
|
|
403
1270
|
}
|
|
1271
|
+
}
|
|
1272
|
+
async function resolveAuthoritativeBinding(workspaceRoot, _options = {}) {
|
|
404
1273
|
const candidates = normalizeWorkspaceSearchRoots(workspaceRoot);
|
|
405
1274
|
if (candidates.length === 0) {
|
|
406
|
-
throw new
|
|
1275
|
+
throw new ReconnectRequiredError(
|
|
1276
|
+
"[memoraone-mcp] Could not resolve workspace root. Open a connected repository folder.\nRun: memoraone-mcp connect <code>"
|
|
1277
|
+
);
|
|
407
1278
|
}
|
|
408
1279
|
const bindings = [];
|
|
409
1280
|
for (const root of candidates) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
bindings.push(
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
bindingSource: "workspace-search",
|
|
420
|
-
apiKeySource: resolved.apiKeySource
|
|
421
|
-
});
|
|
1281
|
+
await warnLegacyM1IfPresent(root);
|
|
1282
|
+
try {
|
|
1283
|
+
const local = await resolveLocalBinding(root);
|
|
1284
|
+
bindings.push(toResolvedBinding(local));
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
if (err instanceof ReconnectRequiredError) {
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
throw err;
|
|
422
1290
|
}
|
|
423
1291
|
}
|
|
424
1292
|
if (bindings.length === 0) {
|
|
425
|
-
throw new
|
|
1293
|
+
throw new ReconnectRequiredError(
|
|
1294
|
+
"[memoraone-mcp] No local MemoraOne binding for this workspace.\nRun: memoraone-mcp connect <code>"
|
|
1295
|
+
);
|
|
426
1296
|
}
|
|
427
|
-
const
|
|
428
|
-
if (
|
|
1297
|
+
const distinctIds = new Set(bindings.map((b) => b.repositoryBindingId));
|
|
1298
|
+
if (distinctIds.size > 1) {
|
|
429
1299
|
const lines = bindings.map(
|
|
430
|
-
(b) => ` - workspace=${b.workspaceRoot}
|
|
1300
|
+
(b) => ` - workspace=${b.workspaceRoot} binding=${b.repositoryBindingId} project=${b.projectId}`
|
|
431
1301
|
);
|
|
432
1302
|
throw new Error(
|
|
433
|
-
"[memoraone-mcp] Ambiguous workspace binding: multiple open roots map to different
|
|
1303
|
+
"[memoraone-mcp] Ambiguous workspace binding: multiple open roots map to different repository bindings.\n" + lines.join("\n") + "\nOpen one repo per window."
|
|
434
1304
|
);
|
|
435
1305
|
}
|
|
436
1306
|
return bindings[0];
|
|
437
1307
|
}
|
|
1308
|
+
function normalizeWorkspaceSearchRoots(workspaceRoot) {
|
|
1309
|
+
if (workspaceRoot === void 0) {
|
|
1310
|
+
return [];
|
|
1311
|
+
}
|
|
1312
|
+
const list = Array.isArray(workspaceRoot) ? workspaceRoot : [workspaceRoot];
|
|
1313
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1314
|
+
const out = [];
|
|
1315
|
+
for (const raw of list) {
|
|
1316
|
+
if (raw === void 0) continue;
|
|
1317
|
+
const trimmed = String(raw).trim();
|
|
1318
|
+
if (trimmed === "") continue;
|
|
1319
|
+
const resolved = path8.resolve(trimmed);
|
|
1320
|
+
if (!seen.has(resolved)) {
|
|
1321
|
+
seen.add(resolved);
|
|
1322
|
+
out.push(resolved);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return out;
|
|
1326
|
+
}
|
|
438
1327
|
function bindingRelevantValuesMatch(a, b) {
|
|
439
1328
|
const envA = a.environment ?? void 0;
|
|
440
1329
|
const envB = b.environment ?? void 0;
|
|
441
|
-
return a.projectId.trim().toLowerCase() === b.projectId.trim().toLowerCase() &&
|
|
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;
|
|
442
1331
|
}
|
|
443
1332
|
async function reconcileResolvedBindingWithDisk(cached) {
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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>`
|
|
448
1338
|
);
|
|
449
1339
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
if (record.status === "reconnect_required" || !record.projectId) {
|
|
1348
|
+
throw new ReconnectRequiredError(
|
|
1349
|
+
"[memoraone-mcp] Installation requires reconnect. Run: memoraone-mcp connect <code>"
|
|
1350
|
+
);
|
|
460
1351
|
}
|
|
461
|
-
const parsed2 = parseAndValidateM1(content, m1Path);
|
|
462
|
-
const resolved = resolveApiKeyWithSource(parsed2.apiKey);
|
|
463
1352
|
const fresh = {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
bindingSource:
|
|
470
|
-
|
|
1353
|
+
repositoryBindingId,
|
|
1354
|
+
projectId: record.projectId,
|
|
1355
|
+
workspaceRoot,
|
|
1356
|
+
installationPublicId: record.installationPublicId,
|
|
1357
|
+
environment: record.environment,
|
|
1358
|
+
bindingSource: "local-binding",
|
|
1359
|
+
status: record.status
|
|
471
1360
|
};
|
|
472
1361
|
if (bindingRelevantValuesMatch(cached, fresh)) {
|
|
473
1362
|
return { binding: fresh, cacheRefreshed: false };
|
|
@@ -475,21 +1364,30 @@ async function reconcileResolvedBindingWithDisk(cached) {
|
|
|
475
1364
|
return { binding: fresh, cacheRefreshed: true };
|
|
476
1365
|
}
|
|
477
1366
|
function encodeResolvedBinding(binding) {
|
|
478
|
-
|
|
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");
|
|
479
1377
|
}
|
|
480
1378
|
|
|
481
1379
|
// src/initializeBinding.ts
|
|
482
|
-
var
|
|
1380
|
+
var path11 = __toESM(require("path"), 1);
|
|
483
1381
|
var import_node_url = require("url");
|
|
484
1382
|
|
|
485
1383
|
// src/bindingIdentity.ts
|
|
486
1384
|
var crypto2 = __toESM(require("crypto"), 1);
|
|
487
|
-
var
|
|
1385
|
+
var path9 = __toESM(require("path"), 1);
|
|
488
1386
|
var BINDING_SOCKET_HASH_LENGTH = 16;
|
|
489
|
-
function hashBindingIdentity(
|
|
1387
|
+
function hashBindingIdentity(repositoryBindingId, workspaceRoot, ideType) {
|
|
490
1388
|
const input = [
|
|
491
|
-
|
|
492
|
-
|
|
1389
|
+
repositoryBindingId.trim(),
|
|
1390
|
+
path9.resolve(workspaceRoot),
|
|
493
1391
|
ideType
|
|
494
1392
|
].join("|");
|
|
495
1393
|
return crypto2.createHash("sha256").update(input).digest("hex").slice(0, BINDING_SOCKET_HASH_LENGTH);
|
|
@@ -497,7 +1395,7 @@ function hashBindingIdentity(projectId, workspaceRoot, ideType) {
|
|
|
497
1395
|
function bindingsMatch(a, b) {
|
|
498
1396
|
const envA = a.environment ?? void 0;
|
|
499
1397
|
const envB = b.environment ?? void 0;
|
|
500
|
-
return a.projectId.trim().toLowerCase() === b.projectId.trim().toLowerCase() &&
|
|
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;
|
|
501
1399
|
}
|
|
502
1400
|
function formatMissingInitializeWorkspaceError(options) {
|
|
503
1401
|
const lines = [
|
|
@@ -525,10 +1423,10 @@ function formatMissingInitializeWorkspaceError(options) {
|
|
|
525
1423
|
}
|
|
526
1424
|
|
|
527
1425
|
// src/socketPaths.ts
|
|
528
|
-
var
|
|
529
|
-
var
|
|
530
|
-
var
|
|
531
|
-
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");
|
|
532
1430
|
var HASH_SOCKET_FILENAME_RE = new RegExp(
|
|
533
1431
|
`^mcp-[0-9a-f]{${BINDING_SOCKET_HASH_LENGTH}}\\.sock$`,
|
|
534
1432
|
"i"
|
|
@@ -549,11 +1447,11 @@ function resolveBindingIdeType(env2 = process.env) {
|
|
|
549
1447
|
}
|
|
550
1448
|
function getBindingSocketFilename(binding, env2 = process.env) {
|
|
551
1449
|
const ideType = resolveBindingIdeType(env2);
|
|
552
|
-
const hash = hashBindingIdentity(binding.
|
|
1450
|
+
const hash = hashBindingIdentity(binding.repositoryBindingId, binding.workspaceRoot, ideType);
|
|
553
1451
|
return `mcp-${hash}.sock`;
|
|
554
1452
|
}
|
|
555
1453
|
function getBindingSocketPath(binding, env2 = process.env) {
|
|
556
|
-
return
|
|
1454
|
+
return path10.join(BASE_DIR, getBindingSocketFilename(binding, env2));
|
|
557
1455
|
}
|
|
558
1456
|
|
|
559
1457
|
// src/initializeBinding.ts
|
|
@@ -575,8 +1473,8 @@ function getEnvWorkspaceRootCandidates() {
|
|
|
575
1473
|
const raw = process.env.WORKSPACE_FOLDER_PATHS;
|
|
576
1474
|
const parts = [];
|
|
577
1475
|
if (raw !== void 0 && raw.trim() !== "") {
|
|
578
|
-
for (const p of raw.split(
|
|
579
|
-
parts.push(
|
|
1476
|
+
for (const p of raw.split(path11.delimiter).map((s) => s.trim()).filter(Boolean)) {
|
|
1477
|
+
parts.push(path11.resolve(p));
|
|
580
1478
|
}
|
|
581
1479
|
}
|
|
582
1480
|
parts.push(process.cwd());
|
|
@@ -600,7 +1498,7 @@ function extractWorkspaceRootsFromInitialize(params) {
|
|
|
600
1498
|
if (uri === void 0 || uri.trim() === "") {
|
|
601
1499
|
return;
|
|
602
1500
|
}
|
|
603
|
-
const resolved =
|
|
1501
|
+
const resolved = path11.resolve(uriToPath(uri));
|
|
604
1502
|
if (!seen.has(resolved)) {
|
|
605
1503
|
seen.add(resolved);
|
|
606
1504
|
roots.push(resolved);
|
|
@@ -622,11 +1520,11 @@ function getRepoScopedWorkspaceHint(env2 = process.env) {
|
|
|
622
1520
|
if (raw === void 0 || raw.trim() === "") {
|
|
623
1521
|
return null;
|
|
624
1522
|
}
|
|
625
|
-
return
|
|
1523
|
+
return path11.resolve(raw.trim());
|
|
626
1524
|
}
|
|
627
1525
|
function formatWorkspaceAmbiguityError(bindings) {
|
|
628
1526
|
const lines = bindings.map(
|
|
629
|
-
(b) => ` - workspace=${b.workspaceRoot}
|
|
1527
|
+
(b) => ` - workspace=${b.workspaceRoot} binding=${b.repositoryBindingId} project=${b.projectId}`
|
|
630
1528
|
);
|
|
631
1529
|
return "[memoraone-mcp] Ambiguous workspace binding: multiple open roots map to different MemoraOne projects.\n" + lines.join("\n") + `
|
|
632
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.`;
|
|
@@ -634,7 +1532,7 @@ Open one repo per Cursor window, or ensure this repo's managed .cursor/mcp.json
|
|
|
634
1532
|
function formatRepoHintInitializeMismatchError(repoHintRoot, initializeBinding) {
|
|
635
1533
|
return `[memoraone-mcp] Repo-scoped workspace hint conflicts with MCP initialize workspace.
|
|
636
1534
|
${MEMORAONE_WORKSPACE_ROOT_ENV}: ${repoHintRoot}
|
|
637
|
-
initialize:
|
|
1535
|
+
initialize: binding=${initializeBinding.repositoryBindingId} project=${initializeBinding.projectId} workspace=${initializeBinding.workspaceRoot}
|
|
638
1536
|
Reload MCP in the Cursor window for this repo, or re-run setup-ide-files --cursor here if the hint is stale.`;
|
|
639
1537
|
}
|
|
640
1538
|
function formatRepoHintNotInRootsListError(repoHintRoot, rootsListPaths) {
|
|
@@ -645,13 +1543,16 @@ Reload MCP in the Cursor window for this repo, or re-run setup-ide-files --curso
|
|
|
645
1543
|
}
|
|
646
1544
|
function formatBindingMismatchError(daemonHint, sessionBinding) {
|
|
647
1545
|
return `[memoraone-mcp] Project binding mismatch between daemon and MCP initialize workspace.
|
|
648
|
-
daemon:
|
|
649
|
-
initialize:
|
|
650
|
-
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>`;
|
|
651
1550
|
}
|
|
652
1551
|
async function resolveBindingFromWorkspaceRoots(workspaceRoots, options = {}) {
|
|
653
1552
|
if (workspaceRoots.length === 0) {
|
|
654
|
-
throw new Error(
|
|
1553
|
+
throw new Error(
|
|
1554
|
+
"[memoraone-mcp] No local MemoraOne binding for this workspace.\nRun: memoraone-mcp connect <code>"
|
|
1555
|
+
);
|
|
655
1556
|
}
|
|
656
1557
|
const bindings = [];
|
|
657
1558
|
for (const root of workspaceRoots) {
|
|
@@ -662,14 +1563,16 @@ async function resolveBindingFromWorkspaceRoots(workspaceRoots, options = {}) {
|
|
|
662
1563
|
})
|
|
663
1564
|
);
|
|
664
1565
|
} catch (err) {
|
|
665
|
-
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")) {
|
|
666
1567
|
continue;
|
|
667
1568
|
}
|
|
668
1569
|
throw err;
|
|
669
1570
|
}
|
|
670
1571
|
}
|
|
671
1572
|
if (bindings.length === 0) {
|
|
672
|
-
throw new Error(
|
|
1573
|
+
throw new Error(
|
|
1574
|
+
"[memoraone-mcp] No local MemoraOne binding for this workspace.\nRun: memoraone-mcp connect <code>"
|
|
1575
|
+
);
|
|
673
1576
|
}
|
|
674
1577
|
const distinctProjectIds = new Set(bindings.map((b) => b.projectId));
|
|
675
1578
|
if (distinctProjectIds.size > 1) {
|
|
@@ -700,8 +1603,8 @@ async function resolveBindingFromInitializeParams(params, options = {}) {
|
|
|
700
1603
|
const rootsListUris = options.rootsListUris ?? [];
|
|
701
1604
|
const rootsListPaths = rootsListUris.map((uri) => uriToPath(uri)).filter(Boolean);
|
|
702
1605
|
if (rootsListPaths.length > 1 && repoHint !== null) {
|
|
703
|
-
const hintResolved =
|
|
704
|
-
const matchingRoot = rootsListPaths.find((root) =>
|
|
1606
|
+
const hintResolved = path11.resolve(repoHint);
|
|
1607
|
+
const matchingRoot = rootsListPaths.find((root) => path11.resolve(root) === hintResolved);
|
|
705
1608
|
if (!matchingRoot) {
|
|
706
1609
|
throw new Error(formatRepoHintNotInRootsListError(repoHint, rootsListPaths));
|
|
707
1610
|
}
|
|
@@ -725,108 +1628,42 @@ async function resolveBindingFromInitializeParams(params, options = {}) {
|
|
|
725
1628
|
return resolveAuthoritativeBinding(fallbackRoots, {
|
|
726
1629
|
respectExplicitM1Path: options.respectExplicitM1Path
|
|
727
1630
|
});
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// src/bindingSidecar.ts
|
|
731
|
-
var
|
|
732
|
-
var
|
|
733
|
-
function bindingSidecarPath(socketPath) {
|
|
734
|
-
if (socketPath.endsWith(".sock")) {
|
|
735
|
-
return `${socketPath.slice(0, -".sock".length)}.binding.json`;
|
|
736
|
-
}
|
|
737
|
-
return `${socketPath}.binding.json`;
|
|
738
|
-
}
|
|
739
|
-
function writeBindingSidecar(socketPath, binding, ideType = resolveBindingIdeType()) {
|
|
740
|
-
const payload = encodeResolvedBinding(binding);
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
// src/bridgeClientRoots.ts
|
|
756
|
-
var readline = __toESM(require("readline"), 1);
|
|
757
|
-
var TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
758
|
-
function isInitializeDebugEnabled(env2 = process.env) {
|
|
759
|
-
return TRUTHY.has(String(env2.MEMORAONE_DEBUG_INIT ?? "").trim().toLowerCase());
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// src/sourceRegistration.ts
|
|
763
|
-
var path7 = __toESM(require("path"), 1);
|
|
764
|
-
var import_node_url2 = require("url");
|
|
765
|
-
var LOG_PREFIX = "[memoraone-mcp][source-registration]";
|
|
766
|
-
function buildRepoSourcePayload(normalizedRepoPath, ideType) {
|
|
767
|
-
const body = {
|
|
768
|
-
kind: "repo",
|
|
769
|
-
label: path7.basename(normalizedRepoPath),
|
|
770
|
-
uri: (0, import_node_url2.pathToFileURL)(normalizedRepoPath).href
|
|
771
|
-
};
|
|
772
|
-
if (ideType) {
|
|
773
|
-
body.metadata = {
|
|
774
|
-
ide_type: ideType
|
|
775
|
-
};
|
|
776
|
-
}
|
|
777
|
-
return body;
|
|
778
|
-
}
|
|
779
|
-
async function registerRepoSource(client, projectId, repoPath, ideType) {
|
|
780
|
-
try {
|
|
781
|
-
if (repoPath === void 0 || repoPath === null || String(repoPath).trim() === "") {
|
|
782
|
-
process.stderr.write(
|
|
783
|
-
`${LOG_PREFIX} skipped: empty repoPath (cannot register workspace)
|
|
784
|
-
`
|
|
785
|
-
);
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
788
|
-
const normalizedRepoPath = path7.resolve(String(repoPath));
|
|
789
|
-
const body = buildRepoSourcePayload(normalizedRepoPath, ideType);
|
|
790
|
-
const primaryPath = `/v1/projects/${projectId}/sources`;
|
|
791
|
-
const alternatePath = `/v1/projects/${projectId}/sources/register`;
|
|
792
|
-
process.stderr.write(
|
|
793
|
-
`${LOG_PREFIX} registering projectId=${projectId} path=${normalizedRepoPath} ideType=${ideType ?? "(none)"}
|
|
794
|
-
`
|
|
795
|
-
);
|
|
796
|
-
try {
|
|
797
|
-
await client.post(primaryPath, body, {
|
|
798
|
-
acceptStatuses: [409],
|
|
799
|
-
log: false,
|
|
800
|
-
quietHttpStatuses: [404, 405, 501]
|
|
801
|
-
});
|
|
802
|
-
process.stderr.write(`${LOG_PREFIX} ok: POST ${primaryPath}
|
|
803
|
-
`);
|
|
804
|
-
return;
|
|
805
|
-
} catch (err) {
|
|
806
|
-
if (err instanceof MemoraOneHttpError && (err.status === 404 || err.status === 405 || err.status === 501)) {
|
|
807
|
-
process.stderr.write(
|
|
808
|
-
`${LOG_PREFIX} primary route returned ${err.status}; retrying POST ${alternatePath}
|
|
809
|
-
`
|
|
810
|
-
);
|
|
811
|
-
await client.post(alternatePath, body, { acceptStatuses: [409], log: false });
|
|
812
|
-
process.stderr.write(`${LOG_PREFIX} ok: POST ${alternatePath}
|
|
813
|
-
`);
|
|
814
|
-
return;
|
|
815
|
-
}
|
|
816
|
-
throw err;
|
|
817
|
-
}
|
|
818
|
-
} catch (err) {
|
|
819
|
-
const msg = String(err?.message ?? err);
|
|
820
|
-
process.stderr.write(`${LOG_PREFIX} failed: ${msg}
|
|
821
|
-
`);
|
|
822
|
-
if (err instanceof MemoraOneHttpError) {
|
|
823
|
-
const bodyStr = typeof err.body === "string" ? err.body : JSON.stringify(err.body ?? null);
|
|
824
|
-
process.stderr.write(
|
|
825
|
-
`${LOG_PREFIX} http status=${err.status} body=${bodyStr.length > 500 ? bodyStr.slice(0, 500) + "..." : bodyStr}
|
|
826
|
-
`
|
|
827
|
-
);
|
|
828
|
-
}
|
|
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");
|
|
829
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());
|
|
830
1667
|
}
|
|
831
1668
|
|
|
832
1669
|
// src/tools/postEvent.ts
|
|
@@ -930,21 +1767,11 @@ var logCommandShape = {
|
|
|
930
1767
|
stats: import_v410.z.record(import_v410.z.string(), import_v410.z.any()).optional()
|
|
931
1768
|
};
|
|
932
1769
|
|
|
933
|
-
// src/tools/listProjects.ts
|
|
934
|
-
var listProjectsShape = {};
|
|
935
|
-
|
|
936
|
-
// src/tools/setProject.ts
|
|
937
|
-
var import_v411 = require("zod/v4");
|
|
938
|
-
var setProjectShape = {
|
|
939
|
-
projectKey: import_v411.z.string().min(1).optional(),
|
|
940
|
-
projectId: import_v411.z.string().min(1).optional()
|
|
941
|
-
};
|
|
942
|
-
|
|
943
1770
|
// src/tools/bindingStatus.ts
|
|
944
1771
|
var bindingStatusShape = {};
|
|
945
1772
|
|
|
946
1773
|
// src/tools/handlers/postEvent.ts
|
|
947
|
-
var
|
|
1774
|
+
var import_v411 = require("zod/v4");
|
|
948
1775
|
var crypto4 = __toESM(require("crypto"), 1);
|
|
949
1776
|
|
|
950
1777
|
// src/runContext.ts
|
|
@@ -977,9 +1804,6 @@ function getBoundProjectId() {
|
|
|
977
1804
|
function setBoundProjectId(id) {
|
|
978
1805
|
getSessionContext().boundProjectId = id;
|
|
979
1806
|
}
|
|
980
|
-
function setBoundApiKey(key) {
|
|
981
|
-
getSessionContext().boundApiKey = key;
|
|
982
|
-
}
|
|
983
1807
|
function getCurrentRunId() {
|
|
984
1808
|
return getSessionContext().currentRunId;
|
|
985
1809
|
}
|
|
@@ -992,9 +1816,6 @@ function getCurrentProjectId() {
|
|
|
992
1816
|
function setCurrentProjectId(id) {
|
|
993
1817
|
getSessionContext().currentProjectId = id;
|
|
994
1818
|
}
|
|
995
|
-
function setCurrentApiKey(key) {
|
|
996
|
-
getSessionContext().currentApiKey = key;
|
|
997
|
-
}
|
|
998
1819
|
function resolveRunId(passed) {
|
|
999
1820
|
if (passed) {
|
|
1000
1821
|
return passed;
|
|
@@ -1006,14 +1827,14 @@ function generateRunId() {
|
|
|
1006
1827
|
}
|
|
1007
1828
|
|
|
1008
1829
|
// src/tools/handlers/postEvent.ts
|
|
1009
|
-
var postEventInputSchema =
|
|
1010
|
-
kind:
|
|
1011
|
-
actor:
|
|
1012
|
-
identifier:
|
|
1013
|
-
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()
|
|
1014
1835
|
}),
|
|
1015
|
-
content:
|
|
1016
|
-
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()
|
|
1017
1838
|
});
|
|
1018
1839
|
function buildPostEventContentFields(content) {
|
|
1019
1840
|
if (typeof content.message === "string") {
|
|
@@ -1049,7 +1870,9 @@ async function handlePostEvent(client, args) {
|
|
|
1049
1870
|
const parsed2 = postEventInputSchema.parse(args ?? {});
|
|
1050
1871
|
const projectKey = getCurrentProjectId();
|
|
1051
1872
|
if (!projectKey) {
|
|
1052
|
-
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
|
+
);
|
|
1053
1876
|
}
|
|
1054
1877
|
const content = parsed2.content ?? {};
|
|
1055
1878
|
const { message, new_value } = buildPostEventContentFields(content);
|
|
@@ -1083,16 +1906,18 @@ async function handlePostEvent(client, args) {
|
|
|
1083
1906
|
}
|
|
1084
1907
|
|
|
1085
1908
|
// src/tools/handlers/createFact.ts
|
|
1086
|
-
var
|
|
1087
|
-
var createFactInputSchema =
|
|
1088
|
-
content:
|
|
1089
|
-
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()
|
|
1090
1913
|
});
|
|
1091
1914
|
async function handleCreateFact(client, args) {
|
|
1092
1915
|
const parsed2 = createFactInputSchema.parse(args ?? {});
|
|
1093
1916
|
const projectKey = getCurrentProjectId();
|
|
1094
1917
|
if (!projectKey) {
|
|
1095
|
-
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
|
+
);
|
|
1096
1921
|
}
|
|
1097
1922
|
const content = parsed2.content.trim();
|
|
1098
1923
|
if (!content) {
|
|
@@ -1129,13 +1954,13 @@ async function handleCreateFact(client, args) {
|
|
|
1129
1954
|
}
|
|
1130
1955
|
|
|
1131
1956
|
// src/tools/handlers/addPersonalContext.ts
|
|
1132
|
-
var
|
|
1133
|
-
var addPersonalContextInputSchema =
|
|
1134
|
-
content:
|
|
1135
|
-
category:
|
|
1136
|
-
tags:
|
|
1137
|
-
scope_type:
|
|
1138
|
-
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()
|
|
1139
1964
|
});
|
|
1140
1965
|
async function handleAddPersonalContext(client, args) {
|
|
1141
1966
|
const parsed2 = addPersonalContextInputSchema.parse(args ?? {});
|
|
@@ -1171,12 +1996,12 @@ async function handleAddPersonalContext(client, args) {
|
|
|
1171
1996
|
}
|
|
1172
1997
|
|
|
1173
1998
|
// src/tools/handlers/getPersonalContext.ts
|
|
1174
|
-
var
|
|
1175
|
-
var getPersonalContextInputSchema =
|
|
1176
|
-
query:
|
|
1177
|
-
scope_type:
|
|
1178
|
-
scope_id:
|
|
1179
|
-
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()
|
|
1180
2005
|
});
|
|
1181
2006
|
function buildPersonalContextPath(parsed2) {
|
|
1182
2007
|
const params = new URLSearchParams();
|
|
@@ -1197,9 +2022,9 @@ function buildPersonalContextPath(parsed2) {
|
|
|
1197
2022
|
}
|
|
1198
2023
|
async function handleGetPersonalContext(client, args) {
|
|
1199
2024
|
const parsed2 = getPersonalContextInputSchema.parse(args ?? {});
|
|
1200
|
-
const
|
|
2025
|
+
const path14 = buildPersonalContextPath(parsed2);
|
|
1201
2026
|
try {
|
|
1202
|
-
const result = await client.get(
|
|
2027
|
+
const result = await client.get(path14);
|
|
1203
2028
|
return { ok: true, result };
|
|
1204
2029
|
} catch (err) {
|
|
1205
2030
|
if (err instanceof MemoraOneHttpError) {
|
|
@@ -1213,13 +2038,13 @@ async function handleGetPersonalContext(client, args) {
|
|
|
1213
2038
|
}
|
|
1214
2039
|
|
|
1215
2040
|
// src/tools/handlers/askWithMemory.ts
|
|
1216
|
-
var
|
|
1217
|
-
var askWithMemoryInputSchema =
|
|
1218
|
-
question:
|
|
1219
|
-
code_context:
|
|
1220
|
-
file_path:
|
|
1221
|
-
selected_text:
|
|
1222
|
-
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()
|
|
1223
2048
|
}).optional()
|
|
1224
2049
|
});
|
|
1225
2050
|
function isAskWithMemoryResponse(value) {
|
|
@@ -1229,7 +2054,9 @@ async function handleAskWithMemory(client, args) {
|
|
|
1229
2054
|
const parsed2 = askWithMemoryInputSchema.parse(args ?? {});
|
|
1230
2055
|
const projectKey = getCurrentProjectId();
|
|
1231
2056
|
if (!projectKey) {
|
|
1232
|
-
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
|
+
);
|
|
1233
2060
|
}
|
|
1234
2061
|
const payload = {
|
|
1235
2062
|
question: parsed2.question,
|
|
@@ -1253,19 +2080,21 @@ async function handleAskWithMemory(client, args) {
|
|
|
1253
2080
|
}
|
|
1254
2081
|
|
|
1255
2082
|
// src/tools/handlers/logIntent.ts
|
|
1256
|
-
var
|
|
1257
|
-
var logIntentInputSchema =
|
|
1258
|
-
intent:
|
|
1259
|
-
message:
|
|
1260
|
-
context:
|
|
1261
|
-
intent_source:
|
|
1262
|
-
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()
|
|
1263
2090
|
});
|
|
1264
2091
|
async function handleLogIntent(client, args) {
|
|
1265
2092
|
const parsed2 = logIntentInputSchema.parse(args ?? {});
|
|
1266
2093
|
const projectKey = getCurrentProjectId();
|
|
1267
2094
|
if (!projectKey) {
|
|
1268
|
-
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
|
+
);
|
|
1269
2098
|
}
|
|
1270
2099
|
const intent = parsed2.intent;
|
|
1271
2100
|
const message = parsed2.message.trim();
|
|
@@ -1291,521 +2120,168 @@ async function handleLogIntent(client, args) {
|
|
|
1291
2120
|
// 'task' | 'decision'
|
|
1292
2121
|
intent_source: intent_source ?? "cursor_chat",
|
|
1293
2122
|
run_id,
|
|
1294
|
-
...context ? { context } : {}
|
|
1295
|
-
}
|
|
1296
|
-
};
|
|
1297
|
-
await client.post("/timeline/events", body);
|
|
1298
|
-
return { ok: true, run_id };
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
// src/tools/handlers/logChangeSummary.ts
|
|
1302
|
-
var import_v418 = require("zod/v4");
|
|
1303
|
-
var logChangeSummaryInputSchema = import_v418.z.object({
|
|
1304
|
-
summary: import_v418.z.string().min(1),
|
|
1305
|
-
scope: import_v418.z.string().min(1).optional(),
|
|
1306
|
-
files: import_v418.z.array(import_v418.z.string().min(1)).optional(),
|
|
1307
|
-
stats: import_v418.z.object({
|
|
1308
|
-
files: import_v418.z.number().int().nonnegative().optional(),
|
|
1309
|
-
add: import_v418.z.number().int().nonnegative().optional(),
|
|
1310
|
-
del: import_v418.z.number().int().nonnegative().optional()
|
|
1311
|
-
}).optional(),
|
|
1312
|
-
commit: import_v418.z.string().min(1).optional(),
|
|
1313
|
-
run_id: import_v418.z.string().min(1).optional()
|
|
1314
|
-
});
|
|
1315
|
-
async function handleLogChangeSummary(client, args) {
|
|
1316
|
-
const parsed2 = logChangeSummaryInputSchema.parse(args ?? {});
|
|
1317
|
-
const projectKey = getCurrentProjectId();
|
|
1318
|
-
if (!projectKey) {
|
|
1319
|
-
throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
|
|
1320
|
-
}
|
|
1321
|
-
const { summary, scope, files, stats, commit } = parsed2;
|
|
1322
|
-
const message = summary.startsWith("CHANGE:") ? summary : `CHANGE: ${scope ?? "code"} \u2014 ${summary}`;
|
|
1323
|
-
const run_id = resolveRunId(parsed2.run_id);
|
|
1324
|
-
const body = {
|
|
1325
|
-
kind: "note",
|
|
1326
|
-
concept: "concept:change_summary",
|
|
1327
|
-
actor: { type: config2.agentType, name: config2.agentName },
|
|
1328
|
-
message,
|
|
1329
|
-
projectKey,
|
|
1330
|
-
metadata: {
|
|
1331
|
-
source: config2.source,
|
|
1332
|
-
purpose: "change_summary",
|
|
1333
|
-
tool: "memora_log_change_summary",
|
|
1334
|
-
...scope ? { scope } : {},
|
|
1335
|
-
...files ? { files } : {},
|
|
1336
|
-
...stats ? { stats } : {},
|
|
1337
|
-
...commit ? { commit } : {},
|
|
1338
|
-
...run_id ? { run_id } : {}
|
|
1339
|
-
}
|
|
1340
|
-
};
|
|
1341
|
-
await client.post("/timeline/events", body);
|
|
1342
|
-
return { ok: true };
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
// src/tools/handlers/logToolResult.ts
|
|
1346
|
-
var import_v419 = require("zod/v4");
|
|
1347
|
-
var logToolResultInputSchema = import_v419.z.object({
|
|
1348
|
-
tool: import_v419.z.string().min(1),
|
|
1349
|
-
status: import_v419.z.enum(["ok", "error", "partial"]),
|
|
1350
|
-
summary: import_v419.z.string().min(1),
|
|
1351
|
-
run_id: import_v419.z.string().min(1).optional(),
|
|
1352
|
-
duration_ms: import_v419.z.number().int().nonnegative().optional(),
|
|
1353
|
-
error_code: import_v419.z.string().min(1).optional(),
|
|
1354
|
-
error_message: import_v419.z.string().min(1).optional(),
|
|
1355
|
-
error_kind: import_v419.z.enum(["infra", "logic", "auth", "rate_limit", "validation", "unknown"]).optional(),
|
|
1356
|
-
stats: import_v419.z.record(import_v419.z.string(), import_v419.z.any()).optional()
|
|
1357
|
-
});
|
|
1358
|
-
async function handleLogToolResult(client, args) {
|
|
1359
|
-
const parsed2 = logToolResultInputSchema.parse(args ?? {});
|
|
1360
|
-
const projectKey = getCurrentProjectId();
|
|
1361
|
-
if (!projectKey) {
|
|
1362
|
-
throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
|
|
1363
|
-
}
|
|
1364
|
-
const { tool, status, summary, duration_ms, error_code, error_message, error_kind, stats } = parsed2;
|
|
1365
|
-
const message = summary.startsWith("RESULT:") ? summary : `RESULT: ${tool} \u2014 ${status} \u2014 ${summary}`;
|
|
1366
|
-
const run_id = resolveRunId(parsed2.run_id);
|
|
1367
|
-
const body = {
|
|
1368
|
-
kind: "note",
|
|
1369
|
-
concept: "concept:tool_result",
|
|
1370
|
-
actor: { type: config2.agentType, name: config2.agentName },
|
|
1371
|
-
message,
|
|
1372
|
-
projectKey,
|
|
1373
|
-
metadata: {
|
|
1374
|
-
source: config2.source,
|
|
1375
|
-
purpose: "tool_result",
|
|
1376
|
-
tool: "memora_log_tool_result",
|
|
1377
|
-
tool_name: tool,
|
|
1378
|
-
status,
|
|
1379
|
-
...run_id ? { run_id } : {},
|
|
1380
|
-
...duration_ms ? { duration_ms } : {},
|
|
1381
|
-
...error_code ? { error_code } : {},
|
|
1382
|
-
...error_message ? { error_message } : {},
|
|
1383
|
-
...error_kind ? { error_kind } : {},
|
|
1384
|
-
...stats ? { stats } : {}
|
|
1385
|
-
}
|
|
1386
|
-
};
|
|
1387
|
-
await client.post("/timeline/events", body);
|
|
1388
|
-
return { ok: true };
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
// src/tools/handlers/logCommand.ts
|
|
1392
|
-
var import_v420 = require("zod/v4");
|
|
1393
|
-
var logCommandInputSchema = import_v420.z.object({
|
|
1394
|
-
cmd: import_v420.z.string().min(1),
|
|
1395
|
-
summary: import_v420.z.string().min(1),
|
|
1396
|
-
cwd: import_v420.z.string().min(1).optional(),
|
|
1397
|
-
exit_code: import_v420.z.number().int().optional(),
|
|
1398
|
-
duration_ms: import_v420.z.number().int().nonnegative().optional(),
|
|
1399
|
-
run_id: import_v420.z.string().min(1).optional(),
|
|
1400
|
-
stats: import_v420.z.record(import_v420.z.string(), import_v420.z.any()).optional()
|
|
1401
|
-
});
|
|
1402
|
-
async function handleLogCommand(client, args) {
|
|
1403
|
-
const parsed2 = logCommandInputSchema.parse(args ?? {});
|
|
1404
|
-
const projectKey = getCurrentProjectId();
|
|
1405
|
-
if (!projectKey) {
|
|
1406
|
-
throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
|
|
1407
|
-
}
|
|
1408
|
-
const { cmd, summary, cwd: cwd2, exit_code, duration_ms, stats } = parsed2;
|
|
1409
|
-
const message = summary.startsWith("COMMAND:") ? summary : `COMMAND: ${cmd} \u2014 ${summary}`;
|
|
1410
|
-
const run_id = resolveRunId(parsed2.run_id);
|
|
1411
|
-
const body = {
|
|
1412
|
-
kind: "note",
|
|
1413
|
-
concept: "concept:command",
|
|
1414
|
-
actor: { type: config2.agentType, name: config2.agentName },
|
|
1415
|
-
message,
|
|
1416
|
-
projectKey,
|
|
1417
|
-
metadata: {
|
|
1418
|
-
source: config2.source,
|
|
1419
|
-
purpose: "command",
|
|
1420
|
-
tool: "memora_log_command",
|
|
1421
|
-
cmd,
|
|
1422
|
-
...cwd2 ? { cwd: cwd2 } : {},
|
|
1423
|
-
...exit_code !== void 0 ? { exit_code } : {},
|
|
1424
|
-
...duration_ms !== void 0 ? { duration_ms } : {},
|
|
1425
|
-
...run_id ? { run_id } : {},
|
|
1426
|
-
...stats ? { stats } : {}
|
|
1427
|
-
}
|
|
1428
|
-
};
|
|
1429
|
-
await client.post("/timeline/events", body);
|
|
1430
|
-
return { ok: true };
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
// src/tools/handlers/listProjects.ts
|
|
1434
|
-
async function handleListProjects(client) {
|
|
1435
|
-
const res = await client.get("/v1/projects");
|
|
1436
|
-
return res ?? { items: [] };
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
// src/tools/handlers/setProject.ts
|
|
1440
|
-
var import_v421 = require("zod/v4");
|
|
1441
|
-
|
|
1442
|
-
// src/repoFingerprint.ts
|
|
1443
|
-
var fs5 = __toESM(require("fs"), 1);
|
|
1444
|
-
var path8 = __toESM(require("path"), 1);
|
|
1445
|
-
var crypto5 = __toESM(require("crypto"), 1);
|
|
1446
|
-
var parseBooleanFlag3 = (value) => {
|
|
1447
|
-
if (!value) {
|
|
1448
|
-
return false;
|
|
1449
|
-
}
|
|
1450
|
-
const normalized = value.trim().toLowerCase();
|
|
1451
|
-
return ["1", "true", "yes", "on"].includes(normalized);
|
|
1452
|
-
};
|
|
1453
|
-
var debugEnabled2 = parseBooleanFlag3(process.env.MEMORAONE_DEV_MODE);
|
|
1454
|
-
var debugLog = (message) => {
|
|
1455
|
-
if (!debugEnabled2) {
|
|
1456
|
-
return;
|
|
1457
|
-
}
|
|
1458
|
-
process.stderr.write(`[memoraone-mcp][debug] ${message}
|
|
1459
|
-
`);
|
|
1460
|
-
};
|
|
1461
|
-
var normalizeRemoteUrl = (remoteUrl) => {
|
|
1462
|
-
let normalized = remoteUrl.trim();
|
|
1463
|
-
normalized = normalized.replace(/^[a-z]+:\/\//i, "");
|
|
1464
|
-
normalized = normalized.replace(/^git@([^:]+):/i, "$1/");
|
|
1465
|
-
normalized = normalized.replace(/\.git$/i, "");
|
|
1466
|
-
normalized = normalized.replace(/\/+$/, "");
|
|
1467
|
-
return normalized.toLowerCase();
|
|
1468
|
-
};
|
|
1469
|
-
var sha256 = (value) => {
|
|
1470
|
-
return crypto5.createHash("sha256").update(value).digest("hex");
|
|
1471
|
-
};
|
|
1472
|
-
var resolveGitDir = (gitPath) => {
|
|
1473
|
-
try {
|
|
1474
|
-
const stat2 = fs5.statSync(gitPath);
|
|
1475
|
-
if (stat2.isDirectory()) {
|
|
1476
|
-
return gitPath;
|
|
1477
|
-
}
|
|
1478
|
-
if (stat2.isFile()) {
|
|
1479
|
-
const content = fs5.readFileSync(gitPath, "utf8");
|
|
1480
|
-
const match = content.match(/^gitdir:\s*(.+)$/m);
|
|
1481
|
-
if (match) {
|
|
1482
|
-
const gitDir = match[1].trim();
|
|
1483
|
-
return path8.resolve(path8.dirname(gitPath), gitDir);
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
} catch {
|
|
1487
|
-
return null;
|
|
1488
|
-
}
|
|
1489
|
-
return null;
|
|
1490
|
-
};
|
|
1491
|
-
var findGitRoot = (start) => {
|
|
1492
|
-
let current = path8.resolve(start);
|
|
1493
|
-
while (true) {
|
|
1494
|
-
const gitPath = path8.join(current, ".git");
|
|
1495
|
-
if (fs5.existsSync(gitPath)) {
|
|
1496
|
-
const gitDir = resolveGitDir(gitPath);
|
|
1497
|
-
if (gitDir) {
|
|
1498
|
-
return { gitRoot: current, gitDir };
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
const parent = path8.dirname(current);
|
|
1502
|
-
if (parent === current) {
|
|
1503
|
-
break;
|
|
1504
|
-
}
|
|
1505
|
-
current = parent;
|
|
1506
|
-
}
|
|
1507
|
-
return null;
|
|
1508
|
-
};
|
|
1509
|
-
var readOriginRemote = (gitDir) => {
|
|
1510
|
-
const configPath = path8.join(gitDir, "config");
|
|
1511
|
-
try {
|
|
1512
|
-
const content = fs5.readFileSync(configPath, "utf8");
|
|
1513
|
-
const lines = content.split(/\r?\n/);
|
|
1514
|
-
let inOrigin = false;
|
|
1515
|
-
for (const line of lines) {
|
|
1516
|
-
const sectionMatch = line.match(/^\s*\[(.+)]\s*$/);
|
|
1517
|
-
if (sectionMatch) {
|
|
1518
|
-
inOrigin = sectionMatch[1].trim() === 'remote "origin"';
|
|
1519
|
-
continue;
|
|
1520
|
-
}
|
|
1521
|
-
if (inOrigin) {
|
|
1522
|
-
const urlMatch = line.match(/^\s*url\s*=\s*(.+)\s*$/);
|
|
1523
|
-
if (urlMatch) {
|
|
1524
|
-
return urlMatch[1].trim();
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
} catch {
|
|
1529
|
-
return null;
|
|
1530
|
-
}
|
|
1531
|
-
return null;
|
|
1532
|
-
};
|
|
1533
|
-
function resolveRepoFingerprint(cwd2) {
|
|
1534
|
-
const found = findGitRoot(cwd2);
|
|
1535
|
-
if (!found) {
|
|
1536
|
-
const fallbackPath = path8.resolve(cwd2);
|
|
1537
|
-
const fingerprint2 = sha256(fallbackPath);
|
|
1538
|
-
debugLog(`repo fingerprint=${fingerprint2} source=path-fallback`);
|
|
1539
|
-
return {
|
|
1540
|
-
fingerprint: fingerprint2,
|
|
1541
|
-
gitRoot: fallbackPath,
|
|
1542
|
-
source: "path-fallback"
|
|
1543
|
-
};
|
|
1544
|
-
}
|
|
1545
|
-
const { gitRoot, gitDir } = found;
|
|
1546
|
-
const remoteUrl = readOriginRemote(gitDir);
|
|
1547
|
-
if (remoteUrl) {
|
|
1548
|
-
const normalized = normalizeRemoteUrl(remoteUrl);
|
|
1549
|
-
const fingerprint2 = sha256(normalized);
|
|
1550
|
-
debugLog(`repo fingerprint=${fingerprint2} source=git-remote`);
|
|
1551
|
-
return {
|
|
1552
|
-
fingerprint: fingerprint2,
|
|
1553
|
-
gitRoot,
|
|
1554
|
-
remoteUrl,
|
|
1555
|
-
source: "git-remote"
|
|
1556
|
-
};
|
|
1557
|
-
}
|
|
1558
|
-
const fingerprint = sha256(path8.resolve(gitRoot));
|
|
1559
|
-
debugLog(`repo fingerprint=${fingerprint} source=path-fallback`);
|
|
1560
|
-
return {
|
|
1561
|
-
fingerprint,
|
|
1562
|
-
gitRoot,
|
|
1563
|
-
source: "path-fallback"
|
|
1564
|
-
};
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
// src/workspaceMap.ts
|
|
1568
|
-
var fs6 = __toESM(require("fs/promises"), 1);
|
|
1569
|
-
var path9 = __toESM(require("path"), 1);
|
|
1570
|
-
var import_node_os = __toESM(require("os"), 1);
|
|
1571
|
-
var parseBooleanFlag4 = (value) => {
|
|
1572
|
-
if (!value) {
|
|
1573
|
-
return false;
|
|
1574
|
-
}
|
|
1575
|
-
const normalized = value.trim().toLowerCase();
|
|
1576
|
-
return ["1", "true", "yes", "on"].includes(normalized);
|
|
1577
|
-
};
|
|
1578
|
-
var debugEnabled3 = parseBooleanFlag4(process.env.MEMORAONE_DEV_MODE);
|
|
1579
|
-
var debugLog2 = (message) => {
|
|
1580
|
-
if (!debugEnabled3) {
|
|
1581
|
-
return;
|
|
1582
|
-
}
|
|
1583
|
-
process.stderr.write(`[memoraone-mcp][debug] ${message}
|
|
1584
|
-
`);
|
|
1585
|
-
};
|
|
1586
|
-
var fingerprintRegex = /^[0-9a-f]{64}$/i;
|
|
1587
|
-
function getWorkspaceMapPath() {
|
|
1588
|
-
return path9.join(import_node_os.default.homedir(), ".memoraone", "workspaces.json");
|
|
1589
|
-
}
|
|
1590
|
-
var ensureWorkspaceDir = async () => {
|
|
1591
|
-
const dir = path9.dirname(getWorkspaceMapPath());
|
|
1592
|
-
await fs6.mkdir(dir, { recursive: true });
|
|
1593
|
-
};
|
|
1594
|
-
async function acquireWorkspaceMapLock() {
|
|
1595
|
-
const filePath = getWorkspaceMapPath();
|
|
1596
|
-
const lockPath = `${filePath}.lock`;
|
|
1597
|
-
const maxRetries = 10;
|
|
1598
|
-
const retryDelayMs = 50;
|
|
1599
|
-
const maxLockAgeMs = 5e3;
|
|
1600
|
-
await ensureWorkspaceDir();
|
|
1601
|
-
let lockAcquired = false;
|
|
1602
|
-
let retries = 0;
|
|
1603
|
-
while (!lockAcquired && retries < maxRetries) {
|
|
1604
|
-
try {
|
|
1605
|
-
try {
|
|
1606
|
-
const stat2 = await fs6.stat(lockPath);
|
|
1607
|
-
const ageMs = Date.now() - stat2.mtimeMs;
|
|
1608
|
-
if (ageMs > maxLockAgeMs) {
|
|
1609
|
-
await fs6.unlink(lockPath);
|
|
1610
|
-
debugLog2(`removed stale workspace map lock (age: ${ageMs}ms)`);
|
|
1611
|
-
}
|
|
1612
|
-
} catch (err) {
|
|
1613
|
-
if (err?.code !== "ENOENT") {
|
|
1614
|
-
throw err;
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
const fd = await fs6.open(lockPath, "wx");
|
|
1618
|
-
await fd.close();
|
|
1619
|
-
lockAcquired = true;
|
|
1620
|
-
} catch (err) {
|
|
1621
|
-
if (err?.code === "EEXIST") {
|
|
1622
|
-
retries++;
|
|
1623
|
-
if (retries < maxRetries) {
|
|
1624
|
-
await new Promise((resolve8) => setTimeout(resolve8, retryDelayMs));
|
|
1625
|
-
continue;
|
|
1626
|
-
}
|
|
1627
|
-
throw new Error(
|
|
1628
|
-
`[memoraone-mcp] Failed to acquire workspace map lock after ${maxRetries} retries`
|
|
1629
|
-
);
|
|
1630
|
-
}
|
|
1631
|
-
throw err;
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
return async () => {
|
|
1635
|
-
try {
|
|
1636
|
-
await fs6.unlink(lockPath);
|
|
1637
|
-
} catch (err) {
|
|
1638
|
-
if (err?.code !== "ENOENT") {
|
|
1639
|
-
debugLog2(`failed to release workspace map lock: ${String(err)}`);
|
|
1640
|
-
}
|
|
2123
|
+
...context ? { context } : {}
|
|
1641
2124
|
}
|
|
1642
2125
|
};
|
|
2126
|
+
await client.post("/timeline/events", body);
|
|
2127
|
+
return { ok: true, run_id };
|
|
1643
2128
|
}
|
|
1644
|
-
|
|
1645
|
-
|
|
2129
|
+
|
|
2130
|
+
// src/tools/handlers/logChangeSummary.ts
|
|
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()
|
|
2140
|
+
}).optional(),
|
|
2141
|
+
commit: import_v417.z.string().min(1).optional(),
|
|
2142
|
+
run_id: import_v417.z.string().min(1).optional()
|
|
2143
|
+
});
|
|
2144
|
+
async function handleLogChangeSummary(client, args) {
|
|
2145
|
+
const parsed2 = logChangeSummaryInputSchema.parse(args ?? {});
|
|
2146
|
+
const projectKey = getCurrentProjectId();
|
|
2147
|
+
if (!projectKey) {
|
|
1646
2148
|
throw new Error(
|
|
1647
|
-
|
|
2149
|
+
"No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
|
|
1648
2150
|
);
|
|
1649
2151
|
}
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
}
|
|
1669
|
-
const projectKey = entry.projectKey ?? entry.project_id;
|
|
1670
|
-
if (!projectKey || !projectKey.trim()) {
|
|
1671
|
-
throw new Error(
|
|
1672
|
-
`[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
|
|
1673
|
-
);
|
|
1674
|
-
}
|
|
1675
|
-
const source = entry.source;
|
|
1676
|
-
if (source !== void 0 && typeof source !== "string") {
|
|
1677
|
-
throw new Error(
|
|
1678
|
-
`[memoraone-mcp] Invalid workspace source in ${filePath}`
|
|
1679
|
-
);
|
|
1680
|
-
}
|
|
1681
|
-
const linkedAt = entry.linked_at;
|
|
1682
|
-
if (linkedAt !== void 0 && typeof linkedAt !== "string") {
|
|
1683
|
-
throw new Error(
|
|
1684
|
-
`[memoraone-mcp] Invalid workspace linked_at in ${filePath}`
|
|
1685
|
-
);
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
};
|
|
1689
|
-
async function readWorkspaceMap() {
|
|
1690
|
-
const filePath = getWorkspaceMapPath();
|
|
1691
|
-
try {
|
|
1692
|
-
const content = await fs6.readFile(filePath, "utf8");
|
|
1693
|
-
const parsed2 = JSON.parse(content);
|
|
1694
|
-
validateWorkspaceMap(parsed2, filePath);
|
|
1695
|
-
const typed = parsed2;
|
|
1696
|
-
let migrated = false;
|
|
1697
|
-
const normalized = {};
|
|
1698
|
-
for (const [fingerprint, entry] of Object.entries(typed)) {
|
|
1699
|
-
if (typeof entry === "string") {
|
|
1700
|
-
normalized[fingerprint] = entry;
|
|
1701
|
-
continue;
|
|
1702
|
-
}
|
|
1703
|
-
const projectKey = entry.projectKey ?? entry.project_id ?? "";
|
|
1704
|
-
if (entry.project_id && !entry.projectKey) {
|
|
1705
|
-
migrated = true;
|
|
1706
|
-
}
|
|
1707
|
-
normalized[fingerprint] = {
|
|
1708
|
-
...projectKey ? { projectKey } : {},
|
|
1709
|
-
...entry.source ? { source: entry.source } : {},
|
|
1710
|
-
...entry.linked_at ? { linked_at: entry.linked_at } : {}
|
|
1711
|
-
};
|
|
1712
|
-
}
|
|
1713
|
-
debugLog2(
|
|
1714
|
-
`workspace map loaded path=${filePath} entries=${Object.keys(normalized).length}`
|
|
1715
|
-
);
|
|
1716
|
-
return { map: normalized, needsMigration: migrated };
|
|
1717
|
-
} catch (err) {
|
|
1718
|
-
if (err?.code === "ENOENT") {
|
|
1719
|
-
const emptyMap = {};
|
|
1720
|
-
debugLog2(`workspace map loaded path=${filePath} entries=0`);
|
|
1721
|
-
return { map: emptyMap, needsMigration: false };
|
|
1722
|
-
}
|
|
1723
|
-
if (err instanceof SyntaxError) {
|
|
1724
|
-
throw new Error(
|
|
1725
|
-
`[memoraone-mcp] Failed to parse workspace map at ${filePath}`
|
|
1726
|
-
);
|
|
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 } : {}
|
|
1727
2170
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
}
|
|
1731
|
-
async function writeWorkspaceMap(map) {
|
|
1732
|
-
const filePath = getWorkspaceMapPath();
|
|
1733
|
-
validateWorkspaceMap(map, filePath);
|
|
1734
|
-
await ensureWorkspaceDir();
|
|
1735
|
-
const tempPath = `${filePath}.tmp`;
|
|
1736
|
-
const content = JSON.stringify(map, null, 2);
|
|
1737
|
-
await fs6.writeFile(tempPath, content, "utf8");
|
|
1738
|
-
await fs6.rename(tempPath, filePath);
|
|
2171
|
+
};
|
|
2172
|
+
await client.post("/timeline/events", body);
|
|
2173
|
+
return { ok: true };
|
|
1739
2174
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
debugLog2(
|
|
1761
|
-
`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."
|
|
1762
2195
|
);
|
|
1763
|
-
} finally {
|
|
1764
|
-
await releaseLock();
|
|
1765
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 };
|
|
1766
2222
|
}
|
|
1767
2223
|
|
|
1768
|
-
// src/tools/handlers/
|
|
1769
|
-
var
|
|
1770
|
-
|
|
1771
|
-
|
|
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()
|
|
1772
2234
|
});
|
|
1773
|
-
async function
|
|
1774
|
-
const parsed2 =
|
|
1775
|
-
const
|
|
1776
|
-
if (!
|
|
1777
|
-
throw new Error("projectKey is required");
|
|
1778
|
-
}
|
|
1779
|
-
const requested = resolvedProjectKey.trim();
|
|
1780
|
-
const bound = getBoundProjectId();
|
|
1781
|
-
if (bound !== null && requested !== bound) {
|
|
2235
|
+
async function handleLogCommand(client, args) {
|
|
2236
|
+
const parsed2 = logCommandInputSchema.parse(args ?? {});
|
|
2237
|
+
const projectKey = getCurrentProjectId();
|
|
2238
|
+
if (!projectKey) {
|
|
1782
2239
|
throw new Error(
|
|
1783
|
-
|
|
2240
|
+
"No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
|
|
1784
2241
|
);
|
|
1785
2242
|
}
|
|
1786
|
-
|
|
1787
|
-
const
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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 };
|
|
1794
2266
|
}
|
|
1795
2267
|
|
|
1796
2268
|
// src/tools/handlers/bindingStatus.ts
|
|
1797
2269
|
function buildBindingStatus(binding, options = {}) {
|
|
1798
2270
|
const status = {
|
|
2271
|
+
repositoryBindingId: binding.repositoryBindingId,
|
|
1799
2272
|
projectId: binding.projectId,
|
|
1800
2273
|
workspaceRoot: binding.workspaceRoot,
|
|
1801
|
-
m1Path: binding.m1Path,
|
|
1802
2274
|
bindingSource: binding.bindingSource,
|
|
1803
|
-
|
|
2275
|
+
status: binding.status,
|
|
2276
|
+
credentialSource: "keyring",
|
|
1804
2277
|
cacheRefreshed: options.cacheRefreshed === true
|
|
1805
2278
|
};
|
|
1806
2279
|
if (binding.environment !== void 0) {
|
|
1807
2280
|
status.environment = binding.environment;
|
|
1808
2281
|
}
|
|
2282
|
+
if (binding.installationPublicId !== void 0) {
|
|
2283
|
+
status.installationPublicId = binding.installationPublicId;
|
|
2284
|
+
}
|
|
1809
2285
|
return status;
|
|
1810
2286
|
}
|
|
1811
2287
|
function handleBindingStatus(binding, options = {}) {
|
|
@@ -1816,9 +2292,24 @@ function handleBindingStatus(binding, options = {}) {
|
|
|
1816
2292
|
}
|
|
1817
2293
|
|
|
1818
2294
|
// src/heartbeat.ts
|
|
1819
|
-
var
|
|
1820
|
-
|
|
1821
|
-
|
|
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")}`;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// src/heartbeat.ts
|
|
2311
|
+
function fingerprintAccessToken(accessToken) {
|
|
2312
|
+
return crypto5.createHash("sha256").update(accessToken).digest("hex").slice(0, 12);
|
|
1822
2313
|
}
|
|
1823
2314
|
function isHeartbeatDebugEnabled() {
|
|
1824
2315
|
const value = String(process.env.MEMORAONE_DEBUG_HEARTBEAT ?? "").trim().toLowerCase();
|
|
@@ -1827,12 +2318,80 @@ function isHeartbeatDebugEnabled() {
|
|
|
1827
2318
|
function resolveHeartbeatIntervalMs() {
|
|
1828
2319
|
return Number.isFinite(config2.heartbeatIntervalMs) ? Math.max(1e3, config2.heartbeatIntervalMs) : 3e4;
|
|
1829
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
|
+
}
|
|
1830
2389
|
async function sendProjectHeartbeat(client, ctx) {
|
|
1831
2390
|
try {
|
|
1832
2391
|
const pid = ctx.projectId?.trim();
|
|
1833
2392
|
if (isHeartbeatDebugEnabled()) {
|
|
1834
2393
|
process.stderr.write(
|
|
1835
|
-
`[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"}
|
|
1836
2395
|
`
|
|
1837
2396
|
);
|
|
1838
2397
|
}
|
|
@@ -1841,18 +2400,183 @@ async function sendProjectHeartbeat(client, ctx) {
|
|
|
1841
2400
|
}
|
|
1842
2401
|
const body = {};
|
|
1843
2402
|
if (ctx.ideType) body.ide_type = ctx.ideType;
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
"x-project-id": pid
|
|
1848
|
-
}
|
|
1849
|
-
});
|
|
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) };
|
|
1850
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
|
+
}
|
|
1851
2420
|
process.stderr.write(
|
|
1852
|
-
`[memoraone-mcp][info] heartbeat error (silent) ${String(err)}
|
|
2421
|
+
`[memoraone-mcp][info] heartbeat error (silent) ${redactSensitiveText(String(err))}
|
|
1853
2422
|
`
|
|
1854
2423
|
);
|
|
1855
|
-
|
|
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
|
+
};
|
|
1856
2580
|
}
|
|
1857
2581
|
|
|
1858
2582
|
// src/ideType.ts
|
|
@@ -1981,8 +2705,8 @@ function registerToolWithWorklog(server, runtime, sessionContext, toolName, desc
|
|
|
1981
2705
|
async function main(opts = {}) {
|
|
1982
2706
|
let bindingReadyResolve = null;
|
|
1983
2707
|
let bindingReadyReject = null;
|
|
1984
|
-
const bindingReady = new Promise((
|
|
1985
|
-
bindingReadyResolve =
|
|
2708
|
+
const bindingReady = new Promise((resolve9, reject) => {
|
|
2709
|
+
bindingReadyResolve = resolve9;
|
|
1986
2710
|
bindingReadyReject = reject;
|
|
1987
2711
|
});
|
|
1988
2712
|
const devMode = Boolean(config2.devMode);
|
|
@@ -1991,8 +2715,9 @@ async function main(opts = {}) {
|
|
|
1991
2715
|
const runtime = {
|
|
1992
2716
|
client: null,
|
|
1993
2717
|
projectId: null,
|
|
1994
|
-
|
|
1995
|
-
|
|
2718
|
+
repositoryBindingId: null,
|
|
2719
|
+
credentialSource: null,
|
|
2720
|
+
accessTokenFingerprint: null,
|
|
1996
2721
|
authoritativeBinding: null,
|
|
1997
2722
|
bindingCacheRefreshed: false,
|
|
1998
2723
|
ideType: void 0
|
|
@@ -2014,7 +2739,7 @@ async function main(opts = {}) {
|
|
|
2014
2739
|
runtime.bindingCacheRefreshed = reconciled.cacheRefreshed;
|
|
2015
2740
|
if (reconciled.cacheRefreshed) {
|
|
2016
2741
|
console.error(
|
|
2017
|
-
`[memoraone-mcp] refreshed stale cached binding
|
|
2742
|
+
`[memoraone-mcp] refreshed stale cached binding ${reconciled.binding.repositoryBindingId}: project=${reconciled.binding.projectId}`
|
|
2018
2743
|
);
|
|
2019
2744
|
try {
|
|
2020
2745
|
const socketPath = getBindingSocketPath(opts.daemonBindingHint);
|
|
@@ -2120,32 +2845,6 @@ async function main(opts = {}) {
|
|
|
2120
2845
|
})
|
|
2121
2846
|
);
|
|
2122
2847
|
registeredToolNames.push("memora_get_personal_context");
|
|
2123
|
-
server.tool(
|
|
2124
|
-
"memora_list_projects",
|
|
2125
|
-
"List projects available to the current API key",
|
|
2126
|
-
listProjectsShape,
|
|
2127
|
-
async () => runWithSessionContext(sessionContext, async () => {
|
|
2128
|
-
if (!runtime.client || !runtime.projectId) return notInitializedResult;
|
|
2129
|
-
const result = await handleListProjects(runtime.client);
|
|
2130
|
-
return {
|
|
2131
|
-
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
2132
|
-
};
|
|
2133
|
-
})
|
|
2134
|
-
);
|
|
2135
|
-
registeredToolNames.push("memora_list_projects");
|
|
2136
|
-
server.tool(
|
|
2137
|
-
"memora_set_project",
|
|
2138
|
-
"Set the current project key for subsequent tool calls",
|
|
2139
|
-
setProjectShape,
|
|
2140
|
-
async (args) => runWithSessionContext(sessionContext, async () => {
|
|
2141
|
-
if (!runtime.client || !runtime.projectId) return notInitializedResult;
|
|
2142
|
-
const result = await handleSetProject(args);
|
|
2143
|
-
return {
|
|
2144
|
-
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
2145
|
-
};
|
|
2146
|
-
})
|
|
2147
|
-
);
|
|
2148
|
-
registeredToolNames.push("memora_set_project");
|
|
2149
2848
|
server.tool(
|
|
2150
2849
|
"memora_status",
|
|
2151
2850
|
"Return non-secret project binding metadata for this MCP session",
|
|
@@ -2276,23 +2975,31 @@ async function main(opts = {}) {
|
|
|
2276
2975
|
const debugAuth = ["1", "true", "yes", "on"].includes(
|
|
2277
2976
|
String(process.env.MEMORAONE_DEBUG_AUTH ?? "").trim().toLowerCase()
|
|
2278
2977
|
);
|
|
2279
|
-
const
|
|
2978
|
+
const debugLog = config2.devMode || debugAuth;
|
|
2280
2979
|
const binding = await resolveSessionBindingFromInitialize(params);
|
|
2281
|
-
if (opts.daemonBindingHint &&
|
|
2980
|
+
if (opts.daemonBindingHint && opts.daemonBindingHint.repositoryBindingId !== binding.repositoryBindingId) {
|
|
2282
2981
|
const errMsg = formatBindingMismatchError(opts.daemonBindingHint, binding);
|
|
2283
2982
|
console.error(`[memoraone-mcp][ERROR] ${errMsg}`);
|
|
2284
2983
|
bindingReadyReject?.(new Error(errMsg));
|
|
2285
2984
|
throw new Error(errMsg);
|
|
2286
2985
|
}
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
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>"
|
|
2291
2995
|
);
|
|
2292
2996
|
}
|
|
2293
|
-
if (
|
|
2997
|
+
if (debugLog) {
|
|
2998
|
+
console.error("[memoraone-mcp][debug] Resolved installation credentials from OS keyring");
|
|
2999
|
+
}
|
|
3000
|
+
if (binding.legacyM1WarningPath) {
|
|
2294
3001
|
console.error(
|
|
2295
|
-
|
|
3002
|
+
`[memoraone-mcp] warning: ignoring legacy memoraone.m1 at ${binding.legacyM1WarningPath}`
|
|
2296
3003
|
);
|
|
2297
3004
|
}
|
|
2298
3005
|
const projectId = binding.projectId;
|
|
@@ -2300,9 +3007,8 @@ async function main(opts = {}) {
|
|
|
2300
3007
|
if (existing !== null && existing !== projectId) {
|
|
2301
3008
|
if (runtime.bindingCacheRefreshed) {
|
|
2302
3009
|
setBoundProjectId(projectId);
|
|
2303
|
-
setBoundApiKey(apiKeyToUse);
|
|
2304
3010
|
console.error(
|
|
2305
|
-
`[memoraone-mcp] ${sessionLabel} rebound to project ${projectId} after
|
|
3011
|
+
`[memoraone-mcp] ${sessionLabel} rebound to project ${projectId} after local binding refresh (was ${existing})`
|
|
2306
3012
|
);
|
|
2307
3013
|
} else {
|
|
2308
3014
|
const requestedRoot = binding.workspaceRoot ?? workspaceRoot ?? process.cwd();
|
|
@@ -2318,38 +3024,30 @@ async function main(opts = {}) {
|
|
|
2318
3024
|
}
|
|
2319
3025
|
if (existing === null) {
|
|
2320
3026
|
setBoundProjectId(projectId);
|
|
2321
|
-
setBoundApiKey(apiKeyToUse);
|
|
2322
3027
|
console.error(
|
|
2323
3028
|
`[memoraone-mcp] ${sessionLabel} bound to project ${projectId} (Option A: single-project binding)`
|
|
2324
3029
|
);
|
|
2325
3030
|
}
|
|
2326
3031
|
setCurrentProjectId(projectId);
|
|
2327
|
-
setCurrentApiKey(apiKeyToUse);
|
|
2328
3032
|
runtime.projectId = projectId;
|
|
2329
|
-
runtime.
|
|
2330
|
-
runtime.
|
|
3033
|
+
runtime.repositoryBindingId = binding.repositoryBindingId;
|
|
3034
|
+
runtime.credentialSource = "keyring";
|
|
3035
|
+
runtime.accessTokenFingerprint = fingerprintAccessToken(creds.accessToken);
|
|
2331
3036
|
runtime.authoritativeBinding = binding;
|
|
2332
|
-
runtime.client = new memoraClient_default(config2,
|
|
3037
|
+
runtime.client = new memoraClient_default(config2, {
|
|
3038
|
+
repositoryBindingId: binding.repositoryBindingId,
|
|
3039
|
+
projectId
|
|
3040
|
+
});
|
|
2333
3041
|
workspaceRoot = binding.workspaceRoot;
|
|
2334
|
-
const environmentLog = binding.environment !== void 0 ? ` environment=${binding.environment}` : "";
|
|
2335
|
-
process.stderr.write(
|
|
2336
|
-
`[memoraone-mcp] registering workspace source bindingSource=${binding.bindingSource} workspaceRoot=${workspaceRoot ?? "(unset)"} m1Path=${binding.m1Path}${environmentLog}
|
|
2337
|
-
`
|
|
2338
|
-
);
|
|
2339
|
-
await registerRepoSource(
|
|
2340
|
-
runtime.client,
|
|
2341
|
-
runtime.projectId,
|
|
2342
|
-
binding.workspaceRoot,
|
|
2343
|
-
runtime.ideType
|
|
2344
|
-
);
|
|
2345
3042
|
if (debugAuth) {
|
|
2346
3043
|
console.error("[memoraone-mcp][auth] repo root:", binding.workspaceRoot);
|
|
2347
3044
|
console.error("[memoraone-mcp][auth] project_id:", projectId);
|
|
2348
|
-
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");
|
|
2349
3047
|
}
|
|
2350
3048
|
const bindingEnvironmentLog = binding.environment !== void 0 ? ` environment=${binding.environment}` : "";
|
|
2351
3049
|
console.error(
|
|
2352
|
-
`[memoraone-mcp] ${sessionLabel} authoritative binding:
|
|
3050
|
+
`[memoraone-mcp] ${sessionLabel} authoritative binding: binding=${binding.repositoryBindingId} project=${binding.projectId} workspace=${binding.workspaceRoot} source=${binding.bindingSource}${bindingEnvironmentLog}`
|
|
2353
3051
|
);
|
|
2354
3052
|
bindingReadyResolve?.(runtime.client);
|
|
2355
3053
|
return server.server._oninitialize(request);
|
|
@@ -2365,37 +3063,34 @@ async function main(opts = {}) {
|
|
|
2365
3063
|
const transport = opts.transport ?? new import_stdio.StdioServerTransport();
|
|
2366
3064
|
await server.connect(transport);
|
|
2367
3065
|
const activeClient = await bindingReady;
|
|
2368
|
-
let
|
|
3066
|
+
let ownedHeartbeat = null;
|
|
2369
3067
|
const daemonSession = Boolean(opts.sessionSocket);
|
|
2370
3068
|
if (config2.heartbeatEnabled && daemonSession) {
|
|
2371
3069
|
console.error(
|
|
2372
3070
|
`[memoraone-mcp] ${sessionLabel} defers heartbeat to daemon for project ${runtime.projectId}`
|
|
2373
3071
|
);
|
|
2374
|
-
} else if (config2.heartbeatEnabled) {
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
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
|
+
});
|
|
2382
3081
|
console.error(
|
|
2383
|
-
`[memoraone-mcp] ${sessionLabel} owns heartbeat for project ${runtime.projectId}
|
|
3082
|
+
`[memoraone-mcp] ${sessionLabel} owns heartbeat for project ${runtime.projectId}`
|
|
2384
3083
|
);
|
|
2385
|
-
await
|
|
2386
|
-
heartbeatInterval = setInterval(() => {
|
|
2387
|
-
sendProjectHeartbeat(activeClient, heartbeatCtx).catch(() => {
|
|
2388
|
-
});
|
|
2389
|
-
}, intervalMs);
|
|
3084
|
+
await ownedHeartbeat.start();
|
|
2390
3085
|
}
|
|
2391
3086
|
const onSigInt = () => shutdown("SIGINT");
|
|
2392
3087
|
const onSigTerm = () => shutdown("SIGTERM");
|
|
2393
3088
|
const shutdown = (signal, exitProcess = true) => {
|
|
2394
3089
|
process.off("SIGINT", onSigInt);
|
|
2395
3090
|
process.off("SIGTERM", onSigTerm);
|
|
2396
|
-
if (
|
|
2397
|
-
|
|
2398
|
-
|
|
3091
|
+
if (ownedHeartbeat?.isRunning()) {
|
|
3092
|
+
ownedHeartbeat.stop();
|
|
3093
|
+
ownedHeartbeat = null;
|
|
2399
3094
|
if (runtime.projectId) {
|
|
2400
3095
|
console.error(
|
|
2401
3096
|
`[memoraone-mcp] ${sessionLabel} released session heartbeat for project ${runtime.projectId}`
|
|
@@ -2415,10 +3110,10 @@ async function main(opts = {}) {
|
|
|
2415
3110
|
console.error("[memoraone-mcp] MCP server ready");
|
|
2416
3111
|
}
|
|
2417
3112
|
if (opts.sessionSocket) {
|
|
2418
|
-
await new Promise((
|
|
3113
|
+
await new Promise((resolve9) => {
|
|
2419
3114
|
opts.sessionSocket.once("close", () => {
|
|
2420
3115
|
shutdown("session closed", false);
|
|
2421
|
-
|
|
3116
|
+
resolve9();
|
|
2422
3117
|
});
|
|
2423
3118
|
});
|
|
2424
3119
|
}
|