@portel/photon 1.6.1 → 1.8.0

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.
Files changed (113) hide show
  1. package/README.md +111 -160
  2. package/dist/auto-ui/beam.d.ts.map +1 -1
  3. package/dist/auto-ui/beam.js +218 -106
  4. package/dist/auto-ui/beam.js.map +1 -1
  5. package/dist/auto-ui/design-system/tokens.d.ts +1 -1
  6. package/dist/auto-ui/design-system/tokens.d.ts.map +1 -1
  7. package/dist/auto-ui/design-system/tokens.js +2 -2
  8. package/dist/auto-ui/design-system/tokens.js.map +1 -1
  9. package/dist/auto-ui/frontend/index.html +1 -1
  10. package/dist/auto-ui/platform-compat.d.ts.map +1 -1
  11. package/dist/auto-ui/platform-compat.js +12 -2
  12. package/dist/auto-ui/platform-compat.js.map +1 -1
  13. package/dist/auto-ui/playground-html.js +5 -5
  14. package/dist/auto-ui/rendering/components.d.ts.map +1 -1
  15. package/dist/auto-ui/rendering/components.js +568 -0
  16. package/dist/auto-ui/rendering/components.js.map +1 -1
  17. package/dist/auto-ui/rendering/field-analyzer.d.ts +56 -0
  18. package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -1
  19. package/dist/auto-ui/rendering/field-analyzer.js +177 -0
  20. package/dist/auto-ui/rendering/field-analyzer.js.map +1 -1
  21. package/dist/auto-ui/rendering/layout-selector.d.ts +14 -2
  22. package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -1
  23. package/dist/auto-ui/rendering/layout-selector.js +125 -1
  24. package/dist/auto-ui/rendering/layout-selector.js.map +1 -1
  25. package/dist/auto-ui/streamable-http-transport.d.ts +1 -1
  26. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  27. package/dist/auto-ui/streamable-http-transport.js +370 -26
  28. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  29. package/dist/auto-ui/types.d.ts +7 -1
  30. package/dist/auto-ui/types.d.ts.map +1 -1
  31. package/dist/auto-ui/types.js.map +1 -1
  32. package/dist/beam.bundle.js +21932 -3307
  33. package/dist/beam.bundle.js.map +4 -4
  34. package/dist/cli/commands/info.d.ts.map +1 -1
  35. package/dist/cli/commands/info.js +37 -0
  36. package/dist/cli/commands/info.js.map +1 -1
  37. package/dist/cli/commands/package.d.ts.map +1 -1
  38. package/dist/cli/commands/package.js +16 -0
  39. package/dist/cli/commands/package.js.map +1 -1
  40. package/dist/cli.d.ts.map +1 -1
  41. package/dist/cli.js +640 -17
  42. package/dist/cli.js.map +1 -1
  43. package/dist/context-store.d.ts +79 -0
  44. package/dist/context-store.d.ts.map +1 -0
  45. package/dist/context-store.js +210 -0
  46. package/dist/context-store.js.map +1 -0
  47. package/dist/daemon/client.d.ts +13 -4
  48. package/dist/daemon/client.d.ts.map +1 -1
  49. package/dist/daemon/client.js +138 -77
  50. package/dist/daemon/client.js.map +1 -1
  51. package/dist/daemon/manager.d.ts +0 -25
  52. package/dist/daemon/manager.d.ts.map +1 -1
  53. package/dist/daemon/manager.js +10 -38
  54. package/dist/daemon/manager.js.map +1 -1
  55. package/dist/daemon/protocol.d.ts +7 -2
  56. package/dist/daemon/protocol.d.ts.map +1 -1
  57. package/dist/daemon/protocol.js.map +1 -1
  58. package/dist/daemon/server.js +317 -83
  59. package/dist/daemon/server.js.map +1 -1
  60. package/dist/daemon/session-manager.d.ts +24 -4
  61. package/dist/daemon/session-manager.d.ts.map +1 -1
  62. package/dist/daemon/session-manager.js +62 -12
  63. package/dist/daemon/session-manager.js.map +1 -1
  64. package/dist/index.d.ts +0 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +0 -3
  67. package/dist/index.js.map +1 -1
  68. package/dist/loader.d.ts +3 -20
  69. package/dist/loader.d.ts.map +1 -1
  70. package/dist/loader.js +87 -77
  71. package/dist/loader.js.map +1 -1
  72. package/dist/markdown-utils.d.ts.map +1 -1
  73. package/dist/markdown-utils.js +2 -1
  74. package/dist/markdown-utils.js.map +1 -1
  75. package/dist/marketplace-manager.d.ts.map +1 -1
  76. package/dist/marketplace-manager.js +20 -3
  77. package/dist/marketplace-manager.js.map +1 -1
  78. package/dist/photon-cli-runner.d.ts.map +1 -1
  79. package/dist/photon-cli-runner.js +258 -218
  80. package/dist/photon-cli-runner.js.map +1 -1
  81. package/dist/photon-doc-extractor.d.ts +2 -0
  82. package/dist/photon-doc-extractor.d.ts.map +1 -1
  83. package/dist/photon-doc-extractor.js +45 -7
  84. package/dist/photon-doc-extractor.js.map +1 -1
  85. package/dist/photons/maker.photon.d.ts.map +1 -1
  86. package/dist/photons/maker.photon.js +22 -4
  87. package/dist/photons/maker.photon.js.map +1 -1
  88. package/dist/photons/maker.photon.ts +47 -11
  89. package/dist/security-scanner.d.ts.map +1 -1
  90. package/dist/security-scanner.js +8 -2
  91. package/dist/security-scanner.js.map +1 -1
  92. package/dist/serv/index.d.ts +1 -1
  93. package/dist/serv/index.d.ts.map +1 -1
  94. package/dist/serv/index.js +6 -4
  95. package/dist/serv/index.js.map +1 -1
  96. package/dist/server.d.ts +32 -15
  97. package/dist/server.d.ts.map +1 -1
  98. package/dist/server.js +525 -483
  99. package/dist/server.js.map +1 -1
  100. package/dist/shared/security.d.ts +79 -0
  101. package/dist/shared/security.d.ts.map +1 -0
  102. package/dist/shared/security.js +251 -0
  103. package/dist/shared/security.js.map +1 -0
  104. package/dist/shell-completions.d.ts +21 -0
  105. package/dist/shell-completions.d.ts.map +1 -0
  106. package/dist/shell-completions.js +102 -0
  107. package/dist/shell-completions.js.map +1 -0
  108. package/dist/template-manager.d.ts.map +1 -1
  109. package/dist/template-manager.js +10 -3
  110. package/dist/template-manager.js.map +1 -1
  111. package/dist/version.d.ts.map +1 -1
  112. package/dist/version.js.map +1 -1
  113. package/package.json +12 -7
@@ -124,6 +124,20 @@ function generateConfigurationSchema(photons) {
124
124
  }
125
125
  return schema;
126
126
  }
127
+ /**
128
+ * Format a tool result for MCP content text.
129
+ * Mirrors server.ts formatResult(): strings returned as-is, objects/arrays JSON-stringified,
130
+ * other primitives converted via String().
131
+ */
132
+ function formatResultText(result) {
133
+ if (result === undefined || result === null)
134
+ return 'Done';
135
+ if (typeof result === 'string')
136
+ return result;
137
+ if (typeof result === 'object')
138
+ return JSON.stringify(result, null, 2);
139
+ return String(result);
140
+ }
127
141
  const handlers = {
128
142
  // ─────────────────────────────────────────────────────────────────────────────
129
143
  // Lifecycle
@@ -149,6 +163,7 @@ const handlers = {
149
163
  },
150
164
  capabilities: {
151
165
  tools: { listChanged: true },
166
+ prompts: { listChanged: true },
152
167
  resources: { listChanged: true },
153
168
  },
154
169
  // SEP-1596 inspired: configuration schema for unconfigured photons
@@ -192,14 +207,14 @@ const handlers = {
192
207
  // Client notifies what resource they're viewing (for on-demand subscriptions)
193
208
  // photonId: hash of photon path (unique across servers)
194
209
  // itemId: whatever the photon uses to identify the item (e.g., board name)
195
- // lastEventId: optional - for replay of missed events on reconnect
210
+ // lastTimestamp: optional - for delta sync of missed events on reconnect
196
211
  'beam/viewing': async (req, session, ctx) => {
197
212
  const params = req.params;
198
213
  const photonId = params?.photonId;
199
214
  const itemId = params?.itemId;
200
- const lastEventId = params?.lastEventId;
215
+ const lastTimestamp = params?.lastTimestamp;
201
216
  if (photonId && itemId && ctx.subscriptionManager) {
202
- ctx.subscriptionManager.onClientViewingBoard(session.id, photonId, itemId, lastEventId);
217
+ ctx.subscriptionManager.onClientViewingBoard(session.id, photonId, itemId, lastTimestamp);
203
218
  }
204
219
  // Notification - no response needed
205
220
  return { jsonrpc: '2.0' };
@@ -226,6 +241,7 @@ const handlers = {
226
241
  'x-photon-description': photon.description,
227
242
  'x-photon-icon': photon.icon,
228
243
  'x-photon-internal': photon.internal,
244
+ 'x-photon-stateful': photon.stateful || false,
229
245
  'x-photon-prompt-count': photon.promptCount ?? 0,
230
246
  'x-photon-resource-count': photon.resourceCount ?? 0,
231
247
  ...buildToolMetadataExtensions(method),
@@ -245,6 +261,33 @@ const handlers = {
245
261
  });
246
262
  }
247
263
  }
264
+ // Add runtime-injected instance tools for stateful photons
265
+ for (const photon of ctx.photons) {
266
+ if (!photon.configured || !photon.stateful)
267
+ continue;
268
+ tools.push({
269
+ name: `${photon.name}/_use`,
270
+ description: `Switch to a named instance of ${photon.name}. Omit name to select interactively.`,
271
+ inputSchema: {
272
+ type: 'object',
273
+ properties: {
274
+ name: {
275
+ type: 'string',
276
+ description: 'Instance name (empty for default). Omit to select interactively.',
277
+ },
278
+ },
279
+ },
280
+ 'x-photon-id': photon.id,
281
+ 'x-photon-internal': true,
282
+ });
283
+ tools.push({
284
+ name: `${photon.name}/_instances`,
285
+ description: `List all available instances of ${photon.name}.`,
286
+ inputSchema: { type: 'object', properties: {} },
287
+ 'x-photon-id': photon.id,
288
+ 'x-photon-internal': true,
289
+ });
290
+ }
248
291
  // Add external MCP tools (from mcpServers in config.json)
249
292
  if (ctx.externalMCPs) {
250
293
  for (const mcp of ctx.externalMCPs) {
@@ -547,7 +590,7 @@ const handlers = {
547
590
  jsonrpc: '2.0',
548
591
  id: req.id,
549
592
  result: {
550
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
593
+ content: [{ type: 'text', text: formatResultText(result) }],
551
594
  isError: false,
552
595
  },
553
596
  };
@@ -576,6 +619,134 @@ const handlers = {
576
619
  if (methodInfo?.outputFormat) {
577
620
  uiMetadata['x-output-format'] = methodInfo.outputFormat;
578
621
  }
622
+ // Stateful photons: route through daemon for shared instance across all clients
623
+ if (photonInfo?.stateful && photonInfo.path) {
624
+ try {
625
+ const { sendCommand, pingDaemon } = await import('../daemon/client.js');
626
+ const { isGlobalDaemonRunning, startGlobalDaemon } = await import('../daemon/manager.js');
627
+ // Ensure daemon is running
628
+ if (!isGlobalDaemonRunning()) {
629
+ await startGlobalDaemon(true);
630
+ // Wait for daemon readiness
631
+ for (let i = 0; i < 10; i++) {
632
+ await new Promise((r) => setTimeout(r, 500));
633
+ if (await pingDaemon(photonName))
634
+ break;
635
+ }
636
+ }
637
+ // Each browser tab gets its own daemon session via the MCP session ID.
638
+ // Instance state is tracked per-session on the daemon — no global persistence.
639
+ const beamSessionId = `beam-${session.id}`;
640
+ const sendOpts = {
641
+ photonPath: photonInfo.path,
642
+ sessionId: beamSessionId,
643
+ instanceName: session.instanceName,
644
+ };
645
+ // Elicitation-based instance selection when _use called without name
646
+ if (methodName === '_use' && (!args || !('name' in args))) {
647
+ const instancesResult = (await sendCommand(photonName, '_instances', {}, sendOpts));
648
+ const instances = instancesResult?.instances || ['default'];
649
+ // Build select options for elicitation modal
650
+ const selectOptions = instances.map((inst) => ({
651
+ value: inst,
652
+ label: inst === 'default' ? '(default)' : inst,
653
+ selected: inst === (instancesResult?.current || 'default'),
654
+ }));
655
+ selectOptions.push({ value: '__create_new__', label: 'Create new...', selected: false });
656
+ const elicitResult = await requestBeamElicitation({
657
+ ask: 'select',
658
+ message: 'Select an instance',
659
+ options: selectOptions,
660
+ });
661
+ if (elicitResult.action !== 'accept' || !elicitResult.content) {
662
+ return {
663
+ jsonrpc: '2.0',
664
+ id: req.id,
665
+ result: {
666
+ content: [{ type: 'text', text: 'Cancelled' }],
667
+ isError: false,
668
+ },
669
+ };
670
+ }
671
+ let selectedName = elicitResult.content;
672
+ // Handle "Create new..." selection
673
+ if (selectedName === '__create_new__') {
674
+ const nameResult = await requestBeamElicitation({
675
+ ask: 'text',
676
+ message: 'Enter a name for the new instance',
677
+ placeholder: 'e.g. groceries, work, personal',
678
+ });
679
+ if (nameResult.action !== 'accept' || !nameResult.content) {
680
+ return {
681
+ jsonrpc: '2.0',
682
+ id: req.id,
683
+ result: {
684
+ content: [{ type: 'text', text: 'Cancelled' }],
685
+ isError: false,
686
+ },
687
+ };
688
+ }
689
+ selectedName = nameResult.content;
690
+ }
691
+ const useResult = await sendCommand(photonName, '_use', { name: selectedName }, sendOpts);
692
+ session.instanceName = selectedName;
693
+ // Notify UI to refresh after instance switch
694
+ broadcastToBeam('photon/state-changed', {
695
+ photon: photonName,
696
+ method: '_use',
697
+ data: { instance: selectedName },
698
+ });
699
+ return {
700
+ jsonrpc: '2.0',
701
+ id: req.id,
702
+ result: {
703
+ content: [{ type: 'text', text: formatResultText(useResult) }],
704
+ isError: false,
705
+ },
706
+ };
707
+ }
708
+ // For direct _use with name, also broadcast state-changed
709
+ if (methodName === '_use') {
710
+ const result = await sendCommand(photonName, methodName, (args || {}), sendOpts);
711
+ session.instanceName = String(args?.name || '');
712
+ broadcastToBeam('photon/state-changed', {
713
+ photon: photonName,
714
+ method: '_use',
715
+ data: { instance: args?.name || 'default' },
716
+ });
717
+ return {
718
+ jsonrpc: '2.0',
719
+ id: req.id,
720
+ result: {
721
+ content: [{ type: 'text', text: formatResultText(result) }],
722
+ isError: false,
723
+ },
724
+ };
725
+ }
726
+ const result = await sendCommand(photonName, methodName, (args || {}), sendOpts);
727
+ const resultText = formatResultText(result);
728
+ return {
729
+ jsonrpc: '2.0',
730
+ id: req.id,
731
+ result: {
732
+ content: [{ type: 'text', text: resultText }],
733
+ isError: false,
734
+ ...uiMetadata,
735
+ },
736
+ };
737
+ }
738
+ catch (error) {
739
+ const message = error instanceof Error ? error.message : String(error);
740
+ return {
741
+ jsonrpc: '2.0',
742
+ id: req.id,
743
+ result: {
744
+ content: [{ type: 'text', text: `Error: ${message}` }],
745
+ isError: true,
746
+ },
747
+ };
748
+ }
749
+ }
579
750
  const mcp = ctx.photonMCPs.get(photonName);
580
751
  if (!mcp?.instance) {
581
752
  // Check if it's a disconnected external MCP
@@ -787,14 +958,13 @@ const handlers = {
787
958
  }
788
959
  }
789
960
  // Use return value if no chunks were yielded, otherwise use chunks
790
- const finalResult = chunks.length > 0
791
- ? (chunks.length === 1 ? chunks[0] : chunks)
792
- : returnValue;
961
+ const finalResult = chunks.length > 0 ? (chunks.length === 1 ? chunks[0] : chunks) : returnValue;
962
+ const genResultText = formatResultText(finalResult);
793
963
  const genResponse = {
794
964
  jsonrpc: '2.0',
795
965
  id: req.id,
796
966
  result: {
797
- content: [{ type: 'text', text: JSON.stringify(finalResult, null, 2) }],
967
+ content: [{ type: 'text', text: genResultText }],
798
968
  isError: false,
799
969
  ...uiMetadata,
800
970
  },
@@ -813,11 +983,13 @@ const handlers = {
813
983
  }
814
984
  return genResponse;
815
985
  }
986
+ // For void methods, provide a success acknowledgment so the UI shows feedback
987
+ const resultText = formatResultText(result);
816
988
  const toolResponse = {
817
989
  jsonrpc: '2.0',
818
990
  id: req.id,
819
991
  result: {
820
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
992
+ content: [{ type: 'text', text: resultText }],
821
993
  isError: false,
822
994
  ...uiMetadata,
823
995
  },
@@ -898,6 +1070,83 @@ const handlers = {
898
1070
  },
899
1071
  };
900
1072
  },
1073
+ 'prompts/list': async (req, _session, ctx) => {
1074
+ const prompts = [];
1075
+ for (const photon of ctx.photons) {
1076
+ if (!photon.configured)
1077
+ continue;
1078
+ const mcp = ctx.photonMCPs.get(photon.name);
1079
+ if (!mcp?.templates)
1080
+ continue;
1081
+ for (const template of mcp.templates) {
1082
+ prompts.push({
1083
+ name: `${photon.name}/${template.name}`,
1084
+ description: template.description,
1085
+ arguments: Object.entries(template.inputSchema?.properties || {}).map(([name, schema]) => ({
1086
+ name,
1087
+ description: (typeof schema === 'object' && schema && 'description' in schema
1088
+ ? schema.description
1089
+ : '') || '',
1090
+ required: template.inputSchema?.required?.includes(name) || false,
1091
+ })),
1092
+ });
1093
+ }
1094
+ }
1095
+ return { jsonrpc: '2.0', id: req.id, result: { prompts } };
1096
+ },
1097
+ 'prompts/get': async (req, _session, ctx) => {
1098
+ const { name } = req.params;
1099
+ const args = req.params.arguments || {};
1100
+ const slashIndex = name.indexOf('/');
1101
+ if (slashIndex === -1) {
1102
+ return {
1103
+ jsonrpc: '2.0',
1104
+ id: req.id,
1105
+ error: { code: -32602, message: `Invalid prompt name: ${name}` },
1106
+ };
1107
+ }
1108
+ const photonName = name.slice(0, slashIndex);
1109
+ const promptName = name.slice(slashIndex + 1);
1110
+ const mcp = ctx.photonMCPs.get(photonName);
1111
+ if (!mcp) {
1112
+ return {
1113
+ jsonrpc: '2.0',
1114
+ id: req.id,
1115
+ error: { code: -32602, message: `Photon not found: ${photonName}` },
1116
+ };
1117
+ }
1118
+ const template = mcp.templates?.find((t) => t.name === promptName);
1119
+ if (!template) {
1120
+ return {
1121
+ jsonrpc: '2.0',
1122
+ id: req.id,
1123
+ error: { code: -32602, message: `Prompt not found: ${promptName}` },
1124
+ };
1125
+ }
1126
+ try {
1127
+ const result = await ctx.loader.executeTool(mcp, promptName, args);
1128
+ // Format as prompt response
1129
+ if (result && typeof result === 'object' && 'messages' in result) {
1130
+ return { jsonrpc: '2.0', id: req.id, result: { messages: result.messages } };
1131
+ }
1132
+ const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
1133
+ return {
1134
+ jsonrpc: '2.0',
1135
+ id: req.id,
1136
+ result: {
1137
+ messages: [{ role: 'user', content: { type: 'text', text } }],
1138
+ },
1139
+ };
1140
+ }
1141
+ catch (error) {
1142
+ const message = error instanceof Error ? error.message : String(error);
1143
+ return {
1144
+ jsonrpc: '2.0',
1145
+ id: req.id,
1146
+ error: { code: -32603, message: `Prompt execution failed: ${message}` },
1147
+ };
1148
+ }
1149
+ },
901
1150
  };
902
1151
  // ════════════════════════════════════════════════════════════════════════════════
903
1152
  // BEAM SYSTEM TOOLS
@@ -1461,7 +1710,7 @@ async function handleBeamStudioWrite(req, ctx, args) {
1461
1710
  const versionMatch = source.match(/@version\s+(\S+)/);
1462
1711
  const runtimeMatch = source.match(/@runtime\s+(\S+)/);
1463
1712
  const iconMatch = source.match(/@icon\s+(\S+)/);
1464
- const statefulMatch = source.match(/@stateful\s+true/);
1713
+ const statefulMatch = source.match(/@stateful\b/);
1465
1714
  const depsMatch = source.match(/@dependencies\s+(.+)/);
1466
1715
  const tagsMatch = source.match(/@tags\s+(.+)/);
1467
1716
  parseResult = {
@@ -1471,8 +1720,14 @@ async function handleBeamStudioWrite(req, ctx, args) {
1471
1720
  version: versionMatch?.[1],
1472
1721
  runtime: runtimeMatch?.[1],
1473
1722
  stateful: !!statefulMatch,
1474
- dependencies: depsMatch?.[1]?.split(',').map((d) => d.trim()).filter(Boolean),
1475
- tags: tagsMatch?.[1]?.split(',').map((t) => t.trim()).filter(Boolean),
1723
+ dependencies: depsMatch?.[1]
1724
+ ?.split(',')
1725
+ .map((d) => d.trim())
1726
+ .filter(Boolean),
1727
+ tags: tagsMatch?.[1]
1728
+ ?.split(',')
1729
+ .map((t) => t.trim())
1730
+ .filter(Boolean),
1476
1731
  methods: schemas
1477
1732
  .filter((s) => !['onInitialize', 'onShutdown', 'constructor'].includes(s.name))
1478
1733
  .map((s) => ({
@@ -1554,7 +1809,7 @@ async function handleBeamStudioParse(req, args) {
1554
1809
  const versionMatch = source.match(/@version\s+(\S+)/);
1555
1810
  const runtimeMatch = source.match(/@runtime\s+(\S+)/);
1556
1811
  const iconMatch = source.match(/@icon\s+(\S+)/);
1557
- const statefulMatch = source.match(/@stateful\s+true/);
1812
+ const statefulMatch = source.match(/@stateful\b/);
1558
1813
  const depsMatch = source.match(/@dependencies\s+(.+)/);
1559
1814
  const tagsMatch = source.match(/@tags\s+(.+)/);
1560
1815
  const errors = [];
@@ -1570,8 +1825,14 @@ async function handleBeamStudioParse(req, args) {
1570
1825
  version: versionMatch?.[1],
1571
1826
  runtime: runtimeMatch?.[1],
1572
1827
  stateful: !!statefulMatch,
1573
- dependencies: depsMatch?.[1]?.split(',').map((d) => d.trim()).filter(Boolean),
1574
- tags: tagsMatch?.[1]?.split(',').map((t) => t.trim()).filter(Boolean),
1828
+ dependencies: depsMatch?.[1]
1829
+ ?.split(',')
1830
+ .map((d) => d.trim())
1831
+ .filter(Boolean),
1832
+ tags: tagsMatch?.[1]
1833
+ ?.split(',')
1834
+ .map((t) => t.trim())
1835
+ .filter(Boolean),
1575
1836
  methods: schemas
1576
1837
  .filter((s) => !['onInitialize', 'onShutdown', 'constructor'].includes(s.name))
1577
1838
  .map((s) => ({
@@ -1663,20 +1924,40 @@ export async function handleStreamableHTTP(req, res, options) {
1663
1924
  });
1664
1925
  // Disable Nagle's algorithm for immediate writes
1665
1926
  res.socket?.setNoDelay(true);
1927
+ // Enable TCP keepalive to prevent connection drops from intermediaries
1928
+ res.socket?.setKeepAlive(true, 60000);
1666
1929
  // Store SSE response for server-initiated messages
1667
1930
  session.sseResponse = res;
1668
- // Keep connection alive
1931
+ // Keep connection alive with SSE data events (every 15s for better reliability)
1669
1932
  const keepAlive = setInterval(() => {
1670
- res.write(': keepalive\n\n');
1671
- }, 30000);
1672
- req.on('close', () => {
1933
+ // Check if response is still writable before sending keepalive
1934
+ if (!res.writableEnded && !res.destroyed) {
1935
+ try {
1936
+ // Send as data event so client onmessage handler fires and updates lastMessageTime
1937
+ res.write('data: {"type":"keepalive"}\n\n');
1938
+ }
1939
+ catch (err) {
1940
+ // If write fails, connection is dead - clean up
1941
+ clearInterval(keepAlive);
1942
+ session.sseResponse = undefined;
1943
+ }
1944
+ }
1945
+ else {
1946
+ clearInterval(keepAlive);
1947
+ }
1948
+ }, 15000); // Reduced from 30s to 15s for better responsiveness
1949
+ // Handle client disconnect
1950
+ const cleanup = () => {
1673
1951
  clearInterval(keepAlive);
1674
1952
  session.sseResponse = undefined;
1675
1953
  // Clean up subscriptions when client disconnects
1676
1954
  if (options.subscriptionManager) {
1677
1955
  options.subscriptionManager.onClientDisconnect(session.id);
1678
1956
  }
1679
- });
1957
+ };
1958
+ req.on('close', cleanup);
1959
+ req.on('error', cleanup);
1960
+ res.on('error', cleanup);
1680
1961
  return true;
1681
1962
  }
1682
1963
  // POST - Handle JSON-RPC requests
@@ -1781,12 +2062,33 @@ export function broadcastNotification(method, params, beamOnly = false) {
1781
2062
  method,
1782
2063
  params,
1783
2064
  };
1784
- for (const session of sessions.values()) {
1785
- if (session.sseResponse && !session.sseResponse.writableEnded) {
2065
+ const data = `data: ${JSON.stringify(notification)}\n\n`;
2066
+ const deadSessions = [];
2067
+ for (const [sessionId, session] of sessions) {
2068
+ if (session.sseResponse &&
2069
+ !session.sseResponse.writableEnded &&
2070
+ !session.sseResponse.destroyed) {
1786
2071
  // Skip non-Beam clients if beamOnly is true
1787
2072
  if (beamOnly && !session.isBeam)
1788
2073
  continue;
1789
- session.sseResponse.write(`data: ${JSON.stringify(notification)}\n\n`);
2074
+ try {
2075
+ session.sseResponse.write(data);
2076
+ }
2077
+ catch (err) {
2078
+ // Mark session for cleanup if write fails
2079
+ deadSessions.push(sessionId);
2080
+ }
2081
+ }
2082
+ else if (session.sseResponse) {
2083
+ // Response is ended/destroyed - mark for cleanup
2084
+ deadSessions.push(sessionId);
2085
+ }
2086
+ }
2087
+ // Clean up dead sessions
2088
+ for (const sessionId of deadSessions) {
2089
+ const session = sessions.get(sessionId);
2090
+ if (session) {
2091
+ session.sseResponse = undefined;
1790
2092
  }
1791
2093
  }
1792
2094
  }
@@ -1817,7 +2119,7 @@ export function getActiveSessionCount() {
1817
2119
  */
1818
2120
  export function sendToSession(sessionId, method, params) {
1819
2121
  const session = sessions.get(sessionId);
1820
- if (!session?.sseResponse || session.sseResponse.writableEnded) {
2122
+ if (!session?.sseResponse || session.sseResponse.writableEnded || session.sseResponse.destroyed) {
1821
2123
  return false;
1822
2124
  }
1823
2125
  const notification = {
@@ -1825,8 +2127,15 @@ export function sendToSession(sessionId, method, params) {
1825
2127
  method,
1826
2128
  params,
1827
2129
  };
1828
- session.sseResponse.write(`data: ${JSON.stringify(notification)}\n\n`);
1829
- return true;
2130
+ try {
2131
+ session.sseResponse.write(`data: ${JSON.stringify(notification)}\n\n`);
2132
+ return true;
2133
+ }
2134
+ catch (err) {
2135
+ // Write failed - connection is dead
2136
+ session.sseResponse = undefined;
2137
+ return false;
2138
+ }
1830
2139
  }
1831
2140
  /**
1832
2141
  * Request elicitation from the frontend for an external MCP.
@@ -1872,4 +2181,39 @@ export function requestExternalElicitation(mcpName, request) {
1872
2181
  }, 300000);
1873
2182
  });
1874
2183
  }
2184
+ /**
2185
+ * Request elicitation from Beam using Photon-native ask types (select, text, etc.)
2186
+ * Unlike requestExternalElicitation which uses MCP form/url mode, this sends
2187
+ * the ask type directly so the elicitation modal renders the appropriate UI.
2188
+ */
2189
+ function requestBeamElicitation(data) {
2190
+ const elicitationId = randomUUID();
2191
+ return new Promise((resolve) => {
2192
+ pendingElicitations.set(elicitationId, {
2193
+ resolve: (value) => {
2194
+ resolve({ action: 'accept', content: value });
2195
+ },
2196
+ reject: (error) => {
2197
+ if (error.message.includes('cancelled')) {
2198
+ resolve({ action: 'cancel' });
2199
+ }
2200
+ else {
2201
+ resolve({ action: 'decline' });
2202
+ }
2203
+ },
2204
+ sessionId: '',
2205
+ });
2206
+ // Broadcast with Photon-native ask format (not MCP form mode)
2207
+ broadcastToBeam('beam/elicitation', {
2208
+ elicitationId,
2209
+ ...data,
2210
+ });
2211
+ setTimeout(() => {
2212
+ if (pendingElicitations.has(elicitationId)) {
2213
+ pendingElicitations.delete(elicitationId);
2214
+ resolve({ action: 'cancel' });
2215
+ }
2216
+ }, 300000);
2217
+ });
2218
+ }
1875
2219
  //# sourceMappingURL=streamable-http-transport.js.map