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