@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/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() {
@@ -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
- return { projectId: projectId.trim(), apiKey };
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 message = typeof content.message === "string" ? content.message : typeof content.text === "string" ? content.text : JSON.stringify(content);
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/index.ts
1475
- var notInitializedResult = {
1476
- content: [
1477
- {
1478
- type: "text",
1479
- text: "MemoraOne MCP not initialized (project binding missing)."
1480
- }
1481
- ]
1482
- };
1483
- var initializeDiagDumped = false;
1484
- var uriToPath = (uri) => {
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 uri;
1489
- };
1490
- function getCursorWorkspaceRootFromEnv() {
1491
- const raw = process.env.WORKSPACE_FOLDER_PATHS;
1492
- if (raw === void 0 || raw.trim() === "") {
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
- const parts = raw.split(path7.delimiter).map((p) => p.trim()).filter(Boolean);
1496
- const first = parts[0];
1497
- return first ? path7.resolve(first) : void 0;
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 fingerprintApiKey(apiKey) {
1504
- 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;
1505
1573
  }
1506
- function inferIdeType(params) {
1507
- if (config2.ideType) {
1508
- 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;
1509
1671
  }
1510
1672
  const clientInfoName = String(params?.clientInfo?.name ?? "").toLowerCase();
1511
1673
  const clientInfoVersion = String(params?.clientInfo?.version ?? "").toLowerCase();
1512
- const termProgram = String(process.env.TERM_PROGRAM ?? "").toLowerCase();
1513
- const argv = process.argv.join(" ").toLowerCase();
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(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(
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
- async function sendHeartbeat(client, runtime) {
1541
- try {
1542
- const pid = runtime.projectId?.trim();
1543
- if (isHeartbeatDebugEnabled()) {
1544
- process.stderr.write(
1545
- `[memoraone-mcp][diag] heartbeat projectId=${pid ?? "unknown"} apiKeySource=${runtime.apiKeySource ?? "unknown"} apiKeyFingerprint=${runtime.apiKeyFingerprint ?? "unknown"}
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
- const body = {};
1553
- if (runtime.ideType) body.ide_type = runtime.ideType;
1554
- await client.post(`/v1/projects/${pid}/heartbeat`, body, {
1555
- log: false,
1556
- headers: {
1557
- "x-project-id": pid
1558
- }
1559
- });
1560
- } catch (err) {
1561
- process.stderr.write(
1562
- `[memoraone-mcp][info] heartbeat error (silent) ${String(err)}
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
- "Forward an event to MemoraOne timeline",
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
- if (config2.heartbeatEnabled) {
1968
- await sendHeartbeat(activeClient, runtime);
1969
- 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
+ };
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
- sendHeartbeat(activeClient, runtime).catch(() => {
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) clearInterval(heartbeatInterval);
1984
- heartbeatInterval = null;
1985
- if (runtime.projectId) {
1986
- 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
+ }
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 socketPath = getSocketPath(projectId);
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
  });