@memoraone/mcp 0.1.34 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/dist/cli.cjs +3267 -1585
  2. package/dist/daemon.cjs +1803 -1112
  3. package/dist/index.cjs +1773 -959
  4. package/package.json +3 -2
package/dist/daemon.cjs CHANGED
@@ -32,7 +32,7 @@ __export(daemon_exports, {
32
32
  runDaemon: () => runDaemon
33
33
  });
34
34
  module.exports = __toCommonJS(daemon_exports);
35
- var fs7 = __toESM(require("fs"), 1);
35
+ var fs11 = __toESM(require("fs"), 1);
36
36
  var net = __toESM(require("net"), 1);
37
37
  var import_stdio2 = require("@modelcontextprotocol/sdk/server/stdio.js");
38
38
 
@@ -45,16 +45,18 @@ var fs = __toESM(require("fs"), 1);
45
45
  var crypto = __toESM(require("crypto"), 1);
46
46
  var path = __toESM(require("path"), 1);
47
47
  var BINDING_SOCKET_HASH_LENGTH = 16;
48
- function hashBindingIdentity(projectId, workspaceRoot, ideType) {
48
+ function hashBindingIdentity(repositoryBindingId, workspaceRoot, ideType) {
49
49
  const input = [
50
- projectId.trim().toLowerCase(),
50
+ repositoryBindingId.trim(),
51
51
  path.resolve(workspaceRoot),
52
52
  ideType
53
53
  ].join("|");
54
54
  return crypto.createHash("sha256").update(input).digest("hex").slice(0, BINDING_SOCKET_HASH_LENGTH);
55
55
  }
56
56
  function bindingsMatch(a, b) {
57
- return a.projectId.trim().toLowerCase() === b.projectId.trim().toLowerCase() && path.resolve(a.workspaceRoot) === path.resolve(b.workspaceRoot) && path.resolve(a.m1Path) === path.resolve(b.m1Path);
57
+ const envA = a.environment ?? void 0;
58
+ const envB = b.environment ?? void 0;
59
+ return a.repositoryBindingId === b.repositoryBindingId && a.projectId.trim().toLowerCase() === b.projectId.trim().toLowerCase() && path.resolve(a.workspaceRoot) === path.resolve(b.workspaceRoot) && (a.installationPublicId ?? void 0) === (b.installationPublicId ?? void 0) && envA === envB && a.status === b.status;
58
60
  }
59
61
  function formatMissingInitializeWorkspaceError(options) {
60
62
  const lines = [
@@ -110,7 +112,7 @@ function resolveBindingIdeType(env2 = process.env) {
110
112
  }
111
113
  function getBindingSocketFilename(binding, env2 = process.env) {
112
114
  const ideType = resolveBindingIdeType(env2);
113
- const hash = hashBindingIdentity(binding.projectId, binding.workspaceRoot, ideType);
115
+ const hash = hashBindingIdentity(binding.repositoryBindingId, binding.workspaceRoot, ideType);
114
116
  return `mcp-${hash}.sock`;
115
117
  }
116
118
  function getBindingSocketPath(binding, env2 = process.env) {
@@ -122,9 +124,1118 @@ function ensureBaseDir() {
122
124
  }
123
125
 
124
126
  // src/projectBinding.ts
127
+ var fs7 = __toESM(require("fs/promises"), 1);
128
+ var path9 = __toESM(require("path"), 1);
129
+
130
+ // src/localState/resolveLocalBinding.ts
131
+ var fs6 = __toESM(require("fs/promises"), 1);
132
+ var path8 = __toESM(require("path"), 1);
133
+
134
+ // src/localState/bindingStore.ts
135
+ var fs4 = __toESM(require("fs/promises"), 1);
136
+ var path5 = __toESM(require("path"), 1);
137
+
138
+ // src/localState/atomicFs.ts
125
139
  var fs2 = __toESM(require("fs/promises"), 1);
126
140
  var path3 = __toESM(require("path"), 1);
141
+ var import_node_crypto = require("crypto");
142
+ var STATE_DIR_MODE = 448;
143
+ var STATE_FILE_MODE = 384;
144
+ async function ensurePrivateDir(dirPath) {
145
+ await fs2.mkdir(dirPath, { recursive: true, mode: STATE_DIR_MODE });
146
+ try {
147
+ await fs2.chmod(dirPath, STATE_DIR_MODE);
148
+ } catch {
149
+ }
150
+ }
151
+ async function writeFileAtomic(filePath, content, options = {}) {
152
+ const mode = options.mode ?? STATE_FILE_MODE;
153
+ const dir = path3.dirname(filePath);
154
+ await ensurePrivateDir(dir);
155
+ const tmpPath = path3.join(
156
+ dir,
157
+ `.${path3.basename(filePath)}.${process.pid}.${(0, import_node_crypto.randomBytes)(8).toString("hex")}.tmp`
158
+ );
159
+ try {
160
+ await fs2.writeFile(tmpPath, content, { encoding: "utf8", mode });
161
+ await fs2.rename(tmpPath, filePath);
162
+ try {
163
+ await fs2.chmod(filePath, mode);
164
+ } catch {
165
+ }
166
+ } catch (err) {
167
+ try {
168
+ await fs2.unlink(tmpPath);
169
+ } catch {
170
+ }
171
+ throw err;
172
+ }
173
+ }
174
+ async function readJsonFile(filePath) {
175
+ try {
176
+ const raw = await fs2.readFile(filePath, "utf8");
177
+ return JSON.parse(raw);
178
+ } catch (err) {
179
+ if (err?.code === "ENOENT") {
180
+ return null;
181
+ }
182
+ if (err instanceof SyntaxError) {
183
+ throw new Error(`[memoraone-mcp] Corrupt JSON at ${filePath}`);
184
+ }
185
+ throw err;
186
+ }
187
+ }
188
+ async function writeJsonAtomic(filePath, value, options = {}) {
189
+ await writeFileAtomic(filePath, `${JSON.stringify(value, null, 2)}
190
+ `, options);
191
+ }
192
+
193
+ // src/localState/repositoryBindingId.ts
194
+ var import_node_crypto2 = require("crypto");
195
+ var MRB_PREFIX = "mrb_";
196
+ var MRB_RE = /^mrb_[A-Za-z0-9_-]{43}$/;
197
+ function generateRepositoryBindingId(random = () => (0, import_node_crypto2.randomBytes)(32)) {
198
+ const bytes = random();
199
+ if (bytes.length !== 32) {
200
+ throw new Error("[memoraone-mcp] repository binding id requires exactly 32 random bytes");
201
+ }
202
+ return `${MRB_PREFIX}${bytes.toString("base64url")}`;
203
+ }
204
+ function isRepositoryBindingId(value) {
205
+ return MRB_RE.test(value);
206
+ }
207
+ function assertRepositoryBindingId(value) {
208
+ if (!isRepositoryBindingId(value)) {
209
+ throw new Error(`[memoraone-mcp] Invalid repository_binding_id: ${value}`);
210
+ }
211
+ return value;
212
+ }
213
+
214
+ // src/localState/bindingRecord.ts
215
+ var BINDING_RECORD_VERSION = 1;
216
+ var SECRET_KEYS = [
217
+ "accessToken",
218
+ "refreshToken",
219
+ "access_token",
220
+ "refresh_token",
221
+ "apiKey",
222
+ "api_key",
223
+ "MEMORAONE_API_KEY",
224
+ "clientRedeemKey",
225
+ "client_redeem_key",
226
+ "clientRefreshKey",
227
+ "client_refresh_key",
228
+ "code",
229
+ "connectCode",
230
+ "connect_code"
231
+ ];
232
+ function assertNoSecretsInBindingRecord(record) {
233
+ for (const key of SECRET_KEYS) {
234
+ if (key in record && record[key] != null && record[key] !== "") {
235
+ throw new Error(`[memoraone-mcp] Binding record must not contain secret field: ${key}`);
236
+ }
237
+ }
238
+ }
239
+ function parseBindingRecord(raw) {
240
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
241
+ throw new Error("[memoraone-mcp] Corrupt binding record");
242
+ }
243
+ const obj = raw;
244
+ assertNoSecretsInBindingRecord(obj);
245
+ if (obj.v !== BINDING_RECORD_VERSION) {
246
+ throw new Error(`[memoraone-mcp] Unsupported binding record version: ${String(obj.v)}`);
247
+ }
248
+ const repositoryBindingId = assertRepositoryBindingId(String(obj.repositoryBindingId ?? ""));
249
+ const workspaceRoot = typeof obj.workspaceRoot === "string" ? obj.workspaceRoot : "";
250
+ if (!workspaceRoot) {
251
+ throw new Error("[memoraone-mcp] Binding record missing workspaceRoot");
252
+ }
253
+ const fsId = obj.filesystemIdentity;
254
+ if (!fsId || typeof fsId !== "object" || Array.isArray(fsId)) {
255
+ throw new Error("[memoraone-mcp] Binding record missing filesystemIdentity");
256
+ }
257
+ const identity = fsId;
258
+ const birthtimeMs = Number(identity.birthtimeMs);
259
+ if (typeof identity.platform !== "string" || typeof identity.deviceId !== "string" || typeof identity.inode !== "string" || !Number.isFinite(birthtimeMs) || birthtimeMs <= 0) {
260
+ throw new Error("[memoraone-mcp] Binding record has invalid filesystemIdentity");
261
+ }
262
+ const status = obj.status;
263
+ if (status !== "connected" && status !== "reconnect_required" && status !== "pending") {
264
+ throw new Error("[memoraone-mcp] Binding record has invalid status");
265
+ }
266
+ const record = {
267
+ v: BINDING_RECORD_VERSION,
268
+ repositoryBindingId,
269
+ workspaceRoot,
270
+ filesystemIdentity: {
271
+ platform: identity.platform,
272
+ deviceId: identity.deviceId,
273
+ inode: identity.inode,
274
+ birthtimeMs
275
+ },
276
+ rootFingerprint: typeof obj.rootFingerprint === "string" ? obj.rootFingerprint : "",
277
+ displayName: typeof obj.displayName === "string" ? obj.displayName : "",
278
+ environment: typeof obj.environment === "string" ? obj.environment : "local",
279
+ normalizedGitRemote: obj.normalizedGitRemote === null || typeof obj.normalizedGitRemote === "string" ? obj.normalizedGitRemote : null,
280
+ status,
281
+ createdAt: typeof obj.createdAt === "string" ? obj.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
282
+ updatedAt: typeof obj.updatedAt === "string" ? obj.updatedAt : (/* @__PURE__ */ new Date()).toISOString()
283
+ };
284
+ if (typeof obj.apiUrl === "string" && obj.apiUrl.trim()) {
285
+ record.apiUrl = obj.apiUrl.trim().replace(/\/+$/, "");
286
+ }
287
+ if (typeof obj.installationPublicId === "string" && obj.installationPublicId) {
288
+ record.installationPublicId = obj.installationPublicId;
289
+ }
290
+ if (typeof obj.projectId === "string" && obj.projectId) {
291
+ record.projectId = obj.projectId;
292
+ }
293
+ if (obj.packageVersion === null || typeof obj.packageVersion === "string") {
294
+ record.packageVersion = obj.packageVersion;
295
+ }
296
+ if (obj.ideType === null || typeof obj.ideType === "string") {
297
+ record.ideType = obj.ideType;
298
+ }
299
+ return record;
300
+ }
301
+
302
+ // src/localState/localLocks.ts
303
+ var fs3 = __toESM(require("fs/promises"), 1);
304
+
305
+ // src/localState/statePaths.ts
306
+ var os2 = __toESM(require("os"), 1);
307
+ var path4 = __toESM(require("path"), 1);
308
+ var MEMORAONE_STATE_DIRNAME = ".memoraone";
309
+ function getMemoraoneStateDir(homeDir = os2.homedir()) {
310
+ return path4.join(homeDir, MEMORAONE_STATE_DIRNAME);
311
+ }
312
+ function getPathIndexPath(homeDir = os2.homedir()) {
313
+ return path4.join(getMemoraoneStateDir(homeDir), "path-index.json");
314
+ }
315
+ function getBindingsDir(homeDir = os2.homedir()) {
316
+ return path4.join(getMemoraoneStateDir(homeDir), "bindings");
317
+ }
318
+ function getBindingFilePath(repositoryBindingId, homeDir = os2.homedir()) {
319
+ return path4.join(getBindingsDir(homeDir), `${repositoryBindingId}.json`);
320
+ }
321
+ function getLocksDir(homeDir = os2.homedir()) {
322
+ return path4.join(getMemoraoneStateDir(homeDir), "locks");
323
+ }
324
+ function getLockPath(lockName, homeDir = os2.homedir()) {
325
+ return path4.join(getLocksDir(homeDir), `${lockName}.lock`);
326
+ }
327
+
328
+ // src/localState/localLocks.ts
329
+ async function acquireLocalLock(lockName, options = {}) {
330
+ const homeDir = options.homeDir;
331
+ const maxRetries = options.maxRetries ?? 40;
332
+ const retryDelayMs = options.retryDelayMs ?? 50;
333
+ const maxLockAgeMs = options.maxLockAgeMs ?? 15e3;
334
+ const lockPath = getLockPath(lockName, homeDir);
335
+ await ensurePrivateDir(getLocksDir(homeDir));
336
+ let retries = 0;
337
+ while (retries <= maxRetries) {
338
+ try {
339
+ try {
340
+ const stat3 = await fs3.stat(lockPath);
341
+ if (Date.now() - stat3.mtimeMs > maxLockAgeMs) {
342
+ await fs3.unlink(lockPath);
343
+ }
344
+ } catch (err) {
345
+ if (err?.code !== "ENOENT") {
346
+ throw err;
347
+ }
348
+ }
349
+ const fd = await fs3.open(lockPath, "wx");
350
+ await fd.writeFile(
351
+ JSON.stringify({
352
+ pid: process.pid,
353
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
354
+ }),
355
+ "utf8"
356
+ );
357
+ await fd.close();
358
+ return async () => {
359
+ try {
360
+ await fs3.unlink(lockPath);
361
+ } catch (err) {
362
+ if (err?.code !== "ENOENT") {
363
+ }
364
+ }
365
+ };
366
+ } catch (err) {
367
+ if (err?.code === "EEXIST") {
368
+ retries += 1;
369
+ if (retries > maxRetries) {
370
+ throw new Error(
371
+ `[memoraone-mcp] Failed to acquire lock ${lockName} after ${maxRetries} retries`
372
+ );
373
+ }
374
+ await new Promise((resolve9) => setTimeout(resolve9, retryDelayMs));
375
+ continue;
376
+ }
377
+ throw err;
378
+ }
379
+ }
380
+ throw new Error(`[memoraone-mcp] Failed to acquire lock ${lockName}`);
381
+ }
382
+ async function withLocalLock(lockName, fn, options = {}) {
383
+ const release = await acquireLocalLock(lockName, options);
384
+ try {
385
+ return await fn();
386
+ } finally {
387
+ await release();
388
+ }
389
+ }
390
+
391
+ // src/localState/bindingStore.ts
392
+ async function ensureBindingsDir(homeDir) {
393
+ const dir = getBindingsDir(homeDir);
394
+ await ensurePrivateDir(dir);
395
+ return dir;
396
+ }
397
+ async function readBindingRecord(repositoryBindingId, homeDir) {
398
+ const id = assertRepositoryBindingId(repositoryBindingId);
399
+ const filePath = getBindingFilePath(id, homeDir);
400
+ const raw = await readJsonFile(filePath);
401
+ if (raw == null) {
402
+ return null;
403
+ }
404
+ return parseBindingRecord(raw);
405
+ }
406
+ async function writeBindingRecord(record, homeDir) {
407
+ assertNoSecretsInBindingRecord(record);
408
+ const id = assertRepositoryBindingId(record.repositoryBindingId);
409
+ await ensureBindingsDir(homeDir);
410
+ const next = {
411
+ ...record,
412
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
413
+ };
414
+ await withLocalLock(
415
+ `binding-${id}`,
416
+ async () => {
417
+ await writeJsonAtomic(getBindingFilePath(id, homeDir), next);
418
+ },
419
+ { homeDir }
420
+ );
421
+ }
422
+
423
+ // src/localState/installationCredentials.ts
424
+ var import_node_crypto3 = require("crypto");
425
+
426
+ // src/localState/keyringStore.ts
427
+ var KEYRING_SERVICE = "MemoraOne Local MCP";
428
+ function keyringAccountForBinding(repositoryBindingId) {
429
+ return `binding:${assertRepositoryBindingId(repositoryBindingId)}`;
430
+ }
431
+ var KeyringUnavailableError = class extends Error {
432
+ constructor(message, cause) {
433
+ super(message);
434
+ this.name = "KeyringUnavailableError";
435
+ if (cause !== void 0) {
436
+ this.cause = cause;
437
+ }
438
+ }
439
+ };
440
+ var KeyringOperationError = class extends Error {
441
+ constructor(message, cause) {
442
+ super(message);
443
+ this.name = "KeyringOperationError";
444
+ if (cause !== void 0) {
445
+ this.cause = cause;
446
+ }
447
+ }
448
+ };
449
+ var cachedModule;
450
+ var loadError;
451
+ async function loadKeyringModule(loader = defaultKeyringLoader) {
452
+ if (cachedModule) {
453
+ return cachedModule;
454
+ }
455
+ if (cachedModule === null) {
456
+ throw new KeyringUnavailableError(
457
+ "[memoraone-mcp] OS keyring unavailable; cannot store or load credentials. No plaintext fallback.",
458
+ loadError
459
+ );
460
+ }
461
+ try {
462
+ cachedModule = await loader();
463
+ return cachedModule;
464
+ } catch (err) {
465
+ cachedModule = null;
466
+ loadError = err;
467
+ throw new KeyringUnavailableError(
468
+ "[memoraone-mcp] OS keyring unavailable; cannot store or load credentials. No plaintext fallback.",
469
+ err
470
+ );
471
+ }
472
+ }
473
+ async function defaultKeyringLoader() {
474
+ const mod = await import("@napi-rs/keyring");
475
+ if (!mod?.Entry) {
476
+ throw new Error("Entry export missing from @napi-rs/keyring");
477
+ }
478
+ return mod;
479
+ }
480
+ async function keyringSetPassword(repositoryBindingId, password, options = {}) {
481
+ const mod = await loadKeyringModule(options.loader);
482
+ const account = keyringAccountForBinding(repositoryBindingId);
483
+ try {
484
+ const entry = new mod.Entry(KEYRING_SERVICE, account);
485
+ entry.setPassword(password);
486
+ } catch (err) {
487
+ if (err instanceof KeyringUnavailableError) throw err;
488
+ throw new KeyringOperationError(
489
+ `[memoraone-mcp] Failed to write credentials to OS keyring for ${account}`,
490
+ err
491
+ );
492
+ }
493
+ }
494
+ async function keyringGetPassword(repositoryBindingId, options = {}) {
495
+ const mod = await loadKeyringModule(options.loader);
496
+ const account = keyringAccountForBinding(repositoryBindingId);
497
+ try {
498
+ const entry = new mod.Entry(KEYRING_SERVICE, account);
499
+ return entry.getPassword();
500
+ } catch (err) {
501
+ const message = err instanceof Error ? err.message : String(err);
502
+ if (/NoEntry|not found|no entry/i.test(message)) {
503
+ return null;
504
+ }
505
+ if (err instanceof KeyringUnavailableError) throw err;
506
+ throw new KeyringOperationError(
507
+ `[memoraone-mcp] Failed to read credentials from OS keyring for ${account}`,
508
+ err
509
+ );
510
+ }
511
+ }
512
+
513
+ // src/localState/installationCredentials.ts
514
+ function parsePayload(raw, repositoryBindingId) {
515
+ let parsed2;
516
+ try {
517
+ parsed2 = JSON.parse(raw);
518
+ } catch {
519
+ throw new Error("[memoraone-mcp] Corrupt keyring credential payload");
520
+ }
521
+ if (!parsed2 || typeof parsed2 !== "object" || Array.isArray(parsed2)) {
522
+ throw new Error("[memoraone-mcp] Corrupt keyring credential payload");
523
+ }
524
+ const obj = parsed2;
525
+ const id = assertRepositoryBindingId(String(obj.repositoryBindingId ?? repositoryBindingId));
526
+ if (id !== repositoryBindingId) {
527
+ throw new Error("[memoraone-mcp] Keyring credential payload binding id mismatch");
528
+ }
529
+ const payload = {
530
+ repositoryBindingId: id,
531
+ updatedAt: typeof obj.updatedAt === "string" ? obj.updatedAt : (/* @__PURE__ */ new Date()).toISOString()
532
+ };
533
+ const optionalString = (key) => {
534
+ const value = obj[key];
535
+ if (typeof value === "string" && value.trim() !== "") {
536
+ payload[key] = value;
537
+ }
538
+ };
539
+ optionalString("installationPublicId");
540
+ optionalString("projectId");
541
+ optionalString("accessToken");
542
+ optionalString("refreshToken");
543
+ optionalString("accessTokenExpiresAt");
544
+ optionalString("refreshTokenExpiresAt");
545
+ optionalString("clientRedeemKey");
546
+ optionalString("clientRefreshKey");
547
+ return payload;
548
+ }
549
+ async function readInstallationCredentials(repositoryBindingId, options = {}) {
550
+ const id = assertRepositoryBindingId(repositoryBindingId);
551
+ const raw = await keyringGetPassword(id, options);
552
+ if (raw == null || raw.trim() === "") {
553
+ return null;
554
+ }
555
+ return parsePayload(raw, id);
556
+ }
557
+ async function writeInstallationCredentials(payload, options = {}) {
558
+ const id = assertRepositoryBindingId(payload.repositoryBindingId);
559
+ const next = {
560
+ ...payload,
561
+ repositoryBindingId: id,
562
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
563
+ };
564
+ await keyringSetPassword(id, JSON.stringify(next), options);
565
+ return next;
566
+ }
567
+ async function updateInstallationCredentials(repositoryBindingId, patch, options = {}) {
568
+ const id = assertRepositoryBindingId(repositoryBindingId);
569
+ const existing = await readInstallationCredentials(id, options) ?? {
570
+ repositoryBindingId: id,
571
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
572
+ };
573
+ const merged = {
574
+ ...existing,
575
+ ...Object.fromEntries(
576
+ Object.entries(patch).filter(([, value]) => value !== void 0)
577
+ ),
578
+ repositoryBindingId: id
579
+ };
580
+ for (const key of options.clearKeys ?? []) {
581
+ if (key !== "repositoryBindingId" && key !== "updatedAt") {
582
+ delete merged[key];
583
+ }
584
+ }
585
+ return writeInstallationCredentials(merged, options);
586
+ }
587
+ async function ensureClientRefreshKey(repositoryBindingId, options = {}) {
588
+ const existing = await readInstallationCredentials(repositoryBindingId, options);
589
+ if (existing?.clientRefreshKey) {
590
+ return { payload: existing, clientRefreshKey: existing.clientRefreshKey, created: false };
591
+ }
592
+ const clientRefreshKey = (0, import_node_crypto3.randomUUID)();
593
+ const payload = await updateInstallationCredentials(
594
+ repositoryBindingId,
595
+ { clientRefreshKey },
596
+ options
597
+ );
598
+ return { payload, clientRefreshKey, created: true };
599
+ }
600
+ async function clearClientRefreshKey(repositoryBindingId, options = {}) {
601
+ const existing = await readInstallationCredentials(repositoryBindingId, options);
602
+ if (!existing?.clientRefreshKey) return;
603
+ await updateInstallationCredentials(repositoryBindingId, {}, {
604
+ ...options,
605
+ clearKeys: ["clientRefreshKey"]
606
+ });
607
+ }
608
+ function hasUsableAccessToken(payload) {
609
+ return Boolean(payload?.accessToken && payload.accessToken.startsWith("mia_"));
610
+ }
611
+
612
+ // src/localState/pathIndex.ts
613
+ var path7 = __toESM(require("path"), 1);
614
+
615
+ // src/localState/rootFilesystemIdentity.ts
616
+ var fs5 = __toESM(require("fs/promises"), 1);
617
+ var os3 = __toESM(require("os"), 1);
618
+ var path6 = __toESM(require("path"), 1);
619
+ var import_node_child_process = require("child_process");
620
+ var import_node_util = require("util");
621
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
622
+ function filesystemIdentityKey(identity) {
623
+ return [
624
+ identity.platform,
625
+ identity.deviceId,
626
+ identity.inode,
627
+ String(identity.birthtimeMs)
628
+ ].join("|");
629
+ }
630
+ function identitiesMatch(a, b) {
631
+ return filesystemIdentityKey(a) === filesystemIdentityKey(b);
632
+ }
633
+ async function readDarwinDeviceId() {
634
+ const { stdout } = await execFileAsync("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
635
+ const match = stdout.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
636
+ if (!match?.[1]) {
637
+ throw new Error("[memoraone-mcp] Unable to read macOS IOPlatformUUID");
638
+ }
639
+ return match[1].trim();
640
+ }
641
+ async function readLinuxDeviceId() {
642
+ try {
643
+ const content = await fs5.readFile("/etc/machine-id", "utf8");
644
+ const id = content.trim();
645
+ if (id) return id;
646
+ } catch {
647
+ }
648
+ try {
649
+ const content = await fs5.readFile("/var/lib/dbus/machine-id", "utf8");
650
+ const id = content.trim();
651
+ if (id) return id;
652
+ } catch {
653
+ }
654
+ throw new Error("[memoraone-mcp] Unable to read Linux machine-id");
655
+ }
656
+ async function readWindowsDeviceId() {
657
+ const { stdout } = await execFileAsync("reg", [
658
+ "query",
659
+ "HKLM\\SOFTWARE\\Microsoft\\Cryptography",
660
+ "/v",
661
+ "MachineGuid"
662
+ ]);
663
+ const match = stdout.match(/MachineGuid\s+REG_SZ\s+(.+)/i);
664
+ if (!match?.[1]) {
665
+ throw new Error("[memoraone-mcp] Unable to read Windows MachineGuid");
666
+ }
667
+ return match[1].trim();
668
+ }
669
+ async function resolveDeviceId(platform2 = os3.platform()) {
670
+ if (platform2 === "darwin") return readDarwinDeviceId();
671
+ if (platform2 === "linux") return readLinuxDeviceId();
672
+ if (platform2 === "win32") return readWindowsDeviceId();
673
+ try {
674
+ const { stdout } = await execFileAsync("hostid", []);
675
+ const id = stdout.trim();
676
+ if (id) return id;
677
+ } catch {
678
+ }
679
+ throw new Error(`[memoraone-mcp] Unsupported platform for device ID: ${platform2}`);
680
+ }
681
+ async function captureRootFilesystemIdentity(rootPath, deps = {}) {
682
+ const resolved = path6.resolve(rootPath);
683
+ const platform2 = deps.platform ?? os3.platform();
684
+ const statRoot = deps.statRoot ?? (async (p) => {
685
+ const st2 = await fs5.stat(p);
686
+ return {
687
+ ino: st2.ino,
688
+ dev: st2.dev,
689
+ birthtimeMs: st2.birthtimeMs,
690
+ isDirectory: () => st2.isDirectory()
691
+ };
692
+ });
693
+ const readDeviceId = deps.readDeviceId ?? (() => resolveDeviceId(platform2));
694
+ const st = await statRoot(resolved);
695
+ if (!st.isDirectory()) {
696
+ throw new Error(`[memoraone-mcp] Workspace root is not a directory: ${resolved}`);
697
+ }
698
+ const birthtimeMs = Number(st.birthtimeMs);
699
+ if (!Number.isFinite(birthtimeMs) || birthtimeMs <= 0) {
700
+ throw new Error(
701
+ "[memoraone-mcp] Root birth time unavailable; cannot bind this working tree. Reconnect required."
702
+ );
703
+ }
704
+ const inode = typeof st.ino === "bigint" ? st.ino.toString() : String(st.ino);
705
+ const deviceId = await readDeviceId();
706
+ if (!deviceId || !inode) {
707
+ throw new Error(
708
+ "[memoraone-mcp] Ambiguous filesystem identity; cannot bind this working tree. Reconnect required."
709
+ );
710
+ }
711
+ return {
712
+ platform: platform2,
713
+ deviceId,
714
+ inode,
715
+ birthtimeMs
716
+ };
717
+ }
718
+
719
+ // src/localState/pathIndex.ts
720
+ var PATH_INDEX_VERSION = 1;
721
+ function emptyIndex() {
722
+ return {
723
+ v: PATH_INDEX_VERSION,
724
+ byPath: {},
725
+ byIdentity: {},
726
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
727
+ };
728
+ }
729
+ function parsePathIndex(raw) {
730
+ if (raw == null) {
731
+ return emptyIndex();
732
+ }
733
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
734
+ throw new Error("[memoraone-mcp] Corrupt path-index.json");
735
+ }
736
+ const obj = raw;
737
+ if (obj.v !== PATH_INDEX_VERSION) {
738
+ throw new Error(`[memoraone-mcp] Unsupported path-index version: ${String(obj.v)}`);
739
+ }
740
+ const byPath = obj.byPath && typeof obj.byPath === "object" && !Array.isArray(obj.byPath) ? obj.byPath : {};
741
+ const byIdentity = obj.byIdentity && typeof obj.byIdentity === "object" && !Array.isArray(obj.byIdentity) ? obj.byIdentity : {};
742
+ return {
743
+ v: PATH_INDEX_VERSION,
744
+ byPath: { ...byPath },
745
+ byIdentity: { ...byIdentity },
746
+ updatedAt: typeof obj.updatedAt === "string" ? obj.updatedAt : (/* @__PURE__ */ new Date()).toISOString()
747
+ };
748
+ }
749
+ async function loadPathIndex(homeDir) {
750
+ const filePath = getPathIndexPath(homeDir);
751
+ try {
752
+ const raw = await readJsonFile(filePath);
753
+ return parsePathIndex(raw);
754
+ } catch (err) {
755
+ if (err instanceof Error && err.message.includes("Corrupt")) {
756
+ throw err;
757
+ }
758
+ throw err;
759
+ }
760
+ }
761
+ async function savePathIndex(index, homeDir) {
762
+ const next = {
763
+ ...index,
764
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
765
+ };
766
+ await writeJsonAtomic(getPathIndexPath(homeDir), next);
767
+ }
768
+ function lookupPathIndex(index, workspaceRoot, identity) {
769
+ const resolved = path7.resolve(workspaceRoot);
770
+ const byPathId = index.byPath[resolved];
771
+ if (byPathId) {
772
+ return { kind: "path", repositoryBindingId: assertRepositoryBindingId(byPathId) };
773
+ }
774
+ const identityKey = filesystemIdentityKey(identity);
775
+ const byIdentityId = index.byIdentity[identityKey];
776
+ if (!byIdentityId) {
777
+ return { kind: "none" };
778
+ }
779
+ const previousPath = Object.entries(index.byPath).find(([, id]) => id === byIdentityId)?.[0];
780
+ if (!previousPath) {
781
+ return {
782
+ kind: "identity-rename",
783
+ repositoryBindingId: assertRepositoryBindingId(byIdentityId),
784
+ previousPath: resolved
785
+ };
786
+ }
787
+ return {
788
+ kind: "identity-rename",
789
+ repositoryBindingId: assertRepositoryBindingId(byIdentityId),
790
+ previousPath
791
+ };
792
+ }
793
+ async function upsertPathIndexEntry(options) {
794
+ const repositoryBindingId = assertRepositoryBindingId(options.repositoryBindingId);
795
+ const resolved = path7.resolve(options.workspaceRoot);
796
+ const identityKey = filesystemIdentityKey(options.identity);
797
+ return withLocalLock(
798
+ "path-index",
799
+ async () => {
800
+ const index = await loadPathIndex(options.homeDir);
801
+ if (options.previousPath && path7.resolve(options.previousPath) !== resolved) {
802
+ delete index.byPath[path7.resolve(options.previousPath)];
803
+ }
804
+ for (const [p, id] of Object.entries(index.byPath)) {
805
+ if (id === repositoryBindingId && p !== resolved) {
806
+ delete index.byPath[p];
807
+ }
808
+ }
809
+ for (const [key, id] of Object.entries(index.byIdentity)) {
810
+ if (id === repositoryBindingId && key !== identityKey) {
811
+ delete index.byIdentity[key];
812
+ }
813
+ }
814
+ index.byPath[resolved] = repositoryBindingId;
815
+ index.byIdentity[identityKey] = repositoryBindingId;
816
+ await savePathIndex(index, options.homeDir);
817
+ return index;
818
+ },
819
+ { homeDir: options.homeDir }
820
+ );
821
+ }
822
+ function identityMatchesStored(stored, current) {
823
+ return identitiesMatch(stored, current);
824
+ }
825
+
826
+ // src/client/memoraClient.ts
827
+ var crypto2 = __toESM(require("crypto"), 1);
828
+ var PROJECT_ID_HEADER = "x-project-id";
127
829
  var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
830
+ var parseBooleanFlag = (value) => {
831
+ if (!value) {
832
+ return false;
833
+ }
834
+ const normalized = value.trim().toLowerCase();
835
+ return ["1", "true", "yes", "on"].includes(normalized);
836
+ };
837
+ var debugEnabled = parseBooleanFlag(process.env.MEMORAONE_DEV_MODE);
838
+ async function requestJson(url, method, headers, body) {
839
+ const res = await fetch(url, {
840
+ method,
841
+ headers,
842
+ body: method === "GET" ? void 0 : JSON.stringify(body ?? {})
843
+ });
844
+ const text = await res.text();
845
+ return {
846
+ status: res.status,
847
+ statusText: res.statusText,
848
+ ok: res.ok,
849
+ text
850
+ };
851
+ }
852
+ var MemoraOneHttpError = class extends Error {
853
+ constructor(status, statusText, body) {
854
+ super(`MemoraOne request failed: ${status} ${statusText}`);
855
+ this.name = "MemoraOneHttpError";
856
+ this.status = status;
857
+ this.body = body;
858
+ }
859
+ };
860
+ var MemoraClient = class {
861
+ constructor(cfg, auth) {
862
+ if (!uuidRegex.test(auth.projectId)) {
863
+ throw new Error("[memoraone-mcp] Invalid project_id for MemoraClient");
864
+ }
865
+ this.baseUrl = cfg.apiUrl;
866
+ this.projectId = auth.projectId;
867
+ this.repositoryBindingId = auth.repositoryBindingId;
868
+ this.credentialOptions = auth.credentialOptions;
869
+ this.homeDir = auth.homeDir;
870
+ this.getAccessTokenOverride = auth.getAccessToken;
871
+ this.refreshAccessTokenOverride = auth.refreshAccessToken;
872
+ }
873
+ getRepositoryBindingId() {
874
+ return this.repositoryBindingId;
875
+ }
876
+ resolveProjectId() {
877
+ const projectId = this.projectId?.trim();
878
+ if (!projectId) {
879
+ throw new Error(`Missing ${PROJECT_ID_HEADER}: select a project first`);
880
+ }
881
+ if (!uuidRegex.test(projectId)) {
882
+ throw new Error("[memoraone-mcp] Invalid project_id for request");
883
+ }
884
+ return projectId;
885
+ }
886
+ async resolveAccessToken() {
887
+ if (this.getAccessTokenOverride) {
888
+ return this.getAccessTokenOverride();
889
+ }
890
+ const creds = await readInstallationCredentials(
891
+ this.repositoryBindingId,
892
+ this.credentialOptions
893
+ );
894
+ const token = creds?.accessToken?.trim();
895
+ if (!token || !token.startsWith("mia_")) {
896
+ throw new ReconnectRequiredError(
897
+ "[memoraone-mcp] Missing installation access token. Run: memoraone-mcp connect <code>"
898
+ );
899
+ }
900
+ return token;
901
+ }
902
+ async refreshAccessToken() {
903
+ if (this.refreshAccessTokenOverride) {
904
+ return this.refreshAccessTokenOverride();
905
+ }
906
+ return refreshInstallationAccessToken({
907
+ apiUrl: this.baseUrl,
908
+ repositoryBindingId: this.repositoryBindingId,
909
+ homeDir: this.homeDir,
910
+ ...this.credentialOptions
911
+ });
912
+ }
913
+ async markReconnectRequired() {
914
+ try {
915
+ const record = await readBindingRecord(this.repositoryBindingId, this.homeDir);
916
+ if (record) {
917
+ await writeBindingRecord(
918
+ { ...record, status: "reconnect_required", updatedAt: (/* @__PURE__ */ new Date()).toISOString() },
919
+ this.homeDir
920
+ );
921
+ }
922
+ } catch {
923
+ }
924
+ }
925
+ async buildHeaders(options) {
926
+ const projectId = this.resolveProjectId();
927
+ const accessToken = await this.resolveAccessToken();
928
+ return {
929
+ "content-type": "application/json",
930
+ Authorization: `Bearer ${accessToken}`,
931
+ [PROJECT_ID_HEADER]: projectId,
932
+ ...options?.headers ?? {}
933
+ };
934
+ }
935
+ async perform(method, path14, body, options, retried = false) {
936
+ const nonce = crypto2.randomBytes(8).toString("hex");
937
+ const url = `${this.baseUrl}${path14.startsWith("/") ? path14 : `/${path14}`}`;
938
+ this.resolveProjectId();
939
+ console.error(
940
+ `[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_fetch method=${method} url=${url}`
941
+ );
942
+ const headers = await this.buildHeaders(options);
943
+ delete headers["x-api-key"];
944
+ const res = await requestJson(url, method, headers, body);
945
+ if (debugEnabled && options?.log !== false) {
946
+ const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
947
+ console.error(
948
+ `[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_response_log`
949
+ );
950
+ const line = `[memoraone-mcp][info] http response method=${method} url=${url} status=${res.status} body=${snippet}`;
951
+ console.error(line);
952
+ console.error(
953
+ `[memoraone-mcp][info] requestJson nonce=${nonce} stage=after_response_log`
954
+ );
955
+ }
956
+ if (!res.ok) {
957
+ const accepted = options?.acceptStatuses?.includes(res.status);
958
+ if (accepted) {
959
+ return res.text ? JSON.parse(res.text) : null;
960
+ }
961
+ if ((res.status === 401 || res.status === 403) && !options?.skipAuthRefresh && !retried) {
962
+ try {
963
+ await this.refreshAccessToken();
964
+ return this.perform(method, path14, body, options, true);
965
+ } catch (err) {
966
+ if (err instanceof ReconnectRequiredError) {
967
+ await this.markReconnectRequired();
968
+ }
969
+ throw err;
970
+ }
971
+ }
972
+ if (res.status === 401 || res.status === 403) {
973
+ await this.markReconnectRequired();
974
+ }
975
+ const quiet = options?.quietHttpStatuses?.includes(res.status);
976
+ if (!quiet) {
977
+ const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
978
+ process.stderr.write(
979
+ `[memoraone-mcp][error] http error method=${method} url=${url} status=${res.status} body=${snippet}
980
+ `
981
+ );
982
+ }
983
+ throw new MemoraOneHttpError(res.status, res.statusText, res.text);
984
+ }
985
+ return res.text ? JSON.parse(res.text) : null;
986
+ }
987
+ async post(path14, body, options) {
988
+ console.error(`[memoraone-mcp][info] MemoraClient.post ENTER path=${path14}`);
989
+ const result = await this.perform("POST", path14, body, options);
990
+ console.error(`[memoraone-mcp][info] MemoraClient.post EXIT path=${path14}`);
991
+ return result;
992
+ }
993
+ async get(path14, options) {
994
+ return this.perform("GET", path14, void 0, options);
995
+ }
996
+ };
997
+ var memoraClient_default = MemoraClient;
998
+
999
+ // src/localState/localConnectClient.ts
1000
+ async function requestJson2(baseUrl, method, path14, options = {}) {
1001
+ const fetchImpl = options.fetchImpl ?? fetch;
1002
+ const url = `${baseUrl.replace(/\/+$/, "")}${path14.startsWith("/") ? path14 : `/${path14}`}`;
1003
+ const res = await fetchImpl(url, {
1004
+ method,
1005
+ headers: {
1006
+ "content-type": "application/json",
1007
+ ...options.headers ?? {}
1008
+ },
1009
+ body: method === "GET" ? void 0 : JSON.stringify(options.body ?? {})
1010
+ });
1011
+ const text = await res.text();
1012
+ let json = null;
1013
+ if (text) {
1014
+ try {
1015
+ json = JSON.parse(text);
1016
+ } catch {
1017
+ json = text;
1018
+ }
1019
+ }
1020
+ if (!res.ok && !options.acceptStatuses?.includes(res.status)) {
1021
+ throw new MemoraOneHttpError(res.status, res.statusText, json);
1022
+ }
1023
+ return { status: res.status, statusText: res.statusText, ok: res.ok, json };
1024
+ }
1025
+ async function refreshLocalMcpToken(apiUrl, body, options = {}) {
1026
+ const res = await requestJson2(apiUrl, "POST", "/v1/local-mcp/token/refresh", {
1027
+ body,
1028
+ fetchImpl: options.fetchImpl
1029
+ });
1030
+ const data = res.json;
1031
+ if (typeof data?.access_token !== "string") {
1032
+ throw new Error("[memoraone-mcp] Invalid refresh response");
1033
+ }
1034
+ return {
1035
+ access_token: data.access_token,
1036
+ refresh_token: typeof data.refresh_token === "string" ? data.refresh_token : void 0,
1037
+ access_token_expires_at: typeof data.access_token_expires_at === "string" ? data.access_token_expires_at : null,
1038
+ refresh_token_expires_at: typeof data.refresh_token_expires_at === "string" ? data.refresh_token_expires_at : null,
1039
+ installation_public_id: typeof data.installation_public_id === "string" ? data.installation_public_id : void 0,
1040
+ project_id: typeof data.project_id === "string" ? data.project_id : void 0,
1041
+ repository_binding_id: typeof data.repository_binding_id === "string" ? data.repository_binding_id : void 0
1042
+ };
1043
+ }
1044
+
1045
+ // src/localState/tokenRefreshCoordinator.ts
1046
+ var ReconnectRequiredError = class extends Error {
1047
+ constructor(message) {
1048
+ super(message);
1049
+ this.name = "ReconnectRequiredError";
1050
+ }
1051
+ };
1052
+ async function refreshInstallationAccessToken(options) {
1053
+ const { repositoryBindingId, apiUrl, homeDir } = options;
1054
+ return withLocalLock(
1055
+ `refresh-${repositoryBindingId}`,
1056
+ async () => {
1057
+ const latest = await readInstallationCredentials(repositoryBindingId, options);
1058
+ if (latest?.accessToken?.startsWith("mia_")) {
1059
+ const expiresAt = latest.accessTokenExpiresAt ? Date.parse(latest.accessTokenExpiresAt) : NaN;
1060
+ if (!Number.isFinite(expiresAt) || expiresAt - Date.now() > 3e4) {
1061
+ }
1062
+ }
1063
+ if (!latest?.refreshToken?.startsWith("mir_")) {
1064
+ throw new ReconnectRequiredError(
1065
+ "[memoraone-mcp] Installation credentials missing refresh token. Run: memoraone-mcp connect <code>"
1066
+ );
1067
+ }
1068
+ const { clientRefreshKey } = await ensureClientRefreshKey(repositoryBindingId, options);
1069
+ try {
1070
+ const refreshed = await refreshLocalMcpToken(
1071
+ apiUrl,
1072
+ {
1073
+ refresh_token: latest.refreshToken,
1074
+ client_refresh_key: clientRefreshKey
1075
+ },
1076
+ { fetchImpl: options.fetchImpl }
1077
+ );
1078
+ await updateInstallationCredentials(
1079
+ repositoryBindingId,
1080
+ {
1081
+ accessToken: refreshed.access_token,
1082
+ refreshToken: refreshed.refresh_token ?? latest.refreshToken,
1083
+ accessTokenExpiresAt: refreshed.access_token_expires_at ?? void 0,
1084
+ refreshTokenExpiresAt: refreshed.refresh_token_expires_at ?? void 0,
1085
+ installationPublicId: refreshed.installation_public_id ?? latest.installationPublicId,
1086
+ projectId: refreshed.project_id ?? latest.projectId,
1087
+ clientRefreshKey: void 0
1088
+ },
1089
+ options
1090
+ );
1091
+ await clearClientRefreshKey(repositoryBindingId, options);
1092
+ return refreshed.access_token;
1093
+ } catch (err) {
1094
+ if (err instanceof MemoraOneHttpError && (err.status === 401 || err.status === 403)) {
1095
+ throw new ReconnectRequiredError(
1096
+ "[memoraone-mcp] Installation revoked or refresh rejected. Run: memoraone-mcp connect <code>"
1097
+ );
1098
+ }
1099
+ throw err;
1100
+ }
1101
+ },
1102
+ { homeDir }
1103
+ );
1104
+ }
1105
+
1106
+ // src/localState/resolveLocalBinding.ts
1107
+ async function detectLegacyM1Warning(workspaceRoot) {
1108
+ const candidate = path8.join(path8.resolve(workspaceRoot), CANONICAL_M1_FILENAME);
1109
+ try {
1110
+ await fs6.access(candidate);
1111
+ return candidate;
1112
+ } catch {
1113
+ return void 0;
1114
+ }
1115
+ }
1116
+ async function ensureRepositoryBindingForRoot(workspaceRoot, options = {}) {
1117
+ const resolved = path8.resolve(workspaceRoot);
1118
+ const identity = await captureRootFilesystemIdentity(resolved, options.identityDeps);
1119
+ const legacyM1WarningPath = await detectLegacyM1Warning(resolved);
1120
+ const index = await loadPathIndex(options.homeDir);
1121
+ const lookup = lookupPathIndex(index, resolved, identity);
1122
+ if (lookup.kind === "path") {
1123
+ const record = await readBindingRecord(lookup.repositoryBindingId, options.homeDir);
1124
+ if (record && identityMatchesStored(record.filesystemIdentity, identity)) {
1125
+ if (path8.resolve(record.workspaceRoot) !== resolved) {
1126
+ const updated = {
1127
+ ...record,
1128
+ workspaceRoot: resolved,
1129
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1130
+ };
1131
+ await writeBindingRecord(updated, options.homeDir);
1132
+ await upsertPathIndexEntry({
1133
+ repositoryBindingId: record.repositoryBindingId,
1134
+ workspaceRoot: resolved,
1135
+ identity,
1136
+ homeDir: options.homeDir,
1137
+ previousPath: record.workspaceRoot
1138
+ });
1139
+ }
1140
+ return {
1141
+ repositoryBindingId: record.repositoryBindingId,
1142
+ identity,
1143
+ created: false,
1144
+ legacyM1WarningPath
1145
+ };
1146
+ }
1147
+ }
1148
+ if (lookup.kind === "identity-rename") {
1149
+ const record = await readBindingRecord(lookup.repositoryBindingId, options.homeDir);
1150
+ if (record && identityMatchesStored(record.filesystemIdentity, identity)) {
1151
+ const updated = {
1152
+ ...record,
1153
+ workspaceRoot: resolved,
1154
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1155
+ };
1156
+ await writeBindingRecord(updated, options.homeDir);
1157
+ await upsertPathIndexEntry({
1158
+ repositoryBindingId: record.repositoryBindingId,
1159
+ workspaceRoot: resolved,
1160
+ identity,
1161
+ homeDir: options.homeDir,
1162
+ previousPath: lookup.previousPath
1163
+ });
1164
+ return {
1165
+ repositoryBindingId: record.repositoryBindingId,
1166
+ identity,
1167
+ created: false,
1168
+ renamedFrom: lookup.previousPath,
1169
+ legacyM1WarningPath
1170
+ };
1171
+ }
1172
+ }
1173
+ if (!options.createIfMissing) {
1174
+ throw new ReconnectRequiredError(
1175
+ `[memoraone-mcp] No local binding for workspace ${resolved}. Run: memoraone-mcp connect <code>`
1176
+ );
1177
+ }
1178
+ const repositoryBindingId = generateRepositoryBindingId();
1179
+ return {
1180
+ repositoryBindingId,
1181
+ identity,
1182
+ created: true,
1183
+ legacyM1WarningPath
1184
+ };
1185
+ }
1186
+ async function resolveLocalBinding(workspaceRoot, options = {}) {
1187
+ const resolved = path8.resolve(workspaceRoot);
1188
+ const { repositoryBindingId, legacyM1WarningPath } = await ensureRepositoryBindingForRoot(
1189
+ resolved,
1190
+ { ...options, createIfMissing: false }
1191
+ );
1192
+ const record = await readBindingRecord(repositoryBindingId, options.homeDir);
1193
+ if (!record) {
1194
+ throw new ReconnectRequiredError(
1195
+ `[memoraone-mcp] Binding metadata missing for ${repositoryBindingId}. Run: memoraone-mcp connect <code>`
1196
+ );
1197
+ }
1198
+ const identity = await captureRootFilesystemIdentity(resolved, options.identityDeps);
1199
+ if (!identityMatchesStored(record.filesystemIdentity, identity)) {
1200
+ throw new ReconnectRequiredError(
1201
+ "[memoraone-mcp] Workspace filesystem identity changed. Run: memoraone-mcp connect <code>"
1202
+ );
1203
+ }
1204
+ if (record.status === "reconnect_required") {
1205
+ throw new ReconnectRequiredError(
1206
+ "[memoraone-mcp] Installation requires reconnect. Run: memoraone-mcp connect <code>"
1207
+ );
1208
+ }
1209
+ const creds = await readInstallationCredentials(
1210
+ repositoryBindingId,
1211
+ options.credentialOptions
1212
+ );
1213
+ if (!hasUsableAccessToken(creds) || !creds?.refreshToken) {
1214
+ throw new ReconnectRequiredError(
1215
+ "[memoraone-mcp] Installation credentials missing. Run: memoraone-mcp connect <code>"
1216
+ );
1217
+ }
1218
+ const projectId = record.projectId ?? creds.projectId;
1219
+ if (!projectId) {
1220
+ throw new ReconnectRequiredError(
1221
+ "[memoraone-mcp] Binding missing project id. Run: memoraone-mcp connect <code>"
1222
+ );
1223
+ }
1224
+ return {
1225
+ repositoryBindingId,
1226
+ projectId,
1227
+ workspaceRoot: resolved,
1228
+ installationPublicId: record.installationPublicId ?? creds.installationPublicId,
1229
+ environment: record.environment,
1230
+ bindingSource: "local-binding",
1231
+ status: record.status,
1232
+ legacyM1WarningPath
1233
+ };
1234
+ }
1235
+
1236
+ // src/projectBinding.ts
1237
+ var uuidRegex2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1238
+ var CANONICAL_M1_FILENAME = "memoraone.m1";
128
1239
  function normalizeEnvironment(raw) {
129
1240
  if (raw === void 0 || raw === null || typeof raw !== "string") {
130
1241
  return void 0;
@@ -132,63 +1243,66 @@ function normalizeEnvironment(raw) {
132
1243
  const trimmed = raw.trim();
133
1244
  return trimmed === "" ? void 0 : trimmed;
134
1245
  }
135
- function parseAndValidateM1(content, markerPath) {
136
- let parsed2;
1246
+ function toResolvedBinding(local) {
1247
+ return {
1248
+ repositoryBindingId: local.repositoryBindingId,
1249
+ projectId: local.projectId,
1250
+ workspaceRoot: local.workspaceRoot,
1251
+ installationPublicId: local.installationPublicId,
1252
+ environment: local.environment,
1253
+ bindingSource: "local-binding",
1254
+ status: local.status,
1255
+ legacyM1WarningPath: local.legacyM1WarningPath
1256
+ };
1257
+ }
1258
+ async function warnLegacyM1IfPresent(workspaceRoot) {
1259
+ const candidate = path9.join(path9.resolve(workspaceRoot), CANONICAL_M1_FILENAME);
137
1260
  try {
138
- parsed2 = JSON.parse(content);
1261
+ await fs7.access(candidate);
1262
+ process.stderr.write(
1263
+ `[memoraone-mcp] warning: ignoring legacy ${CANONICAL_M1_FILENAME} (not used for credentials or binding)
1264
+ `
1265
+ );
1266
+ return candidate;
139
1267
  } catch {
140
- throw new Error(`[memoraone-mcp] Invalid memoraone.m1 JSON at ${markerPath}`);
141
- }
142
- const projectId = parsed2?.projectId ?? parsed2?.project_id;
143
- if (!projectId || typeof projectId !== "string") {
144
- throw new Error(`[memoraone-mcp] memoraone.m1 missing projectId at ${markerPath}`);
145
- }
146
- if (!uuidRegex.test(projectId.trim())) {
147
- throw new Error(`[memoraone-mcp] memoraone.m1 projectId is not a UUID at ${markerPath}`);
1268
+ return void 0;
148
1269
  }
149
- const apiKeyRaw = parsed2?.MEMORAONE_API_KEY ?? parsed2?.api_key;
150
- const apiKey = apiKeyRaw !== void 0 && apiKeyRaw !== null && typeof apiKeyRaw === "string" && apiKeyRaw.trim() !== "" ? apiKeyRaw.trim() : null;
151
- const environment = normalizeEnvironment(parsed2?.environment);
152
- return environment === void 0 ? { projectId: projectId.trim(), apiKey } : { projectId: projectId.trim(), apiKey, environment };
153
1270
  }
154
- async function resolveProjectIdFromExplicitM1Path() {
155
- const raw = process.env.MEMORAONE_M1_PATH;
156
- if (raw === void 0 || raw.trim() === "") {
157
- return null;
158
- }
159
- const markerPath = path3.resolve(raw);
160
- try {
161
- const content = await fs2.readFile(markerPath, "utf8");
162
- const { projectId, apiKey, environment } = parseAndValidateM1(content, markerPath);
163
- return environment === void 0 ? { projectId, apiKey, foundAt: markerPath } : { projectId, apiKey, environment, foundAt: markerPath };
164
- } catch (err) {
165
- if (err?.code === "ENOENT") {
166
- return null;
167
- }
168
- throw err;
1271
+ async function resolveAuthoritativeBinding(workspaceRoot, _options = {}) {
1272
+ const candidates = normalizeWorkspaceSearchRoots(workspaceRoot);
1273
+ if (candidates.length === 0) {
1274
+ throw new ReconnectRequiredError(
1275
+ "[memoraone-mcp] Could not resolve workspace root. Open a connected repository folder.\nRun: memoraone-mcp connect <code>"
1276
+ );
169
1277
  }
170
- }
171
- async function findM1WalkingUp(workspaceRoot) {
172
- let current = path3.resolve(workspaceRoot);
173
- while (true) {
174
- const markerPath = path3.join(current, "memoraone.m1");
1278
+ const bindings = [];
1279
+ for (const root of candidates) {
1280
+ await warnLegacyM1IfPresent(root);
175
1281
  try {
176
- const content = await fs2.readFile(markerPath, "utf8");
177
- const { projectId, apiKey, environment } = parseAndValidateM1(content, markerPath);
178
- const repoRoot = path3.dirname(markerPath);
179
- return environment === void 0 ? { projectId, apiKey, repoRoot, markerPath } : { projectId, apiKey, environment, repoRoot, markerPath };
1282
+ const local = await resolveLocalBinding(root);
1283
+ bindings.push(toResolvedBinding(local));
180
1284
  } catch (err) {
181
- if (err?.code !== "ENOENT") {
182
- throw err;
1285
+ if (err instanceof ReconnectRequiredError) {
1286
+ continue;
183
1287
  }
1288
+ throw err;
184
1289
  }
185
- const parent = path3.dirname(current);
186
- if (parent === current) {
187
- break;
188
- }
189
- current = parent;
190
1290
  }
191
- return null;
1291
+ if (bindings.length === 0) {
1292
+ throw new ReconnectRequiredError(
1293
+ "[memoraone-mcp] No local MemoraOne binding for this workspace.\nRun: memoraone-mcp connect <code>"
1294
+ );
1295
+ }
1296
+ const distinctIds = new Set(bindings.map((b) => b.repositoryBindingId));
1297
+ if (distinctIds.size > 1) {
1298
+ const lines = bindings.map(
1299
+ (b) => ` - workspace=${b.workspaceRoot} binding=${b.repositoryBindingId} project=${b.projectId}`
1300
+ );
1301
+ throw new Error(
1302
+ "[memoraone-mcp] Ambiguous workspace binding: multiple open roots map to different repository bindings.\n" + lines.join("\n") + "\nOpen one repo per window."
1303
+ );
1304
+ }
1305
+ return bindings[0];
192
1306
  }
193
1307
  function normalizeWorkspaceSearchRoots(workspaceRoot) {
194
1308
  if (workspaceRoot === void 0) {
@@ -198,14 +1312,10 @@ function normalizeWorkspaceSearchRoots(workspaceRoot) {
198
1312
  const seen = /* @__PURE__ */ new Set();
199
1313
  const out = [];
200
1314
  for (const raw of list) {
201
- if (raw === void 0) {
202
- continue;
203
- }
1315
+ if (raw === void 0) continue;
204
1316
  const trimmed = String(raw).trim();
205
- if (trimmed === "") {
206
- continue;
207
- }
208
- const resolved = path3.resolve(trimmed);
1317
+ if (trimmed === "") continue;
1318
+ const resolved = path9.resolve(trimmed);
209
1319
  if (!seen.has(resolved)) {
210
1320
  seen.add(resolved);
211
1321
  out.push(resolved);
@@ -213,73 +1323,56 @@ function normalizeWorkspaceSearchRoots(workspaceRoot) {
213
1323
  }
214
1324
  return out;
215
1325
  }
216
- function resolveApiKeyWithSource(fileApiKey) {
217
- const envApiKey = process.env.MEMORAONE_API_KEY?.trim();
218
- if (envApiKey) {
219
- return { apiKey: envApiKey, apiKeySource: "env" };
220
- }
221
- const aliasEnvApiKey = process.env.MEMORA_API_KEY?.trim();
222
- if (aliasEnvApiKey) {
223
- return { apiKey: aliasEnvApiKey, apiKeySource: "env" };
224
- }
225
- if (fileApiKey) {
226
- return { apiKey: fileApiKey, apiKeySource: "memoraone.m1" };
227
- }
228
- return { apiKey: null, apiKeySource: "none" };
229
- }
230
- async function resolveAuthoritativeBinding(workspaceRoot, options = {}) {
231
- const respectExplicitM1Path = options.respectExplicitM1Path !== false;
232
- if (respectExplicitM1Path) {
233
- const explicitBinding = await resolveProjectIdFromExplicitM1Path();
234
- if (explicitBinding) {
235
- const resolved = resolveApiKeyWithSource(explicitBinding.apiKey);
236
- return {
237
- projectId: explicitBinding.projectId,
238
- workspaceRoot: path3.dirname(explicitBinding.foundAt),
239
- m1Path: explicitBinding.foundAt,
240
- apiKey: resolved.apiKey,
241
- ...explicitBinding.environment !== void 0 ? { environment: explicitBinding.environment } : {},
242
- bindingSource: "explicit-m1-path",
243
- apiKeySource: resolved.apiKeySource
244
- };
245
- }
246
- }
247
- const candidates = normalizeWorkspaceSearchRoots(workspaceRoot);
248
- if (candidates.length === 0) {
249
- throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
250
- }
251
- const bindings = [];
252
- for (const root of candidates) {
253
- const binding = await findM1WalkingUp(root);
254
- if (binding) {
255
- const resolved = resolveApiKeyWithSource(binding.apiKey);
256
- bindings.push({
257
- projectId: binding.projectId,
258
- workspaceRoot: binding.repoRoot,
259
- m1Path: binding.markerPath,
260
- apiKey: resolved.apiKey,
261
- ...binding.environment !== void 0 ? { environment: binding.environment } : {},
262
- bindingSource: "workspace-search",
263
- apiKeySource: resolved.apiKeySource
264
- });
265
- }
266
- }
267
- if (bindings.length === 0) {
268
- throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
1326
+ function bindingRelevantValuesMatch(a, b) {
1327
+ const envA = a.environment ?? void 0;
1328
+ const envB = b.environment ?? void 0;
1329
+ 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;
1330
+ }
1331
+ async function reconcileResolvedBindingWithDisk(cached) {
1332
+ const repositoryBindingId = assertRepositoryBindingId(cached.repositoryBindingId);
1333
+ const record = await readBindingRecord(repositoryBindingId);
1334
+ if (!record) {
1335
+ throw new ReconnectRequiredError(
1336
+ `[memoraone-mcp] Cached binding missing for ${repositoryBindingId}. Run: memoraone-mcp connect <code>`
1337
+ );
269
1338
  }
270
- const distinctProjectIds = new Set(bindings.map((b) => b.projectId));
271
- if (distinctProjectIds.size > 1) {
272
- const lines = bindings.map(
273
- (b) => ` - workspace=${b.workspaceRoot} project=${b.projectId} m1=${b.m1Path}`
1339
+ const workspaceRoot = path9.resolve(record.workspaceRoot);
1340
+ const identity = await captureRootFilesystemIdentity(workspaceRoot);
1341
+ if (!identitiesMatch(record.filesystemIdentity, identity)) {
1342
+ throw new ReconnectRequiredError(
1343
+ "[memoraone-mcp] Workspace filesystem identity changed. Run: memoraone-mcp connect <code>"
274
1344
  );
275
- throw new Error(
276
- "[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."
1345
+ }
1346
+ if (record.status === "reconnect_required" || !record.projectId) {
1347
+ throw new ReconnectRequiredError(
1348
+ "[memoraone-mcp] Installation requires reconnect. Run: memoraone-mcp connect <code>"
277
1349
  );
278
1350
  }
279
- return bindings[0];
1351
+ const fresh = {
1352
+ repositoryBindingId,
1353
+ projectId: record.projectId,
1354
+ workspaceRoot,
1355
+ installationPublicId: record.installationPublicId,
1356
+ environment: record.environment,
1357
+ bindingSource: "local-binding",
1358
+ status: record.status
1359
+ };
1360
+ if (bindingRelevantValuesMatch(cached, fresh)) {
1361
+ return { binding: fresh, cacheRefreshed: false };
1362
+ }
1363
+ return { binding: fresh, cacheRefreshed: true };
280
1364
  }
281
1365
  function encodeResolvedBinding(binding) {
282
- return Buffer.from(JSON.stringify(binding), "utf8").toString("base64");
1366
+ const payload = {
1367
+ repositoryBindingId: binding.repositoryBindingId,
1368
+ projectId: binding.projectId,
1369
+ workspaceRoot: binding.workspaceRoot,
1370
+ installationPublicId: binding.installationPublicId,
1371
+ environment: binding.environment,
1372
+ bindingSource: binding.bindingSource,
1373
+ status: binding.status
1374
+ };
1375
+ return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
283
1376
  }
284
1377
  function decodeResolvedBinding(value) {
285
1378
  if (!value) {
@@ -291,45 +1384,45 @@ function decodeResolvedBinding(value) {
291
1384
  } catch {
292
1385
  throw new Error("[memoraone-mcp] Invalid encoded binding payload");
293
1386
  }
1387
+ if (parsed2?.apiKey || parsed2?.accessToken || parsed2?.refreshToken || parsed2?.m1Path) {
1388
+ throw new Error(
1389
+ "[memoraone-mcp] Rejected legacy daemon binding payload containing secrets or .m1 path"
1390
+ );
1391
+ }
1392
+ const repositoryBindingId = parsed2?.repositoryBindingId;
294
1393
  const projectId = parsed2?.projectId;
295
1394
  const workspaceRoot = parsed2?.workspaceRoot;
296
- const m1Path = parsed2?.m1Path;
297
- const apiKey = parsed2?.apiKey;
298
1395
  const environment = normalizeEnvironment(parsed2?.environment);
299
- const bindingSource = parsed2?.bindingSource;
300
- const apiKeySource = parsed2?.apiKeySource;
301
- if (!projectId || typeof projectId !== "string" || !uuidRegex.test(projectId.trim())) {
1396
+ const status = parsed2?.status;
1397
+ if (!repositoryBindingId || !isRepositoryBindingId(String(repositoryBindingId))) {
1398
+ throw new Error("[memoraone-mcp] Invalid binding repositoryBindingId");
1399
+ }
1400
+ if (!projectId || typeof projectId !== "string" || !uuidRegex2.test(projectId.trim())) {
302
1401
  throw new Error("[memoraone-mcp] Invalid binding projectId");
303
1402
  }
304
1403
  if (!workspaceRoot || typeof workspaceRoot !== "string") {
305
1404
  throw new Error("[memoraone-mcp] Invalid binding workspaceRoot");
306
1405
  }
307
- if (!m1Path || typeof m1Path !== "string") {
308
- throw new Error("[memoraone-mcp] Invalid binding m1Path");
309
- }
310
- if (apiKey !== null && apiKey !== void 0 && typeof apiKey !== "string") {
311
- throw new Error("[memoraone-mcp] Invalid binding apiKey");
1406
+ if (status !== "connected" && status !== "reconnect_required" && status !== "pending") {
1407
+ throw new Error("[memoraone-mcp] Invalid binding status");
312
1408
  }
313
- if (bindingSource !== "explicit-m1-path" && bindingSource !== "workspace-search") {
1409
+ if (parsed2?.bindingSource !== "local-binding") {
314
1410
  throw new Error("[memoraone-mcp] Invalid binding source");
315
1411
  }
316
- if (apiKeySource !== "env" && apiKeySource !== "memoraone.m1" && apiKeySource !== "none") {
317
- throw new Error("[memoraone-mcp] Invalid binding apiKeySource");
318
- }
319
1412
  return {
1413
+ repositoryBindingId: String(repositoryBindingId),
320
1414
  projectId: projectId.trim(),
321
1415
  workspaceRoot,
322
- m1Path,
323
- apiKey: typeof apiKey === "string" && apiKey.trim() !== "" ? apiKey.trim() : null,
1416
+ installationPublicId: typeof parsed2?.installationPublicId === "string" ? parsed2.installationPublicId : void 0,
324
1417
  ...environment !== void 0 ? { environment } : {},
325
- bindingSource,
326
- apiKeySource
1418
+ bindingSource: "local-binding",
1419
+ status
327
1420
  };
328
1421
  }
329
1422
 
330
1423
  // src/bindingSidecar.ts
331
- var fs3 = __toESM(require("fs"), 1);
332
- var path4 = __toESM(require("path"), 1);
1424
+ var fs8 = __toESM(require("fs"), 1);
1425
+ var path10 = __toESM(require("path"), 1);
333
1426
  function bindingSidecarPath(socketPath) {
334
1427
  if (socketPath.endsWith(".sock")) {
335
1428
  return `${socketPath.slice(0, -".sock".length)}.binding.json`;
@@ -338,19 +1431,26 @@ function bindingSidecarPath(socketPath) {
338
1431
  }
339
1432
  function writeBindingSidecar(socketPath, binding, ideType = resolveBindingIdeType()) {
340
1433
  const payload = encodeResolvedBinding(binding);
1434
+ if (/accessToken|refreshToken|apiKey|mia_|mir_|sk_/.test(payload)) {
1435
+ throw new Error("[memoraone-mcp] Refusing to write sidecar containing secrets");
1436
+ }
341
1437
  const record = {
342
- v: 2,
1438
+ v: 3,
343
1439
  ...ideType ? { ideType } : {},
1440
+ repositoryBindingId: binding.repositoryBindingId,
344
1441
  projectId: binding.projectId,
345
1442
  workspaceRoot: binding.workspaceRoot,
346
- m1Path: binding.m1Path,
347
1443
  binding: payload
348
1444
  };
349
- fs3.writeFileSync(bindingSidecarPath(socketPath), JSON.stringify(record), "utf8");
1445
+ const text = JSON.stringify(record);
1446
+ if (/["']?(accessToken|refreshToken|apiKey|clientRedeemKey|clientRefreshKey)["']?\s*:/.test(text)) {
1447
+ throw new Error("[memoraone-mcp] Refusing to write sidecar containing secrets");
1448
+ }
1449
+ fs8.writeFileSync(bindingSidecarPath(socketPath), text, "utf8");
350
1450
  }
351
1451
  function removeBindingSidecar(socketPath) {
352
1452
  try {
353
- fs3.unlinkSync(bindingSidecarPath(socketPath));
1453
+ fs8.unlinkSync(bindingSidecarPath(socketPath));
354
1454
  } catch {
355
1455
  }
356
1456
  }
@@ -362,8 +1462,8 @@ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
362
1462
 
363
1463
  // src/config.ts
364
1464
  var process2 = __toESM(require("process"), 1);
365
- var fs4 = __toESM(require("fs"), 1);
366
- var path5 = __toESM(require("path"), 1);
1465
+ var fs9 = __toESM(require("fs"), 1);
1466
+ var path11 = __toESM(require("path"), 1);
367
1467
  var dotenv = __toESM(require("dotenv"), 1);
368
1468
  var import_v4 = require("zod/v4");
369
1469
 
@@ -386,8 +1486,8 @@ function resolveApiUrl(env2) {
386
1486
  }
387
1487
 
388
1488
  // src/config.ts
389
- var dotenvPath = path5.resolve(process2.cwd(), ".env");
390
- if (fs4.existsSync(dotenvPath)) {
1489
+ var dotenvPath = path11.resolve(process2.cwd(), ".env");
1490
+ if (fs9.existsSync(dotenvPath)) {
391
1491
  try {
392
1492
  dotenv.config({ path: dotenvPath });
393
1493
  } catch (err) {
@@ -427,7 +1527,7 @@ if (!parsed.success) {
427
1527
  );
428
1528
  throw new Error("Config validation failed");
429
1529
  }
430
- var parseBooleanFlag = (value, defaultValue) => {
1530
+ var parseBooleanFlag2 = (value, defaultValue) => {
431
1531
  if (value === void 0) {
432
1532
  return defaultValue;
433
1533
  }
@@ -447,158 +1547,14 @@ var config2 = {
447
1547
  agentType: parsed.data.MEMORAONE_AGENT_TYPE ?? "agent",
448
1548
  source: parsed.data.MEMORAONE_SOURCE ?? "cursor",
449
1549
  ideType: parsed.data.MEMORAONE_IDE_TYPE,
450
- devMode: parseBooleanFlag(parsed.data.MEMORAONE_DEV_MODE, false),
451
- worklogEnabled: parseBooleanFlag(parsed.data.MEMORAONE_WORKLOG, true),
452
- heartbeatEnabled: parseBooleanFlag(parsed.data.MEMORAONE_HEARTBEAT, true),
1550
+ devMode: parseBooleanFlag2(parsed.data.MEMORAONE_DEV_MODE, false),
1551
+ worklogEnabled: parseBooleanFlag2(parsed.data.MEMORAONE_WORKLOG, true),
1552
+ heartbeatEnabled: parseBooleanFlag2(parsed.data.MEMORAONE_HEARTBEAT, true),
453
1553
  heartbeatIntervalMs: Number.parseInt(parsed.data.MEMORAONE_HEARTBEAT_INTERVAL_MS ?? "30000", 10)
454
1554
  };
455
1555
 
456
- // src/client/memoraClient.ts
457
- var crypto2 = __toESM(require("crypto"), 1);
458
- var PROJECT_ID_HEADER = "x-project-id";
459
- var uuidRegex2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
460
- var parseBooleanFlag2 = (value) => {
461
- if (!value) {
462
- return false;
463
- }
464
- const normalized = value.trim().toLowerCase();
465
- return ["1", "true", "yes", "on"].includes(normalized);
466
- };
467
- var debugEnabled = parseBooleanFlag2(process.env.MEMORAONE_DEV_MODE);
468
- async function requestJson(url, method, headers, body) {
469
- const res = await fetch(url, {
470
- method,
471
- headers,
472
- body: method === "GET" ? void 0 : JSON.stringify(body ?? {})
473
- });
474
- const text = await res.text();
475
- return {
476
- status: res.status,
477
- statusText: res.statusText,
478
- ok: res.ok,
479
- text
480
- };
481
- }
482
- var MemoraOneHttpError = class extends Error {
483
- constructor(status, statusText, body) {
484
- super(`MemoraOne request failed: ${status} ${statusText}`);
485
- this.name = "MemoraOneHttpError";
486
- this.status = status;
487
- this.body = body;
488
- }
489
- };
490
- var MemoraClient = class {
491
- constructor(cfg, projectId, apiKey) {
492
- if (!uuidRegex2.test(projectId)) {
493
- throw new Error("[memoraone-mcp] Invalid project_id for MemoraClient");
494
- }
495
- this.baseUrl = cfg.apiUrl;
496
- this.apiKey = apiKey;
497
- this.projectId = projectId;
498
- }
499
- resolveProjectId() {
500
- const projectId = this.projectId?.trim();
501
- if (!projectId) {
502
- throw new Error(`Missing ${PROJECT_ID_HEADER}: select a project first`);
503
- }
504
- if (!uuidRegex2.test(projectId)) {
505
- throw new Error("[memoraone-mcp] Invalid project_id for request");
506
- }
507
- return projectId;
508
- }
509
- resolveApiKey() {
510
- const key = this.apiKey?.trim();
511
- if (!key) {
512
- throw new Error("[memoraone-mcp] Missing api_key for request");
513
- }
514
- return key;
515
- }
516
- buildHeaders(options) {
517
- const projectId = this.resolveProjectId();
518
- const apiKey = this.resolveApiKey();
519
- return {
520
- "content-type": "application/json",
521
- "x-api-key": apiKey,
522
- [PROJECT_ID_HEADER]: projectId,
523
- ...options?.headers ?? {}
524
- };
525
- }
526
- async post(path10, body, options) {
527
- console.error(
528
- `[memoraone-mcp][info] MemoraClient.post ENTER path=${path10}`
529
- );
530
- const nonce = crypto2.randomBytes(8).toString("hex");
531
- const url = `${this.baseUrl}${path10.startsWith("/") ? path10 : `/${path10}`}`;
532
- this.resolveProjectId();
533
- console.error(
534
- `[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_fetch method=POST url=${url}`
535
- );
536
- const res = await requestJson(url, "POST", this.buildHeaders(options), body);
537
- if (debugEnabled && options?.log !== false) {
538
- const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
539
- console.error(
540
- `[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_response_log`
541
- );
542
- const line = `[memoraone-mcp][info] http response method=POST url=${url} status=${res.status} body=${snippet}`;
543
- console.error(line);
544
- console.error(
545
- `[memoraone-mcp][info] requestJson nonce=${nonce} stage=after_response_log`
546
- );
547
- }
548
- if (!res.ok) {
549
- const accepted = options?.acceptStatuses?.includes(res.status);
550
- if (accepted) {
551
- return res.text ? JSON.parse(res.text) : null;
552
- }
553
- const quiet = options?.quietHttpStatuses?.includes(res.status);
554
- if (!quiet) {
555
- const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
556
- process.stderr.write(
557
- `[memoraone-mcp][error] http error method=POST url=${url} status=${res.status} body=${snippet}
558
- `
559
- );
560
- }
561
- throw new MemoraOneHttpError(res.status, res.statusText, res.text);
562
- }
563
- console.error(
564
- `[memoraone-mcp][info] MemoraClient.post EXIT path=${path10}`
565
- );
566
- return res.text ? JSON.parse(res.text) : null;
567
- }
568
- async get(path10, options) {
569
- const nonce = crypto2.randomBytes(8).toString("hex");
570
- const url = `${this.baseUrl}${path10.startsWith("/") ? path10 : `/${path10}`}`;
571
- this.resolveProjectId();
572
- console.error(
573
- `[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_fetch method=GET url=${url}`
574
- );
575
- const res = await requestJson(url, "GET", this.buildHeaders(options));
576
- if (debugEnabled && options?.log !== false) {
577
- const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
578
- console.error(
579
- `[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_response_log`
580
- );
581
- const line = `[memoraone-mcp][info] http response method=GET url=${url} status=${res.status} body=${snippet}`;
582
- console.error(line);
583
- console.error(
584
- `[memoraone-mcp][info] requestJson nonce=${nonce} stage=after_response_log`
585
- );
586
- }
587
- if (!res.ok) {
588
- const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
589
- process.stderr.write(
590
- `[memoraone-mcp][error] http error method=GET url=${url} status=${res.status} body=${snippet}
591
- `
592
- );
593
- throw new MemoraOneHttpError(res.status, res.statusText, res.text);
594
- }
595
- return res.text ? JSON.parse(res.text) : null;
596
- }
597
- };
598
- var memoraClient_default = MemoraClient;
599
-
600
1556
  // src/initializeBinding.ts
601
- var path6 = __toESM(require("path"), 1);
1557
+ var path12 = __toESM(require("path"), 1);
602
1558
  var import_node_url = require("url");
603
1559
  var MEMORAONE_WORKSPACE_ROOT_ENV = "MEMORAONE_WORKSPACE_ROOT";
604
1560
  function getBridgeBindingResolveOptions(env2 = process.env) {
@@ -618,8 +1574,8 @@ function getEnvWorkspaceRootCandidates() {
618
1574
  const raw = process.env.WORKSPACE_FOLDER_PATHS;
619
1575
  const parts = [];
620
1576
  if (raw !== void 0 && raw.trim() !== "") {
621
- for (const p of raw.split(path6.delimiter).map((s) => s.trim()).filter(Boolean)) {
622
- parts.push(path6.resolve(p));
1577
+ for (const p of raw.split(path12.delimiter).map((s) => s.trim()).filter(Boolean)) {
1578
+ parts.push(path12.resolve(p));
623
1579
  }
624
1580
  }
625
1581
  parts.push(process.cwd());
@@ -643,7 +1599,7 @@ function extractWorkspaceRootsFromInitialize(params) {
643
1599
  if (uri === void 0 || uri.trim() === "") {
644
1600
  return;
645
1601
  }
646
- const resolved = path6.resolve(uriToPath(uri));
1602
+ const resolved = path12.resolve(uriToPath(uri));
647
1603
  if (!seen.has(resolved)) {
648
1604
  seen.add(resolved);
649
1605
  roots.push(resolved);
@@ -665,11 +1621,11 @@ function getRepoScopedWorkspaceHint(env2 = process.env) {
665
1621
  if (raw === void 0 || raw.trim() === "") {
666
1622
  return null;
667
1623
  }
668
- return path6.resolve(raw.trim());
1624
+ return path12.resolve(raw.trim());
669
1625
  }
670
1626
  function formatWorkspaceAmbiguityError(bindings) {
671
1627
  const lines = bindings.map(
672
- (b) => ` - workspace=${b.workspaceRoot} project=${b.projectId} m1=${b.m1Path}`
1628
+ (b) => ` - workspace=${b.workspaceRoot} binding=${b.repositoryBindingId} project=${b.projectId}`
673
1629
  );
674
1630
  return "[memoraone-mcp] Ambiguous workspace binding: multiple open roots map to different MemoraOne projects.\n" + lines.join("\n") + `
675
1631
  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.`;
@@ -677,7 +1633,7 @@ Open one repo per Cursor window, or ensure this repo's managed .cursor/mcp.json
677
1633
  function formatRepoHintInitializeMismatchError(repoHintRoot, initializeBinding) {
678
1634
  return `[memoraone-mcp] Repo-scoped workspace hint conflicts with MCP initialize workspace.
679
1635
  ${MEMORAONE_WORKSPACE_ROOT_ENV}: ${repoHintRoot}
680
- initialize: project=${initializeBinding.projectId} workspace=${initializeBinding.workspaceRoot} m1=${initializeBinding.m1Path}
1636
+ initialize: binding=${initializeBinding.repositoryBindingId} project=${initializeBinding.projectId} workspace=${initializeBinding.workspaceRoot}
681
1637
  Reload MCP in the Cursor window for this repo, or re-run setup-ide-files --cursor here if the hint is stale.`;
682
1638
  }
683
1639
  function formatRepoHintNotInRootsListError(repoHintRoot, rootsListPaths) {
@@ -688,13 +1644,16 @@ Reload MCP in the Cursor window for this repo, or re-run setup-ide-files --curso
688
1644
  }
689
1645
  function formatBindingMismatchError(daemonHint, sessionBinding) {
690
1646
  return `[memoraone-mcp] Project binding mismatch between daemon and MCP initialize workspace.
691
- daemon: project=${daemonHint.projectId} workspace=${daemonHint.workspaceRoot} m1=${daemonHint.m1Path}
692
- initialize: project=${sessionBinding.projectId} workspace=${sessionBinding.workspaceRoot} m1=${sessionBinding.m1Path}
693
- Reconnect or reload the MCP server in this IDE window so the bridge can bind to the correct project.`;
1647
+ daemon: binding=${daemonHint.repositoryBindingId} project=${daemonHint.projectId} workspace=${daemonHint.workspaceRoot}
1648
+ initialize: binding=${sessionBinding.repositoryBindingId} project=${sessionBinding.projectId} workspace=${sessionBinding.workspaceRoot}
1649
+ Reconnect or reload the MCP server in this IDE window so the bridge can bind to the correct project.
1650
+ If this workspace is not connected, run: memoraone-mcp connect <code>`;
694
1651
  }
695
1652
  async function resolveBindingFromWorkspaceRoots(workspaceRoots, options = {}) {
696
1653
  if (workspaceRoots.length === 0) {
697
- throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
1654
+ throw new Error(
1655
+ "[memoraone-mcp] No local MemoraOne binding for this workspace.\nRun: memoraone-mcp connect <code>"
1656
+ );
698
1657
  }
699
1658
  const bindings = [];
700
1659
  for (const root of workspaceRoots) {
@@ -705,14 +1664,16 @@ async function resolveBindingFromWorkspaceRoots(workspaceRoots, options = {}) {
705
1664
  })
706
1665
  );
707
1666
  } catch (err) {
708
- if (err instanceof Error && err.message.includes("Could not find memoraone.m1")) {
1667
+ if (err instanceof Error && (err.message.includes("No local MemoraOne binding") || err.message.includes("memoraone-mcp connect") || err.name === "ReconnectRequiredError")) {
709
1668
  continue;
710
1669
  }
711
1670
  throw err;
712
1671
  }
713
1672
  }
714
1673
  if (bindings.length === 0) {
715
- throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
1674
+ throw new Error(
1675
+ "[memoraone-mcp] No local MemoraOne binding for this workspace.\nRun: memoraone-mcp connect <code>"
1676
+ );
716
1677
  }
717
1678
  const distinctProjectIds = new Set(bindings.map((b) => b.projectId));
718
1679
  if (distinctProjectIds.size > 1) {
@@ -743,8 +1704,8 @@ async function resolveBindingFromInitializeParams(params, options = {}) {
743
1704
  const rootsListUris = options.rootsListUris ?? [];
744
1705
  const rootsListPaths = rootsListUris.map((uri) => uriToPath(uri)).filter(Boolean);
745
1706
  if (rootsListPaths.length > 1 && repoHint !== null) {
746
- const hintResolved = path6.resolve(repoHint);
747
- const matchingRoot = rootsListPaths.find((root) => path6.resolve(root) === hintResolved);
1707
+ const hintResolved = path12.resolve(repoHint);
1708
+ const matchingRoot = rootsListPaths.find((root) => path12.resolve(root) === hintResolved);
748
1709
  if (!matchingRoot) {
749
1710
  throw new Error(formatRepoHintNotInRootsListError(repoHint, rootsListPaths));
750
1711
  }
@@ -777,76 +1738,6 @@ function isInitializeDebugEnabled(env2 = process.env) {
777
1738
  return TRUTHY.has(String(env2.MEMORAONE_DEBUG_INIT ?? "").trim().toLowerCase());
778
1739
  }
779
1740
 
780
- // src/sourceRegistration.ts
781
- var path7 = __toESM(require("path"), 1);
782
- var import_node_url2 = require("url");
783
- var LOG_PREFIX = "[memoraone-mcp][source-registration]";
784
- function buildRepoSourcePayload(normalizedRepoPath, ideType) {
785
- const body = {
786
- kind: "repo",
787
- label: path7.basename(normalizedRepoPath),
788
- uri: (0, import_node_url2.pathToFileURL)(normalizedRepoPath).href
789
- };
790
- if (ideType) {
791
- body.metadata = {
792
- ide_type: ideType
793
- };
794
- }
795
- return body;
796
- }
797
- async function registerRepoSource(client, projectId, repoPath, ideType) {
798
- try {
799
- if (repoPath === void 0 || repoPath === null || String(repoPath).trim() === "") {
800
- process.stderr.write(
801
- `${LOG_PREFIX} skipped: empty repoPath (cannot register workspace)
802
- `
803
- );
804
- return;
805
- }
806
- const normalizedRepoPath = path7.resolve(String(repoPath));
807
- const body = buildRepoSourcePayload(normalizedRepoPath, ideType);
808
- const primaryPath = `/v1/projects/${projectId}/sources`;
809
- const alternatePath = `/v1/projects/${projectId}/sources/register`;
810
- process.stderr.write(
811
- `${LOG_PREFIX} registering projectId=${projectId} path=${normalizedRepoPath} ideType=${ideType ?? "(none)"}
812
- `
813
- );
814
- try {
815
- await client.post(primaryPath, body, {
816
- acceptStatuses: [409],
817
- log: false,
818
- quietHttpStatuses: [404, 405, 501]
819
- });
820
- process.stderr.write(`${LOG_PREFIX} ok: POST ${primaryPath}
821
- `);
822
- return;
823
- } catch (err) {
824
- if (err instanceof MemoraOneHttpError && (err.status === 404 || err.status === 405 || err.status === 501)) {
825
- process.stderr.write(
826
- `${LOG_PREFIX} primary route returned ${err.status}; retrying POST ${alternatePath}
827
- `
828
- );
829
- await client.post(alternatePath, body, { acceptStatuses: [409], log: false });
830
- process.stderr.write(`${LOG_PREFIX} ok: POST ${alternatePath}
831
- `);
832
- return;
833
- }
834
- throw err;
835
- }
836
- } catch (err) {
837
- const msg = String(err?.message ?? err);
838
- process.stderr.write(`${LOG_PREFIX} failed: ${msg}
839
- `);
840
- if (err instanceof MemoraOneHttpError) {
841
- const bodyStr = typeof err.body === "string" ? err.body : JSON.stringify(err.body ?? null);
842
- process.stderr.write(
843
- `${LOG_PREFIX} http status=${err.status} body=${bodyStr.length > 500 ? bodyStr.slice(0, 500) + "..." : bodyStr}
844
- `
845
- );
846
- }
847
- }
848
- }
849
-
850
1741
  // src/tools/postEvent.ts
851
1742
  var import_v42 = require("zod/v4");
852
1743
  var postEventDescription = 'Append a durable project-change note to the MemoraOne timeline. Use after meaningful repository or project changes: decisions, fixes, new endpoints, schema changes, migrations, important wiring, or durable product behavior changes \u2014 not for trivial edits, formatting-only changes, or temporary WIP. Recommended shape: kind "note"; content.title (concise title); content.body (one durable, fact-promotable project-change statement); metadata.source (agent name, e.g. "cursor"); metadata.purpose "dev-log"; metadata.schema "v1".';
@@ -943,26 +1834,16 @@ var logCommandShape = {
943
1834
  summary: import_v410.z.string().min(1),
944
1835
  cwd: import_v410.z.string().min(1).optional(),
945
1836
  exit_code: import_v410.z.number().int().optional(),
946
- duration_ms: import_v410.z.number().int().nonnegative().optional(),
947
- run_id: import_v410.z.string().min(1).optional(),
948
- stats: import_v410.z.record(import_v410.z.string(), import_v410.z.any()).optional()
949
- };
950
-
951
- // src/tools/listProjects.ts
952
- var listProjectsShape = {};
953
-
954
- // src/tools/setProject.ts
955
- var import_v411 = require("zod/v4");
956
- var setProjectShape = {
957
- projectKey: import_v411.z.string().min(1).optional(),
958
- projectId: import_v411.z.string().min(1).optional()
1837
+ duration_ms: import_v410.z.number().int().nonnegative().optional(),
1838
+ run_id: import_v410.z.string().min(1).optional(),
1839
+ stats: import_v410.z.record(import_v410.z.string(), import_v410.z.any()).optional()
959
1840
  };
960
1841
 
961
1842
  // src/tools/bindingStatus.ts
962
1843
  var bindingStatusShape = {};
963
1844
 
964
1845
  // src/tools/handlers/postEvent.ts
965
- var import_v412 = require("zod/v4");
1846
+ var import_v411 = require("zod/v4");
966
1847
  var crypto4 = __toESM(require("crypto"), 1);
967
1848
 
968
1849
  // src/runContext.ts
@@ -995,9 +1876,6 @@ function getBoundProjectId() {
995
1876
  function setBoundProjectId(id) {
996
1877
  getSessionContext().boundProjectId = id;
997
1878
  }
998
- function setBoundApiKey(key) {
999
- getSessionContext().boundApiKey = key;
1000
- }
1001
1879
  function getCurrentRunId() {
1002
1880
  return getSessionContext().currentRunId;
1003
1881
  }
@@ -1010,9 +1888,6 @@ function getCurrentProjectId() {
1010
1888
  function setCurrentProjectId(id) {
1011
1889
  getSessionContext().currentProjectId = id;
1012
1890
  }
1013
- function setCurrentApiKey(key) {
1014
- getSessionContext().currentApiKey = key;
1015
- }
1016
1891
  function resolveRunId(passed) {
1017
1892
  if (passed) {
1018
1893
  return passed;
@@ -1024,14 +1899,14 @@ function generateRunId() {
1024
1899
  }
1025
1900
 
1026
1901
  // src/tools/handlers/postEvent.ts
1027
- var postEventInputSchema = import_v412.z.object({
1028
- kind: import_v412.z.string().min(1),
1029
- actor: import_v412.z.object({
1030
- identifier: import_v412.z.string().min(1),
1031
- id: import_v412.z.string().min(1).optional()
1902
+ var postEventInputSchema = import_v411.z.object({
1903
+ kind: import_v411.z.string().min(1),
1904
+ actor: import_v411.z.object({
1905
+ identifier: import_v411.z.string().min(1),
1906
+ id: import_v411.z.string().min(1).optional()
1032
1907
  }),
1033
- content: import_v412.z.record(import_v412.z.string(), import_v412.z.any()),
1034
- metadata: import_v412.z.record(import_v412.z.string(), import_v412.z.any()).optional()
1908
+ content: import_v411.z.record(import_v411.z.string(), import_v411.z.any()),
1909
+ metadata: import_v411.z.record(import_v411.z.string(), import_v411.z.any()).optional()
1035
1910
  });
1036
1911
  function buildPostEventContentFields(content) {
1037
1912
  if (typeof content.message === "string") {
@@ -1067,7 +1942,9 @@ async function handlePostEvent(client, args) {
1067
1942
  const parsed2 = postEventInputSchema.parse(args ?? {});
1068
1943
  const projectKey = getCurrentProjectId();
1069
1944
  if (!projectKey) {
1070
- throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
1945
+ throw new Error(
1946
+ "No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
1947
+ );
1071
1948
  }
1072
1949
  const content = parsed2.content ?? {};
1073
1950
  const { message, new_value } = buildPostEventContentFields(content);
@@ -1101,16 +1978,18 @@ async function handlePostEvent(client, args) {
1101
1978
  }
1102
1979
 
1103
1980
  // src/tools/handlers/createFact.ts
1104
- var import_v413 = require("zod/v4");
1105
- var createFactInputSchema = import_v413.z.object({
1106
- content: import_v413.z.string().min(1),
1107
- metadata: import_v413.z.record(import_v413.z.string(), import_v413.z.any()).optional()
1981
+ var import_v412 = require("zod/v4");
1982
+ var createFactInputSchema = import_v412.z.object({
1983
+ content: import_v412.z.string().min(1),
1984
+ metadata: import_v412.z.record(import_v412.z.string(), import_v412.z.any()).optional()
1108
1985
  });
1109
1986
  async function handleCreateFact(client, args) {
1110
1987
  const parsed2 = createFactInputSchema.parse(args ?? {});
1111
1988
  const projectKey = getCurrentProjectId();
1112
1989
  if (!projectKey) {
1113
- throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
1990
+ throw new Error(
1991
+ "No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
1992
+ );
1114
1993
  }
1115
1994
  const content = parsed2.content.trim();
1116
1995
  if (!content) {
@@ -1147,13 +2026,13 @@ async function handleCreateFact(client, args) {
1147
2026
  }
1148
2027
 
1149
2028
  // src/tools/handlers/addPersonalContext.ts
1150
- var import_v414 = require("zod/v4");
1151
- var addPersonalContextInputSchema = import_v414.z.object({
1152
- content: import_v414.z.string().min(1),
1153
- category: import_v414.z.string().optional(),
1154
- tags: import_v414.z.array(import_v414.z.string()).optional(),
1155
- scope_type: import_v414.z.enum(["general", "project"]).optional(),
1156
- scope_id: import_v414.z.string().optional()
2029
+ var import_v413 = require("zod/v4");
2030
+ var addPersonalContextInputSchema = import_v413.z.object({
2031
+ content: import_v413.z.string().min(1),
2032
+ category: import_v413.z.string().optional(),
2033
+ tags: import_v413.z.array(import_v413.z.string()).optional(),
2034
+ scope_type: import_v413.z.enum(["general", "project"]).optional(),
2035
+ scope_id: import_v413.z.string().optional()
1157
2036
  });
1158
2037
  async function handleAddPersonalContext(client, args) {
1159
2038
  const parsed2 = addPersonalContextInputSchema.parse(args ?? {});
@@ -1189,12 +2068,12 @@ async function handleAddPersonalContext(client, args) {
1189
2068
  }
1190
2069
 
1191
2070
  // src/tools/handlers/getPersonalContext.ts
1192
- var import_v415 = require("zod/v4");
1193
- var getPersonalContextInputSchema = import_v415.z.object({
1194
- query: import_v415.z.string().optional(),
1195
- scope_type: import_v415.z.enum(["general", "project"]).optional(),
1196
- scope_id: import_v415.z.string().optional(),
1197
- limit: import_v415.z.number().int().positive().optional()
2071
+ var import_v414 = require("zod/v4");
2072
+ var getPersonalContextInputSchema = import_v414.z.object({
2073
+ query: import_v414.z.string().optional(),
2074
+ scope_type: import_v414.z.enum(["general", "project"]).optional(),
2075
+ scope_id: import_v414.z.string().optional(),
2076
+ limit: import_v414.z.number().int().positive().optional()
1198
2077
  });
1199
2078
  function buildPersonalContextPath(parsed2) {
1200
2079
  const params = new URLSearchParams();
@@ -1215,9 +2094,9 @@ function buildPersonalContextPath(parsed2) {
1215
2094
  }
1216
2095
  async function handleGetPersonalContext(client, args) {
1217
2096
  const parsed2 = getPersonalContextInputSchema.parse(args ?? {});
1218
- const path10 = buildPersonalContextPath(parsed2);
2097
+ const path14 = buildPersonalContextPath(parsed2);
1219
2098
  try {
1220
- const result = await client.get(path10);
2099
+ const result = await client.get(path14);
1221
2100
  return { ok: true, result };
1222
2101
  } catch (err) {
1223
2102
  if (err instanceof MemoraOneHttpError) {
@@ -1231,13 +2110,13 @@ async function handleGetPersonalContext(client, args) {
1231
2110
  }
1232
2111
 
1233
2112
  // src/tools/handlers/askWithMemory.ts
1234
- var import_v416 = require("zod/v4");
1235
- var askWithMemoryInputSchema = import_v416.z.object({
1236
- question: import_v416.z.string().min(1),
1237
- code_context: import_v416.z.object({
1238
- file_path: import_v416.z.string().optional(),
1239
- selected_text: import_v416.z.string().optional(),
1240
- language: import_v416.z.string().optional()
2113
+ var import_v415 = require("zod/v4");
2114
+ var askWithMemoryInputSchema = import_v415.z.object({
2115
+ question: import_v415.z.string().min(1),
2116
+ code_context: import_v415.z.object({
2117
+ file_path: import_v415.z.string().optional(),
2118
+ selected_text: import_v415.z.string().optional(),
2119
+ language: import_v415.z.string().optional()
1241
2120
  }).optional()
1242
2121
  });
1243
2122
  function isAskWithMemoryResponse(value) {
@@ -1247,7 +2126,9 @@ async function handleAskWithMemory(client, args) {
1247
2126
  const parsed2 = askWithMemoryInputSchema.parse(args ?? {});
1248
2127
  const projectKey = getCurrentProjectId();
1249
2128
  if (!projectKey) {
1250
- throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
2129
+ throw new Error(
2130
+ "No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
2131
+ );
1251
2132
  }
1252
2133
  const payload = {
1253
2134
  question: parsed2.question,
@@ -1271,19 +2152,21 @@ async function handleAskWithMemory(client, args) {
1271
2152
  }
1272
2153
 
1273
2154
  // src/tools/handlers/logIntent.ts
1274
- var import_v417 = require("zod/v4");
1275
- var logIntentInputSchema = import_v417.z.object({
1276
- intent: import_v417.z.enum(["task", "decision"]),
1277
- message: import_v417.z.string().min(1),
1278
- context: import_v417.z.record(import_v417.z.string(), import_v417.z.any()).optional(),
1279
- intent_source: import_v417.z.string().optional().default("cursor_chat"),
1280
- run_id: import_v417.z.string().min(1).optional()
2155
+ var import_v416 = require("zod/v4");
2156
+ var logIntentInputSchema = import_v416.z.object({
2157
+ intent: import_v416.z.enum(["task", "decision"]),
2158
+ message: import_v416.z.string().min(1),
2159
+ context: import_v416.z.record(import_v416.z.string(), import_v416.z.any()).optional(),
2160
+ intent_source: import_v416.z.string().optional().default("cursor_chat"),
2161
+ run_id: import_v416.z.string().min(1).optional()
1281
2162
  });
1282
2163
  async function handleLogIntent(client, args) {
1283
2164
  const parsed2 = logIntentInputSchema.parse(args ?? {});
1284
2165
  const projectKey = getCurrentProjectId();
1285
2166
  if (!projectKey) {
1286
- throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
2167
+ throw new Error(
2168
+ "No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
2169
+ );
1287
2170
  }
1288
2171
  const intent = parsed2.intent;
1289
2172
  const message = parsed2.message.trim();
@@ -1317,525 +2200,188 @@ async function handleLogIntent(client, args) {
1317
2200
  }
1318
2201
 
1319
2202
  // src/tools/handlers/logChangeSummary.ts
1320
- var import_v418 = require("zod/v4");
1321
- var logChangeSummaryInputSchema = import_v418.z.object({
1322
- summary: import_v418.z.string().min(1),
1323
- scope: import_v418.z.string().min(1).optional(),
1324
- files: import_v418.z.array(import_v418.z.string().min(1)).optional(),
1325
- stats: import_v418.z.object({
1326
- files: import_v418.z.number().int().nonnegative().optional(),
1327
- add: import_v418.z.number().int().nonnegative().optional(),
1328
- del: import_v418.z.number().int().nonnegative().optional()
2203
+ var import_v417 = require("zod/v4");
2204
+ var logChangeSummaryInputSchema = import_v417.z.object({
2205
+ summary: import_v417.z.string().min(1),
2206
+ scope: import_v417.z.string().min(1).optional(),
2207
+ files: import_v417.z.array(import_v417.z.string().min(1)).optional(),
2208
+ stats: import_v417.z.object({
2209
+ files: import_v417.z.number().int().nonnegative().optional(),
2210
+ add: import_v417.z.number().int().nonnegative().optional(),
2211
+ del: import_v417.z.number().int().nonnegative().optional()
1329
2212
  }).optional(),
1330
- commit: import_v418.z.string().min(1).optional(),
1331
- run_id: import_v418.z.string().min(1).optional()
2213
+ commit: import_v417.z.string().min(1).optional(),
2214
+ run_id: import_v417.z.string().min(1).optional()
1332
2215
  });
1333
2216
  async function handleLogChangeSummary(client, args) {
1334
- const parsed2 = logChangeSummaryInputSchema.parse(args ?? {});
1335
- const projectKey = getCurrentProjectId();
1336
- if (!projectKey) {
1337
- throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
1338
- }
1339
- const { summary, scope, files, stats, commit } = parsed2;
1340
- const message = summary.startsWith("CHANGE:") ? summary : `CHANGE: ${scope ?? "code"} \u2014 ${summary}`;
1341
- const run_id = resolveRunId(parsed2.run_id);
1342
- const body = {
1343
- kind: "note",
1344
- concept: "concept:change_summary",
1345
- actor: { type: config2.agentType, name: config2.agentName },
1346
- message,
1347
- projectKey,
1348
- metadata: {
1349
- source: config2.source,
1350
- purpose: "change_summary",
1351
- tool: "memora_log_change_summary",
1352
- ...scope ? { scope } : {},
1353
- ...files ? { files } : {},
1354
- ...stats ? { stats } : {},
1355
- ...commit ? { commit } : {},
1356
- ...run_id ? { run_id } : {}
1357
- }
1358
- };
1359
- await client.post("/timeline/events", body);
1360
- return { ok: true };
1361
- }
1362
-
1363
- // src/tools/handlers/logToolResult.ts
1364
- var import_v419 = require("zod/v4");
1365
- var logToolResultInputSchema = import_v419.z.object({
1366
- tool: import_v419.z.string().min(1),
1367
- status: import_v419.z.enum(["ok", "error", "partial"]),
1368
- summary: import_v419.z.string().min(1),
1369
- run_id: import_v419.z.string().min(1).optional(),
1370
- duration_ms: import_v419.z.number().int().nonnegative().optional(),
1371
- error_code: import_v419.z.string().min(1).optional(),
1372
- error_message: import_v419.z.string().min(1).optional(),
1373
- error_kind: import_v419.z.enum(["infra", "logic", "auth", "rate_limit", "validation", "unknown"]).optional(),
1374
- stats: import_v419.z.record(import_v419.z.string(), import_v419.z.any()).optional()
1375
- });
1376
- async function handleLogToolResult(client, args) {
1377
- const parsed2 = logToolResultInputSchema.parse(args ?? {});
1378
- const projectKey = getCurrentProjectId();
1379
- if (!projectKey) {
1380
- throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
1381
- }
1382
- const { tool, status, summary, duration_ms, error_code, error_message, error_kind, stats } = parsed2;
1383
- const message = summary.startsWith("RESULT:") ? summary : `RESULT: ${tool} \u2014 ${status} \u2014 ${summary}`;
1384
- const run_id = resolveRunId(parsed2.run_id);
1385
- const body = {
1386
- kind: "note",
1387
- concept: "concept:tool_result",
1388
- actor: { type: config2.agentType, name: config2.agentName },
1389
- message,
1390
- projectKey,
1391
- metadata: {
1392
- source: config2.source,
1393
- purpose: "tool_result",
1394
- tool: "memora_log_tool_result",
1395
- tool_name: tool,
1396
- status,
1397
- ...run_id ? { run_id } : {},
1398
- ...duration_ms ? { duration_ms } : {},
1399
- ...error_code ? { error_code } : {},
1400
- ...error_message ? { error_message } : {},
1401
- ...error_kind ? { error_kind } : {},
1402
- ...stats ? { stats } : {}
1403
- }
1404
- };
1405
- await client.post("/timeline/events", body);
1406
- return { ok: true };
1407
- }
1408
-
1409
- // src/tools/handlers/logCommand.ts
1410
- var import_v420 = require("zod/v4");
1411
- var logCommandInputSchema = import_v420.z.object({
1412
- cmd: import_v420.z.string().min(1),
1413
- summary: import_v420.z.string().min(1),
1414
- cwd: import_v420.z.string().min(1).optional(),
1415
- exit_code: import_v420.z.number().int().optional(),
1416
- duration_ms: import_v420.z.number().int().nonnegative().optional(),
1417
- run_id: import_v420.z.string().min(1).optional(),
1418
- stats: import_v420.z.record(import_v420.z.string(), import_v420.z.any()).optional()
1419
- });
1420
- async function handleLogCommand(client, args) {
1421
- const parsed2 = logCommandInputSchema.parse(args ?? {});
1422
- const projectKey = getCurrentProjectId();
1423
- if (!projectKey) {
1424
- throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
1425
- }
1426
- const { cmd, summary, cwd: cwd2, exit_code, duration_ms, stats } = parsed2;
1427
- const message = summary.startsWith("COMMAND:") ? summary : `COMMAND: ${cmd} \u2014 ${summary}`;
1428
- const run_id = resolveRunId(parsed2.run_id);
1429
- const body = {
1430
- kind: "note",
1431
- concept: "concept:command",
1432
- actor: { type: config2.agentType, name: config2.agentName },
1433
- message,
1434
- projectKey,
1435
- metadata: {
1436
- source: config2.source,
1437
- purpose: "command",
1438
- tool: "memora_log_command",
1439
- cmd,
1440
- ...cwd2 ? { cwd: cwd2 } : {},
1441
- ...exit_code !== void 0 ? { exit_code } : {},
1442
- ...duration_ms !== void 0 ? { duration_ms } : {},
1443
- ...run_id ? { run_id } : {},
1444
- ...stats ? { stats } : {}
1445
- }
1446
- };
1447
- await client.post("/timeline/events", body);
1448
- return { ok: true };
1449
- }
1450
-
1451
- // src/tools/handlers/listProjects.ts
1452
- async function handleListProjects(client) {
1453
- const res = await client.get("/v1/projects");
1454
- return res ?? { items: [] };
1455
- }
1456
-
1457
- // src/tools/handlers/setProject.ts
1458
- var import_v421 = require("zod/v4");
1459
-
1460
- // src/repoFingerprint.ts
1461
- var fs5 = __toESM(require("fs"), 1);
1462
- var path8 = __toESM(require("path"), 1);
1463
- var crypto5 = __toESM(require("crypto"), 1);
1464
- var parseBooleanFlag3 = (value) => {
1465
- if (!value) {
1466
- return false;
1467
- }
1468
- const normalized = value.trim().toLowerCase();
1469
- return ["1", "true", "yes", "on"].includes(normalized);
1470
- };
1471
- var debugEnabled2 = parseBooleanFlag3(process.env.MEMORAONE_DEV_MODE);
1472
- var debugLog = (message) => {
1473
- if (!debugEnabled2) {
1474
- return;
1475
- }
1476
- process.stderr.write(`[memoraone-mcp][debug] ${message}
1477
- `);
1478
- };
1479
- var normalizeRemoteUrl = (remoteUrl) => {
1480
- let normalized = remoteUrl.trim();
1481
- normalized = normalized.replace(/^[a-z]+:\/\//i, "");
1482
- normalized = normalized.replace(/^git@([^:]+):/i, "$1/");
1483
- normalized = normalized.replace(/\.git$/i, "");
1484
- normalized = normalized.replace(/\/+$/, "");
1485
- return normalized.toLowerCase();
1486
- };
1487
- var sha256 = (value) => {
1488
- return crypto5.createHash("sha256").update(value).digest("hex");
1489
- };
1490
- var resolveGitDir = (gitPath) => {
1491
- try {
1492
- const stat2 = fs5.statSync(gitPath);
1493
- if (stat2.isDirectory()) {
1494
- return gitPath;
1495
- }
1496
- if (stat2.isFile()) {
1497
- const content = fs5.readFileSync(gitPath, "utf8");
1498
- const match = content.match(/^gitdir:\s*(.+)$/m);
1499
- if (match) {
1500
- const gitDir = match[1].trim();
1501
- return path8.resolve(path8.dirname(gitPath), gitDir);
1502
- }
1503
- }
1504
- } catch {
1505
- return null;
1506
- }
1507
- return null;
1508
- };
1509
- var findGitRoot = (start) => {
1510
- let current = path8.resolve(start);
1511
- while (true) {
1512
- const gitPath = path8.join(current, ".git");
1513
- if (fs5.existsSync(gitPath)) {
1514
- const gitDir = resolveGitDir(gitPath);
1515
- if (gitDir) {
1516
- return { gitRoot: current, gitDir };
1517
- }
1518
- }
1519
- const parent = path8.dirname(current);
1520
- if (parent === current) {
1521
- break;
1522
- }
1523
- current = parent;
1524
- }
1525
- return null;
1526
- };
1527
- var readOriginRemote = (gitDir) => {
1528
- const configPath = path8.join(gitDir, "config");
1529
- try {
1530
- const content = fs5.readFileSync(configPath, "utf8");
1531
- const lines = content.split(/\r?\n/);
1532
- let inOrigin = false;
1533
- for (const line of lines) {
1534
- const sectionMatch = line.match(/^\s*\[(.+)]\s*$/);
1535
- if (sectionMatch) {
1536
- inOrigin = sectionMatch[1].trim() === 'remote "origin"';
1537
- continue;
1538
- }
1539
- if (inOrigin) {
1540
- const urlMatch = line.match(/^\s*url\s*=\s*(.+)\s*$/);
1541
- if (urlMatch) {
1542
- return urlMatch[1].trim();
1543
- }
1544
- }
1545
- }
1546
- } catch {
1547
- return null;
1548
- }
1549
- return null;
1550
- };
1551
- function resolveRepoFingerprint(cwd2) {
1552
- const found = findGitRoot(cwd2);
1553
- if (!found) {
1554
- const fallbackPath = path8.resolve(cwd2);
1555
- const fingerprint2 = sha256(fallbackPath);
1556
- debugLog(`repo fingerprint=${fingerprint2} source=path-fallback`);
1557
- return {
1558
- fingerprint: fingerprint2,
1559
- gitRoot: fallbackPath,
1560
- source: "path-fallback"
1561
- };
1562
- }
1563
- const { gitRoot, gitDir } = found;
1564
- const remoteUrl = readOriginRemote(gitDir);
1565
- if (remoteUrl) {
1566
- const normalized = normalizeRemoteUrl(remoteUrl);
1567
- const fingerprint2 = sha256(normalized);
1568
- debugLog(`repo fingerprint=${fingerprint2} source=git-remote`);
1569
- return {
1570
- fingerprint: fingerprint2,
1571
- gitRoot,
1572
- remoteUrl,
1573
- source: "git-remote"
1574
- };
1575
- }
1576
- const fingerprint = sha256(path8.resolve(gitRoot));
1577
- debugLog(`repo fingerprint=${fingerprint} source=path-fallback`);
1578
- return {
1579
- fingerprint,
1580
- gitRoot,
1581
- source: "path-fallback"
1582
- };
1583
- }
1584
-
1585
- // src/workspaceMap.ts
1586
- var fs6 = __toESM(require("fs/promises"), 1);
1587
- var path9 = __toESM(require("path"), 1);
1588
- var import_node_os = __toESM(require("os"), 1);
1589
- var parseBooleanFlag4 = (value) => {
1590
- if (!value) {
1591
- return false;
1592
- }
1593
- const normalized = value.trim().toLowerCase();
1594
- return ["1", "true", "yes", "on"].includes(normalized);
1595
- };
1596
- var debugEnabled3 = parseBooleanFlag4(process.env.MEMORAONE_DEV_MODE);
1597
- var debugLog2 = (message) => {
1598
- if (!debugEnabled3) {
1599
- return;
1600
- }
1601
- process.stderr.write(`[memoraone-mcp][debug] ${message}
1602
- `);
1603
- };
1604
- var fingerprintRegex = /^[0-9a-f]{64}$/i;
1605
- function getWorkspaceMapPath() {
1606
- return path9.join(import_node_os.default.homedir(), ".memoraone", "workspaces.json");
1607
- }
1608
- var ensureWorkspaceDir = async () => {
1609
- const dir = path9.dirname(getWorkspaceMapPath());
1610
- await fs6.mkdir(dir, { recursive: true });
1611
- };
1612
- async function acquireWorkspaceMapLock() {
1613
- const filePath = getWorkspaceMapPath();
1614
- const lockPath = `${filePath}.lock`;
1615
- const maxRetries = 10;
1616
- const retryDelayMs = 50;
1617
- const maxLockAgeMs = 5e3;
1618
- await ensureWorkspaceDir();
1619
- let lockAcquired = false;
1620
- let retries = 0;
1621
- while (!lockAcquired && retries < maxRetries) {
1622
- try {
1623
- try {
1624
- const stat2 = await fs6.stat(lockPath);
1625
- const ageMs = Date.now() - stat2.mtimeMs;
1626
- if (ageMs > maxLockAgeMs) {
1627
- await fs6.unlink(lockPath);
1628
- debugLog2(`removed stale workspace map lock (age: ${ageMs}ms)`);
1629
- }
1630
- } catch (err) {
1631
- if (err?.code !== "ENOENT") {
1632
- throw err;
1633
- }
1634
- }
1635
- const fd = await fs6.open(lockPath, "wx");
1636
- await fd.close();
1637
- lockAcquired = true;
1638
- } catch (err) {
1639
- if (err?.code === "EEXIST") {
1640
- retries++;
1641
- if (retries < maxRetries) {
1642
- await new Promise((resolve7) => setTimeout(resolve7, retryDelayMs));
1643
- continue;
1644
- }
1645
- throw new Error(
1646
- `[memoraone-mcp] Failed to acquire workspace map lock after ${maxRetries} retries`
1647
- );
1648
- }
1649
- throw err;
1650
- }
1651
- }
1652
- return async () => {
1653
- try {
1654
- await fs6.unlink(lockPath);
1655
- } catch (err) {
1656
- if (err?.code !== "ENOENT") {
1657
- debugLog2(`failed to release workspace map lock: ${String(err)}`);
1658
- }
1659
- }
1660
- };
1661
- }
1662
- var validateWorkspaceMap = (map, filePath) => {
1663
- if (!map || typeof map !== "object" || Array.isArray(map)) {
1664
- throw new Error(
1665
- `[memoraone-mcp] Invalid workspace map schema in ${filePath}`
1666
- );
1667
- }
1668
- for (const [fingerprint, entry] of Object.entries(map)) {
1669
- if (!fingerprintRegex.test(fingerprint)) {
1670
- throw new Error(
1671
- `[memoraone-mcp] Invalid workspace fingerprint in ${filePath}`
1672
- );
1673
- }
1674
- if (typeof entry === "string") {
1675
- if (!entry.trim()) {
1676
- throw new Error(
1677
- `[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
1678
- );
1679
- }
1680
- continue;
1681
- }
1682
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1683
- throw new Error(
1684
- `[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
1685
- );
1686
- }
1687
- const projectKey = entry.projectKey ?? entry.project_id;
1688
- if (!projectKey || !projectKey.trim()) {
1689
- throw new Error(
1690
- `[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
1691
- );
1692
- }
1693
- const source = entry.source;
1694
- if (source !== void 0 && typeof source !== "string") {
1695
- throw new Error(
1696
- `[memoraone-mcp] Invalid workspace source in ${filePath}`
1697
- );
1698
- }
1699
- const linkedAt = entry.linked_at;
1700
- if (linkedAt !== void 0 && typeof linkedAt !== "string") {
1701
- throw new Error(
1702
- `[memoraone-mcp] Invalid workspace linked_at in ${filePath}`
1703
- );
1704
- }
1705
- }
1706
- };
1707
- async function readWorkspaceMap() {
1708
- const filePath = getWorkspaceMapPath();
1709
- try {
1710
- const content = await fs6.readFile(filePath, "utf8");
1711
- const parsed2 = JSON.parse(content);
1712
- validateWorkspaceMap(parsed2, filePath);
1713
- const typed = parsed2;
1714
- let migrated = false;
1715
- const normalized = {};
1716
- for (const [fingerprint, entry] of Object.entries(typed)) {
1717
- if (typeof entry === "string") {
1718
- normalized[fingerprint] = entry;
1719
- continue;
1720
- }
1721
- const projectKey = entry.projectKey ?? entry.project_id ?? "";
1722
- if (entry.project_id && !entry.projectKey) {
1723
- migrated = true;
1724
- }
1725
- normalized[fingerprint] = {
1726
- ...projectKey ? { projectKey } : {},
1727
- ...entry.source ? { source: entry.source } : {},
1728
- ...entry.linked_at ? { linked_at: entry.linked_at } : {}
1729
- };
1730
- }
1731
- debugLog2(
1732
- `workspace map loaded path=${filePath} entries=${Object.keys(normalized).length}`
2217
+ const parsed2 = logChangeSummaryInputSchema.parse(args ?? {});
2218
+ const projectKey = getCurrentProjectId();
2219
+ if (!projectKey) {
2220
+ throw new Error(
2221
+ "No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
1733
2222
  );
1734
- return { map: normalized, needsMigration: migrated };
1735
- } catch (err) {
1736
- if (err?.code === "ENOENT") {
1737
- const emptyMap = {};
1738
- debugLog2(`workspace map loaded path=${filePath} entries=0`);
1739
- return { map: emptyMap, needsMigration: false };
1740
- }
1741
- if (err instanceof SyntaxError) {
1742
- throw new Error(
1743
- `[memoraone-mcp] Failed to parse workspace map at ${filePath}`
1744
- );
1745
- }
1746
- throw err;
1747
2223
  }
2224
+ const { summary, scope, files, stats, commit } = parsed2;
2225
+ const message = summary.startsWith("CHANGE:") ? summary : `CHANGE: ${scope ?? "code"} \u2014 ${summary}`;
2226
+ const run_id = resolveRunId(parsed2.run_id);
2227
+ const body = {
2228
+ kind: "note",
2229
+ concept: "concept:change_summary",
2230
+ actor: { type: config2.agentType, name: config2.agentName },
2231
+ message,
2232
+ projectKey,
2233
+ metadata: {
2234
+ source: config2.source,
2235
+ purpose: "change_summary",
2236
+ tool: "memora_log_change_summary",
2237
+ ...scope ? { scope } : {},
2238
+ ...files ? { files } : {},
2239
+ ...stats ? { stats } : {},
2240
+ ...commit ? { commit } : {},
2241
+ ...run_id ? { run_id } : {}
2242
+ }
2243
+ };
2244
+ await client.post("/timeline/events", body);
2245
+ return { ok: true };
1748
2246
  }
1749
- async function writeWorkspaceMap(map) {
1750
- const filePath = getWorkspaceMapPath();
1751
- validateWorkspaceMap(map, filePath);
1752
- await ensureWorkspaceDir();
1753
- const tempPath = `${filePath}.tmp`;
1754
- const content = JSON.stringify(map, null, 2);
1755
- await fs6.writeFile(tempPath, content, "utf8");
1756
- await fs6.rename(tempPath, filePath);
1757
- }
1758
- async function setProjectIdForFingerprint(args) {
1759
- const { fingerprint, projectKey, source, linked_at } = args;
1760
- if (!fingerprintRegex.test(fingerprint)) {
1761
- throw new Error("[memoraone-mcp] Invalid fingerprint");
1762
- }
1763
- if (!projectKey.trim()) {
1764
- throw new Error("[memoraone-mcp] Invalid projectKey");
1765
- }
1766
- const releaseLock = await acquireWorkspaceMapLock();
1767
- try {
1768
- const { map, needsMigration } = await readWorkspaceMap();
1769
- if (needsMigration) {
1770
- await writeWorkspaceMap(map);
1771
- }
1772
- map[fingerprint] = {
1773
- projectKey,
1774
- ...source ? { source } : {},
1775
- linked_at: linked_at ?? (/* @__PURE__ */ new Date()).toISOString()
1776
- };
1777
- await writeWorkspaceMap(map);
1778
- debugLog2(
1779
- `workspace map set fingerprint=${fingerprint} projectKey=${projectKey}`
2247
+
2248
+ // src/tools/handlers/logToolResult.ts
2249
+ var import_v418 = require("zod/v4");
2250
+ var logToolResultInputSchema = import_v418.z.object({
2251
+ tool: import_v418.z.string().min(1),
2252
+ status: import_v418.z.enum(["ok", "error", "partial"]),
2253
+ summary: import_v418.z.string().min(1),
2254
+ run_id: import_v418.z.string().min(1).optional(),
2255
+ duration_ms: import_v418.z.number().int().nonnegative().optional(),
2256
+ error_code: import_v418.z.string().min(1).optional(),
2257
+ error_message: import_v418.z.string().min(1).optional(),
2258
+ error_kind: import_v418.z.enum(["infra", "logic", "auth", "rate_limit", "validation", "unknown"]).optional(),
2259
+ stats: import_v418.z.record(import_v418.z.string(), import_v418.z.any()).optional()
2260
+ });
2261
+ async function handleLogToolResult(client, args) {
2262
+ const parsed2 = logToolResultInputSchema.parse(args ?? {});
2263
+ const projectKey = getCurrentProjectId();
2264
+ if (!projectKey) {
2265
+ throw new Error(
2266
+ "No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
1780
2267
  );
1781
- } finally {
1782
- await releaseLock();
1783
2268
  }
2269
+ const { tool, status, summary, duration_ms, error_code, error_message, error_kind, stats } = parsed2;
2270
+ const message = summary.startsWith("RESULT:") ? summary : `RESULT: ${tool} \u2014 ${status} \u2014 ${summary}`;
2271
+ const run_id = resolveRunId(parsed2.run_id);
2272
+ const body = {
2273
+ kind: "note",
2274
+ concept: "concept:tool_result",
2275
+ actor: { type: config2.agentType, name: config2.agentName },
2276
+ message,
2277
+ projectKey,
2278
+ metadata: {
2279
+ source: config2.source,
2280
+ purpose: "tool_result",
2281
+ tool: "memora_log_tool_result",
2282
+ tool_name: tool,
2283
+ status,
2284
+ ...run_id ? { run_id } : {},
2285
+ ...duration_ms ? { duration_ms } : {},
2286
+ ...error_code ? { error_code } : {},
2287
+ ...error_message ? { error_message } : {},
2288
+ ...error_kind ? { error_kind } : {},
2289
+ ...stats ? { stats } : {}
2290
+ }
2291
+ };
2292
+ await client.post("/timeline/events", body);
2293
+ return { ok: true };
1784
2294
  }
1785
2295
 
1786
- // src/tools/handlers/setProject.ts
1787
- var setProjectInputSchema = import_v421.z.object({
1788
- projectKey: import_v421.z.string().min(1).optional(),
1789
- projectId: import_v421.z.string().min(1).optional()
2296
+ // src/tools/handlers/logCommand.ts
2297
+ var import_v419 = require("zod/v4");
2298
+ var logCommandInputSchema = import_v419.z.object({
2299
+ cmd: import_v419.z.string().min(1),
2300
+ summary: import_v419.z.string().min(1),
2301
+ cwd: import_v419.z.string().min(1).optional(),
2302
+ exit_code: import_v419.z.number().int().optional(),
2303
+ duration_ms: import_v419.z.number().int().nonnegative().optional(),
2304
+ run_id: import_v419.z.string().min(1).optional(),
2305
+ stats: import_v419.z.record(import_v419.z.string(), import_v419.z.any()).optional()
1790
2306
  });
1791
- async function handleSetProject(args) {
1792
- const parsed2 = setProjectInputSchema.parse(args ?? {});
1793
- const resolvedProjectKey = parsed2.projectKey ?? parsed2.projectId;
1794
- if (!resolvedProjectKey) {
1795
- throw new Error("projectKey is required");
1796
- }
1797
- const requested = resolvedProjectKey.trim();
1798
- const bound = getBoundProjectId();
1799
- if (bound !== null && requested !== bound) {
2307
+ async function handleLogCommand(client, args) {
2308
+ const parsed2 = logCommandInputSchema.parse(args ?? {});
2309
+ const projectKey = getCurrentProjectId();
2310
+ if (!projectKey) {
1800
2311
  throw new Error(
1801
- `Project switching is disabled (Option A). This MCP process is bound to ${bound}. Start a separate MCP instance/window for ${requested}.`
2312
+ "No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
1802
2313
  );
1803
2314
  }
1804
- setCurrentProjectId(resolvedProjectKey);
1805
- const repo = resolveRepoFingerprint(process.cwd());
1806
- await setProjectIdForFingerprint({
1807
- fingerprint: repo.fingerprint,
1808
- projectKey: resolvedProjectKey,
1809
- source: "manual"
1810
- });
1811
- return { ok: true, projectKey: resolvedProjectKey };
2315
+ const { cmd, summary, cwd: cwd2, exit_code, duration_ms, stats } = parsed2;
2316
+ const message = summary.startsWith("COMMAND:") ? summary : `COMMAND: ${cmd} \u2014 ${summary}`;
2317
+ const run_id = resolveRunId(parsed2.run_id);
2318
+ const body = {
2319
+ kind: "note",
2320
+ concept: "concept:command",
2321
+ actor: { type: config2.agentType, name: config2.agentName },
2322
+ message,
2323
+ projectKey,
2324
+ metadata: {
2325
+ source: config2.source,
2326
+ purpose: "command",
2327
+ tool: "memora_log_command",
2328
+ cmd,
2329
+ ...cwd2 ? { cwd: cwd2 } : {},
2330
+ ...exit_code !== void 0 ? { exit_code } : {},
2331
+ ...duration_ms !== void 0 ? { duration_ms } : {},
2332
+ ...run_id ? { run_id } : {},
2333
+ ...stats ? { stats } : {}
2334
+ }
2335
+ };
2336
+ await client.post("/timeline/events", body);
2337
+ return { ok: true };
1812
2338
  }
1813
2339
 
1814
2340
  // src/tools/handlers/bindingStatus.ts
1815
- function buildBindingStatus(binding) {
2341
+ function buildBindingStatus(binding, options = {}) {
1816
2342
  const status = {
2343
+ repositoryBindingId: binding.repositoryBindingId,
1817
2344
  projectId: binding.projectId,
1818
2345
  workspaceRoot: binding.workspaceRoot,
1819
- m1Path: binding.m1Path,
1820
2346
  bindingSource: binding.bindingSource,
1821
- apiKeySource: binding.apiKeySource
2347
+ status: binding.status,
2348
+ credentialSource: "keyring",
2349
+ cacheRefreshed: options.cacheRefreshed === true
1822
2350
  };
1823
2351
  if (binding.environment !== void 0) {
1824
2352
  status.environment = binding.environment;
1825
2353
  }
2354
+ if (binding.installationPublicId !== void 0) {
2355
+ status.installationPublicId = binding.installationPublicId;
2356
+ }
1826
2357
  return status;
1827
2358
  }
1828
- function handleBindingStatus(binding) {
2359
+ function handleBindingStatus(binding, options = {}) {
1829
2360
  if (!binding) {
1830
2361
  throw new Error("[memoraone-mcp] Binding status unavailable (not initialized)");
1831
2362
  }
1832
- return buildBindingStatus(binding);
2363
+ return buildBindingStatus(binding, options);
2364
+ }
2365
+
2366
+ // src/heartbeat.ts
2367
+ var crypto5 = __toESM(require("crypto"), 1);
2368
+ var fs10 = __toESM(require("fs"), 1);
2369
+ var path13 = __toESM(require("path"), 1);
2370
+
2371
+ // src/localState/mcpSessionId.ts
2372
+ var import_node_crypto4 = require("crypto");
2373
+ var MCS_PREFIX = "mcs_";
2374
+ function generateMcpSessionId(random = () => (0, import_node_crypto4.randomBytes)(32)) {
2375
+ const bytes = random();
2376
+ if (bytes.length !== 32) {
2377
+ throw new Error("[memoraone-mcp] mcp session id requires exactly 32 random bytes");
2378
+ }
2379
+ return `${MCS_PREFIX}${bytes.toString("base64url")}`;
1833
2380
  }
1834
2381
 
1835
2382
  // src/heartbeat.ts
1836
- var crypto6 = __toESM(require("crypto"), 1);
1837
- function fingerprintApiKey(apiKey) {
1838
- return crypto6.createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
2383
+ function fingerprintAccessToken(accessToken) {
2384
+ return crypto5.createHash("sha256").update(accessToken).digest("hex").slice(0, 12);
1839
2385
  }
1840
2386
  function isHeartbeatDebugEnabled() {
1841
2387
  const value = String(process.env.MEMORAONE_DEBUG_HEARTBEAT ?? "").trim().toLowerCase();
@@ -1844,15 +2390,80 @@ function isHeartbeatDebugEnabled() {
1844
2390
  function resolveHeartbeatIntervalMs() {
1845
2391
  return Number.isFinite(config2.heartbeatIntervalMs) ? Math.max(1e3, config2.heartbeatIntervalMs) : 3e4;
1846
2392
  }
1847
- function isDaemonIdleShutdownAllowed() {
1848
- return false;
2393
+ function redactSensitiveText(text) {
2394
+ 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]");
2395
+ }
2396
+ function resolvePackageVersion() {
2397
+ const fromEnv = process.env.npm_package_version?.trim();
2398
+ if (fromEnv) return fromEnv;
2399
+ const moduleDir = typeof __dirname !== "undefined" ? __dirname : void 0;
2400
+ const candidates = [
2401
+ ...moduleDir ? [path13.join(moduleDir, "..", "package.json"), path13.join(moduleDir, "package.json")] : [],
2402
+ path13.join(process.cwd(), "package.json"),
2403
+ path13.join(process.cwd(), "packages", "mcp", "package.json")
2404
+ ];
2405
+ for (const candidate of candidates) {
2406
+ try {
2407
+ const pkg = JSON.parse(fs10.readFileSync(candidate, "utf8"));
2408
+ if (typeof pkg.version === "string" && pkg.version.trim()) {
2409
+ return pkg.version.trim();
2410
+ }
2411
+ } catch {
2412
+ }
2413
+ }
2414
+ return void 0;
2415
+ }
2416
+ function parseActiveFlag(payload) {
2417
+ if (!payload || typeof payload !== "object") return null;
2418
+ const active = payload.active;
2419
+ if (typeof active === "boolean") return active;
2420
+ return null;
2421
+ }
2422
+ async function announceIdeSession(client, ctx) {
2423
+ const body = {
2424
+ session_id: ctx.sessionId,
2425
+ ide_type: ctx.ideType
2426
+ };
2427
+ if (ctx.packageVersion) {
2428
+ body.package_version = ctx.packageVersion;
2429
+ }
2430
+ let lastErr;
2431
+ for (let attempt = 0; attempt < 2; attempt++) {
2432
+ try {
2433
+ const raw = await client.post("/v1/local-mcp/session/announce", body, { log: false });
2434
+ const data = raw ?? {};
2435
+ if (data.ok !== true || typeof data.active !== "boolean" || typeof data.ide_type !== "string") {
2436
+ throw new Error("[memoraone-mcp] Invalid session announce response");
2437
+ }
2438
+ return {
2439
+ ok: true,
2440
+ active: data.active,
2441
+ ideType: data.ide_type,
2442
+ announcedAt: typeof data.announced_at === "string" ? data.announced_at : ""
2443
+ };
2444
+ } catch (err) {
2445
+ lastErr = err;
2446
+ if (err instanceof MemoraOneHttpError && (err.status === 404 || err.status === 405)) {
2447
+ throw new Error(
2448
+ "[memoraone-mcp] Session announce endpoint is not supported by this API (POST /v1/local-mcp/session/announce)"
2449
+ );
2450
+ }
2451
+ const msg = String(err);
2452
+ const transient = /fetch failed|ECONNRESET|ETIMEDOUT|ENOTFOUND|network|socket/i.test(msg) && !(err instanceof MemoraOneHttpError && err.status >= 400 && err.status < 500);
2453
+ if (!transient || attempt === 1) {
2454
+ break;
2455
+ }
2456
+ }
2457
+ }
2458
+ const safe = redactSensitiveText(String(lastErr));
2459
+ throw new Error(`[memoraone-mcp] Session announce failed: ${safe}`);
1849
2460
  }
1850
2461
  async function sendProjectHeartbeat(client, ctx) {
1851
2462
  try {
1852
2463
  const pid = ctx.projectId?.trim();
1853
2464
  if (isHeartbeatDebugEnabled()) {
1854
2465
  process.stderr.write(
1855
- `[memoraone-mcp][diag] heartbeat projectId=${pid ?? "unknown"} apiKeySource=${ctx.apiKeySource ?? "unknown"} apiKeyFingerprint=${ctx.apiKeyFingerprint ?? "unknown"} ideType=${ctx.ideType ?? "unknown"}
2466
+ `[memoraone-mcp][diag] heartbeat projectId=${pid ?? "unknown"} binding=${ctx.repositoryBindingId} credentialSource=${ctx.credentialSource ?? "keyring"} accessTokenFingerprint=${ctx.accessTokenFingerprint ?? "unknown"} ideType=${ctx.ideType ?? "unknown"}
1856
2467
  `
1857
2468
  );
1858
2469
  }
@@ -1861,32 +2472,116 @@ async function sendProjectHeartbeat(client, ctx) {
1861
2472
  }
1862
2473
  const body = {};
1863
2474
  if (ctx.ideType) body.ide_type = ctx.ideType;
1864
- await client.post(`/v1/projects/${pid}/heartbeat`, body, {
1865
- log: false,
1866
- headers: {
1867
- "x-project-id": pid
1868
- }
1869
- });
2475
+ if (ctx.sessionId) body.session_id = ctx.sessionId;
2476
+ const raw = await client.post("/v1/local-mcp/heartbeat", body, { log: false });
2477
+ return { active: parseActiveFlag(raw) };
1870
2478
  } catch (err) {
2479
+ if (err instanceof ReconnectRequiredError) {
2480
+ try {
2481
+ const record = await readBindingRecord(ctx.repositoryBindingId);
2482
+ if (record) {
2483
+ await writeBindingRecord({
2484
+ ...record,
2485
+ status: "reconnect_required",
2486
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2487
+ });
2488
+ }
2489
+ } catch {
2490
+ }
2491
+ }
1871
2492
  process.stderr.write(
1872
- `[memoraone-mcp][info] heartbeat error (silent) ${String(err)}
2493
+ `[memoraone-mcp][info] heartbeat error (silent) ${redactSensitiveText(String(err))}
1873
2494
  `
1874
2495
  );
2496
+ return { active: null };
1875
2497
  }
1876
2498
  }
1877
2499
  function createDaemonHeartbeat(opts) {
1878
2500
  let interval = null;
1879
2501
  let client = null;
2502
+ let announced = false;
2503
+ let studioActive = null;
2504
+ let starting = false;
2505
+ let waitingForIdeType = false;
2506
+ const sessionId = opts.sessionId ?? generateMcpSessionId();
2507
+ const packageVersion = opts.packageVersion ?? resolvePackageVersion();
1880
2508
  const ctx = {
1881
2509
  projectId: opts.binding.projectId,
2510
+ repositoryBindingId: opts.binding.repositoryBindingId,
1882
2511
  ideType: opts.ideType,
1883
- apiKeySource: opts.binding.apiKeySource,
1884
- apiKeyFingerprint: opts.binding.apiKey ? fingerprintApiKey(opts.binding.apiKey) : null
2512
+ sessionId,
2513
+ credentialSource: "keyring",
2514
+ accessTokenFingerprint: null
1885
2515
  };
1886
2516
  const log2 = opts.onLog ?? ((msg) => {
1887
- process.stderr.write(`[memoraone-mcp][daemon-heartbeat] ${msg}
2517
+ process.stderr.write(`[memoraone-mcp][daemon-heartbeat] ${redactSensitiveText(msg)}
1888
2518
  `);
1889
2519
  });
2520
+ const ensureClient = async () => {
2521
+ if (client) return client;
2522
+ const creds = await readInstallationCredentials(opts.binding.repositoryBindingId);
2523
+ if (!creds?.accessToken) {
2524
+ log2("cannot start: no access token in keyring");
2525
+ return null;
2526
+ }
2527
+ ctx.accessTokenFingerprint = fingerprintAccessToken(creds.accessToken);
2528
+ client = opts.createClient?.(opts.binding) ?? new memoraClient_default(config2, {
2529
+ repositoryBindingId: opts.binding.repositoryBindingId,
2530
+ projectId: opts.binding.projectId
2531
+ });
2532
+ return client;
2533
+ };
2534
+ const ensureAnnounced = async (activeClient) => {
2535
+ if (announced) return true;
2536
+ if (!ctx.ideType) {
2537
+ waitingForIdeType = true;
2538
+ log2("waiting for ide type before session announce");
2539
+ return false;
2540
+ }
2541
+ waitingForIdeType = false;
2542
+ try {
2543
+ const result = await announceIdeSession(activeClient, {
2544
+ sessionId,
2545
+ ideType: ctx.ideType,
2546
+ packageVersion
2547
+ });
2548
+ announced = true;
2549
+ studioActive = result.active;
2550
+ log2(`session announced ideType=${ctx.ideType} active=${String(result.active)}`);
2551
+ return true;
2552
+ } catch (err) {
2553
+ studioActive = false;
2554
+ log2(`session announce failed: ${redactSensitiveText(String(err))}`);
2555
+ return false;
2556
+ }
2557
+ };
2558
+ const applyHeartbeatOutcome = (outcome) => {
2559
+ if (outcome.active === null) return;
2560
+ if (outcome.active === false && studioActive !== false) {
2561
+ studioActive = false;
2562
+ log2("session superseded (active=false); continuing to serve MCP without re-announce");
2563
+ return;
2564
+ }
2565
+ if (outcome.active === true) {
2566
+ studioActive = true;
2567
+ }
2568
+ };
2569
+ const tick = async () => {
2570
+ if (!client) return;
2571
+ const outcome = await sendProjectHeartbeat(client, ctx);
2572
+ applyHeartbeatOutcome(outcome);
2573
+ };
2574
+ const beginInterval = () => {
2575
+ if (interval) return;
2576
+ const intervalMs = resolveHeartbeatIntervalMs();
2577
+ log2(
2578
+ `daemon owns heartbeat for binding=${opts.binding.repositoryBindingId} project=${opts.binding.projectId} ideType=${ctx.ideType ?? "unknown"} interval=${intervalMs}ms`
2579
+ );
2580
+ void tick();
2581
+ interval = setInterval(() => {
2582
+ void tick();
2583
+ }, intervalMs);
2584
+ };
1890
2585
  const start = async () => {
1891
2586
  if (!config2.heartbeatEnabled) {
1892
2587
  log2("disabled by config");
@@ -1896,45 +2591,64 @@ function createDaemonHeartbeat(opts) {
1896
2591
  log2("already running (skipped duplicate start)");
1897
2592
  return;
1898
2593
  }
1899
- const apiKey = opts.binding.apiKey;
1900
- if (!apiKey) {
1901
- log2("cannot start: no api key in binding");
2594
+ if (starting) {
2595
+ log2("already starting (skipped duplicate start)");
1902
2596
  return;
1903
2597
  }
1904
- const projectId = opts.binding.projectId;
1905
- client = new memoraClient_default(config2, projectId, apiKey);
1906
- const intervalMs = resolveHeartbeatIntervalMs();
1907
- log2(
1908
- `daemon owns heartbeat for project=${projectId} ideType=${ctx.ideType ?? "unknown"} interval=${intervalMs}ms`
1909
- );
1910
- void sendProjectHeartbeat(client, ctx);
1911
- interval = setInterval(() => {
1912
- if (!client) return;
1913
- sendProjectHeartbeat(client, ctx).catch(() => {
1914
- });
1915
- }, intervalMs);
2598
+ starting = true;
2599
+ try {
2600
+ const activeClient = await ensureClient();
2601
+ if (!activeClient) return;
2602
+ const ok = await ensureAnnounced(activeClient);
2603
+ if (!ok) {
2604
+ return;
2605
+ }
2606
+ beginInterval();
2607
+ } finally {
2608
+ starting = false;
2609
+ if (waitingForIdeType && !announced && !interval && ctx.ideType && config2.heartbeatEnabled) {
2610
+ waitingForIdeType = false;
2611
+ void start();
2612
+ }
2613
+ }
1916
2614
  };
1917
2615
  const stop = () => {
1918
2616
  if (interval) {
1919
2617
  clearInterval(interval);
1920
2618
  interval = null;
1921
2619
  }
1922
- client = null;
1923
- log2(`daemon released heartbeat for project=${opts.binding.projectId}`);
2620
+ log2(`daemon released heartbeat for binding=${opts.binding.repositoryBindingId}`);
1924
2621
  };
1925
2622
  const isRunning = () => interval !== null;
1926
2623
  const setIdeType = (ideType) => {
1927
- if (ctx.ideType === ideType) {
2624
+ const previous = ctx.ideType;
2625
+ if (previous === ideType) {
1928
2626
  return;
1929
2627
  }
1930
2628
  ctx.ideType = ideType;
1931
2629
  log2(`daemon heartbeat ideType updated to ${ideType}`);
1932
- if (client) {
1933
- void sendProjectHeartbeat(client, ctx);
2630
+ if (!announced) {
2631
+ void start();
2632
+ return;
2633
+ }
2634
+ if (client && interval) {
2635
+ void tick();
1934
2636
  }
1935
2637
  };
1936
2638
  const getIdeType = () => ctx.ideType;
1937
- return { start, stop, isRunning, setIdeType, getIdeType };
2639
+ const getSessionId = () => sessionId;
2640
+ const isStudioActive = () => studioActive;
2641
+ const hasAnnounced = () => announced;
2642
+ return {
2643
+ start,
2644
+ stop,
2645
+ isRunning,
2646
+ setIdeType,
2647
+ getIdeType,
2648
+ getSessionId,
2649
+ isStudioActive,
2650
+ hasAnnounced
2651
+ };
1938
2652
  }
1939
2653
 
1940
2654
  // src/ideType.ts
@@ -2063,8 +2777,8 @@ function registerToolWithWorklog(server, runtime, sessionContext, toolName, desc
2063
2777
  async function main(opts = {}) {
2064
2778
  let bindingReadyResolve = null;
2065
2779
  let bindingReadyReject = null;
2066
- const bindingReady = new Promise((resolve7, reject) => {
2067
- bindingReadyResolve = resolve7;
2780
+ const bindingReady = new Promise((resolve9, reject) => {
2781
+ bindingReadyResolve = resolve9;
2068
2782
  bindingReadyReject = reject;
2069
2783
  });
2070
2784
  const devMode = Boolean(config2.devMode);
@@ -2073,9 +2787,11 @@ async function main(opts = {}) {
2073
2787
  const runtime = {
2074
2788
  client: null,
2075
2789
  projectId: null,
2076
- apiKeySource: null,
2077
- apiKeyFingerprint: null,
2790
+ repositoryBindingId: null,
2791
+ credentialSource: null,
2792
+ accessTokenFingerprint: null,
2078
2793
  authoritativeBinding: null,
2794
+ bindingCacheRefreshed: false,
2079
2795
  ideType: void 0
2080
2796
  };
2081
2797
  let workspaceRoot;
@@ -2088,10 +2804,25 @@ async function main(opts = {}) {
2088
2804
  if (initializeRoots.length === 0 && opts.daemonBindingHint) {
2089
2805
  if (isInitializeDebugEnabled()) {
2090
2806
  console.error(
2091
- "[memoraone-mcp][init-debug] Workspace resolution strategy: daemonBindingHint (bridge pre-resolved)"
2807
+ "[memoraone-mcp][init-debug] Workspace resolution strategy: daemonBindingHint (bridge pre-resolved, reconciled from disk)"
2808
+ );
2809
+ }
2810
+ const reconciled = await reconcileResolvedBindingWithDisk(opts.daemonBindingHint);
2811
+ runtime.bindingCacheRefreshed = reconciled.cacheRefreshed;
2812
+ if (reconciled.cacheRefreshed) {
2813
+ console.error(
2814
+ `[memoraone-mcp] refreshed stale cached binding ${reconciled.binding.repositoryBindingId}: project=${reconciled.binding.projectId}`
2092
2815
  );
2816
+ try {
2817
+ const socketPath = getBindingSocketPath(opts.daemonBindingHint);
2818
+ writeBindingSidecar(socketPath, reconciled.binding, runtime.ideType ?? "");
2819
+ } catch (err) {
2820
+ console.error(
2821
+ `[memoraone-mcp] warning: could not rewrite binding sidecar after refresh: ${String(err)}`
2822
+ );
2823
+ }
2093
2824
  }
2094
- return opts.daemonBindingHint;
2825
+ return reconciled.binding;
2095
2826
  }
2096
2827
  let rootsListUris;
2097
2828
  let rootsListAttempted = false;
@@ -2186,39 +2917,15 @@ async function main(opts = {}) {
2186
2917
  })
2187
2918
  );
2188
2919
  registeredToolNames.push("memora_get_personal_context");
2189
- server.tool(
2190
- "memora_list_projects",
2191
- "List projects available to the current API key",
2192
- listProjectsShape,
2193
- async () => runWithSessionContext(sessionContext, async () => {
2194
- if (!runtime.client || !runtime.projectId) return notInitializedResult;
2195
- const result = await handleListProjects(runtime.client);
2196
- return {
2197
- content: [{ type: "text", text: JSON.stringify(result) }]
2198
- };
2199
- })
2200
- );
2201
- registeredToolNames.push("memora_list_projects");
2202
- server.tool(
2203
- "memora_set_project",
2204
- "Set the current project key for subsequent tool calls",
2205
- setProjectShape,
2206
- async (args) => runWithSessionContext(sessionContext, async () => {
2207
- if (!runtime.client || !runtime.projectId) return notInitializedResult;
2208
- const result = await handleSetProject(args);
2209
- return {
2210
- content: [{ type: "text", text: JSON.stringify(result) }]
2211
- };
2212
- })
2213
- );
2214
- registeredToolNames.push("memora_set_project");
2215
2920
  server.tool(
2216
2921
  "memora_status",
2217
2922
  "Return non-secret project binding metadata for this MCP session",
2218
2923
  bindingStatusShape,
2219
2924
  async () => runWithSessionContext(sessionContext, async () => {
2220
2925
  if (!runtime.authoritativeBinding) return notInitializedResult;
2221
- const result = handleBindingStatus(runtime.authoritativeBinding);
2926
+ const result = handleBindingStatus(runtime.authoritativeBinding, {
2927
+ cacheRefreshed: runtime.bindingCacheRefreshed
2928
+ });
2222
2929
  return {
2223
2930
  content: [{ type: "text", text: JSON.stringify(result) }]
2224
2931
  };
@@ -2340,72 +3047,79 @@ async function main(opts = {}) {
2340
3047
  const debugAuth = ["1", "true", "yes", "on"].includes(
2341
3048
  String(process.env.MEMORAONE_DEBUG_AUTH ?? "").trim().toLowerCase()
2342
3049
  );
2343
- const debugLog3 = config2.devMode || debugAuth;
3050
+ const debugLog = config2.devMode || debugAuth;
2344
3051
  const binding = await resolveSessionBindingFromInitialize(params);
2345
- if (opts.daemonBindingHint && !bindingsMatch(opts.daemonBindingHint, binding)) {
3052
+ if (opts.daemonBindingHint && opts.daemonBindingHint.repositoryBindingId !== binding.repositoryBindingId) {
2346
3053
  const errMsg = formatBindingMismatchError(opts.daemonBindingHint, binding);
2347
3054
  console.error(`[memoraone-mcp][ERROR] ${errMsg}`);
2348
3055
  bindingReadyReject?.(new Error(errMsg));
2349
3056
  throw new Error(errMsg);
2350
3057
  }
2351
- const apiKeyToUse = binding.apiKey;
2352
- if (!apiKeyToUse) {
2353
- throw new Error(
2354
- "[memoraone-mcp] No actor key. Set MEMORAONE_API_KEY or MEMORA_API_KEY, or add MEMORAONE_API_KEY/api_key to memoraone.m1"
3058
+ if (binding.status === "reconnect_required") {
3059
+ throw new ReconnectRequiredError(
3060
+ "[memoraone-mcp] Installation requires reconnect. Run: memoraone-mcp connect <code>"
3061
+ );
3062
+ }
3063
+ const creds = await readInstallationCredentials(binding.repositoryBindingId);
3064
+ if (!creds?.accessToken?.startsWith("mia_")) {
3065
+ throw new ReconnectRequiredError(
3066
+ "[memoraone-mcp] Missing installation credentials. Run: memoraone-mcp connect <code>"
2355
3067
  );
2356
3068
  }
2357
- if (debugLog3) {
3069
+ if (debugLog) {
3070
+ console.error("[memoraone-mcp][debug] Resolved installation credentials from OS keyring");
3071
+ }
3072
+ if (binding.legacyM1WarningPath) {
2358
3073
  console.error(
2359
- "[memoraone-mcp][debug] Resolved actor key from " + (binding.apiKeySource === "env" ? "ENV" : "memoraone.m1")
3074
+ `[memoraone-mcp] warning: ignoring legacy memoraone.m1 at ${binding.legacyM1WarningPath}`
2360
3075
  );
2361
3076
  }
2362
3077
  const projectId = binding.projectId;
2363
3078
  const existing = getBoundProjectId();
2364
3079
  if (existing !== null && existing !== projectId) {
2365
- const requestedRoot = binding.workspaceRoot ?? workspaceRoot ?? process.cwd();
2366
- const action = "Open this repo in a separate window or configure a separate MCP server instance per root.";
2367
- const errMsg = `[memoraone-mcp] This MCP process is already bound to project ${existing}. Open a new IDE window or start a separate MCP instance for a different project.`;
2368
- console.error(
2369
- `[memoraone-mcp][ERROR] Option A conflict: boundProjectId=${existing} requestedProjectId=${projectId} workspaceRoot=${requestedRoot}. ${action}`
2370
- );
2371
- bindingReadyReject?.(new Error(errMsg));
2372
- setImmediate(() => process.exit(1));
2373
- throw new Error(errMsg);
3080
+ if (runtime.bindingCacheRefreshed) {
3081
+ setBoundProjectId(projectId);
3082
+ console.error(
3083
+ `[memoraone-mcp] ${sessionLabel} rebound to project ${projectId} after local binding refresh (was ${existing})`
3084
+ );
3085
+ } else {
3086
+ const requestedRoot = binding.workspaceRoot ?? workspaceRoot ?? process.cwd();
3087
+ const action = "Open this repo in a separate window or configure a separate MCP server instance per root.";
3088
+ const errMsg = `[memoraone-mcp] This MCP process is already bound to project ${existing}. Open a new IDE window or start a separate MCP instance for a different project.`;
3089
+ console.error(
3090
+ `[memoraone-mcp][ERROR] Option A conflict: boundProjectId=${existing} requestedProjectId=${projectId} workspaceRoot=${requestedRoot}. ${action}`
3091
+ );
3092
+ bindingReadyReject?.(new Error(errMsg));
3093
+ setImmediate(() => process.exit(1));
3094
+ throw new Error(errMsg);
3095
+ }
2374
3096
  }
2375
3097
  if (existing === null) {
2376
3098
  setBoundProjectId(projectId);
2377
- setBoundApiKey(apiKeyToUse);
2378
3099
  console.error(
2379
3100
  `[memoraone-mcp] ${sessionLabel} bound to project ${projectId} (Option A: single-project binding)`
2380
3101
  );
2381
3102
  }
2382
3103
  setCurrentProjectId(projectId);
2383
- setCurrentApiKey(apiKeyToUse);
2384
3104
  runtime.projectId = projectId;
2385
- runtime.apiKeySource = binding.apiKeySource;
2386
- runtime.apiKeyFingerprint = fingerprintApiKey(apiKeyToUse);
3105
+ runtime.repositoryBindingId = binding.repositoryBindingId;
3106
+ runtime.credentialSource = "keyring";
3107
+ runtime.accessTokenFingerprint = fingerprintAccessToken(creds.accessToken);
2387
3108
  runtime.authoritativeBinding = binding;
2388
- runtime.client = new memoraClient_default(config2, projectId, apiKeyToUse);
3109
+ runtime.client = new memoraClient_default(config2, {
3110
+ repositoryBindingId: binding.repositoryBindingId,
3111
+ projectId
3112
+ });
2389
3113
  workspaceRoot = binding.workspaceRoot;
2390
- const environmentLog = binding.environment !== void 0 ? ` environment=${binding.environment}` : "";
2391
- process.stderr.write(
2392
- `[memoraone-mcp] registering workspace source bindingSource=${binding.bindingSource} workspaceRoot=${workspaceRoot ?? "(unset)"} m1Path=${binding.m1Path}${environmentLog}
2393
- `
2394
- );
2395
- await registerRepoSource(
2396
- runtime.client,
2397
- runtime.projectId,
2398
- binding.workspaceRoot,
2399
- runtime.ideType
2400
- );
2401
3114
  if (debugAuth) {
2402
3115
  console.error("[memoraone-mcp][auth] repo root:", binding.workspaceRoot);
2403
3116
  console.error("[memoraone-mcp][auth] project_id:", projectId);
2404
- console.error("[memoraone-mcp][auth] api_key source:", binding.apiKeySource);
3117
+ console.error("[memoraone-mcp][auth] repository_binding_id:", binding.repositoryBindingId);
3118
+ console.error("[memoraone-mcp][auth] credential source: keyring");
2405
3119
  }
2406
3120
  const bindingEnvironmentLog = binding.environment !== void 0 ? ` environment=${binding.environment}` : "";
2407
3121
  console.error(
2408
- `[memoraone-mcp] ${sessionLabel} authoritative binding: project=${binding.projectId} workspace=${binding.workspaceRoot} m1=${binding.m1Path} source=${binding.bindingSource} apiKeySource=${binding.apiKeySource}${bindingEnvironmentLog}`
3122
+ `[memoraone-mcp] ${sessionLabel} authoritative binding: binding=${binding.repositoryBindingId} project=${binding.projectId} workspace=${binding.workspaceRoot} source=${binding.bindingSource}${bindingEnvironmentLog}`
2409
3123
  );
2410
3124
  bindingReadyResolve?.(runtime.client);
2411
3125
  return server.server._oninitialize(request);
@@ -2421,37 +3135,34 @@ async function main(opts = {}) {
2421
3135
  const transport = opts.transport ?? new import_stdio.StdioServerTransport();
2422
3136
  await server.connect(transport);
2423
3137
  const activeClient = await bindingReady;
2424
- let heartbeatInterval = null;
3138
+ let ownedHeartbeat = null;
2425
3139
  const daemonSession = Boolean(opts.sessionSocket);
2426
3140
  if (config2.heartbeatEnabled && daemonSession) {
2427
3141
  console.error(
2428
3142
  `[memoraone-mcp] ${sessionLabel} defers heartbeat to daemon for project ${runtime.projectId}`
2429
3143
  );
2430
- } else if (config2.heartbeatEnabled) {
2431
- const intervalMs = resolveHeartbeatIntervalMs();
2432
- const heartbeatCtx = {
2433
- projectId: runtime.projectId,
2434
- ideType: runtime.ideType,
2435
- apiKeySource: runtime.apiKeySource,
2436
- apiKeyFingerprint: runtime.apiKeyFingerprint
2437
- };
3144
+ } else if (config2.heartbeatEnabled && runtime.authoritativeBinding) {
3145
+ ownedHeartbeat = createDaemonHeartbeat({
3146
+ binding: runtime.authoritativeBinding,
3147
+ ideType: runtime.ideType ?? config2.ideType,
3148
+ createClient: () => activeClient,
3149
+ onLog: (msg) => {
3150
+ console.error(`[memoraone-mcp][session-heartbeat] ${msg}`);
3151
+ }
3152
+ });
2438
3153
  console.error(
2439
- `[memoraone-mcp] ${sessionLabel} owns heartbeat for project ${runtime.projectId} interval=${intervalMs}ms`
3154
+ `[memoraone-mcp] ${sessionLabel} owns heartbeat for project ${runtime.projectId}`
2440
3155
  );
2441
- await sendProjectHeartbeat(activeClient, heartbeatCtx);
2442
- heartbeatInterval = setInterval(() => {
2443
- sendProjectHeartbeat(activeClient, heartbeatCtx).catch(() => {
2444
- });
2445
- }, intervalMs);
3156
+ await ownedHeartbeat.start();
2446
3157
  }
2447
3158
  const onSigInt = () => shutdown("SIGINT");
2448
3159
  const onSigTerm = () => shutdown("SIGTERM");
2449
3160
  const shutdown = (signal, exitProcess = true) => {
2450
3161
  process.off("SIGINT", onSigInt);
2451
3162
  process.off("SIGTERM", onSigTerm);
2452
- if (heartbeatInterval) {
2453
- clearInterval(heartbeatInterval);
2454
- heartbeatInterval = null;
3163
+ if (ownedHeartbeat?.isRunning()) {
3164
+ ownedHeartbeat.stop();
3165
+ ownedHeartbeat = null;
2455
3166
  if (runtime.projectId) {
2456
3167
  console.error(
2457
3168
  `[memoraone-mcp] ${sessionLabel} released session heartbeat for project ${runtime.projectId}`
@@ -2471,10 +3182,10 @@ async function main(opts = {}) {
2471
3182
  console.error("[memoraone-mcp] MCP server ready");
2472
3183
  }
2473
3184
  if (opts.sessionSocket) {
2474
- await new Promise((resolve7) => {
3185
+ await new Promise((resolve9) => {
2475
3186
  opts.sessionSocket.once("close", () => {
2476
3187
  shutdown("session closed", false);
2477
- resolve7();
3188
+ resolve9();
2478
3189
  });
2479
3190
  });
2480
3191
  }
@@ -2485,35 +3196,40 @@ var log = (msg) => {
2485
3196
  process.stderr.write(`[memoraone-mcp][daemon] ${msg}
2486
3197
  `);
2487
3198
  };
2488
- var IDLE_SHUTDOWN_MS = 5e3;
2489
- function parseProjectIdFromArgv() {
3199
+ function parseBindingIdFromArgv() {
2490
3200
  const args = process.argv.slice(2);
2491
- const idx = args.indexOf("--project-id");
3201
+ const idx = args.indexOf("--binding-id");
2492
3202
  if (idx === -1 || idx + 1 >= args.length) {
2493
- log("--project-id <uuid> required");
3203
+ log("--binding-id <mrb_\u2026> required");
2494
3204
  process.exit(1);
2495
3205
  }
2496
- return args[idx + 1];
3206
+ return assertRepositoryBindingId(args[idx + 1]);
2497
3207
  }
2498
- function parseBindingFromEnv(projectId) {
3208
+ function parseBindingFromEnv(repositoryBindingId) {
2499
3209
  const binding = decodeResolvedBinding(process.env.MEMORAONE_DAEMON_BINDING_B64);
2500
3210
  if (!binding) {
2501
3211
  log("missing MEMORAONE_DAEMON_BINDING_B64");
2502
3212
  process.exit(1);
2503
3213
  }
2504
- if (binding.projectId !== projectId) {
2505
- log(`binding project mismatch: argv=${projectId} payload=${binding.projectId}`);
3214
+ if (binding.repositoryBindingId !== repositoryBindingId) {
3215
+ log(
3216
+ `binding id mismatch: argv=${repositoryBindingId} payload=${binding.repositoryBindingId}`
3217
+ );
3218
+ process.exit(1);
3219
+ }
3220
+ if (process.env.MEMORAONE_API_KEY || process.env.MEMORA_API_KEY || process.env.MEMORAONE_ACCESS_TOKEN || process.env.MEMORAONE_REFRESH_TOKEN) {
3221
+ log("refusing to start daemon with credential env vars set");
2506
3222
  process.exit(1);
2507
3223
  }
2508
3224
  return binding;
2509
3225
  }
2510
3226
  async function ensureSocketClean(socketPath) {
2511
3227
  try {
2512
- fs7.accessSync(socketPath);
3228
+ fs11.accessSync(socketPath);
2513
3229
  } catch {
2514
3230
  return;
2515
3231
  }
2516
- return new Promise((resolve7) => {
3232
+ return new Promise((resolve9) => {
2517
3233
  const client = net.createConnection({ path: socketPath }, () => {
2518
3234
  client.destroy();
2519
3235
  log("daemon already running, exiting");
@@ -2521,32 +3237,29 @@ async function ensureSocketClean(socketPath) {
2521
3237
  });
2522
3238
  client.on("error", () => {
2523
3239
  try {
2524
- fs7.unlinkSync(socketPath);
3240
+ fs11.unlinkSync(socketPath);
2525
3241
  log("stale socket removed");
2526
3242
  } catch {
2527
3243
  }
2528
- resolve7();
3244
+ resolve9();
2529
3245
  });
2530
3246
  });
2531
3247
  }
2532
3248
  async function runDaemon() {
2533
- const projectId = parseProjectIdFromArgv();
2534
- const binding = parseBindingFromEnv(projectId);
3249
+ const repositoryBindingId = parseBindingIdFromArgv();
3250
+ const binding = parseBindingFromEnv(repositoryBindingId);
2535
3251
  const ideType = parseIdeTypeFromArgv(process.argv.slice(2)) ?? config2.ideType ?? resolveIdeTypeFromEnv();
2536
3252
  const socketPath = getBindingSocketPath(binding, process.env);
2537
3253
  let nextSessionId = 1;
2538
3254
  let activeSessions = 0;
2539
- let idleTimer = null;
2540
3255
  let shuttingDown = false;
2541
3256
  const dir = ensureBaseDir();
2542
3257
  log(`directory ensured: ${dir}`);
2543
3258
  log(
2544
- `daemon spawn hint project=${binding.projectId} workspace=${binding.workspaceRoot} m1=${binding.m1Path} source=${binding.bindingSource} apiKeySource=${binding.apiKeySource}`
3259
+ `daemon spawn hint binding=${binding.repositoryBindingId} project=${binding.projectId} workspace=${binding.workspaceRoot} source=${binding.bindingSource}`
2545
3260
  );
2546
- log("session policy: concurrent bridge sessions allowed per project daemon");
2547
- if (!isDaemonIdleShutdownAllowed()) {
2548
- log("idle shutdown disabled while daemon heartbeat is active");
2549
- }
3261
+ log("session policy: concurrent bridge sessions allowed per repository binding daemon");
3262
+ log("lifecycle: daemon exits when the last bridge client disconnects");
2550
3263
  const daemonHeartbeat = createDaemonHeartbeat({
2551
3264
  binding,
2552
3265
  ideType,
@@ -2555,8 +3268,8 @@ async function runDaemon() {
2555
3268
  await ensureSocketClean(socketPath);
2556
3269
  const cleanupSocketFile = () => {
2557
3270
  try {
2558
- if (fs7.existsSync(socketPath)) {
2559
- fs7.unlinkSync(socketPath);
3271
+ if (fs11.existsSync(socketPath)) {
3272
+ fs11.unlinkSync(socketPath);
2560
3273
  log("socket removed");
2561
3274
  }
2562
3275
  removeBindingSidecar(socketPath);
@@ -2564,12 +3277,19 @@ async function runDaemon() {
2564
3277
  log(`socket cleanup warning: ${String(err)}`);
2565
3278
  }
2566
3279
  };
2567
- const server = net.createServer(async (socket) => {
2568
- if (idleTimer) {
2569
- clearTimeout(idleTimer);
2570
- idleTimer = null;
2571
- log("idle shutdown cancelled due to new session");
3280
+ const shutdownNow = (reason) => {
3281
+ if (shuttingDown) return;
3282
+ shuttingDown = true;
3283
+ if (daemonHeartbeat.isRunning()) {
3284
+ daemonHeartbeat.stop();
2572
3285
  }
3286
+ log(`daemon shutdown: ${reason}`);
3287
+ server.close(() => {
3288
+ cleanupSocketFile();
3289
+ process.exit(0);
3290
+ });
3291
+ };
3292
+ const server = net.createServer(async (socket) => {
2573
3293
  const sessionId = nextSessionId++;
2574
3294
  activeSessions += 1;
2575
3295
  let released = false;
@@ -2579,20 +3299,7 @@ async function runDaemon() {
2579
3299
  activeSessions = Math.max(0, activeSessions - 1);
2580
3300
  log(`session=${sessionId} closed activeSessions=${activeSessions}`);
2581
3301
  if (activeSessions === 0 && !shuttingDown) {
2582
- if (!isDaemonIdleShutdownAllowed()) {
2583
- log("idle shutdown skipped (daemon heartbeat active)");
2584
- return;
2585
- }
2586
- idleTimer = setTimeout(() => {
2587
- if (activeSessions !== 0 || shuttingDown) return;
2588
- shuttingDown = true;
2589
- log(`idle timeout reached (${IDLE_SHUTDOWN_MS}ms), shutting down daemon`);
2590
- server.close(() => {
2591
- cleanupSocketFile();
2592
- process.exit(0);
2593
- });
2594
- }, IDLE_SHUTDOWN_MS);
2595
- log(`scheduled idle shutdown in ${IDLE_SHUTDOWN_MS}ms`);
3302
+ shutdownNow("no active bridge clients");
2596
3303
  }
2597
3304
  };
2598
3305
  socket.once("close", releaseActiveSession);
@@ -2622,33 +3329,17 @@ async function runDaemon() {
2622
3329
  cleanupSocketFile();
2623
3330
  process.exit(1);
2624
3331
  });
2625
- const shutdownNow = (reason) => {
2626
- if (shuttingDown) return;
2627
- shuttingDown = true;
2628
- if (idleTimer) {
2629
- clearTimeout(idleTimer);
2630
- idleTimer = null;
2631
- }
2632
- if (daemonHeartbeat.isRunning()) {
2633
- daemonHeartbeat.stop();
2634
- }
2635
- log(`daemon shutdown: ${reason}`);
2636
- server.close(() => {
2637
- cleanupSocketFile();
2638
- process.exit(0);
2639
- });
2640
- };
2641
3332
  process.on("SIGINT", () => shutdownNow("SIGINT"));
2642
3333
  process.on("SIGTERM", () => shutdownNow("SIGTERM"));
2643
3334
  process.on("exit", cleanupSocketFile);
2644
- return new Promise((resolve7) => {
3335
+ return new Promise((resolve9) => {
2645
3336
  server.listen(socketPath, () => {
2646
3337
  writeBindingSidecar(socketPath, binding, ideType ?? "");
2647
3338
  log(`daemon started, listening on ${socketPath}`);
2648
3339
  void daemonHeartbeat.start().catch((err) => {
2649
3340
  log(`daemon heartbeat start error: ${String(err)}`);
2650
3341
  });
2651
- resolve7();
3342
+ resolve9();
2652
3343
  });
2653
3344
  });
2654
3345
  }