@memoraone/mcp 0.1.35 → 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +2040 -547
- package/dist/daemon.cjs +1730 -1114
- package/dist/index.cjs +1654 -959
- 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
|
|
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(
|
|
48
|
+
function hashBindingIdentity(repositoryBindingId, workspaceRoot, ideType) {
|
|
49
49
|
const input = [
|
|
50
|
-
|
|
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) &&
|
|
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.
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
|
183
|
-
|
|
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
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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() &&
|
|
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
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
bindingSource:
|
|
319
|
-
|
|
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
|
-
|
|
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
|
|
345
|
-
|
|
346
|
-
|
|
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 (
|
|
353
|
-
throw new Error("[memoraone-mcp] Invalid binding
|
|
1406
|
+
if (status !== "connected" && status !== "reconnect_required" && status !== "pending") {
|
|
1407
|
+
throw new Error("[memoraone-mcp] Invalid binding status");
|
|
354
1408
|
}
|
|
355
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
1418
|
+
bindingSource: "local-binding",
|
|
1419
|
+
status
|
|
372
1420
|
};
|
|
373
1421
|
}
|
|
374
1422
|
|
|
375
1423
|
// src/bindingSidecar.ts
|
|
376
|
-
var
|
|
377
|
-
var
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
411
|
-
var
|
|
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 =
|
|
435
|
-
if (
|
|
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
|
|
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:
|
|
496
|
-
worklogEnabled:
|
|
497
|
-
heartbeatEnabled:
|
|
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
|
|
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(
|
|
667
|
-
parts.push(
|
|
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 =
|
|
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
|
|
1624
|
+
return path12.resolve(raw.trim());
|
|
714
1625
|
}
|
|
715
1626
|
function formatWorkspaceAmbiguityError(bindings) {
|
|
716
1627
|
const lines = bindings.map(
|
|
717
|
-
(b) => ` - workspace=${b.workspaceRoot}
|
|
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:
|
|
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:
|
|
737
|
-
initialize:
|
|
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(
|
|
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("
|
|
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(
|
|
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 =
|
|
792
|
-
const matchingRoot = rootsListPaths.find((root) =>
|
|
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
|
|
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 =
|
|
1076
|
-
kind:
|
|
1077
|
-
actor:
|
|
1078
|
-
identifier:
|
|
1079
|
-
id:
|
|
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:
|
|
1082
|
-
metadata:
|
|
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(
|
|
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
|
|
1153
|
-
var createFactInputSchema =
|
|
1154
|
-
content:
|
|
1155
|
-
metadata:
|
|
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(
|
|
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
|
|
1199
|
-
var addPersonalContextInputSchema =
|
|
1200
|
-
content:
|
|
1201
|
-
category:
|
|
1202
|
-
tags:
|
|
1203
|
-
scope_type:
|
|
1204
|
-
scope_id:
|
|
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
|
|
1241
|
-
var getPersonalContextInputSchema =
|
|
1242
|
-
query:
|
|
1243
|
-
scope_type:
|
|
1244
|
-
scope_id:
|
|
1245
|
-
limit:
|
|
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
|
|
2097
|
+
const path14 = buildPersonalContextPath(parsed2);
|
|
1267
2098
|
try {
|
|
1268
|
-
const result = await client.get(
|
|
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
|
|
1283
|
-
var askWithMemoryInputSchema =
|
|
1284
|
-
question:
|
|
1285
|
-
code_context:
|
|
1286
|
-
file_path:
|
|
1287
|
-
selected_text:
|
|
1288
|
-
language:
|
|
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(
|
|
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
|
|
1323
|
-
var logIntentInputSchema =
|
|
1324
|
-
intent:
|
|
1325
|
-
message:
|
|
1326
|
-
context:
|
|
1327
|
-
intent_source:
|
|
1328
|
-
run_id:
|
|
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(
|
|
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
|
|
1369
|
-
var logChangeSummaryInputSchema =
|
|
1370
|
-
summary:
|
|
1371
|
-
scope:
|
|
1372
|
-
files:
|
|
1373
|
-
stats:
|
|
1374
|
-
files:
|
|
1375
|
-
add:
|
|
1376
|
-
del:
|
|
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:
|
|
1379
|
-
run_id:
|
|
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(
|
|
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
|
-
|
|
1711
|
-
|
|
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
|
-
|
|
2266
|
+
"No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
|
|
1714
2267
|
);
|
|
1715
2268
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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
|
-
|
|
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/
|
|
1835
|
-
var
|
|
1836
|
-
|
|
1837
|
-
|
|
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
|
|
1840
|
-
const parsed2 =
|
|
1841
|
-
const
|
|
1842
|
-
if (!
|
|
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
|
-
|
|
2312
|
+
"No active repository binding. Reconnect this repository through MemoraOne Studio or run the generated connect command."
|
|
1850
2313
|
);
|
|
1851
2314
|
}
|
|
1852
|
-
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
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
|
-
|
|
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
|
|
1886
|
-
|
|
1887
|
-
|
|
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
|
|
1897
|
-
return
|
|
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"}
|
|
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
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
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
|
-
|
|
1933
|
-
|
|
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
|
-
|
|
1949
|
-
|
|
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
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1982
|
-
void
|
|
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
|
-
|
|
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((
|
|
2116
|
-
bindingReadyResolve =
|
|
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
|
-
|
|
2126
|
-
|
|
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
|
|
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
|
|
3050
|
+
const debugLog = config2.devMode || debugAuth;
|
|
2411
3051
|
const binding = await resolveSessionBindingFromInitialize(params);
|
|
2412
|
-
if (opts.daemonBindingHint &&
|
|
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
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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.
|
|
2461
|
-
runtime.
|
|
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,
|
|
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]
|
|
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:
|
|
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
|
|
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
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
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}
|
|
3154
|
+
`[memoraone-mcp] ${sessionLabel} owns heartbeat for project ${runtime.projectId}`
|
|
2515
3155
|
);
|
|
2516
|
-
await
|
|
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 (
|
|
2528
|
-
|
|
2529
|
-
|
|
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((
|
|
3185
|
+
await new Promise((resolve9) => {
|
|
2550
3186
|
opts.sessionSocket.once("close", () => {
|
|
2551
3187
|
shutdown("session closed", false);
|
|
2552
|
-
|
|
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
|
-
|
|
2564
|
-
function parseProjectIdFromArgv() {
|
|
3199
|
+
function parseBindingIdFromArgv() {
|
|
2565
3200
|
const args = process.argv.slice(2);
|
|
2566
|
-
const idx = args.indexOf("--
|
|
3201
|
+
const idx = args.indexOf("--binding-id");
|
|
2567
3202
|
if (idx === -1 || idx + 1 >= args.length) {
|
|
2568
|
-
log("--
|
|
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(
|
|
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.
|
|
2580
|
-
log(
|
|
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
|
-
|
|
3228
|
+
fs11.accessSync(socketPath);
|
|
2588
3229
|
} catch {
|
|
2589
3230
|
return;
|
|
2590
3231
|
}
|
|
2591
|
-
return new Promise((
|
|
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
|
-
|
|
3240
|
+
fs11.unlinkSync(socketPath);
|
|
2600
3241
|
log("stale socket removed");
|
|
2601
3242
|
} catch {
|
|
2602
3243
|
}
|
|
2603
|
-
|
|
3244
|
+
resolve9();
|
|
2604
3245
|
});
|
|
2605
3246
|
});
|
|
2606
3247
|
}
|
|
2607
3248
|
async function runDaemon() {
|
|
2608
|
-
const
|
|
2609
|
-
const binding = parseBindingFromEnv(
|
|
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
|
|
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
|
|
2622
|
-
|
|
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 (
|
|
2634
|
-
|
|
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
|
|
2643
|
-
if (
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
3342
|
+
resolve9();
|
|
2727
3343
|
});
|
|
2728
3344
|
});
|
|
2729
3345
|
}
|