@memoraone/mcp 0.1.34 → 0.1.36

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