@memoraone/mcp 0.1.35 → 0.1.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/dist/cli.cjs +2330 -637
  2. package/dist/daemon.cjs +1730 -1114
  3. package/dist/index.cjs +1654 -959
  4. package/package.json +12 -10
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,9 +45,9 @@ 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("|");
@@ -56,7 +56,7 @@ function hashBindingIdentity(projectId, workspaceRoot, ideType) {
56
56
  function bindingsMatch(a, b) {
57
57
  const envA = a.environment ?? void 0;
58
58
  const envB = b.environment ?? void 0;
59
- 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) && (a.apiKey ?? null) === (b.apiKey ?? null) && envA === envB;
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;
60
60
  }
61
61
  function formatMissingInitializeWorkspaceError(options) {
62
62
  const lines = [
@@ -112,7 +112,7 @@ function resolveBindingIdeType(env2 = process.env) {
112
112
  }
113
113
  function getBindingSocketFilename(binding, env2 = process.env) {
114
114
  const ideType = resolveBindingIdeType(env2);
115
- const hash = hashBindingIdentity(binding.projectId, binding.workspaceRoot, ideType);
115
+ const hash = hashBindingIdentity(binding.repositoryBindingId, binding.workspaceRoot, ideType);
116
116
  return `mcp-${hash}.sock`;
117
117
  }
118
118
  function getBindingSocketPath(binding, env2 = process.env) {
@@ -124,13 +124,1118 @@ function ensureBaseDir() {
124
124
  }
125
125
 
126
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
127
139
  var fs2 = __toESM(require("fs/promises"), 1);
128
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";
129
829
  var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
130
- var CANONICAL_M1_FILENAME = "memoraone.m1";
131
- function isCanonicalM1Path(m1Path) {
132
- return path3.basename(m1Path) === CANONICAL_M1_FILENAME;
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
+ };
133
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";
134
1239
  function normalizeEnvironment(raw) {
135
1240
  if (raw === void 0 || raw === null || typeof raw !== "string") {
136
1241
  return void 0;
@@ -138,63 +1243,66 @@ function normalizeEnvironment(raw) {
138
1243
  const trimmed = raw.trim();
139
1244
  return trimmed === "" ? void 0 : trimmed;
140
1245
  }
141
- function parseAndValidateM1(content, markerPath) {
142
- 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);
143
1260
  try {
144
- 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;
145
1267
  } catch {
146
- throw new Error(`[memoraone-mcp] Invalid memoraone.m1 JSON at ${markerPath}`);
147
- }
148
- const projectId = parsed2?.projectId ?? parsed2?.project_id;
149
- if (!projectId || typeof projectId !== "string") {
150
- throw new Error(`[memoraone-mcp] memoraone.m1 missing projectId at ${markerPath}`);
151
- }
152
- if (!uuidRegex.test(projectId.trim())) {
153
- throw new Error(`[memoraone-mcp] memoraone.m1 projectId is not a UUID at ${markerPath}`);
1268
+ return void 0;
154
1269
  }
155
- const apiKeyRaw = parsed2?.MEMORAONE_API_KEY ?? parsed2?.api_key;
156
- const apiKey = apiKeyRaw !== void 0 && apiKeyRaw !== null && typeof apiKeyRaw === "string" && apiKeyRaw.trim() !== "" ? apiKeyRaw.trim() : null;
157
- const environment = normalizeEnvironment(parsed2?.environment);
158
- return environment === void 0 ? { projectId: projectId.trim(), apiKey } : { projectId: projectId.trim(), apiKey, environment };
159
1270
  }
160
- async function resolveProjectIdFromExplicitM1Path() {
161
- const raw = process.env.MEMORAONE_M1_PATH;
162
- if (raw === void 0 || raw.trim() === "") {
163
- return null;
164
- }
165
- const markerPath = path3.resolve(raw);
166
- try {
167
- const content = await fs2.readFile(markerPath, "utf8");
168
- const { projectId, apiKey, environment } = parseAndValidateM1(content, markerPath);
169
- return environment === void 0 ? { projectId, apiKey, foundAt: markerPath } : { projectId, apiKey, environment, foundAt: markerPath };
170
- } catch (err) {
171
- if (err?.code === "ENOENT") {
172
- return null;
173
- }
174
- 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
+ );
175
1277
  }
176
- }
177
- async function findM1WalkingUp(workspaceRoot) {
178
- let current = path3.resolve(workspaceRoot);
179
- while (true) {
180
- const markerPath = path3.join(current, CANONICAL_M1_FILENAME);
1278
+ const bindings = [];
1279
+ for (const root of candidates) {
1280
+ await warnLegacyM1IfPresent(root);
181
1281
  try {
182
- const content = await fs2.readFile(markerPath, "utf8");
183
- const { projectId, apiKey, environment } = parseAndValidateM1(content, markerPath);
184
- const repoRoot = path3.dirname(markerPath);
185
- return environment === void 0 ? { projectId, apiKey, repoRoot, markerPath } : { projectId, apiKey, environment, repoRoot, markerPath };
1282
+ const local = await resolveLocalBinding(root);
1283
+ bindings.push(toResolvedBinding(local));
186
1284
  } catch (err) {
187
- if (err?.code !== "ENOENT") {
188
- throw err;
1285
+ if (err instanceof ReconnectRequiredError) {
1286
+ continue;
189
1287
  }
1288
+ throw err;
190
1289
  }
191
- const parent = path3.dirname(current);
192
- if (parent === current) {
193
- break;
194
- }
195
- current = parent;
196
1290
  }
197
- 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];
198
1306
  }
199
1307
  function normalizeWorkspaceSearchRoots(workspaceRoot) {
200
1308
  if (workspaceRoot === void 0) {
@@ -204,14 +1312,10 @@ function normalizeWorkspaceSearchRoots(workspaceRoot) {
204
1312
  const seen = /* @__PURE__ */ new Set();
205
1313
  const out = [];
206
1314
  for (const raw of list) {
207
- if (raw === void 0) {
208
- continue;
209
- }
1315
+ if (raw === void 0) continue;
210
1316
  const trimmed = String(raw).trim();
211
- if (trimmed === "") {
212
- continue;
213
- }
214
- const resolved = path3.resolve(trimmed);
1317
+ if (trimmed === "") continue;
1318
+ const resolved = path9.resolve(trimmed);
215
1319
  if (!seen.has(resolved)) {
216
1320
  seen.add(resolved);
217
1321
  out.push(resolved);
@@ -219,104 +1323,39 @@ function normalizeWorkspaceSearchRoots(workspaceRoot) {
219
1323
  }
220
1324
  return out;
221
1325
  }
222
- function resolveApiKeyWithSource(fileApiKey) {
223
- const envApiKey = process.env.MEMORAONE_API_KEY?.trim();
224
- if (envApiKey) {
225
- return { apiKey: envApiKey, apiKeySource: "env" };
226
- }
227
- const aliasEnvApiKey = process.env.MEMORA_API_KEY?.trim();
228
- if (aliasEnvApiKey) {
229
- return { apiKey: aliasEnvApiKey, apiKeySource: "env" };
230
- }
231
- if (fileApiKey) {
232
- return { apiKey: fileApiKey, apiKeySource: "memoraone.m1" };
233
- }
234
- return { apiKey: null, apiKeySource: "none" };
235
- }
236
- async function resolveAuthoritativeBinding(workspaceRoot, options = {}) {
237
- const respectExplicitM1Path = options.respectExplicitM1Path !== false;
238
- if (respectExplicitM1Path) {
239
- const explicitBinding = await resolveProjectIdFromExplicitM1Path();
240
- if (explicitBinding) {
241
- const resolved = resolveApiKeyWithSource(explicitBinding.apiKey);
242
- return {
243
- projectId: explicitBinding.projectId,
244
- workspaceRoot: path3.dirname(explicitBinding.foundAt),
245
- m1Path: explicitBinding.foundAt,
246
- apiKey: resolved.apiKey,
247
- ...explicitBinding.environment !== void 0 ? { environment: explicitBinding.environment } : {},
248
- bindingSource: "explicit-m1-path",
249
- apiKeySource: resolved.apiKeySource
250
- };
251
- }
252
- }
253
- const candidates = normalizeWorkspaceSearchRoots(workspaceRoot);
254
- if (candidates.length === 0) {
255
- throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
256
- }
257
- const bindings = [];
258
- for (const root of candidates) {
259
- const binding = await findM1WalkingUp(root);
260
- if (binding) {
261
- const resolved = resolveApiKeyWithSource(binding.apiKey);
262
- bindings.push({
263
- projectId: binding.projectId,
264
- workspaceRoot: binding.repoRoot,
265
- m1Path: binding.markerPath,
266
- apiKey: resolved.apiKey,
267
- ...binding.environment !== void 0 ? { environment: binding.environment } : {},
268
- bindingSource: "workspace-search",
269
- apiKeySource: resolved.apiKeySource
270
- });
271
- }
272
- }
273
- if (bindings.length === 0) {
274
- throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
275
- }
276
- const distinctProjectIds = new Set(bindings.map((b) => b.projectId));
277
- if (distinctProjectIds.size > 1) {
278
- const lines = bindings.map(
279
- (b) => ` - workspace=${b.workspaceRoot} project=${b.projectId} m1=${b.m1Path}`
280
- );
281
- throw new Error(
282
- "[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."
283
- );
284
- }
285
- return bindings[0];
286
- }
287
1326
  function bindingRelevantValuesMatch(a, b) {
288
1327
  const envA = a.environment ?? void 0;
289
1328
  const envB = b.environment ?? void 0;
290
- return a.projectId.trim().toLowerCase() === b.projectId.trim().toLowerCase() && path3.resolve(a.workspaceRoot) === path3.resolve(b.workspaceRoot) && path3.resolve(a.m1Path) === path3.resolve(b.m1Path) && (a.apiKey ?? null) === (b.apiKey ?? null) && envA === envB;
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;
291
1330
  }
292
1331
  async function reconcileResolvedBindingWithDisk(cached) {
293
- const m1Path = path3.resolve(cached.m1Path);
294
- if (cached.bindingSource !== "explicit-m1-path" && !isCanonicalM1Path(m1Path)) {
295
- throw new Error(
296
- `[memoraone-mcp] Cached binding m1Path is not the canonical ${CANONICAL_M1_FILENAME}: ${m1Path}`
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>`
297
1337
  );
298
1338
  }
299
- let content;
300
- try {
301
- content = await fs2.readFile(m1Path, "utf8");
302
- } catch (err) {
303
- if (err?.code === "ENOENT") {
304
- throw new Error(
305
- `[memoraone-mcp] Cached binding file missing at ${m1Path}. Open a folder containing ${CANONICAL_M1_FILENAME}.`
306
- );
307
- }
308
- throw err;
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>"
1344
+ );
1345
+ }
1346
+ if (record.status === "reconnect_required" || !record.projectId) {
1347
+ throw new ReconnectRequiredError(
1348
+ "[memoraone-mcp] Installation requires reconnect. Run: memoraone-mcp connect <code>"
1349
+ );
309
1350
  }
310
- const parsed2 = parseAndValidateM1(content, m1Path);
311
- const resolved = resolveApiKeyWithSource(parsed2.apiKey);
312
1351
  const fresh = {
313
- projectId: parsed2.projectId,
314
- workspaceRoot: path3.resolve(path3.dirname(m1Path)),
315
- m1Path,
316
- apiKey: resolved.apiKey,
317
- ...parsed2.environment !== void 0 ? { environment: parsed2.environment } : {},
318
- bindingSource: cached.bindingSource,
319
- apiKeySource: resolved.apiKeySource
1352
+ repositoryBindingId,
1353
+ projectId: record.projectId,
1354
+ workspaceRoot,
1355
+ installationPublicId: record.installationPublicId,
1356
+ environment: record.environment,
1357
+ bindingSource: "local-binding",
1358
+ status: record.status
320
1359
  };
321
1360
  if (bindingRelevantValuesMatch(cached, fresh)) {
322
1361
  return { binding: fresh, cacheRefreshed: false };
@@ -324,7 +1363,16 @@ async function reconcileResolvedBindingWithDisk(cached) {
324
1363
  return { binding: fresh, cacheRefreshed: true };
325
1364
  }
326
1365
  function encodeResolvedBinding(binding) {
327
- 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");
328
1376
  }
329
1377
  function decodeResolvedBinding(value) {
330
1378
  if (!value) {
@@ -336,45 +1384,45 @@ function decodeResolvedBinding(value) {
336
1384
  } catch {
337
1385
  throw new Error("[memoraone-mcp] Invalid encoded binding payload");
338
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;
339
1393
  const projectId = parsed2?.projectId;
340
1394
  const workspaceRoot = parsed2?.workspaceRoot;
341
- const m1Path = parsed2?.m1Path;
342
- const apiKey = parsed2?.apiKey;
343
1395
  const environment = normalizeEnvironment(parsed2?.environment);
344
- const bindingSource = parsed2?.bindingSource;
345
- const apiKeySource = parsed2?.apiKeySource;
346
- 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())) {
347
1401
  throw new Error("[memoraone-mcp] Invalid binding projectId");
348
1402
  }
349
1403
  if (!workspaceRoot || typeof workspaceRoot !== "string") {
350
1404
  throw new Error("[memoraone-mcp] Invalid binding workspaceRoot");
351
1405
  }
352
- if (!m1Path || typeof m1Path !== "string") {
353
- throw new Error("[memoraone-mcp] Invalid binding m1Path");
1406
+ if (status !== "connected" && status !== "reconnect_required" && status !== "pending") {
1407
+ throw new Error("[memoraone-mcp] Invalid binding status");
354
1408
  }
355
- if (apiKey !== null && apiKey !== void 0 && typeof apiKey !== "string") {
356
- throw new Error("[memoraone-mcp] Invalid binding apiKey");
357
- }
358
- if (bindingSource !== "explicit-m1-path" && bindingSource !== "workspace-search") {
1409
+ if (parsed2?.bindingSource !== "local-binding") {
359
1410
  throw new Error("[memoraone-mcp] Invalid binding source");
360
1411
  }
361
- if (apiKeySource !== "env" && apiKeySource !== "memoraone.m1" && apiKeySource !== "none") {
362
- throw new Error("[memoraone-mcp] Invalid binding apiKeySource");
363
- }
364
1412
  return {
1413
+ repositoryBindingId: String(repositoryBindingId),
365
1414
  projectId: projectId.trim(),
366
1415
  workspaceRoot,
367
- m1Path,
368
- apiKey: typeof apiKey === "string" && apiKey.trim() !== "" ? apiKey.trim() : null,
1416
+ installationPublicId: typeof parsed2?.installationPublicId === "string" ? parsed2.installationPublicId : void 0,
369
1417
  ...environment !== void 0 ? { environment } : {},
370
- bindingSource,
371
- apiKeySource
1418
+ bindingSource: "local-binding",
1419
+ status
372
1420
  };
373
1421
  }
374
1422
 
375
1423
  // src/bindingSidecar.ts
376
- var fs3 = __toESM(require("fs"), 1);
377
- var path4 = __toESM(require("path"), 1);
1424
+ var fs8 = __toESM(require("fs"), 1);
1425
+ var path10 = __toESM(require("path"), 1);
378
1426
  function bindingSidecarPath(socketPath) {
379
1427
  if (socketPath.endsWith(".sock")) {
380
1428
  return `${socketPath.slice(0, -".sock".length)}.binding.json`;
@@ -383,19 +1431,26 @@ function bindingSidecarPath(socketPath) {
383
1431
  }
384
1432
  function writeBindingSidecar(socketPath, binding, ideType = resolveBindingIdeType()) {
385
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
+ }
386
1437
  const record = {
387
- v: 2,
1438
+ v: 3,
388
1439
  ...ideType ? { ideType } : {},
1440
+ repositoryBindingId: binding.repositoryBindingId,
389
1441
  projectId: binding.projectId,
390
1442
  workspaceRoot: binding.workspaceRoot,
391
- m1Path: binding.m1Path,
392
1443
  binding: payload
393
1444
  };
394
- 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");
395
1450
  }
396
1451
  function removeBindingSidecar(socketPath) {
397
1452
  try {
398
- fs3.unlinkSync(bindingSidecarPath(socketPath));
1453
+ fs8.unlinkSync(bindingSidecarPath(socketPath));
399
1454
  } catch {
400
1455
  }
401
1456
  }
@@ -407,8 +1462,8 @@ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
407
1462
 
408
1463
  // src/config.ts
409
1464
  var process2 = __toESM(require("process"), 1);
410
- var fs4 = __toESM(require("fs"), 1);
411
- var path5 = __toESM(require("path"), 1);
1465
+ var fs9 = __toESM(require("fs"), 1);
1466
+ var path11 = __toESM(require("path"), 1);
412
1467
  var dotenv = __toESM(require("dotenv"), 1);
413
1468
  var import_v4 = require("zod/v4");
414
1469
 
@@ -431,8 +1486,8 @@ function resolveApiUrl(env2) {
431
1486
  }
432
1487
 
433
1488
  // src/config.ts
434
- var dotenvPath = path5.resolve(process2.cwd(), ".env");
435
- if (fs4.existsSync(dotenvPath)) {
1489
+ var dotenvPath = path11.resolve(process2.cwd(), ".env");
1490
+ if (fs9.existsSync(dotenvPath)) {
436
1491
  try {
437
1492
  dotenv.config({ path: dotenvPath });
438
1493
  } catch (err) {
@@ -472,7 +1527,7 @@ if (!parsed.success) {
472
1527
  );
473
1528
  throw new Error("Config validation failed");
474
1529
  }
475
- var parseBooleanFlag = (value, defaultValue) => {
1530
+ var parseBooleanFlag2 = (value, defaultValue) => {
476
1531
  if (value === void 0) {
477
1532
  return defaultValue;
478
1533
  }
@@ -492,158 +1547,14 @@ var config2 = {
492
1547
  agentType: parsed.data.MEMORAONE_AGENT_TYPE ?? "agent",
493
1548
  source: parsed.data.MEMORAONE_SOURCE ?? "cursor",
494
1549
  ideType: parsed.data.MEMORAONE_IDE_TYPE,
495
- devMode: parseBooleanFlag(parsed.data.MEMORAONE_DEV_MODE, false),
496
- worklogEnabled: parseBooleanFlag(parsed.data.MEMORAONE_WORKLOG, true),
497
- 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),
498
1553
  heartbeatIntervalMs: Number.parseInt(parsed.data.MEMORAONE_HEARTBEAT_INTERVAL_MS ?? "30000", 10)
499
1554
  };
500
1555
 
501
- // src/client/memoraClient.ts
502
- var crypto2 = __toESM(require("crypto"), 1);
503
- var PROJECT_ID_HEADER = "x-project-id";
504
- var uuidRegex2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
505
- var parseBooleanFlag2 = (value) => {
506
- if (!value) {
507
- return false;
508
- }
509
- const normalized = value.trim().toLowerCase();
510
- return ["1", "true", "yes", "on"].includes(normalized);
511
- };
512
- var debugEnabled = parseBooleanFlag2(process.env.MEMORAONE_DEV_MODE);
513
- async function requestJson(url, method, headers, body) {
514
- const res = await fetch(url, {
515
- method,
516
- headers,
517
- body: method === "GET" ? void 0 : JSON.stringify(body ?? {})
518
- });
519
- const text = await res.text();
520
- return {
521
- status: res.status,
522
- statusText: res.statusText,
523
- ok: res.ok,
524
- text
525
- };
526
- }
527
- var MemoraOneHttpError = class extends Error {
528
- constructor(status, statusText, body) {
529
- super(`MemoraOne request failed: ${status} ${statusText}`);
530
- this.name = "MemoraOneHttpError";
531
- this.status = status;
532
- this.body = body;
533
- }
534
- };
535
- var MemoraClient = class {
536
- constructor(cfg, projectId, apiKey) {
537
- if (!uuidRegex2.test(projectId)) {
538
- throw new Error("[memoraone-mcp] Invalid project_id for MemoraClient");
539
- }
540
- this.baseUrl = cfg.apiUrl;
541
- this.apiKey = apiKey;
542
- this.projectId = projectId;
543
- }
544
- resolveProjectId() {
545
- const projectId = this.projectId?.trim();
546
- if (!projectId) {
547
- throw new Error(`Missing ${PROJECT_ID_HEADER}: select a project first`);
548
- }
549
- if (!uuidRegex2.test(projectId)) {
550
- throw new Error("[memoraone-mcp] Invalid project_id for request");
551
- }
552
- return projectId;
553
- }
554
- resolveApiKey() {
555
- const key = this.apiKey?.trim();
556
- if (!key) {
557
- throw new Error("[memoraone-mcp] Missing api_key for request");
558
- }
559
- return key;
560
- }
561
- buildHeaders(options) {
562
- const projectId = this.resolveProjectId();
563
- const apiKey = this.resolveApiKey();
564
- return {
565
- "content-type": "application/json",
566
- "x-api-key": apiKey,
567
- [PROJECT_ID_HEADER]: projectId,
568
- ...options?.headers ?? {}
569
- };
570
- }
571
- async post(path11, body, options) {
572
- console.error(
573
- `[memoraone-mcp][info] MemoraClient.post ENTER path=${path11}`
574
- );
575
- const nonce = crypto2.randomBytes(8).toString("hex");
576
- const url = `${this.baseUrl}${path11.startsWith("/") ? path11 : `/${path11}`}`;
577
- this.resolveProjectId();
578
- console.error(
579
- `[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_fetch method=POST url=${url}`
580
- );
581
- const res = await requestJson(url, "POST", this.buildHeaders(options), body);
582
- if (debugEnabled && options?.log !== false) {
583
- const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
584
- console.error(
585
- `[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_response_log`
586
- );
587
- const line = `[memoraone-mcp][info] http response method=POST url=${url} status=${res.status} body=${snippet}`;
588
- console.error(line);
589
- console.error(
590
- `[memoraone-mcp][info] requestJson nonce=${nonce} stage=after_response_log`
591
- );
592
- }
593
- if (!res.ok) {
594
- const accepted = options?.acceptStatuses?.includes(res.status);
595
- if (accepted) {
596
- return res.text ? JSON.parse(res.text) : null;
597
- }
598
- const quiet = options?.quietHttpStatuses?.includes(res.status);
599
- if (!quiet) {
600
- const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
601
- process.stderr.write(
602
- `[memoraone-mcp][error] http error method=POST url=${url} status=${res.status} body=${snippet}
603
- `
604
- );
605
- }
606
- throw new MemoraOneHttpError(res.status, res.statusText, res.text);
607
- }
608
- console.error(
609
- `[memoraone-mcp][info] MemoraClient.post EXIT path=${path11}`
610
- );
611
- return res.text ? JSON.parse(res.text) : null;
612
- }
613
- async get(path11, options) {
614
- const nonce = crypto2.randomBytes(8).toString("hex");
615
- const url = `${this.baseUrl}${path11.startsWith("/") ? path11 : `/${path11}`}`;
616
- this.resolveProjectId();
617
- console.error(
618
- `[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_fetch method=GET url=${url}`
619
- );
620
- const res = await requestJson(url, "GET", this.buildHeaders(options));
621
- if (debugEnabled && options?.log !== false) {
622
- const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
623
- console.error(
624
- `[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_response_log`
625
- );
626
- const line = `[memoraone-mcp][info] http response method=GET url=${url} status=${res.status} body=${snippet}`;
627
- console.error(line);
628
- console.error(
629
- `[memoraone-mcp][info] requestJson nonce=${nonce} stage=after_response_log`
630
- );
631
- }
632
- if (!res.ok) {
633
- const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
634
- process.stderr.write(
635
- `[memoraone-mcp][error] http error method=GET url=${url} status=${res.status} body=${snippet}
636
- `
637
- );
638
- throw new MemoraOneHttpError(res.status, res.statusText, res.text);
639
- }
640
- return res.text ? JSON.parse(res.text) : null;
641
- }
642
- };
643
- var memoraClient_default = MemoraClient;
644
-
645
1556
  // src/initializeBinding.ts
646
- var path6 = __toESM(require("path"), 1);
1557
+ var path12 = __toESM(require("path"), 1);
647
1558
  var import_node_url = require("url");
648
1559
  var MEMORAONE_WORKSPACE_ROOT_ENV = "MEMORAONE_WORKSPACE_ROOT";
649
1560
  function getBridgeBindingResolveOptions(env2 = process.env) {
@@ -663,8 +1574,8 @@ function getEnvWorkspaceRootCandidates() {
663
1574
  const raw = process.env.WORKSPACE_FOLDER_PATHS;
664
1575
  const parts = [];
665
1576
  if (raw !== void 0 && raw.trim() !== "") {
666
- for (const p of raw.split(path6.delimiter).map((s) => s.trim()).filter(Boolean)) {
667
- 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));
668
1579
  }
669
1580
  }
670
1581
  parts.push(process.cwd());
@@ -688,7 +1599,7 @@ function extractWorkspaceRootsFromInitialize(params) {
688
1599
  if (uri === void 0 || uri.trim() === "") {
689
1600
  return;
690
1601
  }
691
- const resolved = path6.resolve(uriToPath(uri));
1602
+ const resolved = path12.resolve(uriToPath(uri));
692
1603
  if (!seen.has(resolved)) {
693
1604
  seen.add(resolved);
694
1605
  roots.push(resolved);
@@ -710,11 +1621,11 @@ function getRepoScopedWorkspaceHint(env2 = process.env) {
710
1621
  if (raw === void 0 || raw.trim() === "") {
711
1622
  return null;
712
1623
  }
713
- return path6.resolve(raw.trim());
1624
+ return path12.resolve(raw.trim());
714
1625
  }
715
1626
  function formatWorkspaceAmbiguityError(bindings) {
716
1627
  const lines = bindings.map(
717
- (b) => ` - workspace=${b.workspaceRoot} project=${b.projectId} m1=${b.m1Path}`
1628
+ (b) => ` - workspace=${b.workspaceRoot} binding=${b.repositoryBindingId} project=${b.projectId}`
718
1629
  );
719
1630
  return "[memoraone-mcp] Ambiguous workspace binding: multiple open roots map to different MemoraOne projects.\n" + lines.join("\n") + `
720
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.`;
@@ -722,7 +1633,7 @@ Open one repo per Cursor window, or ensure this repo's managed .cursor/mcp.json
722
1633
  function formatRepoHintInitializeMismatchError(repoHintRoot, initializeBinding) {
723
1634
  return `[memoraone-mcp] Repo-scoped workspace hint conflicts with MCP initialize workspace.
724
1635
  ${MEMORAONE_WORKSPACE_ROOT_ENV}: ${repoHintRoot}
725
- initialize: project=${initializeBinding.projectId} workspace=${initializeBinding.workspaceRoot} m1=${initializeBinding.m1Path}
1636
+ initialize: binding=${initializeBinding.repositoryBindingId} project=${initializeBinding.projectId} workspace=${initializeBinding.workspaceRoot}
726
1637
  Reload MCP in the Cursor window for this repo, or re-run setup-ide-files --cursor here if the hint is stale.`;
727
1638
  }
728
1639
  function formatRepoHintNotInRootsListError(repoHintRoot, rootsListPaths) {
@@ -733,13 +1644,16 @@ Reload MCP in the Cursor window for this repo, or re-run setup-ide-files --curso
733
1644
  }
734
1645
  function formatBindingMismatchError(daemonHint, sessionBinding) {
735
1646
  return `[memoraone-mcp] Project binding mismatch between daemon and MCP initialize workspace.
736
- daemon: project=${daemonHint.projectId} workspace=${daemonHint.workspaceRoot} m1=${daemonHint.m1Path}
737
- initialize: project=${sessionBinding.projectId} workspace=${sessionBinding.workspaceRoot} m1=${sessionBinding.m1Path}
738
- 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>`;
739
1651
  }
740
1652
  async function resolveBindingFromWorkspaceRoots(workspaceRoots, options = {}) {
741
1653
  if (workspaceRoots.length === 0) {
742
- 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
+ );
743
1657
  }
744
1658
  const bindings = [];
745
1659
  for (const root of workspaceRoots) {
@@ -750,14 +1664,16 @@ async function resolveBindingFromWorkspaceRoots(workspaceRoots, options = {}) {
750
1664
  })
751
1665
  );
752
1666
  } catch (err) {
753
- 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")) {
754
1668
  continue;
755
1669
  }
756
1670
  throw err;
757
1671
  }
758
1672
  }
759
1673
  if (bindings.length === 0) {
760
- 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
+ );
761
1677
  }
762
1678
  const distinctProjectIds = new Set(bindings.map((b) => b.projectId));
763
1679
  if (distinctProjectIds.size > 1) {
@@ -788,8 +1704,8 @@ async function resolveBindingFromInitializeParams(params, options = {}) {
788
1704
  const rootsListUris = options.rootsListUris ?? [];
789
1705
  const rootsListPaths = rootsListUris.map((uri) => uriToPath(uri)).filter(Boolean);
790
1706
  if (rootsListPaths.length > 1 && repoHint !== null) {
791
- const hintResolved = path6.resolve(repoHint);
792
- 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);
793
1709
  if (!matchingRoot) {
794
1710
  throw new Error(formatRepoHintNotInRootsListError(repoHint, rootsListPaths));
795
1711
  }
@@ -815,9 +1731,6 @@ async function resolveBindingFromInitializeParams(params, options = {}) {
815
1731
  });
816
1732
  }
817
1733
 
818
- // src/index.ts
819
- var path10 = __toESM(require("path"), 1);
820
-
821
1734
  // src/bridgeClientRoots.ts
822
1735
  var readline = __toESM(require("readline"), 1);
823
1736
  var TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
@@ -825,76 +1738,6 @@ function isInitializeDebugEnabled(env2 = process.env) {
825
1738
  return TRUTHY.has(String(env2.MEMORAONE_DEBUG_INIT ?? "").trim().toLowerCase());
826
1739
  }
827
1740
 
828
- // src/sourceRegistration.ts
829
- var path7 = __toESM(require("path"), 1);
830
- var import_node_url2 = require("url");
831
- var LOG_PREFIX = "[memoraone-mcp][source-registration]";
832
- function buildRepoSourcePayload(normalizedRepoPath, ideType) {
833
- const body = {
834
- kind: "repo",
835
- label: path7.basename(normalizedRepoPath),
836
- uri: (0, import_node_url2.pathToFileURL)(normalizedRepoPath).href
837
- };
838
- if (ideType) {
839
- body.metadata = {
840
- ide_type: ideType
841
- };
842
- }
843
- return body;
844
- }
845
- async function registerRepoSource(client, projectId, repoPath, ideType) {
846
- try {
847
- if (repoPath === void 0 || repoPath === null || String(repoPath).trim() === "") {
848
- process.stderr.write(
849
- `${LOG_PREFIX} skipped: empty repoPath (cannot register workspace)
850
- `
851
- );
852
- return;
853
- }
854
- const normalizedRepoPath = path7.resolve(String(repoPath));
855
- const body = buildRepoSourcePayload(normalizedRepoPath, ideType);
856
- const primaryPath = `/v1/projects/${projectId}/sources`;
857
- const alternatePath = `/v1/projects/${projectId}/sources/register`;
858
- process.stderr.write(
859
- `${LOG_PREFIX} registering projectId=${projectId} path=${normalizedRepoPath} ideType=${ideType ?? "(none)"}
860
- `
861
- );
862
- try {
863
- await client.post(primaryPath, body, {
864
- acceptStatuses: [409],
865
- log: false,
866
- quietHttpStatuses: [404, 405, 501]
867
- });
868
- process.stderr.write(`${LOG_PREFIX} ok: POST ${primaryPath}
869
- `);
870
- return;
871
- } catch (err) {
872
- if (err instanceof MemoraOneHttpError && (err.status === 404 || err.status === 405 || err.status === 501)) {
873
- process.stderr.write(
874
- `${LOG_PREFIX} primary route returned ${err.status}; retrying POST ${alternatePath}
875
- `
876
- );
877
- await client.post(alternatePath, body, { acceptStatuses: [409], log: false });
878
- process.stderr.write(`${LOG_PREFIX} ok: POST ${alternatePath}
879
- `);
880
- return;
881
- }
882
- throw err;
883
- }
884
- } catch (err) {
885
- const msg = String(err?.message ?? err);
886
- process.stderr.write(`${LOG_PREFIX} failed: ${msg}
887
- `);
888
- if (err instanceof MemoraOneHttpError) {
889
- const bodyStr = typeof err.body === "string" ? err.body : JSON.stringify(err.body ?? null);
890
- process.stderr.write(
891
- `${LOG_PREFIX} http status=${err.status} body=${bodyStr.length > 500 ? bodyStr.slice(0, 500) + "..." : bodyStr}
892
- `
893
- );
894
- }
895
- }
896
- }
897
-
898
1741
  // src/tools/postEvent.ts
899
1742
  var import_v42 = require("zod/v4");
900
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".';
@@ -996,21 +1839,11 @@ var logCommandShape = {
996
1839
  stats: import_v410.z.record(import_v410.z.string(), import_v410.z.any()).optional()
997
1840
  };
998
1841
 
999
- // src/tools/listProjects.ts
1000
- var listProjectsShape = {};
1001
-
1002
- // src/tools/setProject.ts
1003
- var import_v411 = require("zod/v4");
1004
- var setProjectShape = {
1005
- projectKey: import_v411.z.string().min(1).optional(),
1006
- projectId: import_v411.z.string().min(1).optional()
1007
- };
1008
-
1009
1842
  // src/tools/bindingStatus.ts
1010
1843
  var bindingStatusShape = {};
1011
1844
 
1012
1845
  // src/tools/handlers/postEvent.ts
1013
- var import_v412 = require("zod/v4");
1846
+ var import_v411 = require("zod/v4");
1014
1847
  var crypto4 = __toESM(require("crypto"), 1);
1015
1848
 
1016
1849
  // src/runContext.ts
@@ -1043,9 +1876,6 @@ function getBoundProjectId() {
1043
1876
  function setBoundProjectId(id) {
1044
1877
  getSessionContext().boundProjectId = id;
1045
1878
  }
1046
- function setBoundApiKey(key) {
1047
- getSessionContext().boundApiKey = key;
1048
- }
1049
1879
  function getCurrentRunId() {
1050
1880
  return getSessionContext().currentRunId;
1051
1881
  }
@@ -1058,9 +1888,6 @@ function getCurrentProjectId() {
1058
1888
  function setCurrentProjectId(id) {
1059
1889
  getSessionContext().currentProjectId = id;
1060
1890
  }
1061
- function setCurrentApiKey(key) {
1062
- getSessionContext().currentApiKey = key;
1063
- }
1064
1891
  function resolveRunId(passed) {
1065
1892
  if (passed) {
1066
1893
  return passed;
@@ -1072,14 +1899,14 @@ function generateRunId() {
1072
1899
  }
1073
1900
 
1074
1901
  // src/tools/handlers/postEvent.ts
1075
- var postEventInputSchema = import_v412.z.object({
1076
- kind: import_v412.z.string().min(1),
1077
- actor: import_v412.z.object({
1078
- identifier: import_v412.z.string().min(1),
1079
- 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()
1080
1907
  }),
1081
- content: import_v412.z.record(import_v412.z.string(), import_v412.z.any()),
1082
- 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()
1083
1910
  });
1084
1911
  function buildPostEventContentFields(content) {
1085
1912
  if (typeof content.message === "string") {
@@ -1115,7 +1942,9 @@ async function handlePostEvent(client, args) {
1115
1942
  const parsed2 = postEventInputSchema.parse(args ?? {});
1116
1943
  const projectKey = getCurrentProjectId();
1117
1944
  if (!projectKey) {
1118
- 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
+ );
1119
1948
  }
1120
1949
  const content = parsed2.content ?? {};
1121
1950
  const { message, new_value } = buildPostEventContentFields(content);
@@ -1149,16 +1978,18 @@ async function handlePostEvent(client, args) {
1149
1978
  }
1150
1979
 
1151
1980
  // src/tools/handlers/createFact.ts
1152
- var import_v413 = require("zod/v4");
1153
- var createFactInputSchema = import_v413.z.object({
1154
- content: import_v413.z.string().min(1),
1155
- 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()
1156
1985
  });
1157
1986
  async function handleCreateFact(client, args) {
1158
1987
  const parsed2 = createFactInputSchema.parse(args ?? {});
1159
1988
  const projectKey = getCurrentProjectId();
1160
1989
  if (!projectKey) {
1161
- 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
+ );
1162
1993
  }
1163
1994
  const content = parsed2.content.trim();
1164
1995
  if (!content) {
@@ -1195,13 +2026,13 @@ async function handleCreateFact(client, args) {
1195
2026
  }
1196
2027
 
1197
2028
  // src/tools/handlers/addPersonalContext.ts
1198
- var import_v414 = require("zod/v4");
1199
- var addPersonalContextInputSchema = import_v414.z.object({
1200
- content: import_v414.z.string().min(1),
1201
- category: import_v414.z.string().optional(),
1202
- tags: import_v414.z.array(import_v414.z.string()).optional(),
1203
- scope_type: import_v414.z.enum(["general", "project"]).optional(),
1204
- 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()
1205
2036
  });
1206
2037
  async function handleAddPersonalContext(client, args) {
1207
2038
  const parsed2 = addPersonalContextInputSchema.parse(args ?? {});
@@ -1237,12 +2068,12 @@ async function handleAddPersonalContext(client, args) {
1237
2068
  }
1238
2069
 
1239
2070
  // src/tools/handlers/getPersonalContext.ts
1240
- var import_v415 = require("zod/v4");
1241
- var getPersonalContextInputSchema = import_v415.z.object({
1242
- query: import_v415.z.string().optional(),
1243
- scope_type: import_v415.z.enum(["general", "project"]).optional(),
1244
- scope_id: import_v415.z.string().optional(),
1245
- 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()
1246
2077
  });
1247
2078
  function buildPersonalContextPath(parsed2) {
1248
2079
  const params = new URLSearchParams();
@@ -1263,9 +2094,9 @@ function buildPersonalContextPath(parsed2) {
1263
2094
  }
1264
2095
  async function handleGetPersonalContext(client, args) {
1265
2096
  const parsed2 = getPersonalContextInputSchema.parse(args ?? {});
1266
- const path11 = buildPersonalContextPath(parsed2);
2097
+ const path14 = buildPersonalContextPath(parsed2);
1267
2098
  try {
1268
- const result = await client.get(path11);
2099
+ const result = await client.get(path14);
1269
2100
  return { ok: true, result };
1270
2101
  } catch (err) {
1271
2102
  if (err instanceof MemoraOneHttpError) {
@@ -1279,13 +2110,13 @@ async function handleGetPersonalContext(client, args) {
1279
2110
  }
1280
2111
 
1281
2112
  // src/tools/handlers/askWithMemory.ts
1282
- var import_v416 = require("zod/v4");
1283
- var askWithMemoryInputSchema = import_v416.z.object({
1284
- question: import_v416.z.string().min(1),
1285
- code_context: import_v416.z.object({
1286
- file_path: import_v416.z.string().optional(),
1287
- selected_text: import_v416.z.string().optional(),
1288
- 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()
1289
2120
  }).optional()
1290
2121
  });
1291
2122
  function isAskWithMemoryResponse(value) {
@@ -1295,7 +2126,9 @@ async function handleAskWithMemory(client, args) {
1295
2126
  const parsed2 = askWithMemoryInputSchema.parse(args ?? {});
1296
2127
  const projectKey = getCurrentProjectId();
1297
2128
  if (!projectKey) {
1298
- 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
+ );
1299
2132
  }
1300
2133
  const payload = {
1301
2134
  question: parsed2.question,
@@ -1319,19 +2152,21 @@ async function handleAskWithMemory(client, args) {
1319
2152
  }
1320
2153
 
1321
2154
  // src/tools/handlers/logIntent.ts
1322
- var import_v417 = require("zod/v4");
1323
- var logIntentInputSchema = import_v417.z.object({
1324
- intent: import_v417.z.enum(["task", "decision"]),
1325
- message: import_v417.z.string().min(1),
1326
- context: import_v417.z.record(import_v417.z.string(), import_v417.z.any()).optional(),
1327
- intent_source: import_v417.z.string().optional().default("cursor_chat"),
1328
- 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()
1329
2162
  });
1330
2163
  async function handleLogIntent(client, args) {
1331
2164
  const parsed2 = logIntentInputSchema.parse(args ?? {});
1332
2165
  const projectKey = getCurrentProjectId();
1333
2166
  if (!projectKey) {
1334
- 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
+ );
1335
2170
  }
1336
2171
  const intent = parsed2.intent;
1337
2172
  const message = parsed2.message.trim();
@@ -1365,24 +2200,26 @@ async function handleLogIntent(client, args) {
1365
2200
  }
1366
2201
 
1367
2202
  // src/tools/handlers/logChangeSummary.ts
1368
- var import_v418 = require("zod/v4");
1369
- var logChangeSummaryInputSchema = import_v418.z.object({
1370
- summary: import_v418.z.string().min(1),
1371
- scope: import_v418.z.string().min(1).optional(),
1372
- files: import_v418.z.array(import_v418.z.string().min(1)).optional(),
1373
- stats: import_v418.z.object({
1374
- files: import_v418.z.number().int().nonnegative().optional(),
1375
- add: import_v418.z.number().int().nonnegative().optional(),
1376
- 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()
1377
2212
  }).optional(),
1378
- commit: import_v418.z.string().min(1).optional(),
1379
- 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()
1380
2215
  });
1381
2216
  async function handleLogChangeSummary(client, args) {
1382
2217
  const parsed2 = logChangeSummaryInputSchema.parse(args ?? {});
1383
2218
  const projectKey = getCurrentProjectId();
1384
2219
  if (!projectKey) {
1385
- throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
2220
+ throw new Error(
2221
+ "No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
2222
+ );
1386
2223
  }
1387
2224
  const { summary, scope, files, stats, commit } = parsed2;
1388
2225
  const message = summary.startsWith("CHANGE:") ? summary : `CHANGE: ${scope ?? "code"} \u2014 ${summary}`;
@@ -1396,482 +2233,127 @@ async function handleLogChangeSummary(client, args) {
1396
2233
  metadata: {
1397
2234
  source: config2.source,
1398
2235
  purpose: "change_summary",
1399
- tool: "memora_log_change_summary",
1400
- ...scope ? { scope } : {},
1401
- ...files ? { files } : {},
1402
- ...stats ? { stats } : {},
1403
- ...commit ? { commit } : {},
1404
- ...run_id ? { run_id } : {}
1405
- }
1406
- };
1407
- await client.post("/timeline/events", body);
1408
- return { ok: true };
1409
- }
1410
-
1411
- // src/tools/handlers/logToolResult.ts
1412
- var import_v419 = require("zod/v4");
1413
- var logToolResultInputSchema = import_v419.z.object({
1414
- tool: import_v419.z.string().min(1),
1415
- status: import_v419.z.enum(["ok", "error", "partial"]),
1416
- summary: import_v419.z.string().min(1),
1417
- run_id: import_v419.z.string().min(1).optional(),
1418
- duration_ms: import_v419.z.number().int().nonnegative().optional(),
1419
- error_code: import_v419.z.string().min(1).optional(),
1420
- error_message: import_v419.z.string().min(1).optional(),
1421
- error_kind: import_v419.z.enum(["infra", "logic", "auth", "rate_limit", "validation", "unknown"]).optional(),
1422
- stats: import_v419.z.record(import_v419.z.string(), import_v419.z.any()).optional()
1423
- });
1424
- async function handleLogToolResult(client, args) {
1425
- const parsed2 = logToolResultInputSchema.parse(args ?? {});
1426
- const projectKey = getCurrentProjectId();
1427
- if (!projectKey) {
1428
- throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
1429
- }
1430
- const { tool, status, summary, duration_ms, error_code, error_message, error_kind, stats } = parsed2;
1431
- const message = summary.startsWith("RESULT:") ? summary : `RESULT: ${tool} \u2014 ${status} \u2014 ${summary}`;
1432
- const run_id = resolveRunId(parsed2.run_id);
1433
- const body = {
1434
- kind: "note",
1435
- concept: "concept:tool_result",
1436
- actor: { type: config2.agentType, name: config2.agentName },
1437
- message,
1438
- projectKey,
1439
- metadata: {
1440
- source: config2.source,
1441
- purpose: "tool_result",
1442
- tool: "memora_log_tool_result",
1443
- tool_name: tool,
1444
- status,
1445
- ...run_id ? { run_id } : {},
1446
- ...duration_ms ? { duration_ms } : {},
1447
- ...error_code ? { error_code } : {},
1448
- ...error_message ? { error_message } : {},
1449
- ...error_kind ? { error_kind } : {},
1450
- ...stats ? { stats } : {}
1451
- }
1452
- };
1453
- await client.post("/timeline/events", body);
1454
- return { ok: true };
1455
- }
1456
-
1457
- // src/tools/handlers/logCommand.ts
1458
- var import_v420 = require("zod/v4");
1459
- var logCommandInputSchema = import_v420.z.object({
1460
- cmd: import_v420.z.string().min(1),
1461
- summary: import_v420.z.string().min(1),
1462
- cwd: import_v420.z.string().min(1).optional(),
1463
- exit_code: import_v420.z.number().int().optional(),
1464
- duration_ms: import_v420.z.number().int().nonnegative().optional(),
1465
- run_id: import_v420.z.string().min(1).optional(),
1466
- stats: import_v420.z.record(import_v420.z.string(), import_v420.z.any()).optional()
1467
- });
1468
- async function handleLogCommand(client, args) {
1469
- const parsed2 = logCommandInputSchema.parse(args ?? {});
1470
- const projectKey = getCurrentProjectId();
1471
- if (!projectKey) {
1472
- throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
1473
- }
1474
- const { cmd, summary, cwd: cwd2, exit_code, duration_ms, stats } = parsed2;
1475
- const message = summary.startsWith("COMMAND:") ? summary : `COMMAND: ${cmd} \u2014 ${summary}`;
1476
- const run_id = resolveRunId(parsed2.run_id);
1477
- const body = {
1478
- kind: "note",
1479
- concept: "concept:command",
1480
- actor: { type: config2.agentType, name: config2.agentName },
1481
- message,
1482
- projectKey,
1483
- metadata: {
1484
- source: config2.source,
1485
- purpose: "command",
1486
- tool: "memora_log_command",
1487
- cmd,
1488
- ...cwd2 ? { cwd: cwd2 } : {},
1489
- ...exit_code !== void 0 ? { exit_code } : {},
1490
- ...duration_ms !== void 0 ? { duration_ms } : {},
1491
- ...run_id ? { run_id } : {},
1492
- ...stats ? { stats } : {}
1493
- }
1494
- };
1495
- await client.post("/timeline/events", body);
1496
- return { ok: true };
1497
- }
1498
-
1499
- // src/tools/handlers/listProjects.ts
1500
- async function handleListProjects(client) {
1501
- const res = await client.get("/v1/projects");
1502
- return res ?? { items: [] };
1503
- }
1504
-
1505
- // src/tools/handlers/setProject.ts
1506
- var import_v421 = require("zod/v4");
1507
-
1508
- // src/repoFingerprint.ts
1509
- var fs5 = __toESM(require("fs"), 1);
1510
- var path8 = __toESM(require("path"), 1);
1511
- var crypto5 = __toESM(require("crypto"), 1);
1512
- var parseBooleanFlag3 = (value) => {
1513
- if (!value) {
1514
- return false;
1515
- }
1516
- const normalized = value.trim().toLowerCase();
1517
- return ["1", "true", "yes", "on"].includes(normalized);
1518
- };
1519
- var debugEnabled2 = parseBooleanFlag3(process.env.MEMORAONE_DEV_MODE);
1520
- var debugLog = (message) => {
1521
- if (!debugEnabled2) {
1522
- return;
1523
- }
1524
- process.stderr.write(`[memoraone-mcp][debug] ${message}
1525
- `);
1526
- };
1527
- var normalizeRemoteUrl = (remoteUrl) => {
1528
- let normalized = remoteUrl.trim();
1529
- normalized = normalized.replace(/^[a-z]+:\/\//i, "");
1530
- normalized = normalized.replace(/^git@([^:]+):/i, "$1/");
1531
- normalized = normalized.replace(/\.git$/i, "");
1532
- normalized = normalized.replace(/\/+$/, "");
1533
- return normalized.toLowerCase();
1534
- };
1535
- var sha256 = (value) => {
1536
- return crypto5.createHash("sha256").update(value).digest("hex");
1537
- };
1538
- var resolveGitDir = (gitPath) => {
1539
- try {
1540
- const stat2 = fs5.statSync(gitPath);
1541
- if (stat2.isDirectory()) {
1542
- return gitPath;
1543
- }
1544
- if (stat2.isFile()) {
1545
- const content = fs5.readFileSync(gitPath, "utf8");
1546
- const match = content.match(/^gitdir:\s*(.+)$/m);
1547
- if (match) {
1548
- const gitDir = match[1].trim();
1549
- return path8.resolve(path8.dirname(gitPath), gitDir);
1550
- }
1551
- }
1552
- } catch {
1553
- return null;
1554
- }
1555
- return null;
1556
- };
1557
- var findGitRoot = (start) => {
1558
- let current = path8.resolve(start);
1559
- while (true) {
1560
- const gitPath = path8.join(current, ".git");
1561
- if (fs5.existsSync(gitPath)) {
1562
- const gitDir = resolveGitDir(gitPath);
1563
- if (gitDir) {
1564
- return { gitRoot: current, gitDir };
1565
- }
1566
- }
1567
- const parent = path8.dirname(current);
1568
- if (parent === current) {
1569
- break;
1570
- }
1571
- current = parent;
1572
- }
1573
- return null;
1574
- };
1575
- var readOriginRemote = (gitDir) => {
1576
- const configPath = path8.join(gitDir, "config");
1577
- try {
1578
- const content = fs5.readFileSync(configPath, "utf8");
1579
- const lines = content.split(/\r?\n/);
1580
- let inOrigin = false;
1581
- for (const line of lines) {
1582
- const sectionMatch = line.match(/^\s*\[(.+)]\s*$/);
1583
- if (sectionMatch) {
1584
- inOrigin = sectionMatch[1].trim() === 'remote "origin"';
1585
- continue;
1586
- }
1587
- if (inOrigin) {
1588
- const urlMatch = line.match(/^\s*url\s*=\s*(.+)\s*$/);
1589
- if (urlMatch) {
1590
- return urlMatch[1].trim();
1591
- }
1592
- }
1593
- }
1594
- } catch {
1595
- return null;
1596
- }
1597
- return null;
1598
- };
1599
- function resolveRepoFingerprint(cwd2) {
1600
- const found = findGitRoot(cwd2);
1601
- if (!found) {
1602
- const fallbackPath = path8.resolve(cwd2);
1603
- const fingerprint2 = sha256(fallbackPath);
1604
- debugLog(`repo fingerprint=${fingerprint2} source=path-fallback`);
1605
- return {
1606
- fingerprint: fingerprint2,
1607
- gitRoot: fallbackPath,
1608
- source: "path-fallback"
1609
- };
1610
- }
1611
- const { gitRoot, gitDir } = found;
1612
- const remoteUrl = readOriginRemote(gitDir);
1613
- if (remoteUrl) {
1614
- const normalized = normalizeRemoteUrl(remoteUrl);
1615
- const fingerprint2 = sha256(normalized);
1616
- debugLog(`repo fingerprint=${fingerprint2} source=git-remote`);
1617
- return {
1618
- fingerprint: fingerprint2,
1619
- gitRoot,
1620
- remoteUrl,
1621
- source: "git-remote"
1622
- };
1623
- }
1624
- const fingerprint = sha256(path8.resolve(gitRoot));
1625
- debugLog(`repo fingerprint=${fingerprint} source=path-fallback`);
1626
- return {
1627
- fingerprint,
1628
- gitRoot,
1629
- source: "path-fallback"
1630
- };
1631
- }
1632
-
1633
- // src/workspaceMap.ts
1634
- var fs6 = __toESM(require("fs/promises"), 1);
1635
- var path9 = __toESM(require("path"), 1);
1636
- var import_node_os = __toESM(require("os"), 1);
1637
- var parseBooleanFlag4 = (value) => {
1638
- if (!value) {
1639
- return false;
1640
- }
1641
- const normalized = value.trim().toLowerCase();
1642
- return ["1", "true", "yes", "on"].includes(normalized);
1643
- };
1644
- var debugEnabled3 = parseBooleanFlag4(process.env.MEMORAONE_DEV_MODE);
1645
- var debugLog2 = (message) => {
1646
- if (!debugEnabled3) {
1647
- return;
1648
- }
1649
- process.stderr.write(`[memoraone-mcp][debug] ${message}
1650
- `);
1651
- };
1652
- var fingerprintRegex = /^[0-9a-f]{64}$/i;
1653
- function getWorkspaceMapPath() {
1654
- return path9.join(import_node_os.default.homedir(), ".memoraone", "workspaces.json");
1655
- }
1656
- var ensureWorkspaceDir = async () => {
1657
- const dir = path9.dirname(getWorkspaceMapPath());
1658
- await fs6.mkdir(dir, { recursive: true });
1659
- };
1660
- async function acquireWorkspaceMapLock() {
1661
- const filePath = getWorkspaceMapPath();
1662
- const lockPath = `${filePath}.lock`;
1663
- const maxRetries = 10;
1664
- const retryDelayMs = 50;
1665
- const maxLockAgeMs = 5e3;
1666
- await ensureWorkspaceDir();
1667
- let lockAcquired = false;
1668
- let retries = 0;
1669
- while (!lockAcquired && retries < maxRetries) {
1670
- try {
1671
- try {
1672
- const stat2 = await fs6.stat(lockPath);
1673
- const ageMs = Date.now() - stat2.mtimeMs;
1674
- if (ageMs > maxLockAgeMs) {
1675
- await fs6.unlink(lockPath);
1676
- debugLog2(`removed stale workspace map lock (age: ${ageMs}ms)`);
1677
- }
1678
- } catch (err) {
1679
- if (err?.code !== "ENOENT") {
1680
- throw err;
1681
- }
1682
- }
1683
- const fd = await fs6.open(lockPath, "wx");
1684
- await fd.close();
1685
- lockAcquired = true;
1686
- } catch (err) {
1687
- if (err?.code === "EEXIST") {
1688
- retries++;
1689
- if (retries < maxRetries) {
1690
- await new Promise((resolve8) => setTimeout(resolve8, retryDelayMs));
1691
- continue;
1692
- }
1693
- throw new Error(
1694
- `[memoraone-mcp] Failed to acquire workspace map lock after ${maxRetries} retries`
1695
- );
1696
- }
1697
- throw err;
1698
- }
1699
- }
1700
- return async () => {
1701
- try {
1702
- await fs6.unlink(lockPath);
1703
- } catch (err) {
1704
- if (err?.code !== "ENOENT") {
1705
- debugLog2(`failed to release workspace map lock: ${String(err)}`);
1706
- }
2236
+ tool: "memora_log_change_summary",
2237
+ ...scope ? { scope } : {},
2238
+ ...files ? { files } : {},
2239
+ ...stats ? { stats } : {},
2240
+ ...commit ? { commit } : {},
2241
+ ...run_id ? { run_id } : {}
1707
2242
  }
1708
2243
  };
2244
+ await client.post("/timeline/events", body);
2245
+ return { ok: true };
1709
2246
  }
1710
- var validateWorkspaceMap = (map, filePath) => {
1711
- if (!map || typeof map !== "object" || Array.isArray(map)) {
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) {
1712
2265
  throw new Error(
1713
- `[memoraone-mcp] Invalid workspace map schema in ${filePath}`
2266
+ "No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
1714
2267
  );
1715
2268
  }
1716
- for (const [fingerprint, entry] of Object.entries(map)) {
1717
- if (!fingerprintRegex.test(fingerprint)) {
1718
- throw new Error(
1719
- `[memoraone-mcp] Invalid workspace fingerprint in ${filePath}`
1720
- );
1721
- }
1722
- if (typeof entry === "string") {
1723
- if (!entry.trim()) {
1724
- throw new Error(
1725
- `[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
1726
- );
1727
- }
1728
- continue;
1729
- }
1730
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1731
- throw new Error(
1732
- `[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
1733
- );
1734
- }
1735
- const projectKey = entry.projectKey ?? entry.project_id;
1736
- if (!projectKey || !projectKey.trim()) {
1737
- throw new Error(
1738
- `[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
1739
- );
1740
- }
1741
- const source = entry.source;
1742
- if (source !== void 0 && typeof source !== "string") {
1743
- throw new Error(
1744
- `[memoraone-mcp] Invalid workspace source in ${filePath}`
1745
- );
1746
- }
1747
- const linkedAt = entry.linked_at;
1748
- if (linkedAt !== void 0 && typeof linkedAt !== "string") {
1749
- throw new Error(
1750
- `[memoraone-mcp] Invalid workspace linked_at in ${filePath}`
1751
- );
1752
- }
1753
- }
1754
- };
1755
- async function readWorkspaceMap() {
1756
- const filePath = getWorkspaceMapPath();
1757
- try {
1758
- const content = await fs6.readFile(filePath, "utf8");
1759
- const parsed2 = JSON.parse(content);
1760
- validateWorkspaceMap(parsed2, filePath);
1761
- const typed = parsed2;
1762
- let migrated = false;
1763
- const normalized = {};
1764
- for (const [fingerprint, entry] of Object.entries(typed)) {
1765
- if (typeof entry === "string") {
1766
- normalized[fingerprint] = entry;
1767
- continue;
1768
- }
1769
- const projectKey = entry.projectKey ?? entry.project_id ?? "";
1770
- if (entry.project_id && !entry.projectKey) {
1771
- migrated = true;
1772
- }
1773
- normalized[fingerprint] = {
1774
- ...projectKey ? { projectKey } : {},
1775
- ...entry.source ? { source: entry.source } : {},
1776
- ...entry.linked_at ? { linked_at: entry.linked_at } : {}
1777
- };
1778
- }
1779
- debugLog2(
1780
- `workspace map loaded path=${filePath} entries=${Object.keys(normalized).length}`
1781
- );
1782
- return { map: normalized, needsMigration: migrated };
1783
- } catch (err) {
1784
- if (err?.code === "ENOENT") {
1785
- const emptyMap = {};
1786
- debugLog2(`workspace map loaded path=${filePath} entries=0`);
1787
- return { map: emptyMap, needsMigration: false };
1788
- }
1789
- if (err instanceof SyntaxError) {
1790
- throw new Error(
1791
- `[memoraone-mcp] Failed to parse workspace map at ${filePath}`
1792
- );
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 } : {}
1793
2290
  }
1794
- throw err;
1795
- }
1796
- }
1797
- async function writeWorkspaceMap(map) {
1798
- const filePath = getWorkspaceMapPath();
1799
- validateWorkspaceMap(map, filePath);
1800
- await ensureWorkspaceDir();
1801
- const tempPath = `${filePath}.tmp`;
1802
- const content = JSON.stringify(map, null, 2);
1803
- await fs6.writeFile(tempPath, content, "utf8");
1804
- await fs6.rename(tempPath, filePath);
1805
- }
1806
- async function setProjectIdForFingerprint(args) {
1807
- const { fingerprint, projectKey, source, linked_at } = args;
1808
- if (!fingerprintRegex.test(fingerprint)) {
1809
- throw new Error("[memoraone-mcp] Invalid fingerprint");
1810
- }
1811
- if (!projectKey.trim()) {
1812
- throw new Error("[memoraone-mcp] Invalid projectKey");
1813
- }
1814
- const releaseLock = await acquireWorkspaceMapLock();
1815
- try {
1816
- const { map, needsMigration } = await readWorkspaceMap();
1817
- if (needsMigration) {
1818
- await writeWorkspaceMap(map);
1819
- }
1820
- map[fingerprint] = {
1821
- projectKey,
1822
- ...source ? { source } : {},
1823
- linked_at: linked_at ?? (/* @__PURE__ */ new Date()).toISOString()
1824
- };
1825
- await writeWorkspaceMap(map);
1826
- debugLog2(
1827
- `workspace map set fingerprint=${fingerprint} projectKey=${projectKey}`
1828
- );
1829
- } finally {
1830
- await releaseLock();
1831
- }
2291
+ };
2292
+ await client.post("/timeline/events", body);
2293
+ return { ok: true };
1832
2294
  }
1833
2295
 
1834
- // src/tools/handlers/setProject.ts
1835
- var setProjectInputSchema = import_v421.z.object({
1836
- projectKey: import_v421.z.string().min(1).optional(),
1837
- 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()
1838
2306
  });
1839
- async function handleSetProject(args) {
1840
- const parsed2 = setProjectInputSchema.parse(args ?? {});
1841
- const resolvedProjectKey = parsed2.projectKey ?? parsed2.projectId;
1842
- if (!resolvedProjectKey) {
1843
- throw new Error("projectKey is required");
1844
- }
1845
- const requested = resolvedProjectKey.trim();
1846
- const bound = getBoundProjectId();
1847
- if (bound !== null && requested !== bound) {
2307
+ async function handleLogCommand(client, args) {
2308
+ const parsed2 = logCommandInputSchema.parse(args ?? {});
2309
+ const projectKey = getCurrentProjectId();
2310
+ if (!projectKey) {
1848
2311
  throw new Error(
1849
- `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."
1850
2313
  );
1851
2314
  }
1852
- setCurrentProjectId(resolvedProjectKey);
1853
- const repo = resolveRepoFingerprint(process.cwd());
1854
- await setProjectIdForFingerprint({
1855
- fingerprint: repo.fingerprint,
1856
- projectKey: resolvedProjectKey,
1857
- source: "manual"
1858
- });
1859
- 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 };
1860
2338
  }
1861
2339
 
1862
2340
  // src/tools/handlers/bindingStatus.ts
1863
2341
  function buildBindingStatus(binding, options = {}) {
1864
2342
  const status = {
2343
+ repositoryBindingId: binding.repositoryBindingId,
1865
2344
  projectId: binding.projectId,
1866
2345
  workspaceRoot: binding.workspaceRoot,
1867
- m1Path: binding.m1Path,
1868
2346
  bindingSource: binding.bindingSource,
1869
- apiKeySource: binding.apiKeySource,
2347
+ status: binding.status,
2348
+ credentialSource: "keyring",
1870
2349
  cacheRefreshed: options.cacheRefreshed === true
1871
2350
  };
1872
2351
  if (binding.environment !== void 0) {
1873
2352
  status.environment = binding.environment;
1874
2353
  }
2354
+ if (binding.installationPublicId !== void 0) {
2355
+ status.installationPublicId = binding.installationPublicId;
2356
+ }
1875
2357
  return status;
1876
2358
  }
1877
2359
  function handleBindingStatus(binding, options = {}) {
@@ -1882,9 +2364,24 @@ function handleBindingStatus(binding, options = {}) {
1882
2364
  }
1883
2365
 
1884
2366
  // src/heartbeat.ts
1885
- var crypto6 = __toESM(require("crypto"), 1);
1886
- function fingerprintApiKey(apiKey) {
1887
- return crypto6.createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
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")}`;
2380
+ }
2381
+
2382
+ // src/heartbeat.ts
2383
+ function fingerprintAccessToken(accessToken) {
2384
+ return crypto5.createHash("sha256").update(accessToken).digest("hex").slice(0, 12);
1888
2385
  }
1889
2386
  function isHeartbeatDebugEnabled() {
1890
2387
  const value = String(process.env.MEMORAONE_DEBUG_HEARTBEAT ?? "").trim().toLowerCase();
@@ -1893,15 +2390,80 @@ function isHeartbeatDebugEnabled() {
1893
2390
  function resolveHeartbeatIntervalMs() {
1894
2391
  return Number.isFinite(config2.heartbeatIntervalMs) ? Math.max(1e3, config2.heartbeatIntervalMs) : 3e4;
1895
2392
  }
1896
- function isDaemonIdleShutdownAllowed() {
1897
- 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}`);
1898
2460
  }
1899
2461
  async function sendProjectHeartbeat(client, ctx) {
1900
2462
  try {
1901
2463
  const pid = ctx.projectId?.trim();
1902
2464
  if (isHeartbeatDebugEnabled()) {
1903
2465
  process.stderr.write(
1904
- `[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"}
1905
2467
  `
1906
2468
  );
1907
2469
  }
@@ -1910,32 +2472,116 @@ async function sendProjectHeartbeat(client, ctx) {
1910
2472
  }
1911
2473
  const body = {};
1912
2474
  if (ctx.ideType) body.ide_type = ctx.ideType;
1913
- await client.post(`/v1/projects/${pid}/heartbeat`, body, {
1914
- log: false,
1915
- headers: {
1916
- "x-project-id": pid
1917
- }
1918
- });
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) };
1919
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
+ }
1920
2492
  process.stderr.write(
1921
- `[memoraone-mcp][info] heartbeat error (silent) ${String(err)}
2493
+ `[memoraone-mcp][info] heartbeat error (silent) ${redactSensitiveText(String(err))}
1922
2494
  `
1923
2495
  );
2496
+ return { active: null };
1924
2497
  }
1925
2498
  }
1926
2499
  function createDaemonHeartbeat(opts) {
1927
2500
  let interval = null;
1928
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();
1929
2508
  const ctx = {
1930
2509
  projectId: opts.binding.projectId,
2510
+ repositoryBindingId: opts.binding.repositoryBindingId,
1931
2511
  ideType: opts.ideType,
1932
- apiKeySource: opts.binding.apiKeySource,
1933
- apiKeyFingerprint: opts.binding.apiKey ? fingerprintApiKey(opts.binding.apiKey) : null
2512
+ sessionId,
2513
+ credentialSource: "keyring",
2514
+ accessTokenFingerprint: null
1934
2515
  };
1935
2516
  const log2 = opts.onLog ?? ((msg) => {
1936
- process.stderr.write(`[memoraone-mcp][daemon-heartbeat] ${msg}
2517
+ process.stderr.write(`[memoraone-mcp][daemon-heartbeat] ${redactSensitiveText(msg)}
1937
2518
  `);
1938
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
+ };
1939
2585
  const start = async () => {
1940
2586
  if (!config2.heartbeatEnabled) {
1941
2587
  log2("disabled by config");
@@ -1945,45 +2591,64 @@ function createDaemonHeartbeat(opts) {
1945
2591
  log2("already running (skipped duplicate start)");
1946
2592
  return;
1947
2593
  }
1948
- const apiKey = opts.binding.apiKey;
1949
- if (!apiKey) {
1950
- log2("cannot start: no api key in binding");
2594
+ if (starting) {
2595
+ log2("already starting (skipped duplicate start)");
1951
2596
  return;
1952
2597
  }
1953
- const projectId = opts.binding.projectId;
1954
- client = new memoraClient_default(config2, projectId, apiKey);
1955
- const intervalMs = resolveHeartbeatIntervalMs();
1956
- log2(
1957
- `daemon owns heartbeat for project=${projectId} ideType=${ctx.ideType ?? "unknown"} interval=${intervalMs}ms`
1958
- );
1959
- void sendProjectHeartbeat(client, ctx);
1960
- interval = setInterval(() => {
1961
- if (!client) return;
1962
- sendProjectHeartbeat(client, ctx).catch(() => {
1963
- });
1964
- }, 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
+ }
1965
2614
  };
1966
2615
  const stop = () => {
1967
2616
  if (interval) {
1968
2617
  clearInterval(interval);
1969
2618
  interval = null;
1970
2619
  }
1971
- client = null;
1972
- log2(`daemon released heartbeat for project=${opts.binding.projectId}`);
2620
+ log2(`daemon released heartbeat for binding=${opts.binding.repositoryBindingId}`);
1973
2621
  };
1974
2622
  const isRunning = () => interval !== null;
1975
2623
  const setIdeType = (ideType) => {
1976
- if (ctx.ideType === ideType) {
2624
+ const previous = ctx.ideType;
2625
+ if (previous === ideType) {
1977
2626
  return;
1978
2627
  }
1979
2628
  ctx.ideType = ideType;
1980
2629
  log2(`daemon heartbeat ideType updated to ${ideType}`);
1981
- if (client) {
1982
- void sendProjectHeartbeat(client, ctx);
2630
+ if (!announced) {
2631
+ void start();
2632
+ return;
2633
+ }
2634
+ if (client && interval) {
2635
+ void tick();
1983
2636
  }
1984
2637
  };
1985
2638
  const getIdeType = () => ctx.ideType;
1986
- 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
+ };
1987
2652
  }
1988
2653
 
1989
2654
  // src/ideType.ts
@@ -2112,8 +2777,8 @@ function registerToolWithWorklog(server, runtime, sessionContext, toolName, desc
2112
2777
  async function main(opts = {}) {
2113
2778
  let bindingReadyResolve = null;
2114
2779
  let bindingReadyReject = null;
2115
- const bindingReady = new Promise((resolve8, reject) => {
2116
- bindingReadyResolve = resolve8;
2780
+ const bindingReady = new Promise((resolve9, reject) => {
2781
+ bindingReadyResolve = resolve9;
2117
2782
  bindingReadyReject = reject;
2118
2783
  });
2119
2784
  const devMode = Boolean(config2.devMode);
@@ -2122,8 +2787,9 @@ async function main(opts = {}) {
2122
2787
  const runtime = {
2123
2788
  client: null,
2124
2789
  projectId: null,
2125
- apiKeySource: null,
2126
- apiKeyFingerprint: null,
2790
+ repositoryBindingId: null,
2791
+ credentialSource: null,
2792
+ accessTokenFingerprint: null,
2127
2793
  authoritativeBinding: null,
2128
2794
  bindingCacheRefreshed: false,
2129
2795
  ideType: void 0
@@ -2145,7 +2811,7 @@ async function main(opts = {}) {
2145
2811
  runtime.bindingCacheRefreshed = reconciled.cacheRefreshed;
2146
2812
  if (reconciled.cacheRefreshed) {
2147
2813
  console.error(
2148
- `[memoraone-mcp] refreshed stale cached binding from ${reconciled.binding.m1Path}: project=${reconciled.binding.projectId}`
2814
+ `[memoraone-mcp] refreshed stale cached binding ${reconciled.binding.repositoryBindingId}: project=${reconciled.binding.projectId}`
2149
2815
  );
2150
2816
  try {
2151
2817
  const socketPath = getBindingSocketPath(opts.daemonBindingHint);
@@ -2251,32 +2917,6 @@ async function main(opts = {}) {
2251
2917
  })
2252
2918
  );
2253
2919
  registeredToolNames.push("memora_get_personal_context");
2254
- server.tool(
2255
- "memora_list_projects",
2256
- "List projects available to the current API key",
2257
- listProjectsShape,
2258
- async () => runWithSessionContext(sessionContext, async () => {
2259
- if (!runtime.client || !runtime.projectId) return notInitializedResult;
2260
- const result = await handleListProjects(runtime.client);
2261
- return {
2262
- content: [{ type: "text", text: JSON.stringify(result) }]
2263
- };
2264
- })
2265
- );
2266
- registeredToolNames.push("memora_list_projects");
2267
- server.tool(
2268
- "memora_set_project",
2269
- "Set the current project key for subsequent tool calls",
2270
- setProjectShape,
2271
- async (args) => runWithSessionContext(sessionContext, async () => {
2272
- if (!runtime.client || !runtime.projectId) return notInitializedResult;
2273
- const result = await handleSetProject(args);
2274
- return {
2275
- content: [{ type: "text", text: JSON.stringify(result) }]
2276
- };
2277
- })
2278
- );
2279
- registeredToolNames.push("memora_set_project");
2280
2920
  server.tool(
2281
2921
  "memora_status",
2282
2922
  "Return non-secret project binding metadata for this MCP session",
@@ -2407,23 +3047,31 @@ async function main(opts = {}) {
2407
3047
  const debugAuth = ["1", "true", "yes", "on"].includes(
2408
3048
  String(process.env.MEMORAONE_DEBUG_AUTH ?? "").trim().toLowerCase()
2409
3049
  );
2410
- const debugLog3 = config2.devMode || debugAuth;
3050
+ const debugLog = config2.devMode || debugAuth;
2411
3051
  const binding = await resolveSessionBindingFromInitialize(params);
2412
- if (opts.daemonBindingHint && path10.resolve(opts.daemonBindingHint.m1Path) !== path10.resolve(binding.m1Path)) {
3052
+ if (opts.daemonBindingHint && opts.daemonBindingHint.repositoryBindingId !== binding.repositoryBindingId) {
2413
3053
  const errMsg = formatBindingMismatchError(opts.daemonBindingHint, binding);
2414
3054
  console.error(`[memoraone-mcp][ERROR] ${errMsg}`);
2415
3055
  bindingReadyReject?.(new Error(errMsg));
2416
3056
  throw new Error(errMsg);
2417
3057
  }
2418
- const apiKeyToUse = binding.apiKey;
2419
- if (!apiKeyToUse) {
2420
- throw new Error(
2421
- "[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>"
2422
3067
  );
2423
3068
  }
2424
- if (debugLog3) {
3069
+ if (debugLog) {
3070
+ console.error("[memoraone-mcp][debug] Resolved installation credentials from OS keyring");
3071
+ }
3072
+ if (binding.legacyM1WarningPath) {
2425
3073
  console.error(
2426
- "[memoraone-mcp][debug] Resolved actor key from " + (binding.apiKeySource === "env" ? "ENV" : "memoraone.m1")
3074
+ `[memoraone-mcp] warning: ignoring legacy memoraone.m1 at ${binding.legacyM1WarningPath}`
2427
3075
  );
2428
3076
  }
2429
3077
  const projectId = binding.projectId;
@@ -2431,9 +3079,8 @@ async function main(opts = {}) {
2431
3079
  if (existing !== null && existing !== projectId) {
2432
3080
  if (runtime.bindingCacheRefreshed) {
2433
3081
  setBoundProjectId(projectId);
2434
- setBoundApiKey(apiKeyToUse);
2435
3082
  console.error(
2436
- `[memoraone-mcp] ${sessionLabel} rebound to project ${projectId} after stale memoraone.m1 cache refresh (was ${existing})`
3083
+ `[memoraone-mcp] ${sessionLabel} rebound to project ${projectId} after local binding refresh (was ${existing})`
2437
3084
  );
2438
3085
  } else {
2439
3086
  const requestedRoot = binding.workspaceRoot ?? workspaceRoot ?? process.cwd();
@@ -2449,38 +3096,30 @@ async function main(opts = {}) {
2449
3096
  }
2450
3097
  if (existing === null) {
2451
3098
  setBoundProjectId(projectId);
2452
- setBoundApiKey(apiKeyToUse);
2453
3099
  console.error(
2454
3100
  `[memoraone-mcp] ${sessionLabel} bound to project ${projectId} (Option A: single-project binding)`
2455
3101
  );
2456
3102
  }
2457
3103
  setCurrentProjectId(projectId);
2458
- setCurrentApiKey(apiKeyToUse);
2459
3104
  runtime.projectId = projectId;
2460
- runtime.apiKeySource = binding.apiKeySource;
2461
- runtime.apiKeyFingerprint = fingerprintApiKey(apiKeyToUse);
3105
+ runtime.repositoryBindingId = binding.repositoryBindingId;
3106
+ runtime.credentialSource = "keyring";
3107
+ runtime.accessTokenFingerprint = fingerprintAccessToken(creds.accessToken);
2462
3108
  runtime.authoritativeBinding = binding;
2463
- runtime.client = new memoraClient_default(config2, projectId, apiKeyToUse);
3109
+ runtime.client = new memoraClient_default(config2, {
3110
+ repositoryBindingId: binding.repositoryBindingId,
3111
+ projectId
3112
+ });
2464
3113
  workspaceRoot = binding.workspaceRoot;
2465
- const environmentLog = binding.environment !== void 0 ? ` environment=${binding.environment}` : "";
2466
- process.stderr.write(
2467
- `[memoraone-mcp] registering workspace source bindingSource=${binding.bindingSource} workspaceRoot=${workspaceRoot ?? "(unset)"} m1Path=${binding.m1Path}${environmentLog}
2468
- `
2469
- );
2470
- await registerRepoSource(
2471
- runtime.client,
2472
- runtime.projectId,
2473
- binding.workspaceRoot,
2474
- runtime.ideType
2475
- );
2476
3114
  if (debugAuth) {
2477
3115
  console.error("[memoraone-mcp][auth] repo root:", binding.workspaceRoot);
2478
3116
  console.error("[memoraone-mcp][auth] project_id:", projectId);
2479
- 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");
2480
3119
  }
2481
3120
  const bindingEnvironmentLog = binding.environment !== void 0 ? ` environment=${binding.environment}` : "";
2482
3121
  console.error(
2483
- `[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}`
2484
3123
  );
2485
3124
  bindingReadyResolve?.(runtime.client);
2486
3125
  return server.server._oninitialize(request);
@@ -2496,37 +3135,34 @@ async function main(opts = {}) {
2496
3135
  const transport = opts.transport ?? new import_stdio.StdioServerTransport();
2497
3136
  await server.connect(transport);
2498
3137
  const activeClient = await bindingReady;
2499
- let heartbeatInterval = null;
3138
+ let ownedHeartbeat = null;
2500
3139
  const daemonSession = Boolean(opts.sessionSocket);
2501
3140
  if (config2.heartbeatEnabled && daemonSession) {
2502
3141
  console.error(
2503
3142
  `[memoraone-mcp] ${sessionLabel} defers heartbeat to daemon for project ${runtime.projectId}`
2504
3143
  );
2505
- } else if (config2.heartbeatEnabled) {
2506
- const intervalMs = resolveHeartbeatIntervalMs();
2507
- const heartbeatCtx = {
2508
- projectId: runtime.projectId,
2509
- ideType: runtime.ideType,
2510
- apiKeySource: runtime.apiKeySource,
2511
- apiKeyFingerprint: runtime.apiKeyFingerprint
2512
- };
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
+ });
2513
3153
  console.error(
2514
- `[memoraone-mcp] ${sessionLabel} owns heartbeat for project ${runtime.projectId} interval=${intervalMs}ms`
3154
+ `[memoraone-mcp] ${sessionLabel} owns heartbeat for project ${runtime.projectId}`
2515
3155
  );
2516
- await sendProjectHeartbeat(activeClient, heartbeatCtx);
2517
- heartbeatInterval = setInterval(() => {
2518
- sendProjectHeartbeat(activeClient, heartbeatCtx).catch(() => {
2519
- });
2520
- }, intervalMs);
3156
+ await ownedHeartbeat.start();
2521
3157
  }
2522
3158
  const onSigInt = () => shutdown("SIGINT");
2523
3159
  const onSigTerm = () => shutdown("SIGTERM");
2524
3160
  const shutdown = (signal, exitProcess = true) => {
2525
3161
  process.off("SIGINT", onSigInt);
2526
3162
  process.off("SIGTERM", onSigTerm);
2527
- if (heartbeatInterval) {
2528
- clearInterval(heartbeatInterval);
2529
- heartbeatInterval = null;
3163
+ if (ownedHeartbeat?.isRunning()) {
3164
+ ownedHeartbeat.stop();
3165
+ ownedHeartbeat = null;
2530
3166
  if (runtime.projectId) {
2531
3167
  console.error(
2532
3168
  `[memoraone-mcp] ${sessionLabel} released session heartbeat for project ${runtime.projectId}`
@@ -2546,10 +3182,10 @@ async function main(opts = {}) {
2546
3182
  console.error("[memoraone-mcp] MCP server ready");
2547
3183
  }
2548
3184
  if (opts.sessionSocket) {
2549
- await new Promise((resolve8) => {
3185
+ await new Promise((resolve9) => {
2550
3186
  opts.sessionSocket.once("close", () => {
2551
3187
  shutdown("session closed", false);
2552
- resolve8();
3188
+ resolve9();
2553
3189
  });
2554
3190
  });
2555
3191
  }
@@ -2560,35 +3196,40 @@ var log = (msg) => {
2560
3196
  process.stderr.write(`[memoraone-mcp][daemon] ${msg}
2561
3197
  `);
2562
3198
  };
2563
- var IDLE_SHUTDOWN_MS = 5e3;
2564
- function parseProjectIdFromArgv() {
3199
+ function parseBindingIdFromArgv() {
2565
3200
  const args = process.argv.slice(2);
2566
- const idx = args.indexOf("--project-id");
3201
+ const idx = args.indexOf("--binding-id");
2567
3202
  if (idx === -1 || idx + 1 >= args.length) {
2568
- log("--project-id <uuid> required");
3203
+ log("--binding-id <mrb_\u2026> required");
2569
3204
  process.exit(1);
2570
3205
  }
2571
- return args[idx + 1];
3206
+ return assertRepositoryBindingId(args[idx + 1]);
2572
3207
  }
2573
- function parseBindingFromEnv(projectId) {
3208
+ function parseBindingFromEnv(repositoryBindingId) {
2574
3209
  const binding = decodeResolvedBinding(process.env.MEMORAONE_DAEMON_BINDING_B64);
2575
3210
  if (!binding) {
2576
3211
  log("missing MEMORAONE_DAEMON_BINDING_B64");
2577
3212
  process.exit(1);
2578
3213
  }
2579
- if (binding.projectId !== projectId) {
2580
- 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");
2581
3222
  process.exit(1);
2582
3223
  }
2583
3224
  return binding;
2584
3225
  }
2585
3226
  async function ensureSocketClean(socketPath) {
2586
3227
  try {
2587
- fs7.accessSync(socketPath);
3228
+ fs11.accessSync(socketPath);
2588
3229
  } catch {
2589
3230
  return;
2590
3231
  }
2591
- return new Promise((resolve8) => {
3232
+ return new Promise((resolve9) => {
2592
3233
  const client = net.createConnection({ path: socketPath }, () => {
2593
3234
  client.destroy();
2594
3235
  log("daemon already running, exiting");
@@ -2596,32 +3237,29 @@ async function ensureSocketClean(socketPath) {
2596
3237
  });
2597
3238
  client.on("error", () => {
2598
3239
  try {
2599
- fs7.unlinkSync(socketPath);
3240
+ fs11.unlinkSync(socketPath);
2600
3241
  log("stale socket removed");
2601
3242
  } catch {
2602
3243
  }
2603
- resolve8();
3244
+ resolve9();
2604
3245
  });
2605
3246
  });
2606
3247
  }
2607
3248
  async function runDaemon() {
2608
- const projectId = parseProjectIdFromArgv();
2609
- const binding = parseBindingFromEnv(projectId);
3249
+ const repositoryBindingId = parseBindingIdFromArgv();
3250
+ const binding = parseBindingFromEnv(repositoryBindingId);
2610
3251
  const ideType = parseIdeTypeFromArgv(process.argv.slice(2)) ?? config2.ideType ?? resolveIdeTypeFromEnv();
2611
3252
  const socketPath = getBindingSocketPath(binding, process.env);
2612
3253
  let nextSessionId = 1;
2613
3254
  let activeSessions = 0;
2614
- let idleTimer = null;
2615
3255
  let shuttingDown = false;
2616
3256
  const dir = ensureBaseDir();
2617
3257
  log(`directory ensured: ${dir}`);
2618
3258
  log(
2619
- `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}`
2620
3260
  );
2621
- log("session policy: concurrent bridge sessions allowed per project daemon");
2622
- if (!isDaemonIdleShutdownAllowed()) {
2623
- log("idle shutdown disabled while daemon heartbeat is active");
2624
- }
3261
+ log("session policy: concurrent bridge sessions allowed per repository binding daemon");
3262
+ log("lifecycle: daemon exits when the last bridge client disconnects");
2625
3263
  const daemonHeartbeat = createDaemonHeartbeat({
2626
3264
  binding,
2627
3265
  ideType,
@@ -2630,8 +3268,8 @@ async function runDaemon() {
2630
3268
  await ensureSocketClean(socketPath);
2631
3269
  const cleanupSocketFile = () => {
2632
3270
  try {
2633
- if (fs7.existsSync(socketPath)) {
2634
- fs7.unlinkSync(socketPath);
3271
+ if (fs11.existsSync(socketPath)) {
3272
+ fs11.unlinkSync(socketPath);
2635
3273
  log("socket removed");
2636
3274
  }
2637
3275
  removeBindingSidecar(socketPath);
@@ -2639,12 +3277,19 @@ async function runDaemon() {
2639
3277
  log(`socket cleanup warning: ${String(err)}`);
2640
3278
  }
2641
3279
  };
2642
- const server = net.createServer(async (socket) => {
2643
- if (idleTimer) {
2644
- clearTimeout(idleTimer);
2645
- idleTimer = null;
2646
- 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();
2647
3285
  }
3286
+ log(`daemon shutdown: ${reason}`);
3287
+ server.close(() => {
3288
+ cleanupSocketFile();
3289
+ process.exit(0);
3290
+ });
3291
+ };
3292
+ const server = net.createServer(async (socket) => {
2648
3293
  const sessionId = nextSessionId++;
2649
3294
  activeSessions += 1;
2650
3295
  let released = false;
@@ -2654,20 +3299,7 @@ async function runDaemon() {
2654
3299
  activeSessions = Math.max(0, activeSessions - 1);
2655
3300
  log(`session=${sessionId} closed activeSessions=${activeSessions}`);
2656
3301
  if (activeSessions === 0 && !shuttingDown) {
2657
- if (!isDaemonIdleShutdownAllowed()) {
2658
- log("idle shutdown skipped (daemon heartbeat active)");
2659
- return;
2660
- }
2661
- idleTimer = setTimeout(() => {
2662
- if (activeSessions !== 0 || shuttingDown) return;
2663
- shuttingDown = true;
2664
- log(`idle timeout reached (${IDLE_SHUTDOWN_MS}ms), shutting down daemon`);
2665
- server.close(() => {
2666
- cleanupSocketFile();
2667
- process.exit(0);
2668
- });
2669
- }, IDLE_SHUTDOWN_MS);
2670
- log(`scheduled idle shutdown in ${IDLE_SHUTDOWN_MS}ms`);
3302
+ shutdownNow("no active bridge clients");
2671
3303
  }
2672
3304
  };
2673
3305
  socket.once("close", releaseActiveSession);
@@ -2697,33 +3329,17 @@ async function runDaemon() {
2697
3329
  cleanupSocketFile();
2698
3330
  process.exit(1);
2699
3331
  });
2700
- const shutdownNow = (reason) => {
2701
- if (shuttingDown) return;
2702
- shuttingDown = true;
2703
- if (idleTimer) {
2704
- clearTimeout(idleTimer);
2705
- idleTimer = null;
2706
- }
2707
- if (daemonHeartbeat.isRunning()) {
2708
- daemonHeartbeat.stop();
2709
- }
2710
- log(`daemon shutdown: ${reason}`);
2711
- server.close(() => {
2712
- cleanupSocketFile();
2713
- process.exit(0);
2714
- });
2715
- };
2716
3332
  process.on("SIGINT", () => shutdownNow("SIGINT"));
2717
3333
  process.on("SIGTERM", () => shutdownNow("SIGTERM"));
2718
3334
  process.on("exit", cleanupSocketFile);
2719
- return new Promise((resolve8) => {
3335
+ return new Promise((resolve9) => {
2720
3336
  server.listen(socketPath, () => {
2721
3337
  writeBindingSidecar(socketPath, binding, ideType ?? "");
2722
3338
  log(`daemon started, listening on ${socketPath}`);
2723
3339
  void daemonHeartbeat.start().catch((err) => {
2724
3340
  log(`daemon heartbeat start error: ${String(err)}`);
2725
3341
  });
2726
- resolve8();
3342
+ resolve9();
2727
3343
  });
2728
3344
  });
2729
3345
  }