@memoraone/mcp 0.1.22 → 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/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
- function getSocketPath(projectId) {
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() {
@@ -238,7 +259,6 @@ function decodeResolvedBinding(value) {
238
259
 
239
260
  // src/index.ts
240
261
  var path7 = __toESM(require("path"), 1);
241
- var crypto5 = __toESM(require("crypto"), 1);
242
262
  var import_node_url2 = require("url");
243
263
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
244
264
  var import_types = require("@modelcontextprotocol/sdk/types.js");
@@ -1536,47 +1556,123 @@ function handleBindingStatus(binding) {
1536
1556
  return buildBindingStatus(binding);
1537
1557
  }
1538
1558
 
1539
- // src/index.ts
1540
- var notInitializedResult = {
1541
- content: [
1542
- {
1543
- type: "text",
1544
- text: "MemoraOne MCP not initialized (project binding missing)."
1545
- }
1546
- ]
1547
- };
1548
- var initializeDiagDumped = false;
1549
- var uriToPath = (uri) => {
1550
- if (uri.startsWith("file://")) {
1551
- return (0, import_node_url2.fileURLToPath)(uri);
1552
- }
1553
- return uri;
1554
- };
1555
- function getCursorWorkspaceRootFromEnv() {
1556
- const raw = process.env.WORKSPACE_FOLDER_PATHS;
1557
- if (raw === void 0 || raw.trim() === "") {
1558
- return void 0;
1559
- }
1560
- const parts = raw.split(path7.delimiter).map((p) => p.trim()).filter(Boolean);
1561
- const first = parts[0];
1562
- return first ? path7.resolve(first) : void 0;
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);
1563
1563
  }
1564
1564
  function isHeartbeatDebugEnabled() {
1565
1565
  const value = String(process.env.MEMORAONE_DEBUG_HEARTBEAT ?? "").trim().toLowerCase();
1566
1566
  return ["1", "true", "yes", "on"].includes(value);
1567
1567
  }
1568
- function fingerprintApiKey(apiKey) {
1569
- return crypto5.createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
1568
+ function resolveHeartbeatIntervalMs() {
1569
+ return Number.isFinite(config2.heartbeatIntervalMs) ? Math.max(1e3, config2.heartbeatIntervalMs) : 3e4;
1570
+ }
1571
+ function isDaemonIdleShutdownAllowed() {
1572
+ return !config2.heartbeatEnabled;
1570
1573
  }
1571
- function inferIdeType(params) {
1572
- if (config2.ideType) {
1573
- return config2.ideType;
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;
1574
1671
  }
1575
1672
  const clientInfoName = String(params?.clientInfo?.name ?? "").toLowerCase();
1576
1673
  const clientInfoVersion = String(params?.clientInfo?.version ?? "").toLowerCase();
1577
- const termProgram = String(process.env.TERM_PROGRAM ?? "").toLowerCase();
1578
- const argv = process.argv.join(" ").toLowerCase();
1579
- const envKeys = Object.keys(process.env);
1674
+ const termProgram = String(env2.TERM_PROGRAM ?? "").toLowerCase();
1675
+ const envKeys = Object.keys(env2);
1580
1676
  const hasCursorSignals = envKeys.some((key) => key.startsWith("CURSOR_")) || termProgram === "cursor" || clientInfoName.includes("cursor") || clientInfoVersion.includes("cursor") || argv.includes("cursor");
1581
1677
  if (hasCursorSignals) {
1582
1678
  return "cursor";
@@ -1588,7 +1684,7 @@ function inferIdeType(params) {
1588
1684
  "JETBRAINS_REMOTE_RUN",
1589
1685
  "INTELLIJ_ENVIRONMENT_READER"
1590
1686
  ].includes(key)
1591
- ) || String(process.env.TERMINAL_EMULATOR ?? "").toLowerCase().includes("jetbrains") || /(jetbrains|intellij|pycharm|webstorm|goland|rubymine|clion|phpstorm|rider|datagrip)/.test(
1687
+ ) || String(env2.TERMINAL_EMULATOR ?? "").toLowerCase().includes("jetbrains") || /(jetbrains|intellij|pycharm|webstorm|goland|rubymine|clion|phpstorm|rider|datagrip)/.test(
1592
1688
  clientInfoName
1593
1689
  ) || /(jetbrains|intellij|pycharm|webstorm|goland|rubymine|clion|phpstorm|rider|datagrip)/.test(
1594
1690
  argv
@@ -1602,32 +1698,31 @@ function inferIdeType(params) {
1602
1698
  }
1603
1699
  return void 0;
1604
1700
  }
1605
- async function sendHeartbeat(client, runtime) {
1606
- try {
1607
- const pid = runtime.projectId?.trim();
1608
- if (isHeartbeatDebugEnabled()) {
1609
- process.stderr.write(
1610
- `[memoraone-mcp][diag] heartbeat projectId=${pid ?? "unknown"} apiKeySource=${runtime.apiKeySource ?? "unknown"} apiKeyFingerprint=${runtime.apiKeyFingerprint ?? "unknown"}
1611
- `
1612
- );
1613
- }
1614
- if (!pid) {
1615
- 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)."
1616
1708
  }
1617
- const body = {};
1618
- if (runtime.ideType) body.ide_type = runtime.ideType;
1619
- await client.post(`/v1/projects/${pid}/heartbeat`, body, {
1620
- log: false,
1621
- headers: {
1622
- "x-project-id": pid
1623
- }
1624
- });
1625
- } catch (err) {
1626
- process.stderr.write(
1627
- `[memoraone-mcp][info] heartbeat error (silent) ${String(err)}
1628
- `
1629
- );
1709
+ ]
1710
+ };
1711
+ var initializeDiagDumped = false;
1712
+ var uriToPath = (uri) => {
1713
+ if (uri.startsWith("file://")) {
1714
+ return (0, import_node_url2.fileURLToPath)(uri);
1630
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;
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;
1631
1726
  }
1632
1727
  function redactSensitiveFields(obj) {
1633
1728
  if (obj === null || obj === void 0) return obj;
@@ -1939,6 +2034,9 @@ async function main(opts = {}) {
1939
2034
  try {
1940
2035
  const params = request.params;
1941
2036
  runtime.ideType = inferIdeType(params);
2037
+ if (runtime.ideType && opts.onSessionIdeTypeKnown) {
2038
+ opts.onSessionIdeTypeKnown(runtime.ideType);
2039
+ }
1942
2040
  if (!initializeDiagDumped) {
1943
2041
  initializeDiagDumped = true;
1944
2042
  const folders = Array.isArray(params.workspaceFolders) ? params.workspaceFolders.map((f) => ({ name: f?.name, uri: f?.uri })) : params.workspaceFolders;
@@ -2046,14 +2144,25 @@ async function main(opts = {}) {
2046
2144
  await server.connect(transport);
2047
2145
  const activeClient = await bindingReady;
2048
2146
  let heartbeatInterval = null;
2049
- if (config2.heartbeatEnabled) {
2050
- await sendHeartbeat(activeClient, runtime);
2051
- const intervalMs = Number.isFinite(config2.heartbeatIntervalMs) ? Math.max(1e3, config2.heartbeatIntervalMs) : 3e4;
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
+ };
2052
2160
  console.error(
2053
2161
  `[memoraone-mcp] ${sessionLabel} owns heartbeat for project ${runtime.projectId} interval=${intervalMs}ms`
2054
2162
  );
2163
+ await sendProjectHeartbeat(activeClient, heartbeatCtx);
2055
2164
  heartbeatInterval = setInterval(() => {
2056
- sendHeartbeat(activeClient, runtime).catch(() => {
2165
+ sendProjectHeartbeat(activeClient, heartbeatCtx).catch(() => {
2057
2166
  });
2058
2167
  }, intervalMs);
2059
2168
  }
@@ -2062,10 +2171,14 @@ async function main(opts = {}) {
2062
2171
  const shutdown = (signal, exitProcess = true) => {
2063
2172
  process.off("SIGINT", onSigInt);
2064
2173
  process.off("SIGTERM", onSigTerm);
2065
- if (heartbeatInterval) clearInterval(heartbeatInterval);
2066
- heartbeatInterval = null;
2067
- if (runtime.projectId) {
2068
- console.error(`[memoraone-mcp] ${sessionLabel} released heartbeat for project ${runtime.projectId}`);
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
+ }
2069
2182
  }
2070
2183
  if (devMode) {
2071
2184
  console.error(`[memoraone-mcp] ${sessionLabel} received ${signal}, shutting down`);
@@ -2141,7 +2254,8 @@ async function ensureSocketClean(socketPath) {
2141
2254
  async function runDaemon() {
2142
2255
  const projectId = parseProjectIdFromArgv();
2143
2256
  const binding = parseBindingFromEnv(projectId);
2144
- const socketPath = getSocketPath(projectId);
2257
+ const ideType = parseIdeTypeFromArgv(process.argv.slice(2)) ?? config2.ideType ?? resolveIdeTypeFromEnv();
2258
+ const socketPath = getSocketPath(projectId, ideType);
2145
2259
  let nextSessionId = 1;
2146
2260
  let activeSessions = 0;
2147
2261
  let idleTimer = null;
@@ -2152,6 +2266,14 @@ async function runDaemon() {
2152
2266
  `authoritative binding project=${binding.projectId} workspace=${binding.workspaceRoot} m1=${binding.m1Path} source=${binding.bindingSource} apiKeySource=${binding.apiKeySource}`
2153
2267
  );
2154
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
+ });
2155
2277
  await ensureSocketClean(socketPath);
2156
2278
  const cleanupSocketFile = () => {
2157
2279
  try {
@@ -2178,6 +2300,10 @@ async function runDaemon() {
2178
2300
  activeSessions = Math.max(0, activeSessions - 1);
2179
2301
  log(`session=${sessionId} closed activeSessions=${activeSessions}`);
2180
2302
  if (activeSessions === 0 && !shuttingDown) {
2303
+ if (!isDaemonIdleShutdownAllowed()) {
2304
+ log("idle shutdown skipped (daemon heartbeat active)");
2305
+ return;
2306
+ }
2181
2307
  idleTimer = setTimeout(() => {
2182
2308
  if (activeSessions !== 0 || shuttingDown) return;
2183
2309
  shuttingDown = true;
@@ -2200,7 +2326,10 @@ async function runDaemon() {
2200
2326
  authoritativeBinding: binding,
2201
2327
  transport,
2202
2328
  sessionSocket: socket,
2203
- sessionLabel: `daemon-session-${sessionId}`
2329
+ sessionLabel: `daemon-session-${sessionId}`,
2330
+ onSessionIdeTypeKnown: (ideType2) => {
2331
+ daemonHeartbeat.setIdeType(ideType2);
2332
+ }
2204
2333
  });
2205
2334
  } catch (err) {
2206
2335
  log(`session error: ${String(err)}`);
@@ -2221,6 +2350,9 @@ async function runDaemon() {
2221
2350
  clearTimeout(idleTimer);
2222
2351
  idleTimer = null;
2223
2352
  }
2353
+ if (daemonHeartbeat.isRunning()) {
2354
+ daemonHeartbeat.stop();
2355
+ }
2224
2356
  log(`daemon shutdown: ${reason}`);
2225
2357
  server.close(() => {
2226
2358
  cleanupSocketFile();
@@ -2233,6 +2365,9 @@ async function runDaemon() {
2233
2365
  return new Promise((resolve6) => {
2234
2366
  server.listen(socketPath, () => {
2235
2367
  log(`daemon started, listening on ${socketPath}`);
2368
+ void daemonHeartbeat.start().catch((err) => {
2369
+ log(`daemon heartbeat start error: ${String(err)}`);
2370
+ });
2236
2371
  resolve6();
2237
2372
  });
2238
2373
  });
package/dist/index.cjs CHANGED
@@ -34,7 +34,6 @@ __export(index_exports, {
34
34
  });
35
35
  module.exports = __toCommonJS(index_exports);
36
36
  var path6 = __toESM(require("path"), 1);
37
- var crypto5 = __toESM(require("crypto"), 1);
38
37
  var import_node_url2 = require("url");
39
38
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
40
39
  var import_types = require("@modelcontextprotocol/sdk/types.js");
@@ -1474,47 +1473,58 @@ function handleBindingStatus(binding) {
1474
1473
  return buildBindingStatus(binding);
1475
1474
  }
1476
1475
 
1477
- // src/index.ts
1478
- var notInitializedResult = {
1479
- content: [
1480
- {
1481
- type: "text",
1482
- text: "MemoraOne MCP not initialized (project binding missing)."
1483
- }
1484
- ]
1485
- };
1486
- var initializeDiagDumped = false;
1487
- var uriToPath = (uri) => {
1488
- if (uri.startsWith("file://")) {
1489
- return (0, import_node_url2.fileURLToPath)(uri);
1490
- }
1491
- return uri;
1492
- };
1493
- function getCursorWorkspaceRootFromEnv() {
1494
- const raw = process.env.WORKSPACE_FOLDER_PATHS;
1495
- if (raw === void 0 || raw.trim() === "") {
1496
- return void 0;
1497
- }
1498
- const parts = raw.split(path6.delimiter).map((p) => p.trim()).filter(Boolean);
1499
- const first = parts[0];
1500
- return first ? path6.resolve(first) : void 0;
1476
+ // src/heartbeat.ts
1477
+ var crypto5 = __toESM(require("crypto"), 1);
1478
+ function fingerprintApiKey(apiKey) {
1479
+ return crypto5.createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
1501
1480
  }
1502
1481
  function isHeartbeatDebugEnabled() {
1503
1482
  const value = String(process.env.MEMORAONE_DEBUG_HEARTBEAT ?? "").trim().toLowerCase();
1504
1483
  return ["1", "true", "yes", "on"].includes(value);
1505
1484
  }
1506
- function fingerprintApiKey(apiKey) {
1507
- return crypto5.createHash("sha256").update(apiKey).digest("hex").slice(0, 12);
1485
+ function resolveHeartbeatIntervalMs() {
1486
+ return Number.isFinite(config2.heartbeatIntervalMs) ? Math.max(1e3, config2.heartbeatIntervalMs) : 3e4;
1487
+ }
1488
+ async function sendProjectHeartbeat(client, ctx) {
1489
+ try {
1490
+ const pid = ctx.projectId?.trim();
1491
+ if (isHeartbeatDebugEnabled()) {
1492
+ process.stderr.write(
1493
+ `[memoraone-mcp][diag] heartbeat projectId=${pid ?? "unknown"} apiKeySource=${ctx.apiKeySource ?? "unknown"} apiKeyFingerprint=${ctx.apiKeyFingerprint ?? "unknown"} ideType=${ctx.ideType ?? "unknown"}
1494
+ `
1495
+ );
1496
+ }
1497
+ if (!pid) {
1498
+ throw new Error("[memoraone-mcp] Cannot send heartbeat without an active project binding");
1499
+ }
1500
+ const body = {};
1501
+ if (ctx.ideType) body.ide_type = ctx.ideType;
1502
+ await client.post(`/v1/projects/${pid}/heartbeat`, body, {
1503
+ log: false,
1504
+ headers: {
1505
+ "x-project-id": pid
1506
+ }
1507
+ });
1508
+ } catch (err) {
1509
+ process.stderr.write(
1510
+ `[memoraone-mcp][info] heartbeat error (silent) ${String(err)}
1511
+ `
1512
+ );
1513
+ }
1508
1514
  }
1509
- function inferIdeType(params) {
1510
- if (config2.ideType) {
1511
- return config2.ideType;
1515
+
1516
+ // src/ideType.ts
1517
+ function inferIdeType(params, options = {}) {
1518
+ const env2 = options.env ?? process.env;
1519
+ const argv = (options.argv ?? process.argv).join(" ").toLowerCase();
1520
+ const configIdeType = options.configIdeType ?? config2.ideType;
1521
+ if (configIdeType) {
1522
+ return configIdeType;
1512
1523
  }
1513
1524
  const clientInfoName = String(params?.clientInfo?.name ?? "").toLowerCase();
1514
1525
  const clientInfoVersion = String(params?.clientInfo?.version ?? "").toLowerCase();
1515
- const termProgram = String(process.env.TERM_PROGRAM ?? "").toLowerCase();
1516
- const argv = process.argv.join(" ").toLowerCase();
1517
- const envKeys = Object.keys(process.env);
1526
+ const termProgram = String(env2.TERM_PROGRAM ?? "").toLowerCase();
1527
+ const envKeys = Object.keys(env2);
1518
1528
  const hasCursorSignals = envKeys.some((key) => key.startsWith("CURSOR_")) || termProgram === "cursor" || clientInfoName.includes("cursor") || clientInfoVersion.includes("cursor") || argv.includes("cursor");
1519
1529
  if (hasCursorSignals) {
1520
1530
  return "cursor";
@@ -1526,7 +1536,7 @@ function inferIdeType(params) {
1526
1536
  "JETBRAINS_REMOTE_RUN",
1527
1537
  "INTELLIJ_ENVIRONMENT_READER"
1528
1538
  ].includes(key)
1529
- ) || String(process.env.TERMINAL_EMULATOR ?? "").toLowerCase().includes("jetbrains") || /(jetbrains|intellij|pycharm|webstorm|goland|rubymine|clion|phpstorm|rider|datagrip)/.test(
1539
+ ) || String(env2.TERMINAL_EMULATOR ?? "").toLowerCase().includes("jetbrains") || /(jetbrains|intellij|pycharm|webstorm|goland|rubymine|clion|phpstorm|rider|datagrip)/.test(
1530
1540
  clientInfoName
1531
1541
  ) || /(jetbrains|intellij|pycharm|webstorm|goland|rubymine|clion|phpstorm|rider|datagrip)/.test(
1532
1542
  argv
@@ -1540,32 +1550,31 @@ function inferIdeType(params) {
1540
1550
  }
1541
1551
  return void 0;
1542
1552
  }
1543
- async function sendHeartbeat(client, runtime) {
1544
- try {
1545
- const pid = runtime.projectId?.trim();
1546
- if (isHeartbeatDebugEnabled()) {
1547
- process.stderr.write(
1548
- `[memoraone-mcp][diag] heartbeat projectId=${pid ?? "unknown"} apiKeySource=${runtime.apiKeySource ?? "unknown"} apiKeyFingerprint=${runtime.apiKeyFingerprint ?? "unknown"}
1549
- `
1550
- );
1551
- }
1552
- if (!pid) {
1553
- throw new Error("[memoraone-mcp] Cannot send heartbeat without an active project binding");
1553
+
1554
+ // src/index.ts
1555
+ var notInitializedResult = {
1556
+ content: [
1557
+ {
1558
+ type: "text",
1559
+ text: "MemoraOne MCP not initialized (project binding missing)."
1554
1560
  }
1555
- const body = {};
1556
- if (runtime.ideType) body.ide_type = runtime.ideType;
1557
- await client.post(`/v1/projects/${pid}/heartbeat`, body, {
1558
- log: false,
1559
- headers: {
1560
- "x-project-id": pid
1561
- }
1562
- });
1563
- } catch (err) {
1564
- process.stderr.write(
1565
- `[memoraone-mcp][info] heartbeat error (silent) ${String(err)}
1566
- `
1567
- );
1561
+ ]
1562
+ };
1563
+ var initializeDiagDumped = false;
1564
+ var uriToPath = (uri) => {
1565
+ if (uri.startsWith("file://")) {
1566
+ return (0, import_node_url2.fileURLToPath)(uri);
1567
+ }
1568
+ return uri;
1569
+ };
1570
+ function getCursorWorkspaceRootFromEnv() {
1571
+ const raw = process.env.WORKSPACE_FOLDER_PATHS;
1572
+ if (raw === void 0 || raw.trim() === "") {
1573
+ return void 0;
1568
1574
  }
1575
+ const parts = raw.split(path6.delimiter).map((p) => p.trim()).filter(Boolean);
1576
+ const first = parts[0];
1577
+ return first ? path6.resolve(first) : void 0;
1569
1578
  }
1570
1579
  function redactSensitiveFields(obj) {
1571
1580
  if (obj === null || obj === void 0) return obj;
@@ -1877,6 +1886,9 @@ async function main(opts = {}) {
1877
1886
  try {
1878
1887
  const params = request.params;
1879
1888
  runtime.ideType = inferIdeType(params);
1889
+ if (runtime.ideType && opts.onSessionIdeTypeKnown) {
1890
+ opts.onSessionIdeTypeKnown(runtime.ideType);
1891
+ }
1880
1892
  if (!initializeDiagDumped) {
1881
1893
  initializeDiagDumped = true;
1882
1894
  const folders = Array.isArray(params.workspaceFolders) ? params.workspaceFolders.map((f) => ({ name: f?.name, uri: f?.uri })) : params.workspaceFolders;
@@ -1984,14 +1996,25 @@ async function main(opts = {}) {
1984
1996
  await server.connect(transport);
1985
1997
  const activeClient = await bindingReady;
1986
1998
  let heartbeatInterval = null;
1987
- if (config2.heartbeatEnabled) {
1988
- await sendHeartbeat(activeClient, runtime);
1989
- const intervalMs = Number.isFinite(config2.heartbeatIntervalMs) ? Math.max(1e3, config2.heartbeatIntervalMs) : 3e4;
1999
+ const daemonSession = Boolean(opts.sessionSocket);
2000
+ if (config2.heartbeatEnabled && daemonSession) {
2001
+ console.error(
2002
+ `[memoraone-mcp] ${sessionLabel} defers heartbeat to daemon for project ${runtime.projectId}`
2003
+ );
2004
+ } else if (config2.heartbeatEnabled) {
2005
+ const intervalMs = resolveHeartbeatIntervalMs();
2006
+ const heartbeatCtx = {
2007
+ projectId: runtime.projectId,
2008
+ ideType: runtime.ideType,
2009
+ apiKeySource: runtime.apiKeySource,
2010
+ apiKeyFingerprint: runtime.apiKeyFingerprint
2011
+ };
1990
2012
  console.error(
1991
2013
  `[memoraone-mcp] ${sessionLabel} owns heartbeat for project ${runtime.projectId} interval=${intervalMs}ms`
1992
2014
  );
2015
+ await sendProjectHeartbeat(activeClient, heartbeatCtx);
1993
2016
  heartbeatInterval = setInterval(() => {
1994
- sendHeartbeat(activeClient, runtime).catch(() => {
2017
+ sendProjectHeartbeat(activeClient, heartbeatCtx).catch(() => {
1995
2018
  });
1996
2019
  }, intervalMs);
1997
2020
  }
@@ -2000,10 +2023,14 @@ async function main(opts = {}) {
2000
2023
  const shutdown = (signal, exitProcess = true) => {
2001
2024
  process.off("SIGINT", onSigInt);
2002
2025
  process.off("SIGTERM", onSigTerm);
2003
- if (heartbeatInterval) clearInterval(heartbeatInterval);
2004
- heartbeatInterval = null;
2005
- if (runtime.projectId) {
2006
- console.error(`[memoraone-mcp] ${sessionLabel} released heartbeat for project ${runtime.projectId}`);
2026
+ if (heartbeatInterval) {
2027
+ clearInterval(heartbeatInterval);
2028
+ heartbeatInterval = null;
2029
+ if (runtime.projectId) {
2030
+ console.error(
2031
+ `[memoraone-mcp] ${sessionLabel} released session heartbeat for project ${runtime.projectId}`
2032
+ );
2033
+ }
2007
2034
  }
2008
2035
  if (devMode) {
2009
2036
  console.error(`[memoraone-mcp] ${sessionLabel} received ${signal}, shutting down`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memoraone/mcp",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "bin": {
@@ -13,6 +13,15 @@
13
13
  "publishConfig": {
14
14
  "access": "public"
15
15
  },
16
+ "scripts": {
17
+ "build": "tsup && node scripts/writeBinWrapper.cjs",
18
+ "prepublishOnly": "pnpm run build",
19
+ "dev": "tsx src/cli.ts",
20
+ "lint": "eslint .",
21
+ "lint:contracts": "node scripts/lint-contracts.cjs",
22
+ "test": "pnpm run lint:contracts && node --import=tsx --test test/*.test.js",
23
+ "validate:auth": "node --import=tsx --test test/memoraClient.test.js"
24
+ },
16
25
  "dependencies": {
17
26
  "@modelcontextprotocol/sdk": "^1.25.1",
18
27
  "dotenv": "^16.4.5",
@@ -22,13 +31,5 @@
22
31
  "tsx": "^4.21.0",
23
32
  "tsup": "^8.5.1",
24
33
  "typescript": "^5.9.2"
25
- },
26
- "scripts": {
27
- "build": "tsup && node scripts/writeBinWrapper.cjs",
28
- "dev": "tsx src/cli.ts",
29
- "lint": "eslint .",
30
- "lint:contracts": "node scripts/lint-contracts.cjs",
31
- "test": "pnpm run lint:contracts && node --import=tsx --test test/*.test.js",
32
- "validate:auth": "node --import=tsx --test test/memoraClient.test.js"
33
34
  }
34
- }
35
+ }