@portel/photon 1.7.0 → 1.8.1

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 (94) hide show
  1. package/README.md +23 -24
  2. package/dist/auto-ui/beam.d.ts.map +1 -1
  3. package/dist/auto-ui/beam.js +117 -42
  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 +1 -1
  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/rendering/components.d.ts.map +1 -1
  11. package/dist/auto-ui/rendering/components.js +568 -0
  12. package/dist/auto-ui/rendering/components.js.map +1 -1
  13. package/dist/auto-ui/rendering/field-analyzer.d.ts +56 -0
  14. package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -1
  15. package/dist/auto-ui/rendering/field-analyzer.js +177 -0
  16. package/dist/auto-ui/rendering/field-analyzer.js.map +1 -1
  17. package/dist/auto-ui/rendering/layout-selector.d.ts +14 -2
  18. package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -1
  19. package/dist/auto-ui/rendering/layout-selector.js +125 -1
  20. package/dist/auto-ui/rendering/layout-selector.js.map +1 -1
  21. package/dist/auto-ui/streamable-http-transport.d.ts +1 -1
  22. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  23. package/dist/auto-ui/streamable-http-transport.js +353 -19
  24. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  25. package/dist/auto-ui/types.d.ts +7 -1
  26. package/dist/auto-ui/types.d.ts.map +1 -1
  27. package/dist/auto-ui/types.js.map +1 -1
  28. package/dist/beam.bundle.js +22441 -4216
  29. package/dist/beam.bundle.js.map +4 -4
  30. package/dist/cli/commands/info.d.ts.map +1 -1
  31. package/dist/cli/commands/info.js +37 -0
  32. package/dist/cli/commands/info.js.map +1 -1
  33. package/dist/cli/commands/package.d.ts.map +1 -1
  34. package/dist/cli/commands/package.js +16 -0
  35. package/dist/cli/commands/package.js.map +1 -1
  36. package/dist/cli.d.ts.map +1 -1
  37. package/dist/cli.js +628 -14
  38. package/dist/cli.js.map +1 -1
  39. package/dist/context-store.d.ts +79 -0
  40. package/dist/context-store.d.ts.map +1 -0
  41. package/dist/context-store.js +210 -0
  42. package/dist/context-store.js.map +1 -0
  43. package/dist/daemon/client.d.ts +13 -4
  44. package/dist/daemon/client.d.ts.map +1 -1
  45. package/dist/daemon/client.js +138 -77
  46. package/dist/daemon/client.js.map +1 -1
  47. package/dist/daemon/manager.d.ts +0 -25
  48. package/dist/daemon/manager.d.ts.map +1 -1
  49. package/dist/daemon/manager.js +10 -38
  50. package/dist/daemon/manager.js.map +1 -1
  51. package/dist/daemon/protocol.d.ts +7 -2
  52. package/dist/daemon/protocol.d.ts.map +1 -1
  53. package/dist/daemon/protocol.js.map +1 -1
  54. package/dist/daemon/server.js +257 -35
  55. package/dist/daemon/server.js.map +1 -1
  56. package/dist/daemon/session-manager.d.ts +24 -4
  57. package/dist/daemon/session-manager.d.ts.map +1 -1
  58. package/dist/daemon/session-manager.js +62 -12
  59. package/dist/daemon/session-manager.js.map +1 -1
  60. package/dist/index.d.ts +0 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +0 -3
  63. package/dist/index.js.map +1 -1
  64. package/dist/loader.d.ts +3 -20
  65. package/dist/loader.d.ts.map +1 -1
  66. package/dist/loader.js +53 -75
  67. package/dist/loader.js.map +1 -1
  68. package/dist/photon-cli-runner.d.ts.map +1 -1
  69. package/dist/photon-cli-runner.js +258 -218
  70. package/dist/photon-cli-runner.js.map +1 -1
  71. package/dist/photon-doc-extractor.d.ts +2 -0
  72. package/dist/photon-doc-extractor.d.ts.map +1 -1
  73. package/dist/photon-doc-extractor.js +42 -6
  74. package/dist/photon-doc-extractor.js.map +1 -1
  75. package/dist/photons/maker.photon.d.ts.map +1 -1
  76. package/dist/photons/maker.photon.js +3 -1
  77. package/dist/photons/maker.photon.js.map +1 -1
  78. package/dist/photons/maker.photon.ts +3 -1
  79. package/dist/serv/index.d.ts.map +1 -1
  80. package/dist/serv/index.js.map +1 -1
  81. package/dist/server.d.ts +32 -15
  82. package/dist/server.d.ts.map +1 -1
  83. package/dist/server.js +468 -469
  84. package/dist/server.js.map +1 -1
  85. package/dist/shared/security.d.ts.map +1 -1
  86. package/dist/shared/security.js +4 -8
  87. package/dist/shared/security.js.map +1 -1
  88. package/dist/shell-completions.d.ts +21 -0
  89. package/dist/shell-completions.d.ts.map +1 -0
  90. package/dist/shell-completions.js +102 -0
  91. package/dist/shell-completions.js.map +1 -0
  92. package/dist/template-manager.d.ts.map +1 -1
  93. package/dist/template-manager.js.map +1 -1
  94. package/package.json +10 -6
@@ -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
@@ -788,11 +959,12 @@ const handlers = {
788
959
  }
789
960
  // Use return value if no chunks were yielded, otherwise use chunks
790
961
  const finalResult = chunks.length > 0 ? (chunks.length === 1 ? chunks[0] : chunks) : returnValue;
962
+ const genResultText = formatResultText(finalResult);
791
963
  const genResponse = {
792
964
  jsonrpc: '2.0',
793
965
  id: req.id,
794
966
  result: {
795
- content: [{ type: 'text', text: JSON.stringify(finalResult, null, 2) }],
967
+ content: [{ type: 'text', text: genResultText }],
796
968
  isError: false,
797
969
  ...uiMetadata,
798
970
  },
@@ -811,11 +983,13 @@ const handlers = {
811
983
  }
812
984
  return genResponse;
813
985
  }
986
+ // For void methods, provide a success acknowledgment so the UI shows feedback
987
+ const resultText = formatResultText(result);
814
988
  const toolResponse = {
815
989
  jsonrpc: '2.0',
816
990
  id: req.id,
817
991
  result: {
818
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
992
+ content: [{ type: 'text', text: resultText }],
819
993
  isError: false,
820
994
  ...uiMetadata,
821
995
  },
@@ -896,6 +1070,83 @@ const handlers = {
896
1070
  },
897
1071
  };
898
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
+ },
899
1150
  };
900
1151
  // ════════════════════════════════════════════════════════════════════════════════
901
1152
  // BEAM SYSTEM TOOLS
@@ -1459,7 +1710,7 @@ async function handleBeamStudioWrite(req, ctx, args) {
1459
1710
  const versionMatch = source.match(/@version\s+(\S+)/);
1460
1711
  const runtimeMatch = source.match(/@runtime\s+(\S+)/);
1461
1712
  const iconMatch = source.match(/@icon\s+(\S+)/);
1462
- const statefulMatch = source.match(/@stateful\s+true/);
1713
+ const statefulMatch = source.match(/@stateful\b/);
1463
1714
  const depsMatch = source.match(/@dependencies\s+(.+)/);
1464
1715
  const tagsMatch = source.match(/@tags\s+(.+)/);
1465
1716
  parseResult = {
@@ -1558,7 +1809,7 @@ async function handleBeamStudioParse(req, args) {
1558
1809
  const versionMatch = source.match(/@version\s+(\S+)/);
1559
1810
  const runtimeMatch = source.match(/@runtime\s+(\S+)/);
1560
1811
  const iconMatch = source.match(/@icon\s+(\S+)/);
1561
- const statefulMatch = source.match(/@stateful\s+true/);
1812
+ const statefulMatch = source.match(/@stateful\b/);
1562
1813
  const depsMatch = source.match(/@dependencies\s+(.+)/);
1563
1814
  const tagsMatch = source.match(/@tags\s+(.+)/);
1564
1815
  const errors = [];
@@ -1673,20 +1924,40 @@ export async function handleStreamableHTTP(req, res, options) {
1673
1924
  });
1674
1925
  // Disable Nagle's algorithm for immediate writes
1675
1926
  res.socket?.setNoDelay(true);
1927
+ // Enable TCP keepalive to prevent connection drops from intermediaries
1928
+ res.socket?.setKeepAlive(true, 60000);
1676
1929
  // Store SSE response for server-initiated messages
1677
1930
  session.sseResponse = res;
1678
- // Keep connection alive
1931
+ // Keep connection alive with SSE data events (every 15s for better reliability)
1679
1932
  const keepAlive = setInterval(() => {
1680
- res.write(': keepalive\n\n');
1681
- }, 30000);
1682
- 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 = () => {
1683
1951
  clearInterval(keepAlive);
1684
1952
  session.sseResponse = undefined;
1685
1953
  // Clean up subscriptions when client disconnects
1686
1954
  if (options.subscriptionManager) {
1687
1955
  options.subscriptionManager.onClientDisconnect(session.id);
1688
1956
  }
1689
- });
1957
+ };
1958
+ req.on('close', cleanup);
1959
+ req.on('error', cleanup);
1960
+ res.on('error', cleanup);
1690
1961
  return true;
1691
1962
  }
1692
1963
  // POST - Handle JSON-RPC requests
@@ -1791,12 +2062,33 @@ export function broadcastNotification(method, params, beamOnly = false) {
1791
2062
  method,
1792
2063
  params,
1793
2064
  };
1794
- for (const session of sessions.values()) {
1795
- 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) {
1796
2071
  // Skip non-Beam clients if beamOnly is true
1797
2072
  if (beamOnly && !session.isBeam)
1798
2073
  continue;
1799
- 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;
1800
2092
  }
1801
2093
  }
1802
2094
  }
@@ -1827,7 +2119,7 @@ export function getActiveSessionCount() {
1827
2119
  */
1828
2120
  export function sendToSession(sessionId, method, params) {
1829
2121
  const session = sessions.get(sessionId);
1830
- if (!session?.sseResponse || session.sseResponse.writableEnded) {
2122
+ if (!session?.sseResponse || session.sseResponse.writableEnded || session.sseResponse.destroyed) {
1831
2123
  return false;
1832
2124
  }
1833
2125
  const notification = {
@@ -1835,8 +2127,15 @@ export function sendToSession(sessionId, method, params) {
1835
2127
  method,
1836
2128
  params,
1837
2129
  };
1838
- session.sseResponse.write(`data: ${JSON.stringify(notification)}\n\n`);
1839
- 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
+ }
1840
2139
  }
1841
2140
  /**
1842
2141
  * Request elicitation from the frontend for an external MCP.
@@ -1882,4 +2181,39 @@ export function requestExternalElicitation(mcpName, request) {
1882
2181
  }, 300000);
1883
2182
  });
1884
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
+ }
1885
2219
  //# sourceMappingURL=streamable-http-transport.js.map