@memoraone/mcp 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +559 -56
- package/dist/daemon.cjs +294 -77
- package/dist/index.cjs +181 -74
- package/package.json +1 -1
package/dist/daemon.cjs
CHANGED
|
@@ -41,7 +41,28 @@ var os = __toESM(require("os"), 1);
|
|
|
41
41
|
var path = __toESM(require("path"), 1);
|
|
42
42
|
var fs = __toESM(require("fs"), 1);
|
|
43
43
|
var BASE_DIR = process.env.MEMORAONE_MCP_LOCK_DIR || path.join(os.homedir(), ".memoraone-mcp");
|
|
44
|
-
|
|
44
|
+
var IDE_TYPES = ["cursor", "copilot-vscode", "jetbrains"];
|
|
45
|
+
var IDE_TYPE_SET = new Set(IDE_TYPES);
|
|
46
|
+
function parseIdeType(value) {
|
|
47
|
+
if (value === void 0 || value.trim() === "" || !IDE_TYPE_SET.has(value)) {
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function resolveIdeTypeFromEnv(env2 = process.env) {
|
|
53
|
+
return parseIdeType(env2.MEMORAONE_IDE_TYPE);
|
|
54
|
+
}
|
|
55
|
+
function parseIdeTypeFromArgv(args) {
|
|
56
|
+
const idx = args.indexOf("--ide");
|
|
57
|
+
if (idx === -1 || idx + 1 >= args.length) {
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
return parseIdeType(args[idx + 1]);
|
|
61
|
+
}
|
|
62
|
+
function getSocketPath(projectId, ideType) {
|
|
63
|
+
if (ideType) {
|
|
64
|
+
return path.join(BASE_DIR, `mcp-${projectId}-${ideType}.sock`);
|
|
65
|
+
}
|
|
45
66
|
return path.join(BASE_DIR, `mcp-${projectId}.sock`);
|
|
46
67
|
}
|
|
47
68
|
function ensureBaseDir() {
|
|
@@ -53,6 +74,13 @@ function ensureBaseDir() {
|
|
|
53
74
|
var fs2 = __toESM(require("fs/promises"), 1);
|
|
54
75
|
var path2 = __toESM(require("path"), 1);
|
|
55
76
|
var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
77
|
+
function normalizeEnvironment(raw) {
|
|
78
|
+
if (raw === void 0 || raw === null || typeof raw !== "string") {
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
const trimmed = raw.trim();
|
|
82
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
83
|
+
}
|
|
56
84
|
function parseAndValidateM1(content, markerPath) {
|
|
57
85
|
let parsed2;
|
|
58
86
|
try {
|
|
@@ -69,7 +97,8 @@ function parseAndValidateM1(content, markerPath) {
|
|
|
69
97
|
}
|
|
70
98
|
const apiKeyRaw = parsed2?.MEMORAONE_API_KEY ?? parsed2?.api_key;
|
|
71
99
|
const apiKey = apiKeyRaw !== void 0 && apiKeyRaw !== null && typeof apiKeyRaw === "string" && apiKeyRaw.trim() !== "" ? apiKeyRaw.trim() : null;
|
|
72
|
-
|
|
100
|
+
const environment = normalizeEnvironment(parsed2?.environment);
|
|
101
|
+
return environment === void 0 ? { projectId: projectId.trim(), apiKey } : { projectId: projectId.trim(), apiKey, environment };
|
|
73
102
|
}
|
|
74
103
|
async function resolveProjectIdFromExplicitM1Path() {
|
|
75
104
|
const raw = process.env.MEMORAONE_M1_PATH;
|
|
@@ -79,8 +108,8 @@ async function resolveProjectIdFromExplicitM1Path() {
|
|
|
79
108
|
const markerPath = path2.resolve(raw);
|
|
80
109
|
try {
|
|
81
110
|
const content = await fs2.readFile(markerPath, "utf8");
|
|
82
|
-
const { projectId, apiKey } = parseAndValidateM1(content, markerPath);
|
|
83
|
-
return { projectId, apiKey, foundAt: markerPath };
|
|
111
|
+
const { projectId, apiKey, environment } = parseAndValidateM1(content, markerPath);
|
|
112
|
+
return environment === void 0 ? { projectId, apiKey, foundAt: markerPath } : { projectId, apiKey, environment, foundAt: markerPath };
|
|
84
113
|
} catch (err) {
|
|
85
114
|
if (err?.code === "ENOENT") {
|
|
86
115
|
return null;
|
|
@@ -94,9 +123,9 @@ async function findM1WalkingUp(workspaceRoot) {
|
|
|
94
123
|
const markerPath = path2.join(current, "memoraone.m1");
|
|
95
124
|
try {
|
|
96
125
|
const content = await fs2.readFile(markerPath, "utf8");
|
|
97
|
-
const { projectId, apiKey } = parseAndValidateM1(content, markerPath);
|
|
126
|
+
const { projectId, apiKey, environment } = parseAndValidateM1(content, markerPath);
|
|
98
127
|
const repoRoot = path2.dirname(markerPath);
|
|
99
|
-
return { projectId, apiKey, repoRoot, markerPath };
|
|
128
|
+
return environment === void 0 ? { projectId, apiKey, repoRoot, markerPath } : { projectId, apiKey, environment, repoRoot, markerPath };
|
|
100
129
|
} catch (err) {
|
|
101
130
|
if (err?.code !== "ENOENT") {
|
|
102
131
|
throw err;
|
|
@@ -156,6 +185,7 @@ async function resolveAuthoritativeBinding(workspaceRoot) {
|
|
|
156
185
|
workspaceRoot: path2.dirname(explicitBinding.foundAt),
|
|
157
186
|
m1Path: explicitBinding.foundAt,
|
|
158
187
|
apiKey: resolved.apiKey,
|
|
188
|
+
...explicitBinding.environment !== void 0 ? { environment: explicitBinding.environment } : {},
|
|
159
189
|
bindingSource: "explicit-m1-path",
|
|
160
190
|
apiKeySource: resolved.apiKeySource
|
|
161
191
|
};
|
|
@@ -173,6 +203,7 @@ async function resolveAuthoritativeBinding(workspaceRoot) {
|
|
|
173
203
|
workspaceRoot: binding.repoRoot,
|
|
174
204
|
m1Path: binding.markerPath,
|
|
175
205
|
apiKey: resolved.apiKey,
|
|
206
|
+
...binding.environment !== void 0 ? { environment: binding.environment } : {},
|
|
176
207
|
bindingSource: "workspace-search",
|
|
177
208
|
apiKeySource: resolved.apiKeySource
|
|
178
209
|
};
|
|
@@ -194,6 +225,7 @@ function decodeResolvedBinding(value) {
|
|
|
194
225
|
const workspaceRoot = parsed2?.workspaceRoot;
|
|
195
226
|
const m1Path = parsed2?.m1Path;
|
|
196
227
|
const apiKey = parsed2?.apiKey;
|
|
228
|
+
const environment = normalizeEnvironment(parsed2?.environment);
|
|
197
229
|
const bindingSource = parsed2?.bindingSource;
|
|
198
230
|
const apiKeySource = parsed2?.apiKeySource;
|
|
199
231
|
if (!projectId || typeof projectId !== "string" || !uuidRegex.test(projectId.trim())) {
|
|
@@ -219,6 +251,7 @@ function decodeResolvedBinding(value) {
|
|
|
219
251
|
workspaceRoot,
|
|
220
252
|
m1Path,
|
|
221
253
|
apiKey: typeof apiKey === "string" && apiKey.trim() !== "" ? apiKey.trim() : null,
|
|
254
|
+
...environment !== void 0 ? { environment } : {},
|
|
222
255
|
bindingSource,
|
|
223
256
|
apiKeySource
|
|
224
257
|
};
|
|
@@ -226,7 +259,6 @@ function decodeResolvedBinding(value) {
|
|
|
226
259
|
|
|
227
260
|
// src/index.ts
|
|
228
261
|
var path7 = __toESM(require("path"), 1);
|
|
229
|
-
var crypto5 = __toESM(require("crypto"), 1);
|
|
230
262
|
var import_node_url2 = require("url");
|
|
231
263
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
232
264
|
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
@@ -541,6 +573,7 @@ async function registerRepoSource(client, projectId, repoPath, ideType) {
|
|
|
541
573
|
|
|
542
574
|
// src/tools/postEvent.ts
|
|
543
575
|
var import_v42 = require("zod/v4");
|
|
576
|
+
var postEventDescription = 'Append a durable project-change note to the MemoraOne timeline. Use after meaningful repository or project changes: decisions, fixes, new endpoints, schema changes, migrations, important wiring, or durable product behavior changes \u2014 not for trivial edits, formatting-only changes, or temporary WIP. Recommended shape: kind "note"; content.title (concise title); content.body (one durable, fact-promotable project-change statement); metadata.source (agent name, e.g. "cursor"); metadata.purpose "dev-log"; metadata.schema "v1".';
|
|
544
577
|
var postEventShape = {
|
|
545
578
|
kind: import_v42.z.string().min(1),
|
|
546
579
|
actor: import_v42.z.object({
|
|
@@ -649,6 +682,9 @@ var setProjectShape = {
|
|
|
649
682
|
projectId: import_v411.z.string().min(1).optional()
|
|
650
683
|
};
|
|
651
684
|
|
|
685
|
+
// src/tools/bindingStatus.ts
|
|
686
|
+
var bindingStatusShape = {};
|
|
687
|
+
|
|
652
688
|
// src/tools/handlers/postEvent.ts
|
|
653
689
|
var import_v412 = require("zod/v4");
|
|
654
690
|
var crypto3 = __toESM(require("crypto"), 1);
|
|
@@ -721,6 +757,32 @@ var postEventInputSchema = import_v412.z.object({
|
|
|
721
757
|
content: import_v412.z.record(import_v412.z.string(), import_v412.z.any()),
|
|
722
758
|
metadata: import_v412.z.record(import_v412.z.string(), import_v412.z.any()).optional()
|
|
723
759
|
});
|
|
760
|
+
function buildPostEventContentFields(content) {
|
|
761
|
+
if (typeof content.message === "string") {
|
|
762
|
+
return { message: content.message };
|
|
763
|
+
}
|
|
764
|
+
if (typeof content.text === "string") {
|
|
765
|
+
return { message: content.text };
|
|
766
|
+
}
|
|
767
|
+
const title = typeof content.title === "string" ? content.title : void 0;
|
|
768
|
+
const body = typeof content.body === "string" ? content.body : void 0;
|
|
769
|
+
if (title !== void 0 || body !== void 0) {
|
|
770
|
+
const message = body ?? title ?? "";
|
|
771
|
+
const structuredContent = {};
|
|
772
|
+
for (const [key, value] of Object.entries(content)) {
|
|
773
|
+
if (key === "message" || key === "text") continue;
|
|
774
|
+
structuredContent[key] = value;
|
|
775
|
+
}
|
|
776
|
+
const new_value = {
|
|
777
|
+
message,
|
|
778
|
+
...title !== void 0 ? { title } : {},
|
|
779
|
+
...body !== void 0 ? { body } : {},
|
|
780
|
+
content: structuredContent
|
|
781
|
+
};
|
|
782
|
+
return { message, new_value };
|
|
783
|
+
}
|
|
784
|
+
return { message: JSON.stringify(content) };
|
|
785
|
+
}
|
|
724
786
|
async function handlePostEvent(client, args) {
|
|
725
787
|
const nonce = crypto3.randomBytes(8).toString("hex");
|
|
726
788
|
console.error(
|
|
@@ -732,7 +794,7 @@ async function handlePostEvent(client, args) {
|
|
|
732
794
|
throw new Error("No project selected. Use memora_list_projects and memora_set_project to select a project.");
|
|
733
795
|
}
|
|
734
796
|
const content = parsed2.content ?? {};
|
|
735
|
-
const
|
|
797
|
+
const { message, new_value } = buildPostEventContentFields(content);
|
|
736
798
|
const body = {
|
|
737
799
|
kind: parsed2.kind,
|
|
738
800
|
message,
|
|
@@ -742,6 +804,7 @@ async function handlePostEvent(client, args) {
|
|
|
742
804
|
identifier: config2.agentName,
|
|
743
805
|
...parsed2.actor.id ? { id: parsed2.actor.id } : {}
|
|
744
806
|
},
|
|
807
|
+
...new_value ? { new_value } : {},
|
|
745
808
|
...parsed2.metadata ? { metadata: parsed2.metadata } : {}
|
|
746
809
|
};
|
|
747
810
|
try {
|
|
@@ -926,7 +989,8 @@ async function handleAskWithMemory(client, args) {
|
|
|
926
989
|
}
|
|
927
990
|
return {
|
|
928
991
|
answer: res.answer,
|
|
929
|
-
used: res.used ?? {}
|
|
992
|
+
used: res.used ?? {},
|
|
993
|
+
...res.retrieval !== void 0 ? { retrieval: res.retrieval } : {}
|
|
930
994
|
};
|
|
931
995
|
}
|
|
932
996
|
|
|
@@ -1471,47 +1535,144 @@ async function handleSetProject(args) {
|
|
|
1471
1535
|
return { ok: true, projectKey: resolvedProjectKey };
|
|
1472
1536
|
}
|
|
1473
1537
|
|
|
1474
|
-
// src/
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
};
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
if (uri.startsWith("file://")) {
|
|
1486
|
-
return (0, import_node_url2.fileURLToPath)(uri);
|
|
1538
|
+
// src/tools/handlers/bindingStatus.ts
|
|
1539
|
+
function buildBindingStatus(binding) {
|
|
1540
|
+
const status = {
|
|
1541
|
+
projectId: binding.projectId,
|
|
1542
|
+
workspaceRoot: binding.workspaceRoot,
|
|
1543
|
+
m1Path: binding.m1Path,
|
|
1544
|
+
bindingSource: binding.bindingSource,
|
|
1545
|
+
apiKeySource: binding.apiKeySource
|
|
1546
|
+
};
|
|
1547
|
+
if (binding.environment !== void 0) {
|
|
1548
|
+
status.environment = binding.environment;
|
|
1487
1549
|
}
|
|
1488
|
-
return
|
|
1489
|
-
}
|
|
1490
|
-
function
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
return void 0;
|
|
1550
|
+
return status;
|
|
1551
|
+
}
|
|
1552
|
+
function handleBindingStatus(binding) {
|
|
1553
|
+
if (!binding) {
|
|
1554
|
+
throw new Error("[memoraone-mcp] Binding status unavailable (not initialized)");
|
|
1494
1555
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1556
|
+
return buildBindingStatus(binding);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// src/heartbeat.ts
|
|
1560
|
+
var crypto5 = __toESM(require("crypto"), 1);
|
|
1561
|
+
function fingerprintApiKey(apiKey) {
|
|
1562
|
+
return crypto5.createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
|
|
1498
1563
|
}
|
|
1499
1564
|
function isHeartbeatDebugEnabled() {
|
|
1500
1565
|
const value = String(process.env.MEMORAONE_DEBUG_HEARTBEAT ?? "").trim().toLowerCase();
|
|
1501
1566
|
return ["1", "true", "yes", "on"].includes(value);
|
|
1502
1567
|
}
|
|
1503
|
-
function
|
|
1504
|
-
return
|
|
1568
|
+
function resolveHeartbeatIntervalMs() {
|
|
1569
|
+
return Number.isFinite(config2.heartbeatIntervalMs) ? Math.max(1e3, config2.heartbeatIntervalMs) : 3e4;
|
|
1570
|
+
}
|
|
1571
|
+
function isDaemonIdleShutdownAllowed() {
|
|
1572
|
+
return !config2.heartbeatEnabled;
|
|
1505
1573
|
}
|
|
1506
|
-
function
|
|
1507
|
-
|
|
1508
|
-
|
|
1574
|
+
async function sendProjectHeartbeat(client, ctx) {
|
|
1575
|
+
try {
|
|
1576
|
+
const pid = ctx.projectId?.trim();
|
|
1577
|
+
if (isHeartbeatDebugEnabled()) {
|
|
1578
|
+
process.stderr.write(
|
|
1579
|
+
`[memoraone-mcp][diag] heartbeat projectId=${pid ?? "unknown"} apiKeySource=${ctx.apiKeySource ?? "unknown"} apiKeyFingerprint=${ctx.apiKeyFingerprint ?? "unknown"} ideType=${ctx.ideType ?? "unknown"}
|
|
1580
|
+
`
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
if (!pid) {
|
|
1584
|
+
throw new Error("[memoraone-mcp] Cannot send heartbeat without an active project binding");
|
|
1585
|
+
}
|
|
1586
|
+
const body = {};
|
|
1587
|
+
if (ctx.ideType) body.ide_type = ctx.ideType;
|
|
1588
|
+
await client.post(`/v1/projects/${pid}/heartbeat`, body, {
|
|
1589
|
+
log: false,
|
|
1590
|
+
headers: {
|
|
1591
|
+
"x-project-id": pid
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
} catch (err) {
|
|
1595
|
+
process.stderr.write(
|
|
1596
|
+
`[memoraone-mcp][info] heartbeat error (silent) ${String(err)}
|
|
1597
|
+
`
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
function createDaemonHeartbeat(opts) {
|
|
1602
|
+
let interval = null;
|
|
1603
|
+
let client = null;
|
|
1604
|
+
const ctx = {
|
|
1605
|
+
projectId: opts.binding.projectId,
|
|
1606
|
+
ideType: opts.ideType,
|
|
1607
|
+
apiKeySource: opts.binding.apiKeySource,
|
|
1608
|
+
apiKeyFingerprint: opts.binding.apiKey ? fingerprintApiKey(opts.binding.apiKey) : null
|
|
1609
|
+
};
|
|
1610
|
+
const log2 = opts.onLog ?? ((msg) => {
|
|
1611
|
+
process.stderr.write(`[memoraone-mcp][daemon-heartbeat] ${msg}
|
|
1612
|
+
`);
|
|
1613
|
+
});
|
|
1614
|
+
const start = async () => {
|
|
1615
|
+
if (!config2.heartbeatEnabled) {
|
|
1616
|
+
log2("disabled by config");
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
if (interval) {
|
|
1620
|
+
log2("already running (skipped duplicate start)");
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
const apiKey = opts.binding.apiKey;
|
|
1624
|
+
if (!apiKey) {
|
|
1625
|
+
log2("cannot start: no api key in binding");
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
const projectId = opts.binding.projectId;
|
|
1629
|
+
client = new memoraClient_default(config2, projectId, apiKey);
|
|
1630
|
+
const intervalMs = resolveHeartbeatIntervalMs();
|
|
1631
|
+
log2(
|
|
1632
|
+
`daemon owns heartbeat for project=${projectId} ideType=${ctx.ideType ?? "unknown"} interval=${intervalMs}ms`
|
|
1633
|
+
);
|
|
1634
|
+
void sendProjectHeartbeat(client, ctx);
|
|
1635
|
+
interval = setInterval(() => {
|
|
1636
|
+
if (!client) return;
|
|
1637
|
+
sendProjectHeartbeat(client, ctx).catch(() => {
|
|
1638
|
+
});
|
|
1639
|
+
}, intervalMs);
|
|
1640
|
+
};
|
|
1641
|
+
const stop = () => {
|
|
1642
|
+
if (interval) {
|
|
1643
|
+
clearInterval(interval);
|
|
1644
|
+
interval = null;
|
|
1645
|
+
}
|
|
1646
|
+
client = null;
|
|
1647
|
+
log2(`daemon released heartbeat for project=${opts.binding.projectId}`);
|
|
1648
|
+
};
|
|
1649
|
+
const isRunning = () => interval !== null;
|
|
1650
|
+
const setIdeType = (ideType) => {
|
|
1651
|
+
if (ctx.ideType === ideType) {
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
ctx.ideType = ideType;
|
|
1655
|
+
log2(`daemon heartbeat ideType updated to ${ideType}`);
|
|
1656
|
+
if (client) {
|
|
1657
|
+
void sendProjectHeartbeat(client, ctx);
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
const getIdeType = () => ctx.ideType;
|
|
1661
|
+
return { start, stop, isRunning, setIdeType, getIdeType };
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// src/ideType.ts
|
|
1665
|
+
function inferIdeType(params, options = {}) {
|
|
1666
|
+
const env2 = options.env ?? process.env;
|
|
1667
|
+
const argv = (options.argv ?? process.argv).join(" ").toLowerCase();
|
|
1668
|
+
const configIdeType = options.configIdeType ?? config2.ideType;
|
|
1669
|
+
if (configIdeType) {
|
|
1670
|
+
return configIdeType;
|
|
1509
1671
|
}
|
|
1510
1672
|
const clientInfoName = String(params?.clientInfo?.name ?? "").toLowerCase();
|
|
1511
1673
|
const clientInfoVersion = String(params?.clientInfo?.version ?? "").toLowerCase();
|
|
1512
|
-
const termProgram = String(
|
|
1513
|
-
const
|
|
1514
|
-
const envKeys = Object.keys(process.env);
|
|
1674
|
+
const termProgram = String(env2.TERM_PROGRAM ?? "").toLowerCase();
|
|
1675
|
+
const envKeys = Object.keys(env2);
|
|
1515
1676
|
const hasCursorSignals = envKeys.some((key) => key.startsWith("CURSOR_")) || termProgram === "cursor" || clientInfoName.includes("cursor") || clientInfoVersion.includes("cursor") || argv.includes("cursor");
|
|
1516
1677
|
if (hasCursorSignals) {
|
|
1517
1678
|
return "cursor";
|
|
@@ -1523,7 +1684,7 @@ function inferIdeType(params) {
|
|
|
1523
1684
|
"JETBRAINS_REMOTE_RUN",
|
|
1524
1685
|
"INTELLIJ_ENVIRONMENT_READER"
|
|
1525
1686
|
].includes(key)
|
|
1526
|
-
) || String(
|
|
1687
|
+
) || String(env2.TERMINAL_EMULATOR ?? "").toLowerCase().includes("jetbrains") || /(jetbrains|intellij|pycharm|webstorm|goland|rubymine|clion|phpstorm|rider|datagrip)/.test(
|
|
1527
1688
|
clientInfoName
|
|
1528
1689
|
) || /(jetbrains|intellij|pycharm|webstorm|goland|rubymine|clion|phpstorm|rider|datagrip)/.test(
|
|
1529
1690
|
argv
|
|
@@ -1537,32 +1698,31 @@ function inferIdeType(params) {
|
|
|
1537
1698
|
}
|
|
1538
1699
|
return void 0;
|
|
1539
1700
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
);
|
|
1548
|
-
}
|
|
1549
|
-
if (!pid) {
|
|
1550
|
-
throw new Error("[memoraone-mcp] Cannot send heartbeat without an active project binding");
|
|
1701
|
+
|
|
1702
|
+
// src/index.ts
|
|
1703
|
+
var notInitializedResult = {
|
|
1704
|
+
content: [
|
|
1705
|
+
{
|
|
1706
|
+
type: "text",
|
|
1707
|
+
text: "MemoraOne MCP not initialized (project binding missing)."
|
|
1551
1708
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1709
|
+
]
|
|
1710
|
+
};
|
|
1711
|
+
var initializeDiagDumped = false;
|
|
1712
|
+
var uriToPath = (uri) => {
|
|
1713
|
+
if (uri.startsWith("file://")) {
|
|
1714
|
+
return (0, import_node_url2.fileURLToPath)(uri);
|
|
1715
|
+
}
|
|
1716
|
+
return uri;
|
|
1717
|
+
};
|
|
1718
|
+
function getCursorWorkspaceRootFromEnv() {
|
|
1719
|
+
const raw = process.env.WORKSPACE_FOLDER_PATHS;
|
|
1720
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
1721
|
+
return void 0;
|
|
1565
1722
|
}
|
|
1723
|
+
const parts = raw.split(path7.delimiter).map((p) => p.trim()).filter(Boolean);
|
|
1724
|
+
const first = parts[0];
|
|
1725
|
+
return first ? path7.resolve(first) : void 0;
|
|
1566
1726
|
}
|
|
1567
1727
|
function redactSensitiveFields(obj) {
|
|
1568
1728
|
if (obj === null || obj === void 0) return obj;
|
|
@@ -1654,6 +1814,7 @@ async function main(opts = {}) {
|
|
|
1654
1814
|
projectId: null,
|
|
1655
1815
|
apiKeySource: null,
|
|
1656
1816
|
apiKeyFingerprint: null,
|
|
1817
|
+
authoritativeBinding: opts.authoritativeBinding ?? null,
|
|
1657
1818
|
ideType: void 0
|
|
1658
1819
|
};
|
|
1659
1820
|
let workspaceRoot;
|
|
@@ -1703,7 +1864,7 @@ async function main(opts = {}) {
|
|
|
1703
1864
|
const registeredToolNames = [];
|
|
1704
1865
|
server.tool(
|
|
1705
1866
|
"memora_post_event",
|
|
1706
|
-
|
|
1867
|
+
postEventDescription,
|
|
1707
1868
|
postEventShape,
|
|
1708
1869
|
async (args) => runWithSessionContext(sessionContext, async () => {
|
|
1709
1870
|
if (!runtime.client || !runtime.projectId) return notInitializedResult;
|
|
@@ -1779,6 +1940,19 @@ async function main(opts = {}) {
|
|
|
1779
1940
|
})
|
|
1780
1941
|
);
|
|
1781
1942
|
registeredToolNames.push("memora_set_project");
|
|
1943
|
+
server.tool(
|
|
1944
|
+
"memora_status",
|
|
1945
|
+
"Return non-secret project binding metadata for this MCP session",
|
|
1946
|
+
bindingStatusShape,
|
|
1947
|
+
async () => runWithSessionContext(sessionContext, async () => {
|
|
1948
|
+
if (!runtime.authoritativeBinding) return notInitializedResult;
|
|
1949
|
+
const result = handleBindingStatus(runtime.authoritativeBinding);
|
|
1950
|
+
return {
|
|
1951
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
1952
|
+
};
|
|
1953
|
+
})
|
|
1954
|
+
);
|
|
1955
|
+
registeredToolNames.push("memora_status");
|
|
1782
1956
|
registerToolWithWorklog(
|
|
1783
1957
|
server,
|
|
1784
1958
|
runtime,
|
|
@@ -1860,6 +2034,9 @@ async function main(opts = {}) {
|
|
|
1860
2034
|
try {
|
|
1861
2035
|
const params = request.params;
|
|
1862
2036
|
runtime.ideType = inferIdeType(params);
|
|
2037
|
+
if (runtime.ideType && opts.onSessionIdeTypeKnown) {
|
|
2038
|
+
opts.onSessionIdeTypeKnown(runtime.ideType);
|
|
2039
|
+
}
|
|
1863
2040
|
if (!initializeDiagDumped) {
|
|
1864
2041
|
initializeDiagDumped = true;
|
|
1865
2042
|
const folders = Array.isArray(params.workspaceFolders) ? params.workspaceFolders.map((f) => ({ name: f?.name, uri: f?.uri })) : params.workspaceFolders;
|
|
@@ -1929,10 +2106,12 @@ async function main(opts = {}) {
|
|
|
1929
2106
|
runtime.projectId = projectId;
|
|
1930
2107
|
runtime.apiKeySource = binding.apiKeySource;
|
|
1931
2108
|
runtime.apiKeyFingerprint = fingerprintApiKey(apiKeyToUse);
|
|
2109
|
+
runtime.authoritativeBinding = binding;
|
|
1932
2110
|
runtime.client = new memoraClient_default(config2, projectId, apiKeyToUse);
|
|
1933
2111
|
workspaceRoot = binding.workspaceRoot;
|
|
2112
|
+
const environmentLog = binding.environment !== void 0 ? ` environment=${binding.environment}` : "";
|
|
1934
2113
|
process.stderr.write(
|
|
1935
|
-
`[memoraone-mcp] registering workspace source bindingSource=${binding.bindingSource} workspaceRoot=${workspaceRoot ?? "(unset)"} m1Path=${binding.m1Path}
|
|
2114
|
+
`[memoraone-mcp] registering workspace source bindingSource=${binding.bindingSource} workspaceRoot=${workspaceRoot ?? "(unset)"} m1Path=${binding.m1Path}${environmentLog}
|
|
1936
2115
|
`
|
|
1937
2116
|
);
|
|
1938
2117
|
await registerRepoSource(
|
|
@@ -1946,8 +2125,9 @@ async function main(opts = {}) {
|
|
|
1946
2125
|
console.error("[memoraone-mcp][auth] project_id:", projectId);
|
|
1947
2126
|
console.error("[memoraone-mcp][auth] api_key source:", binding.apiKeySource);
|
|
1948
2127
|
}
|
|
2128
|
+
const bindingEnvironmentLog = binding.environment !== void 0 ? ` environment=${binding.environment}` : "";
|
|
1949
2129
|
console.error(
|
|
1950
|
-
`[memoraone-mcp] ${sessionLabel} authoritative binding: project=${binding.projectId} workspace=${binding.workspaceRoot} m1=${binding.m1Path} source=${binding.bindingSource} apiKeySource=${binding.apiKeySource}`
|
|
2130
|
+
`[memoraone-mcp] ${sessionLabel} authoritative binding: project=${binding.projectId} workspace=${binding.workspaceRoot} m1=${binding.m1Path} source=${binding.bindingSource} apiKeySource=${binding.apiKeySource}${bindingEnvironmentLog}`
|
|
1951
2131
|
);
|
|
1952
2132
|
bindingReadyResolve?.(runtime.client);
|
|
1953
2133
|
return server.server._oninitialize(request);
|
|
@@ -1964,14 +2144,25 @@ async function main(opts = {}) {
|
|
|
1964
2144
|
await server.connect(transport);
|
|
1965
2145
|
const activeClient = await bindingReady;
|
|
1966
2146
|
let heartbeatInterval = null;
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2147
|
+
const daemonSession = Boolean(opts.sessionSocket);
|
|
2148
|
+
if (config2.heartbeatEnabled && daemonSession) {
|
|
2149
|
+
console.error(
|
|
2150
|
+
`[memoraone-mcp] ${sessionLabel} defers heartbeat to daemon for project ${runtime.projectId}`
|
|
2151
|
+
);
|
|
2152
|
+
} else if (config2.heartbeatEnabled) {
|
|
2153
|
+
const intervalMs = resolveHeartbeatIntervalMs();
|
|
2154
|
+
const heartbeatCtx = {
|
|
2155
|
+
projectId: runtime.projectId,
|
|
2156
|
+
ideType: runtime.ideType,
|
|
2157
|
+
apiKeySource: runtime.apiKeySource,
|
|
2158
|
+
apiKeyFingerprint: runtime.apiKeyFingerprint
|
|
2159
|
+
};
|
|
1970
2160
|
console.error(
|
|
1971
2161
|
`[memoraone-mcp] ${sessionLabel} owns heartbeat for project ${runtime.projectId} interval=${intervalMs}ms`
|
|
1972
2162
|
);
|
|
2163
|
+
await sendProjectHeartbeat(activeClient, heartbeatCtx);
|
|
1973
2164
|
heartbeatInterval = setInterval(() => {
|
|
1974
|
-
|
|
2165
|
+
sendProjectHeartbeat(activeClient, heartbeatCtx).catch(() => {
|
|
1975
2166
|
});
|
|
1976
2167
|
}, intervalMs);
|
|
1977
2168
|
}
|
|
@@ -1980,10 +2171,14 @@ async function main(opts = {}) {
|
|
|
1980
2171
|
const shutdown = (signal, exitProcess = true) => {
|
|
1981
2172
|
process.off("SIGINT", onSigInt);
|
|
1982
2173
|
process.off("SIGTERM", onSigTerm);
|
|
1983
|
-
if (heartbeatInterval)
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2174
|
+
if (heartbeatInterval) {
|
|
2175
|
+
clearInterval(heartbeatInterval);
|
|
2176
|
+
heartbeatInterval = null;
|
|
2177
|
+
if (runtime.projectId) {
|
|
2178
|
+
console.error(
|
|
2179
|
+
`[memoraone-mcp] ${sessionLabel} released session heartbeat for project ${runtime.projectId}`
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
1987
2182
|
}
|
|
1988
2183
|
if (devMode) {
|
|
1989
2184
|
console.error(`[memoraone-mcp] ${sessionLabel} received ${signal}, shutting down`);
|
|
@@ -2059,7 +2254,8 @@ async function ensureSocketClean(socketPath) {
|
|
|
2059
2254
|
async function runDaemon() {
|
|
2060
2255
|
const projectId = parseProjectIdFromArgv();
|
|
2061
2256
|
const binding = parseBindingFromEnv(projectId);
|
|
2062
|
-
const
|
|
2257
|
+
const ideType = parseIdeTypeFromArgv(process.argv.slice(2)) ?? config2.ideType ?? resolveIdeTypeFromEnv();
|
|
2258
|
+
const socketPath = getSocketPath(projectId, ideType);
|
|
2063
2259
|
let nextSessionId = 1;
|
|
2064
2260
|
let activeSessions = 0;
|
|
2065
2261
|
let idleTimer = null;
|
|
@@ -2070,6 +2266,14 @@ async function runDaemon() {
|
|
|
2070
2266
|
`authoritative binding project=${binding.projectId} workspace=${binding.workspaceRoot} m1=${binding.m1Path} source=${binding.bindingSource} apiKeySource=${binding.apiKeySource}`
|
|
2071
2267
|
);
|
|
2072
2268
|
log("session policy: concurrent bridge sessions allowed per project daemon");
|
|
2269
|
+
if (!isDaemonIdleShutdownAllowed()) {
|
|
2270
|
+
log("idle shutdown disabled while daemon heartbeat is active");
|
|
2271
|
+
}
|
|
2272
|
+
const daemonHeartbeat = createDaemonHeartbeat({
|
|
2273
|
+
binding,
|
|
2274
|
+
ideType,
|
|
2275
|
+
onLog: (msg) => log(msg)
|
|
2276
|
+
});
|
|
2073
2277
|
await ensureSocketClean(socketPath);
|
|
2074
2278
|
const cleanupSocketFile = () => {
|
|
2075
2279
|
try {
|
|
@@ -2096,6 +2300,10 @@ async function runDaemon() {
|
|
|
2096
2300
|
activeSessions = Math.max(0, activeSessions - 1);
|
|
2097
2301
|
log(`session=${sessionId} closed activeSessions=${activeSessions}`);
|
|
2098
2302
|
if (activeSessions === 0 && !shuttingDown) {
|
|
2303
|
+
if (!isDaemonIdleShutdownAllowed()) {
|
|
2304
|
+
log("idle shutdown skipped (daemon heartbeat active)");
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2099
2307
|
idleTimer = setTimeout(() => {
|
|
2100
2308
|
if (activeSessions !== 0 || shuttingDown) return;
|
|
2101
2309
|
shuttingDown = true;
|
|
@@ -2118,7 +2326,10 @@ async function runDaemon() {
|
|
|
2118
2326
|
authoritativeBinding: binding,
|
|
2119
2327
|
transport,
|
|
2120
2328
|
sessionSocket: socket,
|
|
2121
|
-
sessionLabel: `daemon-session-${sessionId}
|
|
2329
|
+
sessionLabel: `daemon-session-${sessionId}`,
|
|
2330
|
+
onSessionIdeTypeKnown: (ideType2) => {
|
|
2331
|
+
daemonHeartbeat.setIdeType(ideType2);
|
|
2332
|
+
}
|
|
2122
2333
|
});
|
|
2123
2334
|
} catch (err) {
|
|
2124
2335
|
log(`session error: ${String(err)}`);
|
|
@@ -2139,6 +2350,9 @@ async function runDaemon() {
|
|
|
2139
2350
|
clearTimeout(idleTimer);
|
|
2140
2351
|
idleTimer = null;
|
|
2141
2352
|
}
|
|
2353
|
+
if (daemonHeartbeat.isRunning()) {
|
|
2354
|
+
daemonHeartbeat.stop();
|
|
2355
|
+
}
|
|
2142
2356
|
log(`daemon shutdown: ${reason}`);
|
|
2143
2357
|
server.close(() => {
|
|
2144
2358
|
cleanupSocketFile();
|
|
@@ -2151,6 +2365,9 @@ async function runDaemon() {
|
|
|
2151
2365
|
return new Promise((resolve6) => {
|
|
2152
2366
|
server.listen(socketPath, () => {
|
|
2153
2367
|
log(`daemon started, listening on ${socketPath}`);
|
|
2368
|
+
void daemonHeartbeat.start().catch((err) => {
|
|
2369
|
+
log(`daemon heartbeat start error: ${String(err)}`);
|
|
2370
|
+
});
|
|
2154
2371
|
resolve6();
|
|
2155
2372
|
});
|
|
2156
2373
|
});
|