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