@memoraone/mcp 0.1.17 → 0.1.18
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/README.md +18 -0
- package/dist/cli.cjs +213 -1203
- package/dist/daemon.cjs +1923 -0
- package/dist/index.cjs +939 -491
- package/package.json +11 -7
package/dist/daemon.cjs
ADDED
|
@@ -0,0 +1,1923 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// src/daemon.ts
|
|
30
|
+
var daemon_exports = {};
|
|
31
|
+
__export(daemon_exports, {
|
|
32
|
+
runDaemon: () => runDaemon
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(daemon_exports);
|
|
35
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
36
|
+
var net = __toESM(require("net"), 1);
|
|
37
|
+
var import_stdio2 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
38
|
+
|
|
39
|
+
// src/socketPaths.ts
|
|
40
|
+
var os = __toESM(require("os"), 1);
|
|
41
|
+
var path = __toESM(require("path"), 1);
|
|
42
|
+
var fs = __toESM(require("fs"), 1);
|
|
43
|
+
var BASE_DIR = process.env.MEMORAONE_MCP_LOCK_DIR || path.join(os.homedir(), ".memoraone-mcp");
|
|
44
|
+
function getSocketPath(projectId) {
|
|
45
|
+
return path.join(BASE_DIR, `mcp-${projectId}.sock`);
|
|
46
|
+
}
|
|
47
|
+
function ensureBaseDir() {
|
|
48
|
+
fs.mkdirSync(BASE_DIR, { recursive: true });
|
|
49
|
+
return BASE_DIR;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/projectBinding.ts
|
|
53
|
+
var fs2 = __toESM(require("fs/promises"), 1);
|
|
54
|
+
var path2 = __toESM(require("path"), 1);
|
|
55
|
+
var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
56
|
+
function parseAndValidateM1(content, markerPath) {
|
|
57
|
+
let parsed2;
|
|
58
|
+
try {
|
|
59
|
+
parsed2 = JSON.parse(content);
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(`[memoraone-mcp] Invalid memoraone.m1 JSON at ${markerPath}`);
|
|
62
|
+
}
|
|
63
|
+
const projectId = parsed2?.projectId ?? parsed2?.project_id;
|
|
64
|
+
if (!projectId || typeof projectId !== "string") {
|
|
65
|
+
throw new Error(`[memoraone-mcp] memoraone.m1 missing projectId at ${markerPath}`);
|
|
66
|
+
}
|
|
67
|
+
if (!uuidRegex.test(projectId.trim())) {
|
|
68
|
+
throw new Error(`[memoraone-mcp] memoraone.m1 projectId is not a UUID at ${markerPath}`);
|
|
69
|
+
}
|
|
70
|
+
const apiKeyRaw = parsed2?.MEMORAONE_API_KEY ?? parsed2?.api_key;
|
|
71
|
+
const apiKey = apiKeyRaw !== void 0 && apiKeyRaw !== null && typeof apiKeyRaw === "string" && apiKeyRaw.trim() !== "" ? apiKeyRaw.trim() : null;
|
|
72
|
+
return { projectId: projectId.trim(), apiKey };
|
|
73
|
+
}
|
|
74
|
+
async function resolveProjectIdFromExplicitM1Path() {
|
|
75
|
+
const raw = process.env.MEMORAONE_M1_PATH;
|
|
76
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const markerPath = path2.resolve(raw);
|
|
80
|
+
try {
|
|
81
|
+
const content = await fs2.readFile(markerPath, "utf8");
|
|
82
|
+
const { projectId, apiKey } = parseAndValidateM1(content, markerPath);
|
|
83
|
+
return { projectId, apiKey, foundAt: markerPath };
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err?.code === "ENOENT") {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function findM1WalkingUp(workspaceRoot) {
|
|
92
|
+
let current = path2.resolve(workspaceRoot);
|
|
93
|
+
while (true) {
|
|
94
|
+
const markerPath = path2.join(current, "memoraone.m1");
|
|
95
|
+
try {
|
|
96
|
+
const content = await fs2.readFile(markerPath, "utf8");
|
|
97
|
+
const { projectId, apiKey } = parseAndValidateM1(content, markerPath);
|
|
98
|
+
const repoRoot = path2.dirname(markerPath);
|
|
99
|
+
return { projectId, apiKey, repoRoot, markerPath };
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (err?.code !== "ENOENT") {
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const parent = path2.dirname(current);
|
|
106
|
+
if (parent === current) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
current = parent;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function normalizeWorkspaceSearchRoots(workspaceRoot) {
|
|
114
|
+
if (workspaceRoot === void 0) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
const list = Array.isArray(workspaceRoot) ? workspaceRoot : [workspaceRoot];
|
|
118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
119
|
+
const out = [];
|
|
120
|
+
for (const raw of list) {
|
|
121
|
+
if (raw === void 0) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const trimmed = String(raw).trim();
|
|
125
|
+
if (trimmed === "") {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const resolved = path2.resolve(trimmed);
|
|
129
|
+
if (!seen.has(resolved)) {
|
|
130
|
+
seen.add(resolved);
|
|
131
|
+
out.push(resolved);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
function resolveApiKeyWithSource(fileApiKey) {
|
|
137
|
+
const envApiKey = process.env.MEMORAONE_API_KEY?.trim();
|
|
138
|
+
if (envApiKey) {
|
|
139
|
+
return { apiKey: envApiKey, apiKeySource: "env" };
|
|
140
|
+
}
|
|
141
|
+
const aliasEnvApiKey = process.env.MEMORA_API_KEY?.trim();
|
|
142
|
+
if (aliasEnvApiKey) {
|
|
143
|
+
return { apiKey: aliasEnvApiKey, apiKeySource: "env" };
|
|
144
|
+
}
|
|
145
|
+
if (fileApiKey) {
|
|
146
|
+
return { apiKey: fileApiKey, apiKeySource: "memoraone.m1" };
|
|
147
|
+
}
|
|
148
|
+
return { apiKey: null, apiKeySource: "none" };
|
|
149
|
+
}
|
|
150
|
+
async function resolveAuthoritativeBinding(workspaceRoot) {
|
|
151
|
+
const explicitBinding = await resolveProjectIdFromExplicitM1Path();
|
|
152
|
+
if (explicitBinding) {
|
|
153
|
+
const resolved = resolveApiKeyWithSource(explicitBinding.apiKey);
|
|
154
|
+
return {
|
|
155
|
+
projectId: explicitBinding.projectId,
|
|
156
|
+
workspaceRoot: path2.dirname(explicitBinding.foundAt),
|
|
157
|
+
m1Path: explicitBinding.foundAt,
|
|
158
|
+
apiKey: resolved.apiKey,
|
|
159
|
+
bindingSource: "explicit-m1-path",
|
|
160
|
+
apiKeySource: resolved.apiKeySource
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const candidates = normalizeWorkspaceSearchRoots(workspaceRoot);
|
|
164
|
+
if (candidates.length === 0) {
|
|
165
|
+
throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
|
|
166
|
+
}
|
|
167
|
+
for (const root of candidates) {
|
|
168
|
+
const binding = await findM1WalkingUp(root);
|
|
169
|
+
if (binding) {
|
|
170
|
+
const resolved = resolveApiKeyWithSource(binding.apiKey);
|
|
171
|
+
return {
|
|
172
|
+
projectId: binding.projectId,
|
|
173
|
+
workspaceRoot: binding.repoRoot,
|
|
174
|
+
m1Path: binding.markerPath,
|
|
175
|
+
apiKey: resolved.apiKey,
|
|
176
|
+
bindingSource: "workspace-search",
|
|
177
|
+
apiKeySource: resolved.apiKeySource
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
throw new Error("Could not find memoraone.m1 in workspace.\nOpen a folder containing memoraone.m1.");
|
|
182
|
+
}
|
|
183
|
+
function decodeResolvedBinding(value) {
|
|
184
|
+
if (!value) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
let parsed2;
|
|
188
|
+
try {
|
|
189
|
+
parsed2 = JSON.parse(Buffer.from(value, "base64").toString("utf8"));
|
|
190
|
+
} catch {
|
|
191
|
+
throw new Error("[memoraone-mcp] Invalid encoded binding payload");
|
|
192
|
+
}
|
|
193
|
+
const projectId = parsed2?.projectId;
|
|
194
|
+
const workspaceRoot = parsed2?.workspaceRoot;
|
|
195
|
+
const m1Path = parsed2?.m1Path;
|
|
196
|
+
const apiKey = parsed2?.apiKey;
|
|
197
|
+
const bindingSource = parsed2?.bindingSource;
|
|
198
|
+
const apiKeySource = parsed2?.apiKeySource;
|
|
199
|
+
if (!projectId || typeof projectId !== "string" || !uuidRegex.test(projectId.trim())) {
|
|
200
|
+
throw new Error("[memoraone-mcp] Invalid binding projectId");
|
|
201
|
+
}
|
|
202
|
+
if (!workspaceRoot || typeof workspaceRoot !== "string") {
|
|
203
|
+
throw new Error("[memoraone-mcp] Invalid binding workspaceRoot");
|
|
204
|
+
}
|
|
205
|
+
if (!m1Path || typeof m1Path !== "string") {
|
|
206
|
+
throw new Error("[memoraone-mcp] Invalid binding m1Path");
|
|
207
|
+
}
|
|
208
|
+
if (apiKey !== null && apiKey !== void 0 && typeof apiKey !== "string") {
|
|
209
|
+
throw new Error("[memoraone-mcp] Invalid binding apiKey");
|
|
210
|
+
}
|
|
211
|
+
if (bindingSource !== "explicit-m1-path" && bindingSource !== "workspace-search") {
|
|
212
|
+
throw new Error("[memoraone-mcp] Invalid binding source");
|
|
213
|
+
}
|
|
214
|
+
if (apiKeySource !== "env" && apiKeySource !== "memoraone.m1" && apiKeySource !== "none") {
|
|
215
|
+
throw new Error("[memoraone-mcp] Invalid binding apiKeySource");
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
projectId: projectId.trim(),
|
|
219
|
+
workspaceRoot,
|
|
220
|
+
m1Path,
|
|
221
|
+
apiKey: typeof apiKey === "string" && apiKey.trim() !== "" ? apiKey.trim() : null,
|
|
222
|
+
bindingSource,
|
|
223
|
+
apiKeySource
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/index.ts
|
|
228
|
+
var path7 = __toESM(require("path"), 1);
|
|
229
|
+
var crypto5 = __toESM(require("crypto"), 1);
|
|
230
|
+
var import_node_url2 = require("url");
|
|
231
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
232
|
+
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
233
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
234
|
+
|
|
235
|
+
// src/config.ts
|
|
236
|
+
var process2 = __toESM(require("process"), 1);
|
|
237
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
238
|
+
var path3 = __toESM(require("path"), 1);
|
|
239
|
+
var dotenv = __toESM(require("dotenv"), 1);
|
|
240
|
+
var import_v4 = require("zod/v4");
|
|
241
|
+
|
|
242
|
+
// src/configUtils.ts
|
|
243
|
+
var DEFAULT_API_URL = "http://localhost:3001";
|
|
244
|
+
var DEV_API_URL = "http://localhost:3001";
|
|
245
|
+
function resolveApiUrl(env2) {
|
|
246
|
+
const explicitUrl = env2.MEMORAONE_API_URL?.trim();
|
|
247
|
+
if (explicitUrl) {
|
|
248
|
+
return explicitUrl;
|
|
249
|
+
}
|
|
250
|
+
const aliasUrl = env2.MEMORA_API_URL?.trim();
|
|
251
|
+
if (aliasUrl) {
|
|
252
|
+
return aliasUrl;
|
|
253
|
+
}
|
|
254
|
+
if (env2.MEMORAONE_DEV_MODE === "1") {
|
|
255
|
+
return DEV_API_URL;
|
|
256
|
+
}
|
|
257
|
+
return DEFAULT_API_URL;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/config.ts
|
|
261
|
+
var dotenvPath = path3.resolve(process2.cwd(), ".env");
|
|
262
|
+
if (fs3.existsSync(dotenvPath)) {
|
|
263
|
+
try {
|
|
264
|
+
dotenv.config({ path: dotenvPath });
|
|
265
|
+
} catch (err) {
|
|
266
|
+
process2.stderr.write("[memoraone-mcp] Failed to load .env: " + String(err) + "\n");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
var EnvSchema = import_v4.z.object({
|
|
270
|
+
MEMORAONE_API_URL: import_v4.z.string().url().optional(),
|
|
271
|
+
MEMORAONE_API_KEY: import_v4.z.string().min(1).optional(),
|
|
272
|
+
MEMORAONE_DEV_MODE: import_v4.z.string().min(1).optional(),
|
|
273
|
+
MEMORAONE_AGENT_NAME: import_v4.z.string().min(1).optional(),
|
|
274
|
+
MEMORAONE_AGENT_TYPE: import_v4.z.string().min(1).optional(),
|
|
275
|
+
MEMORAONE_SOURCE: import_v4.z.string().min(1).optional(),
|
|
276
|
+
MEMORAONE_IDE_TYPE: import_v4.z.enum(["cursor", "copilot-vscode", "jetbrains"]).optional(),
|
|
277
|
+
MEMORAONE_WORKLOG: import_v4.z.string().min(1).optional(),
|
|
278
|
+
MEMORAONE_HEARTBEAT: import_v4.z.string().min(1).optional(),
|
|
279
|
+
MEMORAONE_HEARTBEAT_INTERVAL_MS: import_v4.z.string().min(1).optional()
|
|
280
|
+
});
|
|
281
|
+
var requiredEnvVars = [];
|
|
282
|
+
var missingEnvVars = requiredEnvVars.filter((key) => {
|
|
283
|
+
const value = process2.env[key];
|
|
284
|
+
return value === void 0 || value.trim() === "";
|
|
285
|
+
});
|
|
286
|
+
if (missingEnvVars.length > 0) {
|
|
287
|
+
for (const key of missingEnvVars) {
|
|
288
|
+
process2.stderr.write(`Missing ${key}
|
|
289
|
+
`);
|
|
290
|
+
}
|
|
291
|
+
process2.exit(1);
|
|
292
|
+
}
|
|
293
|
+
var parsed = EnvSchema.safeParse(process2.env);
|
|
294
|
+
var resolvedApiUrl = resolveApiUrl(process2.env);
|
|
295
|
+
if (!parsed.success) {
|
|
296
|
+
const formatted = parsed.error.format();
|
|
297
|
+
process2.stderr.write(
|
|
298
|
+
"[memoraone-mcp] Invalid environment variables " + JSON.stringify(formatted) + "\n"
|
|
299
|
+
);
|
|
300
|
+
throw new Error("Config validation failed");
|
|
301
|
+
}
|
|
302
|
+
var parseBooleanFlag = (value, defaultValue) => {
|
|
303
|
+
if (value === void 0) {
|
|
304
|
+
return defaultValue;
|
|
305
|
+
}
|
|
306
|
+
const normalized = value.trim().toLowerCase();
|
|
307
|
+
if (["1", "true", "yes", "on"].includes(normalized)) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
if (["0", "false", "no", "off"].includes(normalized)) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
return defaultValue;
|
|
314
|
+
};
|
|
315
|
+
var config2 = {
|
|
316
|
+
apiUrl: resolvedApiUrl.replace(/\/+$/, ""),
|
|
317
|
+
apiKey: parsed.data.MEMORAONE_API_KEY,
|
|
318
|
+
agentName: parsed.data.MEMORAONE_AGENT_NAME ?? "cursor",
|
|
319
|
+
agentType: parsed.data.MEMORAONE_AGENT_TYPE ?? "agent",
|
|
320
|
+
source: parsed.data.MEMORAONE_SOURCE ?? "cursor",
|
|
321
|
+
ideType: parsed.data.MEMORAONE_IDE_TYPE,
|
|
322
|
+
devMode: parseBooleanFlag(parsed.data.MEMORAONE_DEV_MODE, false),
|
|
323
|
+
worklogEnabled: parseBooleanFlag(parsed.data.MEMORAONE_WORKLOG, true),
|
|
324
|
+
heartbeatEnabled: parseBooleanFlag(parsed.data.MEMORAONE_HEARTBEAT, true),
|
|
325
|
+
heartbeatIntervalMs: Number.parseInt(parsed.data.MEMORAONE_HEARTBEAT_INTERVAL_MS ?? "30000", 10)
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// src/client/memoraClient.ts
|
|
329
|
+
var crypto = __toESM(require("crypto"), 1);
|
|
330
|
+
var PROJECT_ID_HEADER = "x-project-id";
|
|
331
|
+
var uuidRegex2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
332
|
+
var parseBooleanFlag2 = (value) => {
|
|
333
|
+
if (!value) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
const normalized = value.trim().toLowerCase();
|
|
337
|
+
return ["1", "true", "yes", "on"].includes(normalized);
|
|
338
|
+
};
|
|
339
|
+
var debugEnabled = parseBooleanFlag2(process.env.MEMORAONE_DEV_MODE);
|
|
340
|
+
async function requestJson(url, method, headers, body) {
|
|
341
|
+
const res = await fetch(url, {
|
|
342
|
+
method,
|
|
343
|
+
headers,
|
|
344
|
+
body: method === "GET" ? void 0 : JSON.stringify(body ?? {})
|
|
345
|
+
});
|
|
346
|
+
const text = await res.text();
|
|
347
|
+
return {
|
|
348
|
+
status: res.status,
|
|
349
|
+
statusText: res.statusText,
|
|
350
|
+
ok: res.ok,
|
|
351
|
+
text
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
var MemoraOneHttpError = class extends Error {
|
|
355
|
+
constructor(status, statusText, body) {
|
|
356
|
+
super(`MemoraOne request failed: ${status} ${statusText}`);
|
|
357
|
+
this.name = "MemoraOneHttpError";
|
|
358
|
+
this.status = status;
|
|
359
|
+
this.body = body;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
var MemoraClient = class {
|
|
363
|
+
constructor(cfg, projectId, apiKey) {
|
|
364
|
+
if (!uuidRegex2.test(projectId)) {
|
|
365
|
+
throw new Error("[memoraone-mcp] Invalid project_id for MemoraClient");
|
|
366
|
+
}
|
|
367
|
+
this.baseUrl = cfg.apiUrl;
|
|
368
|
+
this.apiKey = apiKey;
|
|
369
|
+
this.projectId = projectId;
|
|
370
|
+
}
|
|
371
|
+
resolveProjectId() {
|
|
372
|
+
const projectId = this.projectId?.trim();
|
|
373
|
+
if (!projectId) {
|
|
374
|
+
throw new Error(`Missing ${PROJECT_ID_HEADER}: select a project first`);
|
|
375
|
+
}
|
|
376
|
+
if (!uuidRegex2.test(projectId)) {
|
|
377
|
+
throw new Error("[memoraone-mcp] Invalid project_id for request");
|
|
378
|
+
}
|
|
379
|
+
return projectId;
|
|
380
|
+
}
|
|
381
|
+
resolveApiKey() {
|
|
382
|
+
const key = this.apiKey?.trim();
|
|
383
|
+
if (!key) {
|
|
384
|
+
throw new Error("[memoraone-mcp] Missing api_key for request");
|
|
385
|
+
}
|
|
386
|
+
return key;
|
|
387
|
+
}
|
|
388
|
+
buildHeaders(options) {
|
|
389
|
+
const projectId = this.resolveProjectId();
|
|
390
|
+
const apiKey = this.resolveApiKey();
|
|
391
|
+
return {
|
|
392
|
+
"content-type": "application/json",
|
|
393
|
+
"x-api-key": apiKey,
|
|
394
|
+
[PROJECT_ID_HEADER]: projectId,
|
|
395
|
+
...options?.headers ?? {}
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
async post(path8, body, options) {
|
|
399
|
+
console.error(
|
|
400
|
+
`[memoraone-mcp][info] MemoraClient.post ENTER path=${path8}`
|
|
401
|
+
);
|
|
402
|
+
const nonce = crypto.randomBytes(8).toString("hex");
|
|
403
|
+
const url = `${this.baseUrl}${path8.startsWith("/") ? path8 : `/${path8}`}`;
|
|
404
|
+
this.resolveProjectId();
|
|
405
|
+
console.error(
|
|
406
|
+
`[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_fetch method=POST url=${url}`
|
|
407
|
+
);
|
|
408
|
+
const res = await requestJson(url, "POST", this.buildHeaders(options), body);
|
|
409
|
+
if (debugEnabled && options?.log !== false) {
|
|
410
|
+
const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
|
|
411
|
+
console.error(
|
|
412
|
+
`[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_response_log`
|
|
413
|
+
);
|
|
414
|
+
const line = `[memoraone-mcp][info] http response method=POST url=${url} status=${res.status} body=${snippet}`;
|
|
415
|
+
console.error(line);
|
|
416
|
+
console.error(
|
|
417
|
+
`[memoraone-mcp][info] requestJson nonce=${nonce} stage=after_response_log`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
if (!res.ok) {
|
|
421
|
+
const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
|
|
422
|
+
process.stderr.write(
|
|
423
|
+
`[memoraone-mcp][error] http error method=POST url=${url} status=${res.status} body=${snippet}
|
|
424
|
+
`
|
|
425
|
+
);
|
|
426
|
+
throw new MemoraOneHttpError(res.status, res.statusText, res.text);
|
|
427
|
+
}
|
|
428
|
+
console.error(
|
|
429
|
+
`[memoraone-mcp][info] MemoraClient.post EXIT path=${path8}`
|
|
430
|
+
);
|
|
431
|
+
return res.text ? JSON.parse(res.text) : null;
|
|
432
|
+
}
|
|
433
|
+
async get(path8, options) {
|
|
434
|
+
const nonce = crypto.randomBytes(8).toString("hex");
|
|
435
|
+
const url = `${this.baseUrl}${path8.startsWith("/") ? path8 : `/${path8}`}`;
|
|
436
|
+
this.resolveProjectId();
|
|
437
|
+
console.error(
|
|
438
|
+
`[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_fetch method=GET url=${url}`
|
|
439
|
+
);
|
|
440
|
+
const res = await requestJson(url, "GET", this.buildHeaders(options));
|
|
441
|
+
if (debugEnabled && options?.log !== false) {
|
|
442
|
+
const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
|
|
443
|
+
console.error(
|
|
444
|
+
`[memoraone-mcp][info] requestJson nonce=${nonce} stage=before_response_log`
|
|
445
|
+
);
|
|
446
|
+
const line = `[memoraone-mcp][info] http response method=GET url=${url} status=${res.status} body=${snippet}`;
|
|
447
|
+
console.error(line);
|
|
448
|
+
console.error(
|
|
449
|
+
`[memoraone-mcp][info] requestJson nonce=${nonce} stage=after_response_log`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
if (!res.ok) {
|
|
453
|
+
const snippet = res.text.length > 200 ? `${res.text.slice(0, 200)}...` : res.text;
|
|
454
|
+
process.stderr.write(
|
|
455
|
+
`[memoraone-mcp][error] http error method=GET url=${url} status=${res.status} body=${snippet}
|
|
456
|
+
`
|
|
457
|
+
);
|
|
458
|
+
throw new MemoraOneHttpError(res.status, res.statusText, res.text);
|
|
459
|
+
}
|
|
460
|
+
return res.text ? JSON.parse(res.text) : null;
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
var memoraClient_default = MemoraClient;
|
|
464
|
+
|
|
465
|
+
// src/sourceRegistration.ts
|
|
466
|
+
var path4 = __toESM(require("path"), 1);
|
|
467
|
+
var import_node_url = require("url");
|
|
468
|
+
async function registerRepoSource(client, projectId, repoPath, ideType) {
|
|
469
|
+
const normalizedRepoPath = path4.resolve(repoPath);
|
|
470
|
+
const endpointPath = `/v1/projects/${projectId}/sources`;
|
|
471
|
+
console.error(
|
|
472
|
+
`[memoraone-mcp DEBUG registerRepoSource] registerRepoSource() called projectId=${projectId} repoPath(raw)=${JSON.stringify(repoPath)} repoPath(normalized)=${JSON.stringify(normalizedRepoPath)} ideType=${ideType ?? "(none)"}`
|
|
473
|
+
);
|
|
474
|
+
try {
|
|
475
|
+
const body = {
|
|
476
|
+
kind: "repo",
|
|
477
|
+
label: path4.basename(normalizedRepoPath),
|
|
478
|
+
uri: (0, import_node_url.pathToFileURL)(normalizedRepoPath).href
|
|
479
|
+
};
|
|
480
|
+
if (ideType) {
|
|
481
|
+
body.metadata = {
|
|
482
|
+
ide_type: ideType
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
console.error(
|
|
486
|
+
`[memoraone-mcp DEBUG registerRepoSource] POST endpoint=${endpointPath} body=${JSON.stringify(body)}`
|
|
487
|
+
);
|
|
488
|
+
const result = await client.post(endpointPath, body);
|
|
489
|
+
console.error(
|
|
490
|
+
`[memoraone-mcp DEBUG registerRepoSource] POST succeeded endpoint=${endpointPath} responseSummary=${JSON.stringify(result)}`
|
|
491
|
+
);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
console.error(
|
|
494
|
+
`[memoraone-mcp DEBUG registerRepoSource] POST threw endpoint=${endpointPath} message=${String(err?.message ?? err)}`
|
|
495
|
+
);
|
|
496
|
+
if (err instanceof MemoraOneHttpError) {
|
|
497
|
+
const bodyStr = typeof err.body === "string" ? err.body : JSON.stringify(err.body ?? null);
|
|
498
|
+
console.error(
|
|
499
|
+
`[memoraone-mcp DEBUG registerRepoSource] MemoraOneHttpError status=${err.status} body=${bodyStr}`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
console.warn("[memoraone-mcp] registerRepoSource failed:", err);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/tools/postEvent.ts
|
|
507
|
+
var import_v42 = require("zod/v4");
|
|
508
|
+
var postEventShape = {
|
|
509
|
+
kind: import_v42.z.string().min(1),
|
|
510
|
+
actor: import_v42.z.object({
|
|
511
|
+
identifier: import_v42.z.string().min(1),
|
|
512
|
+
id: import_v42.z.string().min(1).optional()
|
|
513
|
+
}),
|
|
514
|
+
content: import_v42.z.record(import_v42.z.string(), import_v42.z.any()),
|
|
515
|
+
metadata: import_v42.z.record(import_v42.z.string(), import_v42.z.any()).optional()
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// src/tools/askWithMemory.ts
|
|
519
|
+
var import_v43 = require("zod/v4");
|
|
520
|
+
var askWithMemoryShape = {
|
|
521
|
+
question: import_v43.z.string().min(1),
|
|
522
|
+
code_context: import_v43.z.object({
|
|
523
|
+
file_path: import_v43.z.string().optional(),
|
|
524
|
+
selected_text: import_v43.z.string().optional(),
|
|
525
|
+
language: import_v43.z.string().optional()
|
|
526
|
+
}).optional()
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
// src/tools/logIntent.ts
|
|
530
|
+
var import_v44 = require("zod/v4");
|
|
531
|
+
var logIntentShape = {
|
|
532
|
+
intent: import_v44.z.enum(["task", "decision"]),
|
|
533
|
+
message: import_v44.z.string().min(1),
|
|
534
|
+
context: import_v44.z.record(import_v44.z.string(), import_v44.z.any()).optional(),
|
|
535
|
+
intent_source: import_v44.z.string().optional().default("cursor_chat"),
|
|
536
|
+
run_id: import_v44.z.string().min(1).optional()
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// src/tools/logChangeSummary.ts
|
|
540
|
+
var import_v45 = require("zod/v4");
|
|
541
|
+
var logChangeSummaryShape = {
|
|
542
|
+
summary: import_v45.z.string().min(1),
|
|
543
|
+
scope: import_v45.z.string().min(1).optional(),
|
|
544
|
+
files: import_v45.z.array(import_v45.z.string().min(1)).optional(),
|
|
545
|
+
stats: import_v45.z.object({
|
|
546
|
+
files: import_v45.z.number().int().nonnegative().optional(),
|
|
547
|
+
add: import_v45.z.number().int().nonnegative().optional(),
|
|
548
|
+
del: import_v45.z.number().int().nonnegative().optional()
|
|
549
|
+
}).optional(),
|
|
550
|
+
commit: import_v45.z.string().min(1).optional(),
|
|
551
|
+
run_id: import_v45.z.string().min(1).optional()
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// src/tools/logToolResult.ts
|
|
555
|
+
var import_v46 = require("zod/v4");
|
|
556
|
+
var logToolResultShape = {
|
|
557
|
+
tool: import_v46.z.string().min(1),
|
|
558
|
+
status: import_v46.z.enum(["ok", "error", "partial"]),
|
|
559
|
+
summary: import_v46.z.string().min(1),
|
|
560
|
+
run_id: import_v46.z.string().min(1).optional(),
|
|
561
|
+
duration_ms: import_v46.z.number().int().nonnegative().optional(),
|
|
562
|
+
error_code: import_v46.z.string().min(1).optional(),
|
|
563
|
+
error_message: import_v46.z.string().min(1).optional(),
|
|
564
|
+
error_kind: import_v46.z.enum(["infra", "logic", "auth", "rate_limit", "validation", "unknown"]).optional(),
|
|
565
|
+
stats: import_v46.z.record(import_v46.z.string(), import_v46.z.any()).optional()
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// src/tools/logCommand.ts
|
|
569
|
+
var import_v47 = require("zod/v4");
|
|
570
|
+
var logCommandShape = {
|
|
571
|
+
cmd: import_v47.z.string().min(1),
|
|
572
|
+
summary: import_v47.z.string().min(1),
|
|
573
|
+
cwd: import_v47.z.string().min(1).optional(),
|
|
574
|
+
exit_code: import_v47.z.number().int().optional(),
|
|
575
|
+
duration_ms: import_v47.z.number().int().nonnegative().optional(),
|
|
576
|
+
run_id: import_v47.z.string().min(1).optional(),
|
|
577
|
+
stats: import_v47.z.record(import_v47.z.string(), import_v47.z.any()).optional()
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// src/tools/listProjects.ts
|
|
581
|
+
var listProjectsShape = {};
|
|
582
|
+
|
|
583
|
+
// src/tools/setProject.ts
|
|
584
|
+
var import_v48 = require("zod/v4");
|
|
585
|
+
var setProjectShape = {
|
|
586
|
+
projectKey: import_v48.z.string().min(1).optional(),
|
|
587
|
+
projectId: import_v48.z.string().min(1).optional()
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
// src/tools/handlers/postEvent.ts
|
|
591
|
+
var import_v49 = require("zod/v4");
|
|
592
|
+
var crypto3 = __toESM(require("crypto"), 1);
|
|
593
|
+
|
|
594
|
+
// src/runContext.ts
|
|
595
|
+
var import_node_async_hooks = require("async_hooks");
|
|
596
|
+
var crypto2 = __toESM(require("crypto"), 1);
|
|
597
|
+
var sessionContextStorage = new import_node_async_hooks.AsyncLocalStorage();
|
|
598
|
+
function createSessionRunContext(initial = {}) {
|
|
599
|
+
return {
|
|
600
|
+
currentRunId: null,
|
|
601
|
+
currentProjectId: null,
|
|
602
|
+
currentApiKey: null,
|
|
603
|
+
boundProjectId: null,
|
|
604
|
+
boundApiKey: null,
|
|
605
|
+
...initial
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function runWithSessionContext(context, fn) {
|
|
609
|
+
return sessionContextStorage.run(context, fn);
|
|
610
|
+
}
|
|
611
|
+
function getSessionContext() {
|
|
612
|
+
const context = sessionContextStorage.getStore();
|
|
613
|
+
if (!context) {
|
|
614
|
+
throw new Error("[memoraone-mcp] Session context not initialized");
|
|
615
|
+
}
|
|
616
|
+
return context;
|
|
617
|
+
}
|
|
618
|
+
function getBoundProjectId() {
|
|
619
|
+
return getSessionContext().boundProjectId;
|
|
620
|
+
}
|
|
621
|
+
function setBoundProjectId(id) {
|
|
622
|
+
getSessionContext().boundProjectId = id;
|
|
623
|
+
}
|
|
624
|
+
function setBoundApiKey(key) {
|
|
625
|
+
getSessionContext().boundApiKey = key;
|
|
626
|
+
}
|
|
627
|
+
function getCurrentRunId() {
|
|
628
|
+
return getSessionContext().currentRunId;
|
|
629
|
+
}
|
|
630
|
+
function setCurrentRunId(id) {
|
|
631
|
+
getSessionContext().currentRunId = id;
|
|
632
|
+
}
|
|
633
|
+
function getCurrentProjectId() {
|
|
634
|
+
return getSessionContext().currentProjectId;
|
|
635
|
+
}
|
|
636
|
+
function setCurrentProjectId(id) {
|
|
637
|
+
getSessionContext().currentProjectId = id;
|
|
638
|
+
}
|
|
639
|
+
function setCurrentApiKey(key) {
|
|
640
|
+
getSessionContext().currentApiKey = key;
|
|
641
|
+
}
|
|
642
|
+
function resolveRunId(passed) {
|
|
643
|
+
if (passed) {
|
|
644
|
+
return passed;
|
|
645
|
+
}
|
|
646
|
+
return getCurrentRunId();
|
|
647
|
+
}
|
|
648
|
+
function generateRunId() {
|
|
649
|
+
return crypto2.randomBytes(16).toString("hex");
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/tools/handlers/postEvent.ts
|
|
653
|
+
var postEventInputSchema = import_v49.z.object({
|
|
654
|
+
kind: import_v49.z.string().min(1),
|
|
655
|
+
actor: import_v49.z.object({
|
|
656
|
+
identifier: import_v49.z.string().min(1),
|
|
657
|
+
id: import_v49.z.string().min(1).optional()
|
|
658
|
+
}),
|
|
659
|
+
content: import_v49.z.record(import_v49.z.string(), import_v49.z.any()),
|
|
660
|
+
metadata: import_v49.z.record(import_v49.z.string(), import_v49.z.any()).optional()
|
|
661
|
+
});
|
|
662
|
+
async function handlePostEvent(client, args) {
|
|
663
|
+
const nonce = crypto3.randomBytes(8).toString("hex");
|
|
664
|
+
console.error(
|
|
665
|
+
`[memoraone-mcp][debug] tool=memora_post_event toolCallId=unknown nonce=${nonce} stage=before_post`
|
|
666
|
+
);
|
|
667
|
+
const parsed2 = postEventInputSchema.parse(args ?? {});
|
|
668
|
+
const projectKey = getCurrentProjectId();
|
|
669
|
+
if (!projectKey) {
|
|
670
|
+
throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
|
|
671
|
+
}
|
|
672
|
+
const content = parsed2.content ?? {};
|
|
673
|
+
const message = typeof content.message === "string" ? content.message : typeof content.text === "string" ? content.text : JSON.stringify(content);
|
|
674
|
+
const body = {
|
|
675
|
+
kind: parsed2.kind,
|
|
676
|
+
message,
|
|
677
|
+
projectKey,
|
|
678
|
+
actor: {
|
|
679
|
+
type: "agent",
|
|
680
|
+
identifier: config2.agentName,
|
|
681
|
+
...parsed2.actor.id ? { id: parsed2.actor.id } : {}
|
|
682
|
+
},
|
|
683
|
+
...parsed2.metadata ? { metadata: parsed2.metadata } : {}
|
|
684
|
+
};
|
|
685
|
+
try {
|
|
686
|
+
await client.post("/timeline/events", body);
|
|
687
|
+
console.error(
|
|
688
|
+
`[memoraone-mcp][debug] tool=memora_post_event toolCallId=unknown nonce=${nonce} stage=after_post`
|
|
689
|
+
);
|
|
690
|
+
} catch (err) {
|
|
691
|
+
if (err instanceof MemoraOneHttpError) {
|
|
692
|
+
const bodyText = typeof err.body === "string" ? err.body : JSON.stringify(err.body ?? "");
|
|
693
|
+
throw new Error(
|
|
694
|
+
`memora_post_event failed: ${err.status} ${err.message} ${bodyText}`.trim()
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
throw err;
|
|
698
|
+
}
|
|
699
|
+
return { ok: true, forwarded: true };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/tools/handlers/askWithMemory.ts
|
|
703
|
+
var import_v410 = require("zod/v4");
|
|
704
|
+
var askWithMemoryInputSchema = import_v410.z.object({
|
|
705
|
+
question: import_v410.z.string().min(1),
|
|
706
|
+
code_context: import_v410.z.object({
|
|
707
|
+
file_path: import_v410.z.string().optional(),
|
|
708
|
+
selected_text: import_v410.z.string().optional(),
|
|
709
|
+
language: import_v410.z.string().optional()
|
|
710
|
+
}).optional()
|
|
711
|
+
});
|
|
712
|
+
function isAskWithMemoryResponse(value) {
|
|
713
|
+
return typeof value === "object" && value !== null && typeof value.answer === "string" && value.answer !== "";
|
|
714
|
+
}
|
|
715
|
+
async function handleAskWithMemory(client, args) {
|
|
716
|
+
const parsed2 = askWithMemoryInputSchema.parse(args ?? {});
|
|
717
|
+
const projectKey = getCurrentProjectId();
|
|
718
|
+
if (!projectKey) {
|
|
719
|
+
throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
|
|
720
|
+
}
|
|
721
|
+
const payload = {
|
|
722
|
+
question: parsed2.question,
|
|
723
|
+
projectKey
|
|
724
|
+
};
|
|
725
|
+
if (parsed2.code_context) {
|
|
726
|
+
payload.code_context = parsed2.code_context;
|
|
727
|
+
}
|
|
728
|
+
const res = await client.post("/agent/ask-with-memory", payload);
|
|
729
|
+
if (!isAskWithMemoryResponse(res)) {
|
|
730
|
+
const err = new Error("Unexpected response from MemoraOne");
|
|
731
|
+
err.status = 502;
|
|
732
|
+
err.body = res;
|
|
733
|
+
throw err;
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
answer: res.answer,
|
|
737
|
+
used: res.used ?? {}
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// src/tools/handlers/logIntent.ts
|
|
742
|
+
var import_v411 = require("zod/v4");
|
|
743
|
+
var logIntentInputSchema = import_v411.z.object({
|
|
744
|
+
intent: import_v411.z.enum(["task", "decision"]),
|
|
745
|
+
message: import_v411.z.string().min(1),
|
|
746
|
+
context: import_v411.z.record(import_v411.z.string(), import_v411.z.any()).optional(),
|
|
747
|
+
intent_source: import_v411.z.string().optional().default("cursor_chat"),
|
|
748
|
+
run_id: import_v411.z.string().min(1).optional()
|
|
749
|
+
});
|
|
750
|
+
async function handleLogIntent(client, args) {
|
|
751
|
+
const parsed2 = logIntentInputSchema.parse(args ?? {});
|
|
752
|
+
const projectKey = getCurrentProjectId();
|
|
753
|
+
if (!projectKey) {
|
|
754
|
+
throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
|
|
755
|
+
}
|
|
756
|
+
const intent = parsed2.intent;
|
|
757
|
+
const message = parsed2.message.trim();
|
|
758
|
+
if (!message) {
|
|
759
|
+
throw new Error("message cannot be empty after trimming");
|
|
760
|
+
}
|
|
761
|
+
const intent_source = parsed2.intent_source ?? "cursor_chat";
|
|
762
|
+
const context = parsed2.context;
|
|
763
|
+
const purpose = intent;
|
|
764
|
+
const concept = purpose === "task" ? "concept:task" : "concept:decision";
|
|
765
|
+
const run_id = parsed2.run_id ?? generateRunId();
|
|
766
|
+
setCurrentRunId(run_id);
|
|
767
|
+
const body = {
|
|
768
|
+
kind: "note",
|
|
769
|
+
actor: { type: config2.agentType, name: config2.agentName },
|
|
770
|
+
concept,
|
|
771
|
+
// MUST be TOP-LEVEL so it populates timeline_events.concept
|
|
772
|
+
message,
|
|
773
|
+
projectKey,
|
|
774
|
+
metadata: {
|
|
775
|
+
source: config2.source,
|
|
776
|
+
purpose,
|
|
777
|
+
// 'task' | 'decision'
|
|
778
|
+
intent_source: intent_source ?? "cursor_chat",
|
|
779
|
+
run_id,
|
|
780
|
+
...context ? { context } : {}
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
await client.post("/timeline/events", body);
|
|
784
|
+
return { ok: true, run_id };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// src/tools/handlers/logChangeSummary.ts
|
|
788
|
+
var import_v412 = require("zod/v4");
|
|
789
|
+
var logChangeSummaryInputSchema = import_v412.z.object({
|
|
790
|
+
summary: import_v412.z.string().min(1),
|
|
791
|
+
scope: import_v412.z.string().min(1).optional(),
|
|
792
|
+
files: import_v412.z.array(import_v412.z.string().min(1)).optional(),
|
|
793
|
+
stats: import_v412.z.object({
|
|
794
|
+
files: import_v412.z.number().int().nonnegative().optional(),
|
|
795
|
+
add: import_v412.z.number().int().nonnegative().optional(),
|
|
796
|
+
del: import_v412.z.number().int().nonnegative().optional()
|
|
797
|
+
}).optional(),
|
|
798
|
+
commit: import_v412.z.string().min(1).optional(),
|
|
799
|
+
run_id: import_v412.z.string().min(1).optional()
|
|
800
|
+
});
|
|
801
|
+
async function handleLogChangeSummary(client, args) {
|
|
802
|
+
const parsed2 = logChangeSummaryInputSchema.parse(args ?? {});
|
|
803
|
+
const projectKey = getCurrentProjectId();
|
|
804
|
+
if (!projectKey) {
|
|
805
|
+
throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
|
|
806
|
+
}
|
|
807
|
+
const { summary, scope, files, stats, commit } = parsed2;
|
|
808
|
+
const message = summary.startsWith("CHANGE:") ? summary : `CHANGE: ${scope ?? "code"} \u2014 ${summary}`;
|
|
809
|
+
const run_id = resolveRunId(parsed2.run_id);
|
|
810
|
+
const body = {
|
|
811
|
+
kind: "note",
|
|
812
|
+
concept: "concept:change_summary",
|
|
813
|
+
actor: { type: config2.agentType, name: config2.agentName },
|
|
814
|
+
message,
|
|
815
|
+
projectKey,
|
|
816
|
+
metadata: {
|
|
817
|
+
source: config2.source,
|
|
818
|
+
purpose: "change_summary",
|
|
819
|
+
tool: "memora_log_change_summary",
|
|
820
|
+
...scope ? { scope } : {},
|
|
821
|
+
...files ? { files } : {},
|
|
822
|
+
...stats ? { stats } : {},
|
|
823
|
+
...commit ? { commit } : {},
|
|
824
|
+
...run_id ? { run_id } : {}
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
await client.post("/timeline/events", body);
|
|
828
|
+
return { ok: true };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/tools/handlers/logToolResult.ts
|
|
832
|
+
var import_v413 = require("zod/v4");
|
|
833
|
+
var logToolResultInputSchema = import_v413.z.object({
|
|
834
|
+
tool: import_v413.z.string().min(1),
|
|
835
|
+
status: import_v413.z.enum(["ok", "error", "partial"]),
|
|
836
|
+
summary: import_v413.z.string().min(1),
|
|
837
|
+
run_id: import_v413.z.string().min(1).optional(),
|
|
838
|
+
duration_ms: import_v413.z.number().int().nonnegative().optional(),
|
|
839
|
+
error_code: import_v413.z.string().min(1).optional(),
|
|
840
|
+
error_message: import_v413.z.string().min(1).optional(),
|
|
841
|
+
error_kind: import_v413.z.enum(["infra", "logic", "auth", "rate_limit", "validation", "unknown"]).optional(),
|
|
842
|
+
stats: import_v413.z.record(import_v413.z.string(), import_v413.z.any()).optional()
|
|
843
|
+
});
|
|
844
|
+
async function handleLogToolResult(client, args) {
|
|
845
|
+
const parsed2 = logToolResultInputSchema.parse(args ?? {});
|
|
846
|
+
const projectKey = getCurrentProjectId();
|
|
847
|
+
if (!projectKey) {
|
|
848
|
+
throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
|
|
849
|
+
}
|
|
850
|
+
const { tool, status, summary, duration_ms, error_code, error_message, error_kind, stats } = parsed2;
|
|
851
|
+
const message = summary.startsWith("RESULT:") ? summary : `RESULT: ${tool} \u2014 ${status} \u2014 ${summary}`;
|
|
852
|
+
const run_id = resolveRunId(parsed2.run_id);
|
|
853
|
+
const body = {
|
|
854
|
+
kind: "note",
|
|
855
|
+
concept: "concept:tool_result",
|
|
856
|
+
actor: { type: config2.agentType, name: config2.agentName },
|
|
857
|
+
message,
|
|
858
|
+
projectKey,
|
|
859
|
+
metadata: {
|
|
860
|
+
source: config2.source,
|
|
861
|
+
purpose: "tool_result",
|
|
862
|
+
tool: "memora_log_tool_result",
|
|
863
|
+
tool_name: tool,
|
|
864
|
+
status,
|
|
865
|
+
...run_id ? { run_id } : {},
|
|
866
|
+
...duration_ms ? { duration_ms } : {},
|
|
867
|
+
...error_code ? { error_code } : {},
|
|
868
|
+
...error_message ? { error_message } : {},
|
|
869
|
+
...error_kind ? { error_kind } : {},
|
|
870
|
+
...stats ? { stats } : {}
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
await client.post("/timeline/events", body);
|
|
874
|
+
return { ok: true };
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/tools/handlers/logCommand.ts
|
|
878
|
+
var import_v414 = require("zod/v4");
|
|
879
|
+
var logCommandInputSchema = import_v414.z.object({
|
|
880
|
+
cmd: import_v414.z.string().min(1),
|
|
881
|
+
summary: import_v414.z.string().min(1),
|
|
882
|
+
cwd: import_v414.z.string().min(1).optional(),
|
|
883
|
+
exit_code: import_v414.z.number().int().optional(),
|
|
884
|
+
duration_ms: import_v414.z.number().int().nonnegative().optional(),
|
|
885
|
+
run_id: import_v414.z.string().min(1).optional(),
|
|
886
|
+
stats: import_v414.z.record(import_v414.z.string(), import_v414.z.any()).optional()
|
|
887
|
+
});
|
|
888
|
+
async function handleLogCommand(client, args) {
|
|
889
|
+
const parsed2 = logCommandInputSchema.parse(args ?? {});
|
|
890
|
+
const projectKey = getCurrentProjectId();
|
|
891
|
+
if (!projectKey) {
|
|
892
|
+
throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
|
|
893
|
+
}
|
|
894
|
+
const { cmd, summary, cwd: cwd2, exit_code, duration_ms, stats } = parsed2;
|
|
895
|
+
const message = summary.startsWith("COMMAND:") ? summary : `COMMAND: ${cmd} \u2014 ${summary}`;
|
|
896
|
+
const run_id = resolveRunId(parsed2.run_id);
|
|
897
|
+
const body = {
|
|
898
|
+
kind: "note",
|
|
899
|
+
concept: "concept:command",
|
|
900
|
+
actor: { type: config2.agentType, name: config2.agentName },
|
|
901
|
+
message,
|
|
902
|
+
projectKey,
|
|
903
|
+
metadata: {
|
|
904
|
+
source: config2.source,
|
|
905
|
+
purpose: "command",
|
|
906
|
+
tool: "memora_log_command",
|
|
907
|
+
cmd,
|
|
908
|
+
...cwd2 ? { cwd: cwd2 } : {},
|
|
909
|
+
...exit_code !== void 0 ? { exit_code } : {},
|
|
910
|
+
...duration_ms !== void 0 ? { duration_ms } : {},
|
|
911
|
+
...run_id ? { run_id } : {},
|
|
912
|
+
...stats ? { stats } : {}
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
await client.post("/timeline/events", body);
|
|
916
|
+
return { ok: true };
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/tools/handlers/listProjects.ts
|
|
920
|
+
async function handleListProjects(client) {
|
|
921
|
+
const res = await client.get("/v1/projects");
|
|
922
|
+
return res ?? { items: [] };
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/tools/handlers/setProject.ts
|
|
926
|
+
var import_v415 = require("zod/v4");
|
|
927
|
+
|
|
928
|
+
// src/repoFingerprint.ts
|
|
929
|
+
var fs4 = __toESM(require("fs"), 1);
|
|
930
|
+
var path5 = __toESM(require("path"), 1);
|
|
931
|
+
var crypto4 = __toESM(require("crypto"), 1);
|
|
932
|
+
var parseBooleanFlag3 = (value) => {
|
|
933
|
+
if (!value) {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
const normalized = value.trim().toLowerCase();
|
|
937
|
+
return ["1", "true", "yes", "on"].includes(normalized);
|
|
938
|
+
};
|
|
939
|
+
var debugEnabled2 = parseBooleanFlag3(process.env.MEMORAONE_DEV_MODE);
|
|
940
|
+
var debugLog = (message) => {
|
|
941
|
+
if (!debugEnabled2) {
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
process.stderr.write(`[memoraone-mcp][debug] ${message}
|
|
945
|
+
`);
|
|
946
|
+
};
|
|
947
|
+
var normalizeRemoteUrl = (remoteUrl) => {
|
|
948
|
+
let normalized = remoteUrl.trim();
|
|
949
|
+
normalized = normalized.replace(/^[a-z]+:\/\//i, "");
|
|
950
|
+
normalized = normalized.replace(/^git@([^:]+):/i, "$1/");
|
|
951
|
+
normalized = normalized.replace(/\.git$/i, "");
|
|
952
|
+
normalized = normalized.replace(/\/+$/, "");
|
|
953
|
+
return normalized.toLowerCase();
|
|
954
|
+
};
|
|
955
|
+
var sha256 = (value) => {
|
|
956
|
+
return crypto4.createHash("sha256").update(value).digest("hex");
|
|
957
|
+
};
|
|
958
|
+
var resolveGitDir = (gitPath) => {
|
|
959
|
+
try {
|
|
960
|
+
const stat2 = fs4.statSync(gitPath);
|
|
961
|
+
if (stat2.isDirectory()) {
|
|
962
|
+
return gitPath;
|
|
963
|
+
}
|
|
964
|
+
if (stat2.isFile()) {
|
|
965
|
+
const content = fs4.readFileSync(gitPath, "utf8");
|
|
966
|
+
const match = content.match(/^gitdir:\s*(.+)$/m);
|
|
967
|
+
if (match) {
|
|
968
|
+
const gitDir = match[1].trim();
|
|
969
|
+
return path5.resolve(path5.dirname(gitPath), gitDir);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
} catch {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
return null;
|
|
976
|
+
};
|
|
977
|
+
var findGitRoot = (start) => {
|
|
978
|
+
let current = path5.resolve(start);
|
|
979
|
+
while (true) {
|
|
980
|
+
const gitPath = path5.join(current, ".git");
|
|
981
|
+
if (fs4.existsSync(gitPath)) {
|
|
982
|
+
const gitDir = resolveGitDir(gitPath);
|
|
983
|
+
if (gitDir) {
|
|
984
|
+
return { gitRoot: current, gitDir };
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
const parent = path5.dirname(current);
|
|
988
|
+
if (parent === current) {
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
current = parent;
|
|
992
|
+
}
|
|
993
|
+
return null;
|
|
994
|
+
};
|
|
995
|
+
var readOriginRemote = (gitDir) => {
|
|
996
|
+
const configPath = path5.join(gitDir, "config");
|
|
997
|
+
try {
|
|
998
|
+
const content = fs4.readFileSync(configPath, "utf8");
|
|
999
|
+
const lines = content.split(/\r?\n/);
|
|
1000
|
+
let inOrigin = false;
|
|
1001
|
+
for (const line of lines) {
|
|
1002
|
+
const sectionMatch = line.match(/^\s*\[(.+)]\s*$/);
|
|
1003
|
+
if (sectionMatch) {
|
|
1004
|
+
inOrigin = sectionMatch[1].trim() === 'remote "origin"';
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
if (inOrigin) {
|
|
1008
|
+
const urlMatch = line.match(/^\s*url\s*=\s*(.+)\s*$/);
|
|
1009
|
+
if (urlMatch) {
|
|
1010
|
+
return urlMatch[1].trim();
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
} catch {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
return null;
|
|
1018
|
+
};
|
|
1019
|
+
function resolveRepoFingerprint(cwd2) {
|
|
1020
|
+
const found = findGitRoot(cwd2);
|
|
1021
|
+
if (!found) {
|
|
1022
|
+
const fallbackPath = path5.resolve(cwd2);
|
|
1023
|
+
const fingerprint2 = sha256(fallbackPath);
|
|
1024
|
+
debugLog(`repo fingerprint=${fingerprint2} source=path-fallback`);
|
|
1025
|
+
return {
|
|
1026
|
+
fingerprint: fingerprint2,
|
|
1027
|
+
gitRoot: fallbackPath,
|
|
1028
|
+
source: "path-fallback"
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
const { gitRoot, gitDir } = found;
|
|
1032
|
+
const remoteUrl = readOriginRemote(gitDir);
|
|
1033
|
+
if (remoteUrl) {
|
|
1034
|
+
const normalized = normalizeRemoteUrl(remoteUrl);
|
|
1035
|
+
const fingerprint2 = sha256(normalized);
|
|
1036
|
+
debugLog(`repo fingerprint=${fingerprint2} source=git-remote`);
|
|
1037
|
+
return {
|
|
1038
|
+
fingerprint: fingerprint2,
|
|
1039
|
+
gitRoot,
|
|
1040
|
+
remoteUrl,
|
|
1041
|
+
source: "git-remote"
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
const fingerprint = sha256(path5.resolve(gitRoot));
|
|
1045
|
+
debugLog(`repo fingerprint=${fingerprint} source=path-fallback`);
|
|
1046
|
+
return {
|
|
1047
|
+
fingerprint,
|
|
1048
|
+
gitRoot,
|
|
1049
|
+
source: "path-fallback"
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// src/workspaceMap.ts
|
|
1054
|
+
var fs5 = __toESM(require("fs/promises"), 1);
|
|
1055
|
+
var path6 = __toESM(require("path"), 1);
|
|
1056
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
1057
|
+
var parseBooleanFlag4 = (value) => {
|
|
1058
|
+
if (!value) {
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
const normalized = value.trim().toLowerCase();
|
|
1062
|
+
return ["1", "true", "yes", "on"].includes(normalized);
|
|
1063
|
+
};
|
|
1064
|
+
var debugEnabled3 = parseBooleanFlag4(process.env.MEMORAONE_DEV_MODE);
|
|
1065
|
+
var debugLog2 = (message) => {
|
|
1066
|
+
if (!debugEnabled3) {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
process.stderr.write(`[memoraone-mcp][debug] ${message}
|
|
1070
|
+
`);
|
|
1071
|
+
};
|
|
1072
|
+
var fingerprintRegex = /^[0-9a-f]{64}$/i;
|
|
1073
|
+
function getWorkspaceMapPath() {
|
|
1074
|
+
return path6.join(import_node_os.default.homedir(), ".memoraone", "workspaces.json");
|
|
1075
|
+
}
|
|
1076
|
+
var ensureWorkspaceDir = async () => {
|
|
1077
|
+
const dir = path6.dirname(getWorkspaceMapPath());
|
|
1078
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
1079
|
+
};
|
|
1080
|
+
async function acquireWorkspaceMapLock() {
|
|
1081
|
+
const filePath = getWorkspaceMapPath();
|
|
1082
|
+
const lockPath = `${filePath}.lock`;
|
|
1083
|
+
const maxRetries = 10;
|
|
1084
|
+
const retryDelayMs = 50;
|
|
1085
|
+
const maxLockAgeMs = 5e3;
|
|
1086
|
+
let lockAcquired = false;
|
|
1087
|
+
let retries = 0;
|
|
1088
|
+
while (!lockAcquired && retries < maxRetries) {
|
|
1089
|
+
try {
|
|
1090
|
+
try {
|
|
1091
|
+
const stat2 = await fs5.stat(lockPath);
|
|
1092
|
+
const ageMs = Date.now() - stat2.mtimeMs;
|
|
1093
|
+
if (ageMs > maxLockAgeMs) {
|
|
1094
|
+
await fs5.unlink(lockPath);
|
|
1095
|
+
debugLog2(`removed stale workspace map lock (age: ${ageMs}ms)`);
|
|
1096
|
+
}
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
if (err?.code !== "ENOENT") {
|
|
1099
|
+
throw err;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
const fd = await fs5.open(lockPath, "wx");
|
|
1103
|
+
await fd.close();
|
|
1104
|
+
lockAcquired = true;
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
if (err?.code === "EEXIST") {
|
|
1107
|
+
retries++;
|
|
1108
|
+
if (retries < maxRetries) {
|
|
1109
|
+
await new Promise((resolve6) => setTimeout(resolve6, retryDelayMs));
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
throw new Error(
|
|
1113
|
+
`[memoraone-mcp] Failed to acquire workspace map lock after ${maxRetries} retries`
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
throw err;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return async () => {
|
|
1120
|
+
try {
|
|
1121
|
+
await fs5.unlink(lockPath);
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
if (err?.code !== "ENOENT") {
|
|
1124
|
+
debugLog2(`failed to release workspace map lock: ${String(err)}`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
var validateWorkspaceMap = (map, filePath) => {
|
|
1130
|
+
if (!map || typeof map !== "object" || Array.isArray(map)) {
|
|
1131
|
+
throw new Error(
|
|
1132
|
+
`[memoraone-mcp] Invalid workspace map schema in ${filePath}`
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
for (const [fingerprint, entry] of Object.entries(map)) {
|
|
1136
|
+
if (!fingerprintRegex.test(fingerprint)) {
|
|
1137
|
+
throw new Error(
|
|
1138
|
+
`[memoraone-mcp] Invalid workspace fingerprint in ${filePath}`
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
if (typeof entry === "string") {
|
|
1142
|
+
if (!entry.trim()) {
|
|
1143
|
+
throw new Error(
|
|
1144
|
+
`[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
1150
|
+
throw new Error(
|
|
1151
|
+
`[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
const projectKey = entry.projectKey ?? entry.project_id;
|
|
1155
|
+
if (!projectKey || !projectKey.trim()) {
|
|
1156
|
+
throw new Error(
|
|
1157
|
+
`[memoraone-mcp] Invalid workspace projectKey in ${filePath}`
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
const source = entry.source;
|
|
1161
|
+
if (source !== void 0 && typeof source !== "string") {
|
|
1162
|
+
throw new Error(
|
|
1163
|
+
`[memoraone-mcp] Invalid workspace source in ${filePath}`
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
const linkedAt = entry.linked_at;
|
|
1167
|
+
if (linkedAt !== void 0 && typeof linkedAt !== "string") {
|
|
1168
|
+
throw new Error(
|
|
1169
|
+
`[memoraone-mcp] Invalid workspace linked_at in ${filePath}`
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
async function readWorkspaceMap() {
|
|
1175
|
+
const filePath = getWorkspaceMapPath();
|
|
1176
|
+
try {
|
|
1177
|
+
const content = await fs5.readFile(filePath, "utf8");
|
|
1178
|
+
const parsed2 = JSON.parse(content);
|
|
1179
|
+
validateWorkspaceMap(parsed2, filePath);
|
|
1180
|
+
const typed = parsed2;
|
|
1181
|
+
let migrated = false;
|
|
1182
|
+
const normalized = {};
|
|
1183
|
+
for (const [fingerprint, entry] of Object.entries(typed)) {
|
|
1184
|
+
if (typeof entry === "string") {
|
|
1185
|
+
normalized[fingerprint] = entry;
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
const projectKey = entry.projectKey ?? entry.project_id ?? "";
|
|
1189
|
+
if (entry.project_id && !entry.projectKey) {
|
|
1190
|
+
migrated = true;
|
|
1191
|
+
}
|
|
1192
|
+
normalized[fingerprint] = {
|
|
1193
|
+
...projectKey ? { projectKey } : {},
|
|
1194
|
+
...entry.source ? { source: entry.source } : {},
|
|
1195
|
+
...entry.linked_at ? { linked_at: entry.linked_at } : {}
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
debugLog2(
|
|
1199
|
+
`workspace map loaded path=${filePath} entries=${Object.keys(normalized).length}`
|
|
1200
|
+
);
|
|
1201
|
+
return { map: normalized, needsMigration: migrated };
|
|
1202
|
+
} catch (err) {
|
|
1203
|
+
if (err?.code === "ENOENT") {
|
|
1204
|
+
const emptyMap = {};
|
|
1205
|
+
debugLog2(`workspace map loaded path=${filePath} entries=0`);
|
|
1206
|
+
return { map: emptyMap, needsMigration: false };
|
|
1207
|
+
}
|
|
1208
|
+
if (err instanceof SyntaxError) {
|
|
1209
|
+
throw new Error(
|
|
1210
|
+
`[memoraone-mcp] Failed to parse workspace map at ${filePath}`
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
throw err;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
async function writeWorkspaceMap(map) {
|
|
1217
|
+
const filePath = getWorkspaceMapPath();
|
|
1218
|
+
validateWorkspaceMap(map, filePath);
|
|
1219
|
+
await ensureWorkspaceDir();
|
|
1220
|
+
const tempPath = `${filePath}.tmp`;
|
|
1221
|
+
const content = JSON.stringify(map, null, 2);
|
|
1222
|
+
await fs5.writeFile(tempPath, content, "utf8");
|
|
1223
|
+
await fs5.rename(tempPath, filePath);
|
|
1224
|
+
}
|
|
1225
|
+
async function setProjectIdForFingerprint(args) {
|
|
1226
|
+
const { fingerprint, projectKey, source, linked_at } = args;
|
|
1227
|
+
if (!fingerprintRegex.test(fingerprint)) {
|
|
1228
|
+
throw new Error("[memoraone-mcp] Invalid fingerprint");
|
|
1229
|
+
}
|
|
1230
|
+
if (!projectKey.trim()) {
|
|
1231
|
+
throw new Error("[memoraone-mcp] Invalid projectKey");
|
|
1232
|
+
}
|
|
1233
|
+
const releaseLock = await acquireWorkspaceMapLock();
|
|
1234
|
+
try {
|
|
1235
|
+
const { map, needsMigration } = await readWorkspaceMap();
|
|
1236
|
+
if (needsMigration) {
|
|
1237
|
+
await writeWorkspaceMap(map);
|
|
1238
|
+
}
|
|
1239
|
+
map[fingerprint] = {
|
|
1240
|
+
projectKey,
|
|
1241
|
+
...source ? { source } : {},
|
|
1242
|
+
linked_at: linked_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1243
|
+
};
|
|
1244
|
+
await writeWorkspaceMap(map);
|
|
1245
|
+
debugLog2(
|
|
1246
|
+
`workspace map set fingerprint=${fingerprint} projectKey=${projectKey}`
|
|
1247
|
+
);
|
|
1248
|
+
} finally {
|
|
1249
|
+
await releaseLock();
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/tools/handlers/setProject.ts
|
|
1254
|
+
var setProjectInputSchema = import_v415.z.object({
|
|
1255
|
+
projectKey: import_v415.z.string().min(1).optional(),
|
|
1256
|
+
projectId: import_v415.z.string().min(1).optional()
|
|
1257
|
+
});
|
|
1258
|
+
async function handleSetProject(args) {
|
|
1259
|
+
const parsed2 = setProjectInputSchema.parse(args ?? {});
|
|
1260
|
+
const resolvedProjectKey = parsed2.projectKey ?? parsed2.projectId;
|
|
1261
|
+
if (!resolvedProjectKey) {
|
|
1262
|
+
throw new Error("projectKey is required");
|
|
1263
|
+
}
|
|
1264
|
+
const requested = resolvedProjectKey.trim();
|
|
1265
|
+
const bound = getBoundProjectId();
|
|
1266
|
+
if (bound !== null && requested !== bound) {
|
|
1267
|
+
throw new Error(
|
|
1268
|
+
`Project switching is disabled (Option A). This MCP process is bound to ${bound}. Start a separate MCP instance/window for ${requested}.`
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
setCurrentProjectId(resolvedProjectKey);
|
|
1272
|
+
const repo = resolveRepoFingerprint(process.cwd());
|
|
1273
|
+
await setProjectIdForFingerprint({
|
|
1274
|
+
fingerprint: repo.fingerprint,
|
|
1275
|
+
projectKey: resolvedProjectKey,
|
|
1276
|
+
source: "manual"
|
|
1277
|
+
});
|
|
1278
|
+
return { ok: true, projectKey: resolvedProjectKey };
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/index.ts
|
|
1282
|
+
var notInitializedResult = {
|
|
1283
|
+
content: [
|
|
1284
|
+
{
|
|
1285
|
+
type: "text",
|
|
1286
|
+
text: "MemoraOne MCP not initialized (project binding missing)."
|
|
1287
|
+
}
|
|
1288
|
+
]
|
|
1289
|
+
};
|
|
1290
|
+
var initializeDiagDumped = false;
|
|
1291
|
+
var uriToPath = (uri) => {
|
|
1292
|
+
if (uri.startsWith("file://")) {
|
|
1293
|
+
return (0, import_node_url2.fileURLToPath)(uri);
|
|
1294
|
+
}
|
|
1295
|
+
return uri;
|
|
1296
|
+
};
|
|
1297
|
+
function getCursorWorkspaceRootFromEnv() {
|
|
1298
|
+
const raw = process.env.WORKSPACE_FOLDER_PATHS;
|
|
1299
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
1300
|
+
return void 0;
|
|
1301
|
+
}
|
|
1302
|
+
const parts = raw.split(path7.delimiter).map((p) => p.trim()).filter(Boolean);
|
|
1303
|
+
const first = parts[0];
|
|
1304
|
+
return first ? path7.resolve(first) : void 0;
|
|
1305
|
+
}
|
|
1306
|
+
function isHeartbeatDebugEnabled() {
|
|
1307
|
+
const value = String(process.env.MEMORAONE_DEBUG_HEARTBEAT ?? "").trim().toLowerCase();
|
|
1308
|
+
return ["1", "true", "yes", "on"].includes(value);
|
|
1309
|
+
}
|
|
1310
|
+
function fingerprintApiKey(apiKey) {
|
|
1311
|
+
return crypto5.createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
|
|
1312
|
+
}
|
|
1313
|
+
function inferIdeType(params) {
|
|
1314
|
+
if (config2.ideType) {
|
|
1315
|
+
return config2.ideType;
|
|
1316
|
+
}
|
|
1317
|
+
const clientInfoName = String(params?.clientInfo?.name ?? "").toLowerCase();
|
|
1318
|
+
const clientInfoVersion = String(params?.clientInfo?.version ?? "").toLowerCase();
|
|
1319
|
+
const termProgram = String(process.env.TERM_PROGRAM ?? "").toLowerCase();
|
|
1320
|
+
const argv = process.argv.join(" ").toLowerCase();
|
|
1321
|
+
const envKeys = Object.keys(process.env);
|
|
1322
|
+
const hasCursorSignals = envKeys.some((key) => key.startsWith("CURSOR_")) || termProgram === "cursor" || clientInfoName.includes("cursor") || clientInfoVersion.includes("cursor") || argv.includes("cursor");
|
|
1323
|
+
if (hasCursorSignals) {
|
|
1324
|
+
return "cursor";
|
|
1325
|
+
}
|
|
1326
|
+
const hasJetBrainsSignals = envKeys.some(
|
|
1327
|
+
(key) => [
|
|
1328
|
+
"JETBRAINS_IDE",
|
|
1329
|
+
"IDEA_INITIAL_DIRECTORY",
|
|
1330
|
+
"JETBRAINS_REMOTE_RUN",
|
|
1331
|
+
"INTELLIJ_ENVIRONMENT_READER"
|
|
1332
|
+
].includes(key)
|
|
1333
|
+
) || String(process.env.TERMINAL_EMULATOR ?? "").toLowerCase().includes("jetbrains") || /(jetbrains|intellij|pycharm|webstorm|goland|rubymine|clion|phpstorm|rider|datagrip)/.test(
|
|
1334
|
+
clientInfoName
|
|
1335
|
+
) || /(jetbrains|intellij|pycharm|webstorm|goland|rubymine|clion|phpstorm|rider|datagrip)/.test(
|
|
1336
|
+
argv
|
|
1337
|
+
);
|
|
1338
|
+
if (hasJetBrainsSignals) {
|
|
1339
|
+
return "jetbrains";
|
|
1340
|
+
}
|
|
1341
|
+
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);
|
|
1342
|
+
if (hasVsCodeSignals) {
|
|
1343
|
+
return "copilot-vscode";
|
|
1344
|
+
}
|
|
1345
|
+
return void 0;
|
|
1346
|
+
}
|
|
1347
|
+
async function sendHeartbeat(client, runtime) {
|
|
1348
|
+
try {
|
|
1349
|
+
const pid = runtime.projectId?.trim();
|
|
1350
|
+
if (isHeartbeatDebugEnabled()) {
|
|
1351
|
+
process.stderr.write(
|
|
1352
|
+
`[memoraone-mcp][diag] heartbeat projectId=${pid ?? "unknown"} apiKeySource=${runtime.apiKeySource ?? "unknown"} apiKeyFingerprint=${runtime.apiKeyFingerprint ?? "unknown"}
|
|
1353
|
+
`
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
if (!pid) {
|
|
1357
|
+
throw new Error("[memoraone-mcp] Cannot send heartbeat without an active project binding");
|
|
1358
|
+
}
|
|
1359
|
+
const body = {};
|
|
1360
|
+
if (runtime.ideType) body.ide_type = runtime.ideType;
|
|
1361
|
+
await client.post(`/v1/projects/${pid}/heartbeat`, body, {
|
|
1362
|
+
log: false,
|
|
1363
|
+
headers: {
|
|
1364
|
+
"x-project-id": pid
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
} catch (err) {
|
|
1368
|
+
process.stderr.write(
|
|
1369
|
+
`[memoraone-mcp][info] heartbeat error (silent) ${String(err)}
|
|
1370
|
+
`
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
function redactSensitiveFields(obj) {
|
|
1375
|
+
if (obj === null || obj === void 0) return obj;
|
|
1376
|
+
if (typeof obj !== "object") return obj;
|
|
1377
|
+
if (Array.isArray(obj)) return obj.map(redactSensitiveFields);
|
|
1378
|
+
const redacted = {};
|
|
1379
|
+
const sensitiveKeys = /^(headers?|api[_-]?key|token|cookie|authorization|auth|password|secret|credential|bearer)$/i;
|
|
1380
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1381
|
+
if (sensitiveKeys.test(key)) {
|
|
1382
|
+
redacted[key] = "[REDACTED]";
|
|
1383
|
+
} else if (typeof value === "object") {
|
|
1384
|
+
redacted[key] = redactSensitiveFields(value);
|
|
1385
|
+
} else {
|
|
1386
|
+
redacted[key] = value;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return redacted;
|
|
1390
|
+
}
|
|
1391
|
+
function sanitizeArgsSummary(args) {
|
|
1392
|
+
try {
|
|
1393
|
+
const redacted = redactSensitiveFields(args);
|
|
1394
|
+
const summary = JSON.stringify(redacted);
|
|
1395
|
+
return summary.length > 200 ? summary.slice(0, 200) + "..." : summary;
|
|
1396
|
+
} catch {
|
|
1397
|
+
return "[unable to serialize args]";
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
async function postWorklogEvent(client, projectId, message) {
|
|
1401
|
+
try {
|
|
1402
|
+
if (!projectId) {
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
const body = {
|
|
1406
|
+
kind: "note",
|
|
1407
|
+
concept: "concept:worklog",
|
|
1408
|
+
actor: { type: config2.agentType, name: config2.agentName },
|
|
1409
|
+
message,
|
|
1410
|
+
projectKey: projectId,
|
|
1411
|
+
metadata: {
|
|
1412
|
+
source: config2.source,
|
|
1413
|
+
purpose: "worklog"
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
await client.post("/timeline/events", body);
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
console.error("[memoraone-mcp] worklog error (silent)", err);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function registerToolWithWorklog(server, runtime, sessionContext, toolName, description, schema, handler) {
|
|
1422
|
+
const wrapped = async (args) => runWithSessionContext(sessionContext, async () => {
|
|
1423
|
+
if (!runtime.client || !runtime.projectId) return notInitializedResult;
|
|
1424
|
+
if (!config2.worklogEnabled) {
|
|
1425
|
+
return handler(args);
|
|
1426
|
+
}
|
|
1427
|
+
const argsSummary = sanitizeArgsSummary(args);
|
|
1428
|
+
const start = Date.now();
|
|
1429
|
+
await postWorklogEvent(runtime.client, runtime.projectId, `tool_start: ${toolName} ${argsSummary}`);
|
|
1430
|
+
try {
|
|
1431
|
+
const result = await handler(args);
|
|
1432
|
+
const durationMs = Date.now() - start;
|
|
1433
|
+
await postWorklogEvent(runtime.client, runtime.projectId, `tool_end: ${toolName} ok (${durationMs}ms)`);
|
|
1434
|
+
return result;
|
|
1435
|
+
} catch (err) {
|
|
1436
|
+
const durationMs = Date.now() - start;
|
|
1437
|
+
const errorSummary = err?.message || String(err);
|
|
1438
|
+
const shortError = errorSummary.length > 100 ? errorSummary.slice(0, 100) + "..." : errorSummary;
|
|
1439
|
+
await postWorklogEvent(
|
|
1440
|
+
runtime.client,
|
|
1441
|
+
runtime.projectId,
|
|
1442
|
+
`tool_end: ${toolName} error (${durationMs}ms): ${shortError}`
|
|
1443
|
+
);
|
|
1444
|
+
throw err;
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
server.tool(toolName, description, schema, wrapped);
|
|
1448
|
+
}
|
|
1449
|
+
async function main(opts = {}) {
|
|
1450
|
+
let bindingReadyResolve = null;
|
|
1451
|
+
let bindingReadyReject = null;
|
|
1452
|
+
const bindingReady = new Promise((resolve6, reject) => {
|
|
1453
|
+
bindingReadyResolve = resolve6;
|
|
1454
|
+
bindingReadyReject = reject;
|
|
1455
|
+
});
|
|
1456
|
+
const devMode = Boolean(config2.devMode);
|
|
1457
|
+
const sessionLabel = opts.sessionLabel ?? "session";
|
|
1458
|
+
const sessionContext = createSessionRunContext();
|
|
1459
|
+
const runtime = {
|
|
1460
|
+
client: null,
|
|
1461
|
+
projectId: null,
|
|
1462
|
+
apiKeySource: null,
|
|
1463
|
+
apiKeyFingerprint: null,
|
|
1464
|
+
ideType: void 0
|
|
1465
|
+
};
|
|
1466
|
+
let workspaceRoot;
|
|
1467
|
+
const server = new import_mcp.McpServer({
|
|
1468
|
+
name: "memoraone-mcp",
|
|
1469
|
+
version: "1.0.0"
|
|
1470
|
+
});
|
|
1471
|
+
const resolveFallbackBindingFromInitialize = async (params) => {
|
|
1472
|
+
let fallbackWorkspaceRoot = workspaceRoot;
|
|
1473
|
+
try {
|
|
1474
|
+
const rootsResult = await server.server.listRoots({});
|
|
1475
|
+
const roots = Array.isArray(rootsResult?.roots) ? rootsResult.roots : [];
|
|
1476
|
+
console.error(`[memoraone-mcp] roots/list returned ${roots.length}: ${JSON.stringify(roots)}`);
|
|
1477
|
+
if (!fallbackWorkspaceRoot && roots.length > 0 && roots[0]?.uri) {
|
|
1478
|
+
fallbackWorkspaceRoot = uriToPath(roots[0].uri);
|
|
1479
|
+
console.error("[memoraone-mcp] Workspace resolution strategy: roots/list (first root)");
|
|
1480
|
+
}
|
|
1481
|
+
} catch (e) {
|
|
1482
|
+
console.error("[memoraone-mcp] roots/list failed:", String(e));
|
|
1483
|
+
}
|
|
1484
|
+
console.error(`[memoraone-mcp] process.cwd(): ${process.cwd()}`);
|
|
1485
|
+
console.error(
|
|
1486
|
+
`[memoraone-mcp] WORKSPACE_FOLDER_PATHS: ${process.env.WORKSPACE_FOLDER_PATHS ?? "(unset)"}`
|
|
1487
|
+
);
|
|
1488
|
+
if (!fallbackWorkspaceRoot) {
|
|
1489
|
+
if (params.workspaceFolders?.length) {
|
|
1490
|
+
fallbackWorkspaceRoot = uriToPath(params.workspaceFolders[0].uri);
|
|
1491
|
+
console.error("[memoraone-mcp] Workspace resolution strategy: initialize.workspaceFolders");
|
|
1492
|
+
} else if (params.rootUri) {
|
|
1493
|
+
fallbackWorkspaceRoot = uriToPath(params.rootUri);
|
|
1494
|
+
console.error("[memoraone-mcp] Workspace resolution strategy: initialize.rootUri");
|
|
1495
|
+
} else {
|
|
1496
|
+
const cursorRoot = getCursorWorkspaceRootFromEnv();
|
|
1497
|
+
if (cursorRoot !== void 0) {
|
|
1498
|
+
fallbackWorkspaceRoot = cursorRoot;
|
|
1499
|
+
console.error("[memoraone-mcp] Workspace resolution strategy: env.WORKSPACE_FOLDER_PATHS");
|
|
1500
|
+
} else {
|
|
1501
|
+
fallbackWorkspaceRoot = process.cwd();
|
|
1502
|
+
console.error("[memoraone-mcp] Workspace resolution strategy: process.cwd() fallback");
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
const binding = await resolveAuthoritativeBinding(fallbackWorkspaceRoot);
|
|
1507
|
+
workspaceRoot = binding.workspaceRoot;
|
|
1508
|
+
return binding;
|
|
1509
|
+
};
|
|
1510
|
+
const registeredToolNames = [];
|
|
1511
|
+
server.tool(
|
|
1512
|
+
"memora_post_event",
|
|
1513
|
+
"Forward an event to MemoraOne timeline",
|
|
1514
|
+
postEventShape,
|
|
1515
|
+
async (args) => runWithSessionContext(sessionContext, async () => {
|
|
1516
|
+
if (!runtime.client || !runtime.projectId) return notInitializedResult;
|
|
1517
|
+
const result = await handlePostEvent(runtime.client, args);
|
|
1518
|
+
return {
|
|
1519
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
1520
|
+
};
|
|
1521
|
+
})
|
|
1522
|
+
);
|
|
1523
|
+
registeredToolNames.push("memora_post_event");
|
|
1524
|
+
server.tool(
|
|
1525
|
+
"memora_list_projects",
|
|
1526
|
+
"List projects available to the current API key",
|
|
1527
|
+
listProjectsShape,
|
|
1528
|
+
async () => runWithSessionContext(sessionContext, async () => {
|
|
1529
|
+
if (!runtime.client || !runtime.projectId) return notInitializedResult;
|
|
1530
|
+
const result = await handleListProjects(runtime.client);
|
|
1531
|
+
return {
|
|
1532
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
1533
|
+
};
|
|
1534
|
+
})
|
|
1535
|
+
);
|
|
1536
|
+
registeredToolNames.push("memora_list_projects");
|
|
1537
|
+
server.tool(
|
|
1538
|
+
"memora_set_project",
|
|
1539
|
+
"Set the current project key for subsequent tool calls",
|
|
1540
|
+
setProjectShape,
|
|
1541
|
+
async (args) => runWithSessionContext(sessionContext, async () => {
|
|
1542
|
+
if (!runtime.client || !runtime.projectId) return notInitializedResult;
|
|
1543
|
+
const result = await handleSetProject(args);
|
|
1544
|
+
return {
|
|
1545
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
1546
|
+
};
|
|
1547
|
+
})
|
|
1548
|
+
);
|
|
1549
|
+
registeredToolNames.push("memora_set_project");
|
|
1550
|
+
registerToolWithWorklog(
|
|
1551
|
+
server,
|
|
1552
|
+
runtime,
|
|
1553
|
+
sessionContext,
|
|
1554
|
+
"memora_ask_with_memory",
|
|
1555
|
+
"Ask MemoraOne with project memory context",
|
|
1556
|
+
askWithMemoryShape,
|
|
1557
|
+
async (args) => {
|
|
1558
|
+
const result = await handleAskWithMemory(runtime.client, args);
|
|
1559
|
+
return {
|
|
1560
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
);
|
|
1564
|
+
registeredToolNames.push("memora_ask_with_memory");
|
|
1565
|
+
registerToolWithWorklog(
|
|
1566
|
+
server,
|
|
1567
|
+
runtime,
|
|
1568
|
+
sessionContext,
|
|
1569
|
+
"memora_log_intent",
|
|
1570
|
+
"Log a natural-language TASK or DECISION intent to the MemoraOne timeline",
|
|
1571
|
+
logIntentShape,
|
|
1572
|
+
async (args) => {
|
|
1573
|
+
const result = await handleLogIntent(runtime.client, args);
|
|
1574
|
+
return {
|
|
1575
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
);
|
|
1579
|
+
registeredToolNames.push("memora_log_intent");
|
|
1580
|
+
registerToolWithWorklog(
|
|
1581
|
+
server,
|
|
1582
|
+
runtime,
|
|
1583
|
+
sessionContext,
|
|
1584
|
+
"memora_log_change_summary",
|
|
1585
|
+
"Log a concise code change summary to the MemoraOne timeline",
|
|
1586
|
+
logChangeSummaryShape,
|
|
1587
|
+
async (args) => {
|
|
1588
|
+
const result = await handleLogChangeSummary(runtime.client, args);
|
|
1589
|
+
return {
|
|
1590
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
);
|
|
1594
|
+
registeredToolNames.push("memora_log_change_summary");
|
|
1595
|
+
registerToolWithWorklog(
|
|
1596
|
+
server,
|
|
1597
|
+
runtime,
|
|
1598
|
+
sessionContext,
|
|
1599
|
+
"memora_log_tool_result",
|
|
1600
|
+
"Log a tool execution result to the MemoraOne timeline",
|
|
1601
|
+
logToolResultShape,
|
|
1602
|
+
async (args) => {
|
|
1603
|
+
const result = await handleLogToolResult(runtime.client, args);
|
|
1604
|
+
return {
|
|
1605
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
);
|
|
1609
|
+
registeredToolNames.push("memora_log_tool_result");
|
|
1610
|
+
registerToolWithWorklog(
|
|
1611
|
+
server,
|
|
1612
|
+
runtime,
|
|
1613
|
+
sessionContext,
|
|
1614
|
+
"memora_log_command",
|
|
1615
|
+
"Log a command execution to the MemoraOne timeline",
|
|
1616
|
+
logCommandShape,
|
|
1617
|
+
async (args) => {
|
|
1618
|
+
const result = await handleLogCommand(runtime.client, args);
|
|
1619
|
+
return {
|
|
1620
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
);
|
|
1624
|
+
registeredToolNames.push("memora_log_command");
|
|
1625
|
+
server.server.setRequestHandler(
|
|
1626
|
+
import_types.InitializeRequestSchema,
|
|
1627
|
+
async (request) => runWithSessionContext(sessionContext, async () => {
|
|
1628
|
+
try {
|
|
1629
|
+
const params = request.params;
|
|
1630
|
+
runtime.ideType = inferIdeType(params);
|
|
1631
|
+
if (!initializeDiagDumped) {
|
|
1632
|
+
initializeDiagDumped = true;
|
|
1633
|
+
const folders = Array.isArray(params.workspaceFolders) ? params.workspaceFolders.map((f) => ({ name: f?.name, uri: f?.uri })) : params.workspaceFolders;
|
|
1634
|
+
const capKeys = params.capabilities && typeof params.capabilities === "object" ? Object.keys(params.capabilities) : [];
|
|
1635
|
+
const cursorEnv = Object.keys(process.env).filter((k) => k.startsWith("CURSOR_")).reduce((acc, k) => {
|
|
1636
|
+
acc[k] = process.env[k] ?? "";
|
|
1637
|
+
return acc;
|
|
1638
|
+
}, {});
|
|
1639
|
+
const vscodeEnv = Object.keys(process.env).filter((k) => k.startsWith("VSCODE_")).reduce((acc, k) => {
|
|
1640
|
+
acc[k] = process.env[k] ?? "";
|
|
1641
|
+
return acc;
|
|
1642
|
+
}, {});
|
|
1643
|
+
console.error("[memoraone-mcp][diag] Initialize dump (once per process):");
|
|
1644
|
+
console.error(`[memoraone-mcp][diag] params.rootUri: ${JSON.stringify(params.rootUri)}`);
|
|
1645
|
+
console.error(`[memoraone-mcp][diag] params.workspaceFolders: ${JSON.stringify(folders)}`);
|
|
1646
|
+
console.error(`[memoraone-mcp][diag] params.clientInfo: ${JSON.stringify(params.clientInfo)}`);
|
|
1647
|
+
console.error(`[memoraone-mcp][diag] params.capabilities (keys): ${JSON.stringify(capKeys)}`);
|
|
1648
|
+
console.error(`[memoraone-mcp][diag] process.cwd(): ${process.cwd()}`);
|
|
1649
|
+
console.error(`[memoraone-mcp][diag] process.execPath: ${process.execPath}`);
|
|
1650
|
+
console.error(`[memoraone-mcp][diag] process.argv: ${JSON.stringify(process.argv)}`);
|
|
1651
|
+
console.error(`[memoraone-mcp][diag] process.env.WORKSPACE_FOLDER_PATHS: ${JSON.stringify(process.env.WORKSPACE_FOLDER_PATHS)}`);
|
|
1652
|
+
console.error(`[memoraone-mcp][diag] process.env.PWD: ${JSON.stringify(process.env.PWD)}`);
|
|
1653
|
+
console.error(`[memoraone-mcp][diag] process.env.HOME: ${JSON.stringify(process.env.HOME)}`);
|
|
1654
|
+
console.error(`[memoraone-mcp][diag] process.env.CURSOR_*: ${JSON.stringify(cursorEnv)}`);
|
|
1655
|
+
console.error(`[memoraone-mcp][diag] process.env.VSCODE_*: ${JSON.stringify(vscodeEnv)}`);
|
|
1656
|
+
console.error(`[memoraone-mcp][diag] process.env.TERM_PROGRAM: ${JSON.stringify(process.env.TERM_PROGRAM)}`);
|
|
1657
|
+
console.error(`[memoraone-mcp][diag] process.env.MEMORAONE_M1_PATH: ${JSON.stringify(process.env.MEMORAONE_M1_PATH)}`);
|
|
1658
|
+
}
|
|
1659
|
+
const debugAuth = ["1", "true", "yes", "on"].includes(
|
|
1660
|
+
String(process.env.MEMORAONE_DEBUG_AUTH ?? "").trim().toLowerCase()
|
|
1661
|
+
);
|
|
1662
|
+
const debugLog3 = config2.devMode || debugAuth;
|
|
1663
|
+
const binding = opts.authoritativeBinding ? opts.authoritativeBinding : await resolveFallbackBindingFromInitialize(params);
|
|
1664
|
+
const apiKeyToUse = binding.apiKey;
|
|
1665
|
+
if (!apiKeyToUse) {
|
|
1666
|
+
throw new Error(
|
|
1667
|
+
"[memoraone-mcp] No actor key. Set MEMORAONE_API_KEY or MEMORA_API_KEY, or add MEMORAONE_API_KEY/api_key to memoraone.m1"
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
if (debugLog3) {
|
|
1671
|
+
console.error(
|
|
1672
|
+
"[memoraone-mcp][debug] Resolved actor key from " + (binding.apiKeySource === "env" ? "ENV" : "memoraone.m1")
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
const projectId = binding.projectId;
|
|
1676
|
+
const existing = getBoundProjectId();
|
|
1677
|
+
if (existing !== null && existing !== projectId) {
|
|
1678
|
+
const requestedRoot = binding.workspaceRoot ?? workspaceRoot ?? process.cwd();
|
|
1679
|
+
const action = "Open this repo in a separate window or configure a separate MCP server instance per root.";
|
|
1680
|
+
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.`;
|
|
1681
|
+
console.error(
|
|
1682
|
+
`[memoraone-mcp][ERROR] Option A conflict: boundProjectId=${existing} requestedProjectId=${projectId} workspaceRoot=${requestedRoot}. ${action}`
|
|
1683
|
+
);
|
|
1684
|
+
bindingReadyReject?.(new Error(errMsg));
|
|
1685
|
+
setImmediate(() => process.exit(1));
|
|
1686
|
+
throw new Error(errMsg);
|
|
1687
|
+
}
|
|
1688
|
+
if (existing === null) {
|
|
1689
|
+
setBoundProjectId(projectId);
|
|
1690
|
+
setBoundApiKey(apiKeyToUse);
|
|
1691
|
+
console.error(
|
|
1692
|
+
`[memoraone-mcp] ${sessionLabel} bound to project ${projectId} (Option A: single-project binding)`
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
setCurrentProjectId(projectId);
|
|
1696
|
+
setCurrentApiKey(apiKeyToUse);
|
|
1697
|
+
runtime.projectId = projectId;
|
|
1698
|
+
runtime.apiKeySource = binding.apiKeySource;
|
|
1699
|
+
runtime.apiKeyFingerprint = fingerprintApiKey(apiKeyToUse);
|
|
1700
|
+
runtime.client = new memoraClient_default(config2, projectId, apiKeyToUse);
|
|
1701
|
+
workspaceRoot = binding.workspaceRoot;
|
|
1702
|
+
console.error(
|
|
1703
|
+
`[memoraone-mcp DEBUG registerRepoSource] pre-register bindingSource=${binding.bindingSource} workspaceRoot=${JSON.stringify(binding.workspaceRoot)} m1Path=${JSON.stringify(binding.m1Path)} ideType=${runtime.ideType ?? "(none)"}`
|
|
1704
|
+
);
|
|
1705
|
+
await registerRepoSource(runtime.client, runtime.projectId, workspaceRoot, runtime.ideType);
|
|
1706
|
+
if (debugAuth) {
|
|
1707
|
+
console.error("[memoraone-mcp][auth] repo root:", binding.workspaceRoot);
|
|
1708
|
+
console.error("[memoraone-mcp][auth] project_id:", projectId);
|
|
1709
|
+
console.error("[memoraone-mcp][auth] api_key source:", binding.apiKeySource);
|
|
1710
|
+
}
|
|
1711
|
+
console.error(
|
|
1712
|
+
`[memoraone-mcp] ${sessionLabel} authoritative binding: project=${binding.projectId} workspace=${binding.workspaceRoot} m1=${binding.m1Path} source=${binding.bindingSource} apiKeySource=${binding.apiKeySource}`
|
|
1713
|
+
);
|
|
1714
|
+
bindingReadyResolve?.(runtime.client);
|
|
1715
|
+
return server.server._oninitialize(request);
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
bindingReadyReject?.(err);
|
|
1718
|
+
throw err;
|
|
1719
|
+
}
|
|
1720
|
+
})
|
|
1721
|
+
);
|
|
1722
|
+
if (devMode) {
|
|
1723
|
+
console.error("[memoraone-mcp] Starting MCP server with", registeredToolNames.length, "tools");
|
|
1724
|
+
}
|
|
1725
|
+
const transport = opts.transport ?? new import_stdio.StdioServerTransport();
|
|
1726
|
+
await server.connect(transport);
|
|
1727
|
+
const activeClient = await bindingReady;
|
|
1728
|
+
let heartbeatInterval = null;
|
|
1729
|
+
if (config2.heartbeatEnabled) {
|
|
1730
|
+
await sendHeartbeat(activeClient, runtime);
|
|
1731
|
+
const intervalMs = Number.isFinite(config2.heartbeatIntervalMs) ? Math.max(1e3, config2.heartbeatIntervalMs) : 3e4;
|
|
1732
|
+
console.error(
|
|
1733
|
+
`[memoraone-mcp] ${sessionLabel} owns heartbeat for project ${runtime.projectId} interval=${intervalMs}ms`
|
|
1734
|
+
);
|
|
1735
|
+
heartbeatInterval = setInterval(() => {
|
|
1736
|
+
sendHeartbeat(activeClient, runtime).catch(() => {
|
|
1737
|
+
});
|
|
1738
|
+
}, intervalMs);
|
|
1739
|
+
}
|
|
1740
|
+
const onSigInt = () => shutdown("SIGINT");
|
|
1741
|
+
const onSigTerm = () => shutdown("SIGTERM");
|
|
1742
|
+
const shutdown = (signal, exitProcess = true) => {
|
|
1743
|
+
process.off("SIGINT", onSigInt);
|
|
1744
|
+
process.off("SIGTERM", onSigTerm);
|
|
1745
|
+
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
1746
|
+
heartbeatInterval = null;
|
|
1747
|
+
if (runtime.projectId) {
|
|
1748
|
+
console.error(`[memoraone-mcp] ${sessionLabel} released heartbeat for project ${runtime.projectId}`);
|
|
1749
|
+
}
|
|
1750
|
+
if (devMode) {
|
|
1751
|
+
console.error(`[memoraone-mcp] ${sessionLabel} received ${signal}, shutting down`);
|
|
1752
|
+
}
|
|
1753
|
+
if (exitProcess) {
|
|
1754
|
+
process.exit(0);
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
process.on("SIGINT", onSigInt);
|
|
1758
|
+
process.on("SIGTERM", onSigTerm);
|
|
1759
|
+
if (devMode) {
|
|
1760
|
+
console.error("[memoraone-mcp] MCP server ready");
|
|
1761
|
+
}
|
|
1762
|
+
if (opts.sessionSocket) {
|
|
1763
|
+
await new Promise((resolve6) => {
|
|
1764
|
+
opts.sessionSocket.once("close", () => {
|
|
1765
|
+
shutdown("session closed", false);
|
|
1766
|
+
resolve6();
|
|
1767
|
+
});
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// src/daemon.ts
|
|
1773
|
+
var log = (msg) => {
|
|
1774
|
+
process.stderr.write(`[memoraone-mcp][daemon] ${msg}
|
|
1775
|
+
`);
|
|
1776
|
+
};
|
|
1777
|
+
var IDLE_SHUTDOWN_MS = 5e3;
|
|
1778
|
+
function parseProjectIdFromArgv() {
|
|
1779
|
+
const args = process.argv.slice(2);
|
|
1780
|
+
const idx = args.indexOf("--project-id");
|
|
1781
|
+
if (idx === -1 || idx + 1 >= args.length) {
|
|
1782
|
+
log("--project-id <uuid> required");
|
|
1783
|
+
process.exit(1);
|
|
1784
|
+
}
|
|
1785
|
+
return args[idx + 1];
|
|
1786
|
+
}
|
|
1787
|
+
function parseBindingFromEnv(projectId) {
|
|
1788
|
+
const binding = decodeResolvedBinding(process.env.MEMORAONE_DAEMON_BINDING_B64);
|
|
1789
|
+
if (!binding) {
|
|
1790
|
+
log("missing MEMORAONE_DAEMON_BINDING_B64");
|
|
1791
|
+
process.exit(1);
|
|
1792
|
+
}
|
|
1793
|
+
if (binding.projectId !== projectId) {
|
|
1794
|
+
log(`binding project mismatch: argv=${projectId} payload=${binding.projectId}`);
|
|
1795
|
+
process.exit(1);
|
|
1796
|
+
}
|
|
1797
|
+
return binding;
|
|
1798
|
+
}
|
|
1799
|
+
async function ensureSocketClean(socketPath) {
|
|
1800
|
+
try {
|
|
1801
|
+
fs6.accessSync(socketPath);
|
|
1802
|
+
} catch {
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
return new Promise((resolve6) => {
|
|
1806
|
+
const client = net.createConnection({ path: socketPath }, () => {
|
|
1807
|
+
client.destroy();
|
|
1808
|
+
log("daemon already running, exiting");
|
|
1809
|
+
process.exit(0);
|
|
1810
|
+
});
|
|
1811
|
+
client.on("error", () => {
|
|
1812
|
+
try {
|
|
1813
|
+
fs6.unlinkSync(socketPath);
|
|
1814
|
+
log("stale socket removed");
|
|
1815
|
+
} catch {
|
|
1816
|
+
}
|
|
1817
|
+
resolve6();
|
|
1818
|
+
});
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
async function runDaemon() {
|
|
1822
|
+
const projectId = parseProjectIdFromArgv();
|
|
1823
|
+
const binding = parseBindingFromEnv(projectId);
|
|
1824
|
+
const socketPath = getSocketPath(projectId);
|
|
1825
|
+
let nextSessionId = 1;
|
|
1826
|
+
let activeSessions = 0;
|
|
1827
|
+
let idleTimer = null;
|
|
1828
|
+
let shuttingDown = false;
|
|
1829
|
+
const dir = ensureBaseDir();
|
|
1830
|
+
log(`directory ensured: ${dir}`);
|
|
1831
|
+
log(
|
|
1832
|
+
`authoritative binding project=${binding.projectId} workspace=${binding.workspaceRoot} m1=${binding.m1Path} source=${binding.bindingSource} apiKeySource=${binding.apiKeySource}`
|
|
1833
|
+
);
|
|
1834
|
+
log("session policy: concurrent bridge sessions allowed per project daemon");
|
|
1835
|
+
await ensureSocketClean(socketPath);
|
|
1836
|
+
const cleanupSocketFile = () => {
|
|
1837
|
+
try {
|
|
1838
|
+
if (fs6.existsSync(socketPath)) {
|
|
1839
|
+
fs6.unlinkSync(socketPath);
|
|
1840
|
+
log("socket removed");
|
|
1841
|
+
}
|
|
1842
|
+
} catch (err) {
|
|
1843
|
+
log(`socket cleanup warning: ${String(err)}`);
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
const server = net.createServer(async (socket) => {
|
|
1847
|
+
if (idleTimer) {
|
|
1848
|
+
clearTimeout(idleTimer);
|
|
1849
|
+
idleTimer = null;
|
|
1850
|
+
log("idle shutdown cancelled due to new session");
|
|
1851
|
+
}
|
|
1852
|
+
const sessionId = nextSessionId++;
|
|
1853
|
+
activeSessions += 1;
|
|
1854
|
+
let released = false;
|
|
1855
|
+
const releaseActiveSession = () => {
|
|
1856
|
+
if (released) return;
|
|
1857
|
+
released = true;
|
|
1858
|
+
activeSessions = Math.max(0, activeSessions - 1);
|
|
1859
|
+
log(`session=${sessionId} closed activeSessions=${activeSessions}`);
|
|
1860
|
+
if (activeSessions === 0 && !shuttingDown) {
|
|
1861
|
+
idleTimer = setTimeout(() => {
|
|
1862
|
+
if (activeSessions !== 0 || shuttingDown) return;
|
|
1863
|
+
shuttingDown = true;
|
|
1864
|
+
log(`idle timeout reached (${IDLE_SHUTDOWN_MS}ms), shutting down daemon`);
|
|
1865
|
+
server.close(() => {
|
|
1866
|
+
cleanupSocketFile();
|
|
1867
|
+
process.exit(0);
|
|
1868
|
+
});
|
|
1869
|
+
}, IDLE_SHUTDOWN_MS);
|
|
1870
|
+
log(`scheduled idle shutdown in ${IDLE_SHUTDOWN_MS}ms`);
|
|
1871
|
+
}
|
|
1872
|
+
};
|
|
1873
|
+
socket.once("close", releaseActiveSession);
|
|
1874
|
+
socket.once("end", releaseActiveSession);
|
|
1875
|
+
socket.once("error", () => releaseActiveSession);
|
|
1876
|
+
log(`bridge connected session=${sessionId} activeSessions=${activeSessions}`);
|
|
1877
|
+
const transport = new import_stdio2.StdioServerTransport(socket, socket);
|
|
1878
|
+
try {
|
|
1879
|
+
await main({
|
|
1880
|
+
authoritativeBinding: binding,
|
|
1881
|
+
transport,
|
|
1882
|
+
sessionSocket: socket,
|
|
1883
|
+
sessionLabel: `daemon-session-${sessionId}`
|
|
1884
|
+
});
|
|
1885
|
+
} catch (err) {
|
|
1886
|
+
log(`session error: ${String(err)}`);
|
|
1887
|
+
socket.destroy();
|
|
1888
|
+
} finally {
|
|
1889
|
+
releaseActiveSession();
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
server.on("error", (err) => {
|
|
1893
|
+
log(`server error: ${String(err)}`);
|
|
1894
|
+
cleanupSocketFile();
|
|
1895
|
+
process.exit(1);
|
|
1896
|
+
});
|
|
1897
|
+
const shutdownNow = (reason) => {
|
|
1898
|
+
if (shuttingDown) return;
|
|
1899
|
+
shuttingDown = true;
|
|
1900
|
+
if (idleTimer) {
|
|
1901
|
+
clearTimeout(idleTimer);
|
|
1902
|
+
idleTimer = null;
|
|
1903
|
+
}
|
|
1904
|
+
log(`daemon shutdown: ${reason}`);
|
|
1905
|
+
server.close(() => {
|
|
1906
|
+
cleanupSocketFile();
|
|
1907
|
+
process.exit(0);
|
|
1908
|
+
});
|
|
1909
|
+
};
|
|
1910
|
+
process.on("SIGINT", () => shutdownNow("SIGINT"));
|
|
1911
|
+
process.on("SIGTERM", () => shutdownNow("SIGTERM"));
|
|
1912
|
+
process.on("exit", cleanupSocketFile);
|
|
1913
|
+
return new Promise((resolve6) => {
|
|
1914
|
+
server.listen(socketPath, () => {
|
|
1915
|
+
log(`daemon started, listening on ${socketPath}`);
|
|
1916
|
+
resolve6();
|
|
1917
|
+
});
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1921
|
+
0 && (module.exports = {
|
|
1922
|
+
runDaemon
|
|
1923
|
+
});
|