@memoraone/mcp 0.1.17 → 0.1.19

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/index.cjs CHANGED
@@ -5,6 +5,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
8
12
  var __copyProps = (to, from, except, desc) => {
9
13
  if (from && typeof from === "object" || typeof from === "function") {
10
14
  for (let key of __getOwnPropNames(from))
@@ -21,26 +25,40 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
25
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
26
  mod
23
27
  ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
29
 
25
30
  // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ main: () => main
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+ var path6 = __toESM(require("path"), 1);
37
+ var crypto5 = __toESM(require("crypto"), 1);
38
+ var import_node_url2 = require("url");
26
39
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
40
+ var import_types = require("@modelcontextprotocol/sdk/types.js");
27
41
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
28
42
 
29
43
  // src/config.ts
30
44
  var process2 = __toESM(require("process"), 1);
31
45
  var fs = __toESM(require("fs"), 1);
32
46
  var path = __toESM(require("path"), 1);
33
- var import_dotenv = __toESM(require("dotenv"), 1);
47
+ var dotenv = __toESM(require("dotenv"), 1);
34
48
  var import_v4 = require("zod/v4");
35
49
 
36
50
  // src/configUtils.ts
37
- var DEFAULT_API_URL = "https://api.memoraone.com";
51
+ var DEFAULT_API_URL = "http://localhost:3001";
38
52
  var DEV_API_URL = "http://localhost:3001";
39
53
  function resolveApiUrl(env2) {
40
54
  const explicitUrl = env2.MEMORAONE_API_URL?.trim();
41
55
  if (explicitUrl) {
42
56
  return explicitUrl;
43
57
  }
58
+ const aliasUrl = env2.MEMORA_API_URL?.trim();
59
+ if (aliasUrl) {
60
+ return aliasUrl;
61
+ }
44
62
  if (env2.MEMORAONE_DEV_MODE === "1") {
45
63
  return DEV_API_URL;
46
64
  }
@@ -51,23 +69,24 @@ function resolveApiUrl(env2) {
51
69
  var dotenvPath = path.resolve(process2.cwd(), ".env");
52
70
  if (fs.existsSync(dotenvPath)) {
53
71
  try {
54
- import_dotenv.default.config({ path: dotenvPath });
72
+ dotenv.config({ path: dotenvPath });
55
73
  } catch (err) {
56
74
  process2.stderr.write("[memoraone-mcp] Failed to load .env: " + String(err) + "\n");
57
75
  }
58
76
  }
59
77
  var EnvSchema = import_v4.z.object({
60
78
  MEMORAONE_API_URL: import_v4.z.string().url().optional(),
61
- MEMORAONE_API_KEY: import_v4.z.string().min(1),
79
+ MEMORAONE_API_KEY: import_v4.z.string().min(1).optional(),
62
80
  MEMORAONE_DEV_MODE: import_v4.z.string().min(1).optional(),
63
81
  MEMORAONE_AGENT_NAME: import_v4.z.string().min(1).optional(),
64
82
  MEMORAONE_AGENT_TYPE: import_v4.z.string().min(1).optional(),
65
83
  MEMORAONE_SOURCE: import_v4.z.string().min(1).optional(),
84
+ MEMORAONE_IDE_TYPE: import_v4.z.enum(["cursor", "copilot-vscode", "jetbrains"]).optional(),
66
85
  MEMORAONE_WORKLOG: import_v4.z.string().min(1).optional(),
67
86
  MEMORAONE_HEARTBEAT: import_v4.z.string().min(1).optional(),
68
87
  MEMORAONE_HEARTBEAT_INTERVAL_MS: import_v4.z.string().min(1).optional()
69
88
  });
70
- var requiredEnvVars = ["MEMORAONE_API_KEY"];
89
+ var requiredEnvVars = [];
71
90
  var missingEnvVars = requiredEnvVars.filter((key) => {
72
91
  const value = process2.env[key];
73
92
  return value === void 0 || value.trim() === "";
@@ -101,12 +120,13 @@ var parseBooleanFlag = (value, defaultValue) => {
101
120
  }
102
121
  return defaultValue;
103
122
  };
104
- var config = {
123
+ var config2 = {
105
124
  apiUrl: resolvedApiUrl.replace(/\/+$/, ""),
106
125
  apiKey: parsed.data.MEMORAONE_API_KEY,
107
126
  agentName: parsed.data.MEMORAONE_AGENT_NAME ?? "cursor",
108
127
  agentType: parsed.data.MEMORAONE_AGENT_TYPE ?? "agent",
109
128
  source: parsed.data.MEMORAONE_SOURCE ?? "cursor",
129
+ ideType: parsed.data.MEMORAONE_IDE_TYPE,
110
130
  devMode: parseBooleanFlag(parsed.data.MEMORAONE_DEV_MODE, false),
111
131
  worklogEnabled: parseBooleanFlag(parsed.data.MEMORAONE_WORKLOG, true),
112
132
  heartbeatEnabled: parseBooleanFlag(parsed.data.MEMORAONE_HEARTBEAT, true),
@@ -114,32 +134,9 @@ var config = {
114
134
  };
115
135
 
116
136
  // src/client/memoraClient.ts
117
- var crypto2 = __toESM(require("crypto"), 1);
118
-
119
- // src/runContext.ts
120
137
  var crypto = __toESM(require("crypto"), 1);
121
- var currentRunId = null;
122
- var currentProjectId = null;
123
- function setCurrentRunId(id) {
124
- currentRunId = id;
125
- }
126
- function getCurrentProjectId() {
127
- return currentProjectId;
128
- }
129
- function setCurrentProjectId(id) {
130
- currentProjectId = id;
131
- }
132
- function resolveRunId(passed) {
133
- if (passed) {
134
- return passed;
135
- }
136
- return currentRunId;
137
- }
138
- function generateRunId() {
139
- return crypto.randomBytes(16).toString("hex");
140
- }
141
-
142
- // src/client/memoraClient.ts
138
+ var PROJECT_ID_HEADER = "x-project-id";
139
+ var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
143
140
  var parseBooleanFlag2 = (value) => {
144
141
  if (!value) {
145
142
  return false;
@@ -171,36 +168,48 @@ var MemoraOneHttpError = class extends Error {
171
168
  }
172
169
  };
173
170
  var MemoraClient = class {
174
- constructor(cfg, projectKey) {
175
- if (!projectKey?.trim()) {
176
- throw new Error("[memoraone-mcp] Invalid projectKey for MemoraClient");
171
+ constructor(cfg, projectId, apiKey) {
172
+ if (!uuidRegex.test(projectId)) {
173
+ throw new Error("[memoraone-mcp] Invalid project_id for MemoraClient");
177
174
  }
178
175
  this.baseUrl = cfg.apiUrl;
179
- this.apiKey = cfg.apiKey;
180
- this.projectKey = projectKey;
181
- }
182
- resolveProjectKey() {
183
- const selected = getCurrentProjectId();
184
- const projectKey = (selected ?? this.projectKey)?.trim();
185
- if (!projectKey) {
186
- throw new Error("Missing projectKey: select a project first");
176
+ this.apiKey = apiKey;
177
+ this.projectId = projectId;
178
+ }
179
+ resolveProjectId() {
180
+ const projectId = this.projectId?.trim();
181
+ if (!projectId) {
182
+ throw new Error(`Missing ${PROJECT_ID_HEADER}: select a project first`);
183
+ }
184
+ if (!uuidRegex.test(projectId)) {
185
+ throw new Error("[memoraone-mcp] Invalid project_id for request");
186
+ }
187
+ return projectId;
188
+ }
189
+ resolveApiKey() {
190
+ const key = this.apiKey?.trim();
191
+ if (!key) {
192
+ throw new Error("[memoraone-mcp] Missing api_key for request");
187
193
  }
188
- return projectKey;
194
+ return key;
189
195
  }
190
196
  buildHeaders(options) {
197
+ const projectId = this.resolveProjectId();
198
+ const apiKey = this.resolveApiKey();
191
199
  return {
192
200
  "content-type": "application/json",
193
- "x-api-key": this.apiKey,
201
+ "x-api-key": apiKey,
202
+ [PROJECT_ID_HEADER]: projectId,
194
203
  ...options?.headers ?? {}
195
204
  };
196
205
  }
197
- async post(path5, body, options) {
206
+ async post(path7, body, options) {
198
207
  console.error(
199
- `[memoraone-mcp][info] MemoraClient.post ENTER path=${path5}`
208
+ `[memoraone-mcp][info] MemoraClient.post ENTER path=${path7}`
200
209
  );
201
- const nonce = crypto2.randomBytes(8).toString("hex");
202
- const url = `${this.baseUrl}${path5.startsWith("/") ? path5 : `/${path5}`}`;
203
- this.resolveProjectKey();
210
+ const nonce = crypto.randomBytes(8).toString("hex");
211
+ const url = `${this.baseUrl}${path7.startsWith("/") ? path7 : `/${path7}`}`;
212
+ this.resolveProjectId();
204
213
  console.error(
205
214
  `[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_fetch method=POST url=${url}`
206
215
  );
@@ -217,22 +226,29 @@ var MemoraClient = class {
217
226
  );
218
227
  }
219
228
  if (!res.ok) {
220
- const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
221
- process.stderr.write(
222
- `[memoraone-mcp][error] http error method=POST url=${url} status=${res.status} body=${snippet}
229
+ const accepted = options?.acceptStatuses?.includes(res.status);
230
+ if (accepted) {
231
+ return res.text ? JSON.parse(res.text) : null;
232
+ }
233
+ const quiet = options?.quietHttpStatuses?.includes(res.status);
234
+ if (!quiet) {
235
+ const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
236
+ process.stderr.write(
237
+ `[memoraone-mcp][error] http error method=POST url=${url} status=${res.status} body=${snippet}
223
238
  `
224
- );
239
+ );
240
+ }
225
241
  throw new MemoraOneHttpError(res.status, res.statusText, res.text);
226
242
  }
227
243
  console.error(
228
- `[memoraone-mcp][info] MemoraClient.post EXIT path=${path5}`
244
+ `[memoraone-mcp][info] MemoraClient.post EXIT path=${path7}`
229
245
  );
230
246
  return res.text ? JSON.parse(res.text) : null;
231
247
  }
232
- async get(path5, options) {
233
- const nonce = crypto2.randomBytes(8).toString("hex");
234
- const url = `${this.baseUrl}${path5.startsWith("/") ? path5 : `/${path5}`}`;
235
- this.resolveProjectKey();
248
+ async get(path7, options) {
249
+ const nonce = crypto.randomBytes(8).toString("hex");
250
+ const url = `${this.baseUrl}${path7.startsWith("/") ? path7 : `/${path7}`}`;
251
+ this.resolveProjectId();
236
252
  console.error(
237
253
  `[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_fetch method=GET url=${url}`
238
254
  );
@@ -261,63 +277,57 @@ var MemoraClient = class {
261
277
  };
262
278
  var memoraClient_default = MemoraClient;
263
279
 
264
- // src/repoFingerprint.ts
265
- var fs2 = __toESM(require("fs"), 1);
280
+ // src/projectBinding.ts
281
+ var fs2 = __toESM(require("fs/promises"), 1);
266
282
  var path2 = __toESM(require("path"), 1);
267
- var crypto3 = __toESM(require("crypto"), 1);
268
- var parseBooleanFlag3 = (value) => {
269
- if (!value) {
270
- return false;
283
+ var uuidRegex2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
284
+ function parseAndValidateM1(content, markerPath) {
285
+ let parsed2;
286
+ try {
287
+ parsed2 = JSON.parse(content);
288
+ } catch {
289
+ throw new Error(`[memoraone-mcp] Invalid memoraone.m1 JSON at ${markerPath}`);
271
290
  }
272
- const normalized = value.trim().toLowerCase();
273
- return ["1", "true", "yes", "on"].includes(normalized);
274
- };
275
- var debugEnabled2 = parseBooleanFlag3(process.env.MEMORAONE_DEV_MODE);
276
- var debugLog = (message) => {
277
- if (!debugEnabled2) {
278
- return;
291
+ const projectId = parsed2?.projectId ?? parsed2?.project_id;
292
+ if (!projectId || typeof projectId !== "string") {
293
+ throw new Error(`[memoraone-mcp] memoraone.m1 missing projectId at ${markerPath}`);
279
294
  }
280
- process.stderr.write(`[memoraone-mcp][debug] ${message}
281
- `);
282
- };
283
- var normalizeRemoteUrl = (remoteUrl) => {
284
- let normalized = remoteUrl.trim();
285
- normalized = normalized.replace(/^[a-z]+:\/\//i, "");
286
- normalized = normalized.replace(/^git@([^:]+):/i, "$1/");
287
- normalized = normalized.replace(/\.git$/i, "");
288
- normalized = normalized.replace(/\/+$/, "");
289
- return normalized.toLowerCase();
290
- };
291
- var sha256 = (value) => {
292
- return crypto3.createHash("sha256").update(value).digest("hex");
293
- };
294
- var resolveGitDir = (gitPath) => {
295
+ if (!uuidRegex2.test(projectId.trim())) {
296
+ throw new Error(`[memoraone-mcp] memoraone.m1 projectId is not a UUID at ${markerPath}`);
297
+ }
298
+ const apiKeyRaw = parsed2?.MEMORAONE_API_KEY ?? parsed2?.api_key;
299
+ const apiKey = apiKeyRaw !== void 0 && apiKeyRaw !== null && typeof apiKeyRaw === "string" && apiKeyRaw.trim() !== "" ? apiKeyRaw.trim() : null;
300
+ return { projectId: projectId.trim(), apiKey };
301
+ }
302
+ async function resolveProjectIdFromExplicitM1Path() {
303
+ const raw = process.env.MEMORAONE_M1_PATH;
304
+ if (raw === void 0 || raw.trim() === "") {
305
+ return null;
306
+ }
307
+ const markerPath = path2.resolve(raw);
295
308
  try {
296
- const stat = fs2.statSync(gitPath);
297
- if (stat.isDirectory()) {
298
- return gitPath;
299
- }
300
- if (stat.isFile()) {
301
- const content = fs2.readFileSync(gitPath, "utf8");
302
- const match = content.match(/^gitdir:\s*(.+)$/m);
303
- if (match) {
304
- const gitDir = match[1].trim();
305
- return path2.resolve(path2.dirname(gitPath), gitDir);
306
- }
309
+ const content = await fs2.readFile(markerPath, "utf8");
310
+ const { projectId, apiKey } = parseAndValidateM1(content, markerPath);
311
+ return { projectId, apiKey, foundAt: markerPath };
312
+ } catch (err) {
313
+ if (err?.code === "ENOENT") {
314
+ return null;
307
315
  }
308
- } catch {
309
- return null;
316
+ throw err;
310
317
  }
311
- return null;
312
- };
313
- var findGitRoot = (start) => {
314
- let current = path2.resolve(start);
318
+ }
319
+ async function findM1WalkingUp(workspaceRoot) {
320
+ let current = path2.resolve(workspaceRoot);
315
321
  while (true) {
316
- const gitPath = path2.join(current, ".git");
317
- if (fs2.existsSync(gitPath)) {
318
- const gitDir = resolveGitDir(gitPath);
319
- if (gitDir) {
320
- return { gitRoot: current, gitDir };
322
+ const markerPath = path2.join(current, "memoraone.m1");
323
+ try {
324
+ const content = await fs2.readFile(markerPath, "utf8");
325
+ const { projectId, apiKey } = parseAndValidateM1(content, markerPath);
326
+ const repoRoot = path2.dirname(markerPath);
327
+ return { projectId, apiKey, repoRoot, markerPath };
328
+ } catch (err) {
329
+ if (err?.code !== "ENOENT") {
330
+ throw err;
321
331
  }
322
332
  }
323
333
  const parent = path2.dirname(current);
@@ -327,275 +337,158 @@ var findGitRoot = (start) => {
327
337
  current = parent;
328
338
  }
329
339
  return null;
330
- };
331
- var readOriginRemote = (gitDir) => {
332
- const configPath = path2.join(gitDir, "config");
333
- try {
334
- const content = fs2.readFileSync(configPath, "utf8");
335
- const lines = content.split(/\r?\n/);
336
- let inOrigin = false;
337
- for (const line of lines) {
338
- const sectionMatch = line.match(/^\s*\[(.+)]\s*$/);
339
- if (sectionMatch) {
340
- inOrigin = sectionMatch[1].trim() === 'remote "origin"';
341
- continue;
342
- }
343
- if (inOrigin) {
344
- const urlMatch = line.match(/^\s*url\s*=\s*(.+)\s*$/);
345
- if (urlMatch) {
346
- return urlMatch[1].trim();
347
- }
348
- }
340
+ }
341
+ function normalizeWorkspaceSearchRoots(workspaceRoot) {
342
+ if (workspaceRoot === void 0) {
343
+ return [];
344
+ }
345
+ const list = Array.isArray(workspaceRoot) ? workspaceRoot : [workspaceRoot];
346
+ const seen = /* @__PURE__ */ new Set();
347
+ const out = [];
348
+ for (const raw of list) {
349
+ if (raw === void 0) {
350
+ continue;
351
+ }
352
+ const trimmed = String(raw).trim();
353
+ if (trimmed === "") {
354
+ continue;
355
+ }
356
+ const resolved = path2.resolve(trimmed);
357
+ if (!seen.has(resolved)) {
358
+ seen.add(resolved);
359
+ out.push(resolved);
349
360
  }
350
- } catch {
351
- return null;
352
361
  }
353
- return null;
354
- };
355
- function resolveRepoFingerprint(cwd2) {
356
- const found = findGitRoot(cwd2);
357
- if (!found) {
358
- const fallbackPath = path2.resolve(cwd2);
359
- const fingerprint2 = sha256(fallbackPath);
360
- debugLog(`repo fingerprint=${fingerprint2} source=path-fallback`);
361
- return {
362
- fingerprint: fingerprint2,
363
- gitRoot: fallbackPath,
364
- source: "path-fallback"
365
- };
362
+ return out;
363
+ }
364
+ function resolveApiKeyWithSource(fileApiKey) {
365
+ const envApiKey = process.env.MEMORAONE_API_KEY?.trim();
366
+ if (envApiKey) {
367
+ return { apiKey: envApiKey, apiKeySource: "env" };
366
368
  }
367
- const { gitRoot, gitDir } = found;
368
- const remoteUrl = readOriginRemote(gitDir);
369
- if (remoteUrl) {
370
- const normalized = normalizeRemoteUrl(remoteUrl);
371
- const fingerprint2 = sha256(normalized);
372
- debugLog(`repo fingerprint=${fingerprint2} source=git-remote`);
369
+ const aliasEnvApiKey = process.env.MEMORA_API_KEY?.trim();
370
+ if (aliasEnvApiKey) {
371
+ return { apiKey: aliasEnvApiKey, apiKeySource: "env" };
372
+ }
373
+ if (fileApiKey) {
374
+ return { apiKey: fileApiKey, apiKeySource: "memoraone.m1" };
375
+ }
376
+ return { apiKey: null, apiKeySource: "none" };
377
+ }
378
+ async function resolveAuthoritativeBinding(workspaceRoot) {
379
+ const explicitBinding = await resolveProjectIdFromExplicitM1Path();
380
+ if (explicitBinding) {
381
+ const resolved = resolveApiKeyWithSource(explicitBinding.apiKey);
373
382
  return {
374
- fingerprint: fingerprint2,
375
- gitRoot,
376
- remoteUrl,
377
- source: "git-remote"
383
+ projectId: explicitBinding.projectId,
384
+ workspaceRoot: path2.dirname(explicitBinding.foundAt),
385
+ m1Path: explicitBinding.foundAt,
386
+ apiKey: resolved.apiKey,
387
+ bindingSource: "explicit-m1-path",
388
+ apiKeySource: resolved.apiKeySource
378
389
  };
379
390
  }
380
- const fingerprint = sha256(path2.resolve(gitRoot));
381
- debugLog(`repo fingerprint=${fingerprint} source=path-fallback`);
382
- return {
383
- fingerprint,
384
- gitRoot,
385
- source: "path-fallback"
386
- };
391
+ const candidates = normalizeWorkspaceSearchRoots(workspaceRoot);
392
+ if (candidates.length === 0) {
393
+ throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
394
+ }
395
+ for (const root of candidates) {
396
+ const binding = await findM1WalkingUp(root);
397
+ if (binding) {
398
+ const resolved = resolveApiKeyWithSource(binding.apiKey);
399
+ return {
400
+ projectId: binding.projectId,
401
+ workspaceRoot: binding.repoRoot,
402
+ m1Path: binding.markerPath,
403
+ apiKey: resolved.apiKey,
404
+ bindingSource: "workspace-search",
405
+ apiKeySource: resolved.apiKeySource
406
+ };
407
+ }
408
+ }
409
+ throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
387
410
  }
388
411
 
389
- // src/workspaceMap.ts
390
- var fs3 = __toESM(require("fs/promises"), 1);
412
+ // src/sourceRegistration.ts
391
413
  var path3 = __toESM(require("path"), 1);
392
- var import_node_os = __toESM(require("os"), 1);
393
- var parseBooleanFlag4 = (value) => {
394
- if (!value) {
395
- return false;
396
- }
397
- const normalized = value.trim().toLowerCase();
398
- return ["1", "true", "yes", "on"].includes(normalized);
399
- };
400
- var debugEnabled3 = parseBooleanFlag4(process.env.MEMORAONE_DEV_MODE);
401
- var debugLog2 = (message) => {
402
- if (!debugEnabled3) {
403
- return;
414
+ var import_node_url = require("url");
415
+ var LOG_PREFIX = "[memoraone-mcp][source-registration]";
416
+ function buildRepoSourcePayload(normalizedRepoPath, ideType) {
417
+ const body = {
418
+ kind: "repo",
419
+ label: path3.basename(normalizedRepoPath),
420
+ uri: (0, import_node_url.pathToFileURL)(normalizedRepoPath).href
421
+ };
422
+ if (ideType) {
423
+ body.metadata = {
424
+ ide_type: ideType
425
+ };
404
426
  }
405
- process.stderr.write(`[memoraone-mcp][debug] ${message}
406
- `);
407
- };
408
- var fingerprintRegex = /^[0-9a-f]{64}$/i;
409
- function getWorkspaceMapPath() {
410
- return path3.join(import_node_os.default.homedir(), ".memoraone", "workspaces.json");
427
+ return body;
411
428
  }
412
- var ensureWorkspaceDir = async () => {
413
- const dir = path3.dirname(getWorkspaceMapPath());
414
- await fs3.mkdir(dir, { recursive: true });
415
- };
416
- var validateWorkspaceMap = (map, filePath) => {
417
- if (!map || typeof map !== "object" || Array.isArray(map)) {
418
- throw new Error(
419
- `[memoraone-mcp] Invalid workspace map schema in ${filePath}`
420
- );
421
- }
422
- for (const [fingerprint, entry] of Object.entries(map)) {
423
- if (!fingerprintRegex.test(fingerprint)) {
424
- throw new Error(
425
- `[memoraone-mcp] Invalid workspace fingerprint in ${filePath}`
429
+ async function registerRepoSource(client, projectId, repoPath, ideType) {
430
+ try {
431
+ if (repoPath === void 0 || repoPath === null || String(repoPath).trim() === "") {
432
+ process.stderr.write(
433
+ `${LOG_PREFIX} skipped: empty repoPath (cannot register workspace)
434
+ `
426
435
  );
436
+ return;
427
437
  }
428
- if (typeof entry === "string") {
429
- if (!entry.trim()) {
430
- throw new Error(
431
- `[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
438
+ const normalizedRepoPath = path3.resolve(String(repoPath));
439
+ const body = buildRepoSourcePayload(normalizedRepoPath, ideType);
440
+ const primaryPath = `/v1/projects/${projectId}/sources`;
441
+ const alternatePath = `/v1/projects/${projectId}/sources/register`;
442
+ process.stderr.write(
443
+ `${LOG_PREFIX} registering projectId=${projectId} path=${normalizedRepoPath} ideType=${ideType ?? "(none)"}
444
+ `
445
+ );
446
+ try {
447
+ await client.post(primaryPath, body, {
448
+ acceptStatuses: [409],
449
+ log: false,
450
+ quietHttpStatuses: [404, 405, 501]
451
+ });
452
+ process.stderr.write(`${LOG_PREFIX} ok: POST ${primaryPath}
453
+ `);
454
+ return;
455
+ } catch (err) {
456
+ if (err instanceof MemoraOneHttpError && (err.status === 404 || err.status === 405 || err.status === 501)) {
457
+ process.stderr.write(
458
+ `${LOG_PREFIX} primary route returned ${err.status}; retrying POST ${alternatePath}
459
+ `
432
460
  );
461
+ await client.post(alternatePath, body, { acceptStatuses: [409], log: false });
462
+ process.stderr.write(`${LOG_PREFIX} ok: POST ${alternatePath}
463
+ `);
464
+ return;
433
465
  }
434
- continue;
435
- }
436
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
437
- throw new Error(
438
- `[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
439
- );
440
- }
441
- const projectKey = entry.projectKey ?? entry.project_id;
442
- if (!projectKey || !projectKey.trim()) {
443
- throw new Error(
444
- `[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
445
- );
446
- }
447
- const source = entry.source;
448
- if (source !== void 0 && typeof source !== "string") {
449
- throw new Error(
450
- `[memoraone-mcp] Invalid workspace source in ${filePath}`
451
- );
466
+ throw err;
452
467
  }
453
- const linkedAt = entry.linked_at;
454
- if (linkedAt !== void 0 && typeof linkedAt !== "string") {
455
- throw new Error(
456
- `[memoraone-mcp] Invalid workspace linked_at in ${filePath}`
468
+ } catch (err) {
469
+ const msg = String(err?.message ?? err);
470
+ process.stderr.write(`${LOG_PREFIX} failed: ${msg}
471
+ `);
472
+ if (err instanceof MemoraOneHttpError) {
473
+ const bodyStr = typeof err.body === "string" ? err.body : JSON.stringify(err.body ?? null);
474
+ process.stderr.write(
475
+ `${LOG_PREFIX} http status=${err.status} body=${bodyStr.length > 500 ? bodyStr.slice(0, 500) + "..." : bodyStr}
476
+ `
457
477
  );
458
478
  }
459
479
  }
460
- };
461
- async function readWorkspaceMap() {
462
- const filePath = getWorkspaceMapPath();
463
- try {
464
- const content = await fs3.readFile(filePath, "utf8");
465
- const parsed2 = JSON.parse(content);
466
- validateWorkspaceMap(parsed2, filePath);
467
- const typed = parsed2;
468
- let migrated = false;
469
- const normalized = {};
470
- for (const [fingerprint, entry] of Object.entries(typed)) {
471
- if (typeof entry === "string") {
472
- normalized[fingerprint] = entry;
473
- continue;
474
- }
475
- const projectKey = entry.projectKey ?? entry.project_id ?? "";
476
- if (entry.project_id && !entry.projectKey) {
477
- migrated = true;
478
- }
479
- normalized[fingerprint] = {
480
- ...projectKey ? { projectKey } : {},
481
- ...entry.source ? { source: entry.source } : {},
482
- ...entry.linked_at ? { linked_at: entry.linked_at } : {}
483
- };
484
- }
485
- if (migrated) {
486
- await writeWorkspaceMap(normalized);
487
- }
488
- debugLog2(
489
- `workspace map loaded path=${filePath} entries=${Object.keys(normalized).length}`
490
- );
491
- return normalized;
492
- } catch (err) {
493
- if (err?.code === "ENOENT") {
494
- const emptyMap = {};
495
- debugLog2(`workspace map loaded path=${filePath} entries=0`);
496
- return emptyMap;
497
- }
498
- if (err instanceof SyntaxError) {
499
- throw new Error(
500
- `[memoraone-mcp] Failed to parse workspace map at ${filePath}`
501
- );
502
- }
503
- throw err;
504
- }
505
- }
506
- async function writeWorkspaceMap(map) {
507
- const filePath = getWorkspaceMapPath();
508
- validateWorkspaceMap(map, filePath);
509
- await ensureWorkspaceDir();
510
- const tempPath = `${filePath}.tmp`;
511
- const content = JSON.stringify(map, null, 2);
512
- await fs3.writeFile(tempPath, content, "utf8");
513
- await fs3.rename(tempPath, filePath);
514
- }
515
- async function getProjectIdForFingerprint(fingerprint) {
516
- if (!fingerprintRegex.test(fingerprint)) {
517
- throw new Error("[memoraone-mcp] Invalid fingerprint");
518
- }
519
- const map = await readWorkspaceMap();
520
- const entry = map[fingerprint];
521
- if (!entry) {
522
- return { projectKey: null, source: "unknown" };
523
- }
524
- if (typeof entry === "string") {
525
- return { projectKey: entry, source: "unknown" };
526
- }
527
- return {
528
- projectKey: entry.projectKey ?? entry.project_id ?? null,
529
- source: entry.source ?? "unknown"
530
- };
531
- }
532
- async function setProjectIdForFingerprint(args) {
533
- const { fingerprint, projectKey, source, linked_at } = args;
534
- if (!fingerprintRegex.test(fingerprint)) {
535
- throw new Error("[memoraone-mcp] Invalid fingerprint");
536
- }
537
- if (!projectKey.trim()) {
538
- throw new Error("[memoraone-mcp] Invalid projectKey");
539
- }
540
- const map = await readWorkspaceMap();
541
- map[fingerprint] = {
542
- projectKey,
543
- ...source ? { source } : {},
544
- linked_at: linked_at ?? (/* @__PURE__ */ new Date()).toISOString()
545
- };
546
- await writeWorkspaceMap(map);
547
- debugLog2(
548
- `workspace map set fingerprint=${fingerprint} projectKey=${projectKey}`
549
- );
550
- }
551
-
552
- // src/projectBinding.ts
553
- var fs4 = __toESM(require("fs/promises"), 1);
554
- var path4 = __toESM(require("path"), 1);
555
- function readRepoProjectIdPath(gitRoot) {
556
- return path4.join(gitRoot, ".memoraone-project");
557
- }
558
- async function readRepoProjectId(gitRoot) {
559
- const filePath = readRepoProjectIdPath(gitRoot);
560
- try {
561
- const content = await fs4.readFile(filePath, "utf8");
562
- const value = content.trim();
563
- return value ? value : null;
564
- } catch (err) {
565
- if (err?.code === "ENOENT") {
566
- return null;
567
- }
568
- throw err;
569
- }
570
- }
571
- function resolveProjectIdOrThrow(args) {
572
- const envProjectKey = args.envProjectKey?.trim();
573
- if (envProjectKey) {
574
- return { projectKey: envProjectKey, source: "env" };
575
- }
576
- const repoFileProjectKey = args.repoFileProjectKey?.trim();
577
- if (repoFileProjectKey) {
578
- return { projectKey: repoFileProjectKey, source: "repo-file" };
579
- }
580
- const workspaceProjectKey = args.workspaceProjectKey?.trim();
581
- if (workspaceProjectKey) {
582
- return { projectKey: workspaceProjectKey, source: "workspace-map" };
583
- }
584
- throw new Error(
585
- `Repo not linked to a MemoraOne project. Set MEMORAONE_PROJECT_ID or create ${args.repoFilePath} containing a project key.`
586
- );
587
- }
588
-
589
- // src/tools/postEvent.ts
590
- var import_v42 = require("zod/v4");
591
- var postEventShape = {
592
- kind: import_v42.z.string().min(1),
593
- actor: import_v42.z.object({
594
- identifier: import_v42.z.string().min(1),
595
- id: import_v42.z.string().min(1).optional()
596
- }),
597
- content: import_v42.z.record(import_v42.z.string(), import_v42.z.any()),
598
- metadata: import_v42.z.record(import_v42.z.string(), import_v42.z.any()).optional()
480
+ }
481
+
482
+ // src/tools/postEvent.ts
483
+ var import_v42 = require("zod/v4");
484
+ var postEventShape = {
485
+ kind: import_v42.z.string().min(1),
486
+ actor: import_v42.z.object({
487
+ identifier: import_v42.z.string().min(1),
488
+ id: import_v42.z.string().min(1).optional()
489
+ }),
490
+ content: import_v42.z.record(import_v42.z.string(), import_v42.z.any()),
491
+ metadata: import_v42.z.record(import_v42.z.string(), import_v42.z.any()).optional()
599
492
  };
600
493
 
601
494
  // src/tools/askWithMemory.ts
@@ -672,7 +565,67 @@ var setProjectShape = {
672
565
 
673
566
  // src/tools/handlers/postEvent.ts
674
567
  var import_v49 = require("zod/v4");
675
- var crypto4 = __toESM(require("crypto"), 1);
568
+ var crypto3 = __toESM(require("crypto"), 1);
569
+
570
+ // src/runContext.ts
571
+ var import_node_async_hooks = require("async_hooks");
572
+ var crypto2 = __toESM(require("crypto"), 1);
573
+ var sessionContextStorage = new import_node_async_hooks.AsyncLocalStorage();
574
+ function createSessionRunContext(initial = {}) {
575
+ return {
576
+ currentRunId: null,
577
+ currentProjectId: null,
578
+ currentApiKey: null,
579
+ boundProjectId: null,
580
+ boundApiKey: null,
581
+ ...initial
582
+ };
583
+ }
584
+ function runWithSessionContext(context, fn) {
585
+ return sessionContextStorage.run(context, fn);
586
+ }
587
+ function getSessionContext() {
588
+ const context = sessionContextStorage.getStore();
589
+ if (!context) {
590
+ throw new Error("[memoraone-mcp] Session context not initialized");
591
+ }
592
+ return context;
593
+ }
594
+ function getBoundProjectId() {
595
+ return getSessionContext().boundProjectId;
596
+ }
597
+ function setBoundProjectId(id) {
598
+ getSessionContext().boundProjectId = id;
599
+ }
600
+ function setBoundApiKey(key) {
601
+ getSessionContext().boundApiKey = key;
602
+ }
603
+ function getCurrentRunId() {
604
+ return getSessionContext().currentRunId;
605
+ }
606
+ function setCurrentRunId(id) {
607
+ getSessionContext().currentRunId = id;
608
+ }
609
+ function getCurrentProjectId() {
610
+ return getSessionContext().currentProjectId;
611
+ }
612
+ function setCurrentProjectId(id) {
613
+ getSessionContext().currentProjectId = id;
614
+ }
615
+ function setCurrentApiKey(key) {
616
+ getSessionContext().currentApiKey = key;
617
+ }
618
+ function resolveRunId(passed) {
619
+ if (passed) {
620
+ return passed;
621
+ }
622
+ return getCurrentRunId();
623
+ }
624
+ function generateRunId() {
625
+ return crypto2.randomBytes(16).toString("hex");
626
+ }
627
+
628
+ // src/tools/handlers/postEvent.ts
676
629
  var postEventInputSchema = import_v49.z.object({
677
630
  kind: import_v49.z.string().min(1),
678
631
  actor: import_v49.z.object({
@@ -683,7 +636,7 @@ var postEventInputSchema = import_v49.z.object({
683
636
  metadata: import_v49.z.record(import_v49.z.string(), import_v49.z.any()).optional()
684
637
  });
685
638
  async function handlePostEvent(client, args) {
686
- const nonce = crypto4.randomBytes(8).toString("hex");
639
+ const nonce = crypto3.randomBytes(8).toString("hex");
687
640
  console.error(
688
641
  `[memoraone-mcp][debug] tool=memora_post_event toolCallId=unknown nonce=${nonce} stage=before_post`
689
642
  );
@@ -700,7 +653,7 @@ async function handlePostEvent(client, args) {
700
653
  projectKey,
701
654
  actor: {
702
655
  type: "agent",
703
- identifier: config.agentName,
656
+ identifier: config2.agentName,
704
657
  ...parsed2.actor.id ? { id: parsed2.actor.id } : {}
705
658
  },
706
659
  ...parsed2.metadata ? { metadata: parsed2.metadata } : {}
@@ -789,13 +742,13 @@ async function handleLogIntent(client, args) {
789
742
  setCurrentRunId(run_id);
790
743
  const body = {
791
744
  kind: "note",
792
- actor: { type: config.agentType, name: config.agentName },
745
+ actor: { type: config2.agentType, name: config2.agentName },
793
746
  concept,
794
747
  // MUST be TOP-LEVEL so it populates timeline_events.concept
795
748
  message,
796
749
  projectKey,
797
750
  metadata: {
798
- source: config.source,
751
+ source: config2.source,
799
752
  purpose,
800
753
  // 'task' | 'decision'
801
754
  intent_source: intent_source ?? "cursor_chat",
@@ -833,11 +786,11 @@ async function handleLogChangeSummary(client, args) {
833
786
  const body = {
834
787
  kind: "note",
835
788
  concept: "concept:change_summary",
836
- actor: { type: config.agentType, name: config.agentName },
789
+ actor: { type: config2.agentType, name: config2.agentName },
837
790
  message,
838
791
  projectKey,
839
792
  metadata: {
840
- source: config.source,
793
+ source: config2.source,
841
794
  purpose: "change_summary",
842
795
  tool: "memora_log_change_summary",
843
796
  ...scope ? { scope } : {},
@@ -876,11 +829,11 @@ async function handleLogToolResult(client, args) {
876
829
  const body = {
877
830
  kind: "note",
878
831
  concept: "concept:tool_result",
879
- actor: { type: config.agentType, name: config.agentName },
832
+ actor: { type: config2.agentType, name: config2.agentName },
880
833
  message,
881
834
  projectKey,
882
835
  metadata: {
883
- source: config.source,
836
+ source: config2.source,
884
837
  purpose: "tool_result",
885
838
  tool: "memora_log_tool_result",
886
839
  tool_name: tool,
@@ -920,11 +873,11 @@ async function handleLogCommand(client, args) {
920
873
  const body = {
921
874
  kind: "note",
922
875
  concept: "concept:command",
923
- actor: { type: config.agentType, name: config.agentName },
876
+ actor: { type: config2.agentType, name: config2.agentName },
924
877
  message,
925
878
  projectKey,
926
879
  metadata: {
927
- source: config.source,
880
+ source: config2.source,
928
881
  purpose: "command",
929
882
  tool: "memora_log_command",
930
883
  cmd,
@@ -941,12 +894,340 @@ async function handleLogCommand(client, args) {
941
894
 
942
895
  // src/tools/handlers/listProjects.ts
943
896
  async function handleListProjects(client) {
944
- const res = await client.get("/admin/projects");
897
+ const res = await client.get("/v1/projects");
945
898
  return res ?? { items: [] };
946
899
  }
947
900
 
948
901
  // src/tools/handlers/setProject.ts
949
902
  var import_v415 = require("zod/v4");
903
+
904
+ // src/repoFingerprint.ts
905
+ var fs3 = __toESM(require("fs"), 1);
906
+ var path4 = __toESM(require("path"), 1);
907
+ var crypto4 = __toESM(require("crypto"), 1);
908
+ var parseBooleanFlag3 = (value) => {
909
+ if (!value) {
910
+ return false;
911
+ }
912
+ const normalized = value.trim().toLowerCase();
913
+ return ["1", "true", "yes", "on"].includes(normalized);
914
+ };
915
+ var debugEnabled2 = parseBooleanFlag3(process.env.MEMORAONE_DEV_MODE);
916
+ var debugLog = (message) => {
917
+ if (!debugEnabled2) {
918
+ return;
919
+ }
920
+ process.stderr.write(`[memoraone-mcp][debug] ${message}
921
+ `);
922
+ };
923
+ var normalizeRemoteUrl = (remoteUrl) => {
924
+ let normalized = remoteUrl.trim();
925
+ normalized = normalized.replace(/^[a-z]+:\/\//i, "");
926
+ normalized = normalized.replace(/^git@([^:]+):/i, "$1/");
927
+ normalized = normalized.replace(/\.git$/i, "");
928
+ normalized = normalized.replace(/\/+$/, "");
929
+ return normalized.toLowerCase();
930
+ };
931
+ var sha256 = (value) => {
932
+ return crypto4.createHash("sha256").update(value).digest("hex");
933
+ };
934
+ var resolveGitDir = (gitPath) => {
935
+ try {
936
+ const stat2 = fs3.statSync(gitPath);
937
+ if (stat2.isDirectory()) {
938
+ return gitPath;
939
+ }
940
+ if (stat2.isFile()) {
941
+ const content = fs3.readFileSync(gitPath, "utf8");
942
+ const match = content.match(/^gitdir:\s*(.+)$/m);
943
+ if (match) {
944
+ const gitDir = match[1].trim();
945
+ return path4.resolve(path4.dirname(gitPath), gitDir);
946
+ }
947
+ }
948
+ } catch {
949
+ return null;
950
+ }
951
+ return null;
952
+ };
953
+ var findGitRoot = (start) => {
954
+ let current = path4.resolve(start);
955
+ while (true) {
956
+ const gitPath = path4.join(current, ".git");
957
+ if (fs3.existsSync(gitPath)) {
958
+ const gitDir = resolveGitDir(gitPath);
959
+ if (gitDir) {
960
+ return { gitRoot: current, gitDir };
961
+ }
962
+ }
963
+ const parent = path4.dirname(current);
964
+ if (parent === current) {
965
+ break;
966
+ }
967
+ current = parent;
968
+ }
969
+ return null;
970
+ };
971
+ var readOriginRemote = (gitDir) => {
972
+ const configPath = path4.join(gitDir, "config");
973
+ try {
974
+ const content = fs3.readFileSync(configPath, "utf8");
975
+ const lines = content.split(/\r?\n/);
976
+ let inOrigin = false;
977
+ for (const line of lines) {
978
+ const sectionMatch = line.match(/^\s*\[(.+)]\s*$/);
979
+ if (sectionMatch) {
980
+ inOrigin = sectionMatch[1].trim() === 'remote "origin"';
981
+ continue;
982
+ }
983
+ if (inOrigin) {
984
+ const urlMatch = line.match(/^\s*url\s*=\s*(.+)\s*$/);
985
+ if (urlMatch) {
986
+ return urlMatch[1].trim();
987
+ }
988
+ }
989
+ }
990
+ } catch {
991
+ return null;
992
+ }
993
+ return null;
994
+ };
995
+ function resolveRepoFingerprint(cwd2) {
996
+ const found = findGitRoot(cwd2);
997
+ if (!found) {
998
+ const fallbackPath = path4.resolve(cwd2);
999
+ const fingerprint2 = sha256(fallbackPath);
1000
+ debugLog(`repo fingerprint=${fingerprint2} source=path-fallback`);
1001
+ return {
1002
+ fingerprint: fingerprint2,
1003
+ gitRoot: fallbackPath,
1004
+ source: "path-fallback"
1005
+ };
1006
+ }
1007
+ const { gitRoot, gitDir } = found;
1008
+ const remoteUrl = readOriginRemote(gitDir);
1009
+ if (remoteUrl) {
1010
+ const normalized = normalizeRemoteUrl(remoteUrl);
1011
+ const fingerprint2 = sha256(normalized);
1012
+ debugLog(`repo fingerprint=${fingerprint2} source=git-remote`);
1013
+ return {
1014
+ fingerprint: fingerprint2,
1015
+ gitRoot,
1016
+ remoteUrl,
1017
+ source: "git-remote"
1018
+ };
1019
+ }
1020
+ const fingerprint = sha256(path4.resolve(gitRoot));
1021
+ debugLog(`repo fingerprint=${fingerprint} source=path-fallback`);
1022
+ return {
1023
+ fingerprint,
1024
+ gitRoot,
1025
+ source: "path-fallback"
1026
+ };
1027
+ }
1028
+
1029
+ // src/workspaceMap.ts
1030
+ var fs4 = __toESM(require("fs/promises"), 1);
1031
+ var path5 = __toESM(require("path"), 1);
1032
+ var import_node_os = __toESM(require("os"), 1);
1033
+ var parseBooleanFlag4 = (value) => {
1034
+ if (!value) {
1035
+ return false;
1036
+ }
1037
+ const normalized = value.trim().toLowerCase();
1038
+ return ["1", "true", "yes", "on"].includes(normalized);
1039
+ };
1040
+ var debugEnabled3 = parseBooleanFlag4(process.env.MEMORAONE_DEV_MODE);
1041
+ var debugLog2 = (message) => {
1042
+ if (!debugEnabled3) {
1043
+ return;
1044
+ }
1045
+ process.stderr.write(`[memoraone-mcp][debug] ${message}
1046
+ `);
1047
+ };
1048
+ var fingerprintRegex = /^[0-9a-f]{64}$/i;
1049
+ function getWorkspaceMapPath() {
1050
+ return path5.join(import_node_os.default.homedir(), ".memoraone", "workspaces.json");
1051
+ }
1052
+ var ensureWorkspaceDir = async () => {
1053
+ const dir = path5.dirname(getWorkspaceMapPath());
1054
+ await fs4.mkdir(dir, { recursive: true });
1055
+ };
1056
+ async function acquireWorkspaceMapLock() {
1057
+ const filePath = getWorkspaceMapPath();
1058
+ const lockPath = `${filePath}.lock`;
1059
+ const maxRetries = 10;
1060
+ const retryDelayMs = 50;
1061
+ const maxLockAgeMs = 5e3;
1062
+ await ensureWorkspaceDir();
1063
+ let lockAcquired = false;
1064
+ let retries = 0;
1065
+ while (!lockAcquired && retries < maxRetries) {
1066
+ try {
1067
+ try {
1068
+ const stat2 = await fs4.stat(lockPath);
1069
+ const ageMs = Date.now() - stat2.mtimeMs;
1070
+ if (ageMs > maxLockAgeMs) {
1071
+ await fs4.unlink(lockPath);
1072
+ debugLog2(`removed stale workspace map lock (age: ${ageMs}ms)`);
1073
+ }
1074
+ } catch (err) {
1075
+ if (err?.code !== "ENOENT") {
1076
+ throw err;
1077
+ }
1078
+ }
1079
+ const fd = await fs4.open(lockPath, "wx");
1080
+ await fd.close();
1081
+ lockAcquired = true;
1082
+ } catch (err) {
1083
+ if (err?.code === "EEXIST") {
1084
+ retries++;
1085
+ if (retries < maxRetries) {
1086
+ await new Promise((resolve6) => setTimeout(resolve6, retryDelayMs));
1087
+ continue;
1088
+ }
1089
+ throw new Error(
1090
+ `[memoraone-mcp] Failed to acquire workspace map lock after ${maxRetries} retries`
1091
+ );
1092
+ }
1093
+ throw err;
1094
+ }
1095
+ }
1096
+ return async () => {
1097
+ try {
1098
+ await fs4.unlink(lockPath);
1099
+ } catch (err) {
1100
+ if (err?.code !== "ENOENT") {
1101
+ debugLog2(`failed to release workspace map lock: ${String(err)}`);
1102
+ }
1103
+ }
1104
+ };
1105
+ }
1106
+ var validateWorkspaceMap = (map, filePath) => {
1107
+ if (!map || typeof map !== "object" || Array.isArray(map)) {
1108
+ throw new Error(
1109
+ `[memoraone-mcp] Invalid workspace map schema in ${filePath}`
1110
+ );
1111
+ }
1112
+ for (const [fingerprint, entry] of Object.entries(map)) {
1113
+ if (!fingerprintRegex.test(fingerprint)) {
1114
+ throw new Error(
1115
+ `[memoraone-mcp] Invalid workspace fingerprint in ${filePath}`
1116
+ );
1117
+ }
1118
+ if (typeof entry === "string") {
1119
+ if (!entry.trim()) {
1120
+ throw new Error(
1121
+ `[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
1122
+ );
1123
+ }
1124
+ continue;
1125
+ }
1126
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1127
+ throw new Error(
1128
+ `[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
1129
+ );
1130
+ }
1131
+ const projectKey = entry.projectKey ?? entry.project_id;
1132
+ if (!projectKey || !projectKey.trim()) {
1133
+ throw new Error(
1134
+ `[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
1135
+ );
1136
+ }
1137
+ const source = entry.source;
1138
+ if (source !== void 0 && typeof source !== "string") {
1139
+ throw new Error(
1140
+ `[memoraone-mcp] Invalid workspace source in ${filePath}`
1141
+ );
1142
+ }
1143
+ const linkedAt = entry.linked_at;
1144
+ if (linkedAt !== void 0 && typeof linkedAt !== "string") {
1145
+ throw new Error(
1146
+ `[memoraone-mcp] Invalid workspace linked_at in ${filePath}`
1147
+ );
1148
+ }
1149
+ }
1150
+ };
1151
+ async function readWorkspaceMap() {
1152
+ const filePath = getWorkspaceMapPath();
1153
+ try {
1154
+ const content = await fs4.readFile(filePath, "utf8");
1155
+ const parsed2 = JSON.parse(content);
1156
+ validateWorkspaceMap(parsed2, filePath);
1157
+ const typed = parsed2;
1158
+ let migrated = false;
1159
+ const normalized = {};
1160
+ for (const [fingerprint, entry] of Object.entries(typed)) {
1161
+ if (typeof entry === "string") {
1162
+ normalized[fingerprint] = entry;
1163
+ continue;
1164
+ }
1165
+ const projectKey = entry.projectKey ?? entry.project_id ?? "";
1166
+ if (entry.project_id && !entry.projectKey) {
1167
+ migrated = true;
1168
+ }
1169
+ normalized[fingerprint] = {
1170
+ ...projectKey ? { projectKey } : {},
1171
+ ...entry.source ? { source: entry.source } : {},
1172
+ ...entry.linked_at ? { linked_at: entry.linked_at } : {}
1173
+ };
1174
+ }
1175
+ debugLog2(
1176
+ `workspace map loaded path=${filePath} entries=${Object.keys(normalized).length}`
1177
+ );
1178
+ return { map: normalized, needsMigration: migrated };
1179
+ } catch (err) {
1180
+ if (err?.code === "ENOENT") {
1181
+ const emptyMap = {};
1182
+ debugLog2(`workspace map loaded path=${filePath} entries=0`);
1183
+ return { map: emptyMap, needsMigration: false };
1184
+ }
1185
+ if (err instanceof SyntaxError) {
1186
+ throw new Error(
1187
+ `[memoraone-mcp] Failed to parse workspace map at ${filePath}`
1188
+ );
1189
+ }
1190
+ throw err;
1191
+ }
1192
+ }
1193
+ async function writeWorkspaceMap(map) {
1194
+ const filePath = getWorkspaceMapPath();
1195
+ validateWorkspaceMap(map, filePath);
1196
+ await ensureWorkspaceDir();
1197
+ const tempPath = `${filePath}.tmp`;
1198
+ const content = JSON.stringify(map, null, 2);
1199
+ await fs4.writeFile(tempPath, content, "utf8");
1200
+ await fs4.rename(tempPath, filePath);
1201
+ }
1202
+ async function setProjectIdForFingerprint(args) {
1203
+ const { fingerprint, projectKey, source, linked_at } = args;
1204
+ if (!fingerprintRegex.test(fingerprint)) {
1205
+ throw new Error("[memoraone-mcp] Invalid fingerprint");
1206
+ }
1207
+ if (!projectKey.trim()) {
1208
+ throw new Error("[memoraone-mcp] Invalid projectKey");
1209
+ }
1210
+ const releaseLock = await acquireWorkspaceMapLock();
1211
+ try {
1212
+ const { map, needsMigration } = await readWorkspaceMap();
1213
+ if (needsMigration) {
1214
+ await writeWorkspaceMap(map);
1215
+ }
1216
+ map[fingerprint] = {
1217
+ projectKey,
1218
+ ...source ? { source } : {},
1219
+ linked_at: linked_at ?? (/* @__PURE__ */ new Date()).toISOString()
1220
+ };
1221
+ await writeWorkspaceMap(map);
1222
+ debugLog2(
1223
+ `workspace map set fingerprint=${fingerprint} projectKey=${projectKey}`
1224
+ );
1225
+ } finally {
1226
+ await releaseLock();
1227
+ }
1228
+ }
1229
+
1230
+ // src/tools/handlers/setProject.ts
950
1231
  var setProjectInputSchema = import_v415.z.object({
951
1232
  projectKey: import_v415.z.string().min(1).optional(),
952
1233
  projectId: import_v415.z.string().min(1).optional()
@@ -957,6 +1238,13 @@ async function handleSetProject(args) {
957
1238
  if (!resolvedProjectKey) {
958
1239
  throw new Error("projectKey is required");
959
1240
  }
1241
+ const requested = resolvedProjectKey.trim();
1242
+ const bound = getBoundProjectId();
1243
+ if (bound !== null && requested !== bound) {
1244
+ throw new Error(
1245
+ `Project switching is disabled (Option A). This MCP process is bound to ${bound}. Start a separate MCP instance/window for ${requested}.`
1246
+ );
1247
+ }
960
1248
  setCurrentProjectId(resolvedProjectKey);
961
1249
  const repo = resolveRepoFingerprint(process.cwd());
962
1250
  await setProjectIdForFingerprint({
@@ -968,9 +1256,91 @@ async function handleSetProject(args) {
968
1256
  }
969
1257
 
970
1258
  // src/index.ts
971
- async function sendHeartbeat(client) {
1259
+ var notInitializedResult = {
1260
+ content: [
1261
+ {
1262
+ type: "text",
1263
+ text: "MemoraOne MCP not initialized (project binding missing)."
1264
+ }
1265
+ ]
1266
+ };
1267
+ var initializeDiagDumped = false;
1268
+ var uriToPath = (uri) => {
1269
+ if (uri.startsWith("file://")) {
1270
+ return (0, import_node_url2.fileURLToPath)(uri);
1271
+ }
1272
+ return uri;
1273
+ };
1274
+ function getCursorWorkspaceRootFromEnv() {
1275
+ const raw = process.env.WORKSPACE_FOLDER_PATHS;
1276
+ if (raw === void 0 || raw.trim() === "") {
1277
+ return void 0;
1278
+ }
1279
+ const parts = raw.split(path6.delimiter).map((p) => p.trim()).filter(Boolean);
1280
+ const first = parts[0];
1281
+ return first ? path6.resolve(first) : void 0;
1282
+ }
1283
+ function isHeartbeatDebugEnabled() {
1284
+ const value = String(process.env.MEMORAONE_DEBUG_HEARTBEAT ?? "").trim().toLowerCase();
1285
+ return ["1", "true", "yes", "on"].includes(value);
1286
+ }
1287
+ function fingerprintApiKey(apiKey) {
1288
+ return crypto5.createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
1289
+ }
1290
+ function inferIdeType(params) {
1291
+ if (config2.ideType) {
1292
+ return config2.ideType;
1293
+ }
1294
+ const clientInfoName = String(params?.clientInfo?.name ?? "").toLowerCase();
1295
+ const clientInfoVersion = String(params?.clientInfo?.version ?? "").toLowerCase();
1296
+ const termProgram = String(process.env.TERM_PROGRAM ?? "").toLowerCase();
1297
+ const argv = process.argv.join(" ").toLowerCase();
1298
+ const envKeys = Object.keys(process.env);
1299
+ const hasCursorSignals = envKeys.some((key) => key.startsWith("CURSOR_")) || termProgram === "cursor" || clientInfoName.includes("cursor") || clientInfoVersion.includes("cursor") || argv.includes("cursor");
1300
+ if (hasCursorSignals) {
1301
+ return "cursor";
1302
+ }
1303
+ const hasJetBrainsSignals = envKeys.some(
1304
+ (key) => [
1305
+ "JETBRAINS_IDE",
1306
+ "IDEA_INITIAL_DIRECTORY",
1307
+ "JETBRAINS_REMOTE_RUN",
1308
+ "INTELLIJ_ENVIRONMENT_READER"
1309
+ ].includes(key)
1310
+ ) || String(process.env.TERMINAL_EMULATOR ?? "").toLowerCase().includes("jetbrains") || /(jetbrains|intellij|pycharm|webstorm|goland|rubymine|clion|phpstorm|rider|datagrip)/.test(
1311
+ clientInfoName
1312
+ ) || /(jetbrains|intellij|pycharm|webstorm|goland|rubymine|clion|phpstorm|rider|datagrip)/.test(
1313
+ argv
1314
+ );
1315
+ if (hasJetBrainsSignals) {
1316
+ return "jetbrains";
1317
+ }
1318
+ const hasVsCodeSignals = envKeys.some((key) => key.startsWith("VSCODE_")) || /(visual studio code|vscode|vs code|github copilot)/.test(clientInfoName) || /(visual studio code|vscode|vs code)/.test(argv);
1319
+ if (hasVsCodeSignals) {
1320
+ return "copilot-vscode";
1321
+ }
1322
+ return void 0;
1323
+ }
1324
+ async function sendHeartbeat(client, runtime) {
972
1325
  try {
973
- await client.post("/admin/api-keys/heartbeat", {}, { log: false });
1326
+ const pid = runtime.projectId?.trim();
1327
+ if (isHeartbeatDebugEnabled()) {
1328
+ process.stderr.write(
1329
+ `[memoraone-mcp][diag] heartbeat projectId=${pid ?? "unknown"} apiKeySource=${runtime.apiKeySource ?? "unknown"} apiKeyFingerprint=${runtime.apiKeyFingerprint ?? "unknown"}
1330
+ `
1331
+ );
1332
+ }
1333
+ if (!pid) {
1334
+ throw new Error("[memoraone-mcp] Cannot send heartbeat without an active project binding");
1335
+ }
1336
+ const body = {};
1337
+ if (runtime.ideType) body.ide_type = runtime.ideType;
1338
+ await client.post(`/v1/projects/${pid}/heartbeat`, body, {
1339
+ log: false,
1340
+ headers: {
1341
+ "x-project-id": pid
1342
+ }
1343
+ });
974
1344
  } catch (err) {
975
1345
  process.stderr.write(
976
1346
  `[memoraone-mcp][info] heartbeat error (silent) ${String(err)}
@@ -1004,20 +1374,19 @@ function sanitizeArgsSummary(args) {
1004
1374
  return "[unable to serialize args]";
1005
1375
  }
1006
1376
  }
1007
- async function postWorklogEvent(client, message) {
1377
+ async function postWorklogEvent(client, projectId, message) {
1008
1378
  try {
1009
- const projectKey = getCurrentProjectId();
1010
- if (!projectKey) {
1379
+ if (!projectId) {
1011
1380
  return;
1012
1381
  }
1013
1382
  const body = {
1014
1383
  kind: "note",
1015
1384
  concept: "concept:worklog",
1016
- actor: { type: config.agentType, name: config.agentName },
1385
+ actor: { type: config2.agentType, name: config2.agentName },
1017
1386
  message,
1018
- projectKey,
1387
+ projectKey: projectId,
1019
1388
  metadata: {
1020
- source: config.source,
1389
+ source: config2.source,
1021
1390
  purpose: "worklog"
1022
1391
  }
1023
1392
  };
@@ -1026,241 +1395,363 @@ async function postWorklogEvent(client, message) {
1026
1395
  console.error("[memoraone-mcp] worklog error (silent)", err);
1027
1396
  }
1028
1397
  }
1029
- function registerToolWithWorklog(server, client, toolName, description, schema, handler) {
1030
- if (!config.worklogEnabled) {
1031
- server.tool(toolName, description, schema, handler);
1032
- return;
1033
- }
1034
- server.tool(toolName, description, schema, async (args) => {
1398
+ function registerToolWithWorklog(server, runtime, sessionContext, toolName, description, schema, handler) {
1399
+ const wrapped = async (args) => runWithSessionContext(sessionContext, async () => {
1400
+ if (!runtime.client || !runtime.projectId) return notInitializedResult;
1401
+ if (!config2.worklogEnabled) {
1402
+ return handler(args);
1403
+ }
1035
1404
  const argsSummary = sanitizeArgsSummary(args);
1036
1405
  const start = Date.now();
1037
- await postWorklogEvent(client, `tool_start: ${toolName} ${argsSummary}`);
1406
+ await postWorklogEvent(runtime.client, runtime.projectId, `tool_start: ${toolName} ${argsSummary}`);
1038
1407
  try {
1039
1408
  const result = await handler(args);
1040
1409
  const durationMs = Date.now() - start;
1041
- await postWorklogEvent(client, `tool_end: ${toolName} ok (${durationMs}ms)`);
1410
+ await postWorklogEvent(runtime.client, runtime.projectId, `tool_end: ${toolName} ok (${durationMs}ms)`);
1042
1411
  return result;
1043
1412
  } catch (err) {
1044
1413
  const durationMs = Date.now() - start;
1045
1414
  const errorSummary = err?.message || String(err);
1046
1415
  const shortError = errorSummary.length > 100 ? errorSummary.slice(0, 100) + "..." : errorSummary;
1047
- await postWorklogEvent(client, `tool_end: ${toolName} error (${durationMs}ms): ${shortError}`);
1416
+ await postWorklogEvent(
1417
+ runtime.client,
1418
+ runtime.projectId,
1419
+ `tool_end: ${toolName} error (${durationMs}ms): ${shortError}`
1420
+ );
1048
1421
  throw err;
1049
1422
  }
1050
1423
  });
1424
+ server.tool(toolName, description, schema, wrapped);
1051
1425
  }
1052
- async function main() {
1053
- const repo = resolveRepoFingerprint(process.cwd());
1054
- const envProjectKey = process.env.MEMORAONE_PROJECT_ID;
1055
- const repoFilePath = readRepoProjectIdPath(repo.gitRoot);
1056
- const repoFileProjectKey = await readRepoProjectId(repo.gitRoot);
1057
- const workspaceResolved = await getProjectIdForFingerprint(repo.fingerprint);
1058
- const binding = resolveProjectIdOrThrow({
1059
- envProjectKey,
1060
- repoFileProjectKey,
1061
- workspaceProjectKey: workspaceResolved.projectKey,
1062
- repoFilePath
1426
+ async function main(opts = {}) {
1427
+ let bindingReadyResolve = null;
1428
+ let bindingReadyReject = null;
1429
+ const bindingReady = new Promise((resolve6, reject) => {
1430
+ bindingReadyResolve = resolve6;
1431
+ bindingReadyReject = reject;
1063
1432
  });
1064
- const projectKey = binding.projectKey;
1065
- const source = binding.source;
1066
- setCurrentProjectId(projectKey);
1067
- const devMode = Boolean(config.devMode);
1068
- if (devMode) {
1069
- const remoteInfo = repo.remoteUrl ? ` remoteUrl=${repo.remoteUrl}` : "";
1070
- console.error(
1071
- `[memoraone-mcp][debug] repo fingerprint=${repo.fingerprint} projectKey=${projectKey} source=${source} gitRoot=${repo.gitRoot}${remoteInfo}`
1072
- );
1073
- }
1074
- const client = new memoraClient_default(config, projectKey);
1075
- console.error(
1076
- `[memoraone-mcp] Agent identity: name="${config.agentName}", type="${config.agentType}", source="${config.source}"`
1077
- );
1433
+ const devMode = Boolean(config2.devMode);
1434
+ const sessionLabel = opts.sessionLabel ?? "session";
1435
+ const sessionContext = createSessionRunContext();
1436
+ const runtime = {
1437
+ client: null,
1438
+ projectId: null,
1439
+ apiKeySource: null,
1440
+ apiKeyFingerprint: null,
1441
+ ideType: void 0
1442
+ };
1443
+ let workspaceRoot;
1078
1444
  const server = new import_mcp.McpServer({
1079
1445
  name: "memoraone-mcp",
1080
1446
  version: "1.0.0"
1081
1447
  });
1448
+ const resolveFallbackBindingFromInitialize = async (params) => {
1449
+ let fallbackWorkspaceRoot = workspaceRoot;
1450
+ try {
1451
+ const rootsResult = await server.server.listRoots({});
1452
+ const roots = Array.isArray(rootsResult?.roots) ? rootsResult.roots : [];
1453
+ console.error(`[memoraone-mcp] roots/list returned ${roots.length}: ${JSON.stringify(roots)}`);
1454
+ if (!fallbackWorkspaceRoot && roots.length > 0 && roots[0]?.uri) {
1455
+ fallbackWorkspaceRoot = uriToPath(roots[0].uri);
1456
+ console.error("[memoraone-mcp] Workspace resolution strategy: roots/list (first root)");
1457
+ }
1458
+ } catch (e) {
1459
+ console.error("[memoraone-mcp] roots/list failed:", String(e));
1460
+ }
1461
+ console.error(`[memoraone-mcp] process.cwd(): ${process.cwd()}`);
1462
+ console.error(
1463
+ `[memoraone-mcp] WORKSPACE_FOLDER_PATHS: ${process.env.WORKSPACE_FOLDER_PATHS ?? "(unset)"}`
1464
+ );
1465
+ if (!fallbackWorkspaceRoot) {
1466
+ if (params.workspaceFolders?.length) {
1467
+ fallbackWorkspaceRoot = uriToPath(params.workspaceFolders[0].uri);
1468
+ console.error("[memoraone-mcp] Workspace resolution strategy: initialize.workspaceFolders");
1469
+ } else if (params.rootUri) {
1470
+ fallbackWorkspaceRoot = uriToPath(params.rootUri);
1471
+ console.error("[memoraone-mcp] Workspace resolution strategy: initialize.rootUri");
1472
+ } else {
1473
+ const cursorRoot = getCursorWorkspaceRootFromEnv();
1474
+ if (cursorRoot !== void 0) {
1475
+ fallbackWorkspaceRoot = cursorRoot;
1476
+ console.error("[memoraone-mcp] Workspace resolution strategy: env.WORKSPACE_FOLDER_PATHS");
1477
+ } else {
1478
+ fallbackWorkspaceRoot = process.cwd();
1479
+ console.error("[memoraone-mcp] Workspace resolution strategy: process.cwd() fallback");
1480
+ }
1481
+ }
1482
+ }
1483
+ const binding = await resolveAuthoritativeBinding(fallbackWorkspaceRoot);
1484
+ workspaceRoot = binding.workspaceRoot;
1485
+ return binding;
1486
+ };
1082
1487
  const registeredToolNames = [];
1083
- registeredToolNames.push("memora_post_event");
1084
1488
  server.tool(
1085
1489
  "memora_post_event",
1086
1490
  "Forward an event to MemoraOne timeline",
1087
1491
  postEventShape,
1088
- async (args) => {
1089
- const result = await handlePostEvent(client, args);
1492
+ async (args) => runWithSessionContext(sessionContext, async () => {
1493
+ if (!runtime.client || !runtime.projectId) return notInitializedResult;
1494
+ const result = await handlePostEvent(runtime.client, args);
1090
1495
  return {
1091
- content: [
1092
- {
1093
- type: "text",
1094
- text: JSON.stringify(result)
1095
- }
1096
- ]
1496
+ content: [{ type: "text", text: JSON.stringify(result) }]
1097
1497
  };
1098
- }
1498
+ })
1099
1499
  );
1100
- registeredToolNames.push("memora_list_projects");
1500
+ registeredToolNames.push("memora_post_event");
1101
1501
  server.tool(
1102
1502
  "memora_list_projects",
1103
1503
  "List projects available to the current API key",
1104
1504
  listProjectsShape,
1105
- async () => {
1106
- const result = await handleListProjects(client);
1505
+ async () => runWithSessionContext(sessionContext, async () => {
1506
+ if (!runtime.client || !runtime.projectId) return notInitializedResult;
1507
+ const result = await handleListProjects(runtime.client);
1107
1508
  return {
1108
- content: [
1109
- {
1110
- type: "text",
1111
- text: JSON.stringify(result)
1112
- }
1113
- ]
1509
+ content: [{ type: "text", text: JSON.stringify(result) }]
1114
1510
  };
1115
- }
1511
+ })
1116
1512
  );
1117
- registeredToolNames.push("memora_set_project");
1513
+ registeredToolNames.push("memora_list_projects");
1118
1514
  server.tool(
1119
1515
  "memora_set_project",
1120
1516
  "Set the current project key for subsequent tool calls",
1121
1517
  setProjectShape,
1122
- async (args) => {
1518
+ async (args) => runWithSessionContext(sessionContext, async () => {
1519
+ if (!runtime.client || !runtime.projectId) return notInitializedResult;
1123
1520
  const result = await handleSetProject(args);
1124
1521
  return {
1125
- content: [
1126
- {
1127
- type: "text",
1128
- text: JSON.stringify(result)
1129
- }
1130
- ]
1522
+ content: [{ type: "text", text: JSON.stringify(result) }]
1131
1523
  };
1132
- }
1524
+ })
1133
1525
  );
1134
- registeredToolNames.push("memora_ask_with_memory");
1526
+ registeredToolNames.push("memora_set_project");
1135
1527
  registerToolWithWorklog(
1136
1528
  server,
1137
- client,
1529
+ runtime,
1530
+ sessionContext,
1138
1531
  "memora_ask_with_memory",
1139
1532
  "Ask MemoraOne with project memory context",
1140
1533
  askWithMemoryShape,
1141
1534
  async (args) => {
1142
- const result = await handleAskWithMemory(client, args);
1535
+ const result = await handleAskWithMemory(runtime.client, args);
1143
1536
  return {
1144
- content: [
1145
- {
1146
- type: "text",
1147
- text: JSON.stringify(result)
1148
- }
1149
- ]
1537
+ content: [{ type: "text", text: JSON.stringify(result) }]
1150
1538
  };
1151
1539
  }
1152
1540
  );
1153
- registeredToolNames.push("memora_log_intent");
1541
+ registeredToolNames.push("memora_ask_with_memory");
1154
1542
  registerToolWithWorklog(
1155
1543
  server,
1156
- client,
1544
+ runtime,
1545
+ sessionContext,
1157
1546
  "memora_log_intent",
1158
1547
  "Log a natural-language TASK or DECISION intent to the MemoraOne timeline",
1159
1548
  logIntentShape,
1160
1549
  async (args) => {
1161
- const result = await handleLogIntent(client, args);
1550
+ const result = await handleLogIntent(runtime.client, args);
1162
1551
  return {
1163
- content: [
1164
- {
1165
- type: "text",
1166
- text: JSON.stringify(result)
1167
- }
1168
- ]
1552
+ content: [{ type: "text", text: JSON.stringify(result) }]
1169
1553
  };
1170
1554
  }
1171
1555
  );
1172
- registeredToolNames.push("memora_log_change_summary");
1556
+ registeredToolNames.push("memora_log_intent");
1173
1557
  registerToolWithWorklog(
1174
1558
  server,
1175
- client,
1559
+ runtime,
1560
+ sessionContext,
1176
1561
  "memora_log_change_summary",
1177
1562
  "Log a concise code change summary to the MemoraOne timeline",
1178
1563
  logChangeSummaryShape,
1179
1564
  async (args) => {
1180
- const result = await handleLogChangeSummary(client, args);
1565
+ const result = await handleLogChangeSummary(runtime.client, args);
1181
1566
  return {
1182
- content: [
1183
- {
1184
- type: "text",
1185
- text: JSON.stringify(result)
1186
- }
1187
- ]
1567
+ content: [{ type: "text", text: JSON.stringify(result) }]
1188
1568
  };
1189
1569
  }
1190
1570
  );
1191
- registeredToolNames.push("memora_log_tool_result");
1571
+ registeredToolNames.push("memora_log_change_summary");
1192
1572
  registerToolWithWorklog(
1193
1573
  server,
1194
- client,
1574
+ runtime,
1575
+ sessionContext,
1195
1576
  "memora_log_tool_result",
1196
1577
  "Log a tool execution result to the MemoraOne timeline",
1197
1578
  logToolResultShape,
1198
1579
  async (args) => {
1199
- const result = await handleLogToolResult(client, args);
1580
+ const result = await handleLogToolResult(runtime.client, args);
1200
1581
  return {
1201
- content: [
1202
- {
1203
- type: "text",
1204
- text: JSON.stringify(result)
1205
- }
1206
- ]
1582
+ content: [{ type: "text", text: JSON.stringify(result) }]
1207
1583
  };
1208
1584
  }
1209
1585
  );
1210
- registeredToolNames.push("memora_log_command");
1586
+ registeredToolNames.push("memora_log_tool_result");
1211
1587
  registerToolWithWorklog(
1212
1588
  server,
1213
- client,
1589
+ runtime,
1590
+ sessionContext,
1214
1591
  "memora_log_command",
1215
1592
  "Log a command execution to the MemoraOne timeline",
1216
1593
  logCommandShape,
1217
1594
  async (args) => {
1218
- const result = await handleLogCommand(client, args);
1595
+ const result = await handleLogCommand(runtime.client, args);
1219
1596
  return {
1220
- content: [
1221
- {
1222
- type: "text",
1223
- text: JSON.stringify(result)
1224
- }
1225
- ]
1597
+ content: [{ type: "text", text: JSON.stringify(result) }]
1226
1598
  };
1227
1599
  }
1228
1600
  );
1601
+ registeredToolNames.push("memora_log_command");
1602
+ server.server.setRequestHandler(
1603
+ import_types.InitializeRequestSchema,
1604
+ async (request) => runWithSessionContext(sessionContext, async () => {
1605
+ try {
1606
+ const params = request.params;
1607
+ runtime.ideType = inferIdeType(params);
1608
+ if (!initializeDiagDumped) {
1609
+ initializeDiagDumped = true;
1610
+ const folders = Array.isArray(params.workspaceFolders) ? params.workspaceFolders.map((f) => ({ name: f?.name, uri: f?.uri })) : params.workspaceFolders;
1611
+ const capKeys = params.capabilities && typeof params.capabilities === "object" ? Object.keys(params.capabilities) : [];
1612
+ const cursorEnv = Object.keys(process.env).filter((k) => k.startsWith("CURSOR_")).reduce((acc, k) => {
1613
+ acc[k] = process.env[k] ?? "";
1614
+ return acc;
1615
+ }, {});
1616
+ const vscodeEnv = Object.keys(process.env).filter((k) => k.startsWith("VSCODE_")).reduce((acc, k) => {
1617
+ acc[k] = process.env[k] ?? "";
1618
+ return acc;
1619
+ }, {});
1620
+ console.error("[memoraone-mcp][diag] Initialize dump (once per process):");
1621
+ console.error(`[memoraone-mcp][diag] params.rootUri: ${JSON.stringify(params.rootUri)}`);
1622
+ console.error(`[memoraone-mcp][diag] params.workspaceFolders: ${JSON.stringify(folders)}`);
1623
+ console.error(`[memoraone-mcp][diag] params.clientInfo: ${JSON.stringify(params.clientInfo)}`);
1624
+ console.error(`[memoraone-mcp][diag] params.capabilities (keys): ${JSON.stringify(capKeys)}`);
1625
+ console.error(`[memoraone-mcp][diag] process.cwd(): ${process.cwd()}`);
1626
+ console.error(`[memoraone-mcp][diag] process.execPath: ${process.execPath}`);
1627
+ console.error(`[memoraone-mcp][diag] process.argv: ${JSON.stringify(process.argv)}`);
1628
+ console.error(`[memoraone-mcp][diag] process.env.WORKSPACE_FOLDER_PATHS: ${JSON.stringify(process.env.WORKSPACE_FOLDER_PATHS)}`);
1629
+ console.error(`[memoraone-mcp][diag] process.env.PWD: ${JSON.stringify(process.env.PWD)}`);
1630
+ console.error(`[memoraone-mcp][diag] process.env.HOME: ${JSON.stringify(process.env.HOME)}`);
1631
+ console.error(`[memoraone-mcp][diag] process.env.CURSOR_*: ${JSON.stringify(cursorEnv)}`);
1632
+ console.error(`[memoraone-mcp][diag] process.env.VSCODE_*: ${JSON.stringify(vscodeEnv)}`);
1633
+ console.error(`[memoraone-mcp][diag] process.env.TERM_PROGRAM: ${JSON.stringify(process.env.TERM_PROGRAM)}`);
1634
+ console.error(`[memoraone-mcp][diag] process.env.MEMORAONE_M1_PATH: ${JSON.stringify(process.env.MEMORAONE_M1_PATH)}`);
1635
+ }
1636
+ const debugAuth = ["1", "true", "yes", "on"].includes(
1637
+ String(process.env.MEMORAONE_DEBUG_AUTH ?? "").trim().toLowerCase()
1638
+ );
1639
+ const debugLog3 = config2.devMode || debugAuth;
1640
+ const binding = opts.authoritativeBinding ? opts.authoritativeBinding : await resolveFallbackBindingFromInitialize(params);
1641
+ const apiKeyToUse = binding.apiKey;
1642
+ if (!apiKeyToUse) {
1643
+ throw new Error(
1644
+ "[memoraone-mcp] No actor key. Set MEMORAONE_API_KEY or MEMORA_API_KEY, or add MEMORAONE_API_KEY/api_key to memoraone.m1"
1645
+ );
1646
+ }
1647
+ if (debugLog3) {
1648
+ console.error(
1649
+ "[memoraone-mcp][debug] Resolved actor key from " + (binding.apiKeySource === "env" ? "ENV" : "memoraone.m1")
1650
+ );
1651
+ }
1652
+ const projectId = binding.projectId;
1653
+ const existing = getBoundProjectId();
1654
+ if (existing !== null && existing !== projectId) {
1655
+ const requestedRoot = binding.workspaceRoot ?? workspaceRoot ?? process.cwd();
1656
+ const action = "Open this repo in a separate window or configure a separate MCP server instance per root.";
1657
+ 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.`;
1658
+ console.error(
1659
+ `[memoraone-mcp][ERROR] Option A conflict: boundProjectId=${existing} requestedProjectId=${projectId} workspaceRoot=${requestedRoot}. ${action}`
1660
+ );
1661
+ bindingReadyReject?.(new Error(errMsg));
1662
+ setImmediate(() => process.exit(1));
1663
+ throw new Error(errMsg);
1664
+ }
1665
+ if (existing === null) {
1666
+ setBoundProjectId(projectId);
1667
+ setBoundApiKey(apiKeyToUse);
1668
+ console.error(
1669
+ `[memoraone-mcp] ${sessionLabel} bound to project ${projectId} (Option A: single-project binding)`
1670
+ );
1671
+ }
1672
+ setCurrentProjectId(projectId);
1673
+ setCurrentApiKey(apiKeyToUse);
1674
+ runtime.projectId = projectId;
1675
+ runtime.apiKeySource = binding.apiKeySource;
1676
+ runtime.apiKeyFingerprint = fingerprintApiKey(apiKeyToUse);
1677
+ runtime.client = new memoraClient_default(config2, projectId, apiKeyToUse);
1678
+ workspaceRoot = binding.workspaceRoot;
1679
+ process.stderr.write(
1680
+ `[memoraone-mcp] registering workspace source bindingSource=${binding.bindingSource} workspaceRoot=${workspaceRoot ?? "(unset)"} m1Path=${binding.m1Path}
1681
+ `
1682
+ );
1683
+ await registerRepoSource(
1684
+ runtime.client,
1685
+ runtime.projectId,
1686
+ binding.workspaceRoot,
1687
+ runtime.ideType
1688
+ );
1689
+ if (debugAuth) {
1690
+ console.error("[memoraone-mcp][auth] repo root:", binding.workspaceRoot);
1691
+ console.error("[memoraone-mcp][auth] project_id:", projectId);
1692
+ console.error("[memoraone-mcp][auth] api_key source:", binding.apiKeySource);
1693
+ }
1694
+ console.error(
1695
+ `[memoraone-mcp] ${sessionLabel} authoritative binding: project=${binding.projectId} workspace=${binding.workspaceRoot} m1=${binding.m1Path} source=${binding.bindingSource} apiKeySource=${binding.apiKeySource}`
1696
+ );
1697
+ bindingReadyResolve?.(runtime.client);
1698
+ return server.server._oninitialize(request);
1699
+ } catch (err) {
1700
+ bindingReadyReject?.(err);
1701
+ throw err;
1702
+ }
1703
+ })
1704
+ );
1229
1705
  if (devMode) {
1230
1706
  console.error("[memoraone-mcp] Starting MCP server with", registeredToolNames.length, "tools");
1231
1707
  }
1232
- const transport = new import_stdio.StdioServerTransport();
1708
+ const transport = opts.transport ?? new import_stdio.StdioServerTransport();
1233
1709
  await server.connect(transport);
1710
+ const activeClient = await bindingReady;
1234
1711
  let heartbeatInterval = null;
1235
- if (config.heartbeatEnabled) {
1236
- if (globalThis.__memoraHeartbeatInterval) {
1237
- clearInterval(globalThis.__memoraHeartbeatInterval);
1238
- }
1239
- await sendHeartbeat(client);
1240
- const intervalMs = Number.isFinite(config.heartbeatIntervalMs) ? Math.max(1e3, config.heartbeatIntervalMs) : 3e4;
1712
+ if (config2.heartbeatEnabled) {
1713
+ await sendHeartbeat(activeClient, runtime);
1714
+ const intervalMs = Number.isFinite(config2.heartbeatIntervalMs) ? Math.max(1e3, config2.heartbeatIntervalMs) : 3e4;
1715
+ console.error(
1716
+ `[memoraone-mcp] ${sessionLabel} owns heartbeat for project ${runtime.projectId} interval=${intervalMs}ms`
1717
+ );
1241
1718
  heartbeatInterval = setInterval(() => {
1242
- sendHeartbeat(client).catch(() => {
1719
+ sendHeartbeat(activeClient, runtime).catch(() => {
1243
1720
  });
1244
1721
  }, intervalMs);
1245
- globalThis.__memoraHeartbeatInterval = heartbeatInterval;
1246
1722
  }
1247
- const shutdown = (signal) => {
1723
+ const onSigInt = () => shutdown("SIGINT");
1724
+ const onSigTerm = () => shutdown("SIGTERM");
1725
+ const shutdown = (signal, exitProcess = true) => {
1726
+ process.off("SIGINT", onSigInt);
1727
+ process.off("SIGTERM", onSigTerm);
1248
1728
  if (heartbeatInterval) clearInterval(heartbeatInterval);
1249
- if (globalThis.__memoraHeartbeatInterval) {
1250
- clearInterval(globalThis.__memoraHeartbeatInterval);
1729
+ heartbeatInterval = null;
1730
+ if (runtime.projectId) {
1731
+ console.error(`[memoraone-mcp] ${sessionLabel} released heartbeat for project ${runtime.projectId}`);
1251
1732
  }
1252
1733
  if (devMode) {
1253
- console.error(`[memoraone-mcp] Received ${signal}, shutting down`);
1734
+ console.error(`[memoraone-mcp] ${sessionLabel} received ${signal}, shutting down`);
1735
+ }
1736
+ if (exitProcess) {
1737
+ process.exit(0);
1254
1738
  }
1255
- process.exit(0);
1256
1739
  };
1257
- process.on("SIGINT", () => shutdown("SIGINT"));
1258
- process.on("SIGTERM", () => shutdown("SIGTERM"));
1740
+ process.on("SIGINT", onSigInt);
1741
+ process.on("SIGTERM", onSigTerm);
1259
1742
  if (devMode) {
1260
1743
  console.error("[memoraone-mcp] MCP server ready");
1261
1744
  }
1745
+ if (opts.sessionSocket) {
1746
+ await new Promise((resolve6) => {
1747
+ opts.sessionSocket.once("close", () => {
1748
+ shutdown("session closed", false);
1749
+ resolve6();
1750
+ });
1751
+ });
1752
+ }
1262
1753
  }
1263
- main().catch((err) => {
1264
- console.error("[memoraone-mcp] fatal error", err);
1265
- process.exit(1);
1754
+ // Annotate the CommonJS export names for ESM import in node:
1755
+ 0 && (module.exports = {
1756
+ main
1266
1757
  });