@portel/photon 1.19.0 → 1.20.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 (92) hide show
  1. package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
  2. package/dist/auto-ui/beam/routes/api-browse.js +16 -4
  3. package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
  4. package/dist/auto-ui/beam/routes/api-config.js +4 -4
  5. package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
  6. package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -1
  7. package/dist/auto-ui/beam/routes/api-marketplace.js +14 -1
  8. package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
  9. package/dist/auto-ui/beam.d.ts.map +1 -1
  10. package/dist/auto-ui/beam.js +183 -74
  11. package/dist/auto-ui/beam.js.map +1 -1
  12. package/dist/auto-ui/bridge/index.d.ts.map +1 -1
  13. package/dist/auto-ui/bridge/index.js +17 -0
  14. package/dist/auto-ui/bridge/index.js.map +1 -1
  15. package/dist/auto-ui/streamable-http-transport.d.ts +1 -0
  16. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  17. package/dist/auto-ui/streamable-http-transport.js +64 -16
  18. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  19. package/dist/auto-ui/types.d.ts +12 -0
  20. package/dist/auto-ui/types.d.ts.map +1 -1
  21. package/dist/auto-ui/types.js.map +1 -1
  22. package/dist/beam-form.bundle.js +44 -3
  23. package/dist/beam-form.bundle.js.map +2 -2
  24. package/dist/beam.bundle.js +1404 -482
  25. package/dist/beam.bundle.js.map +4 -4
  26. package/dist/capability-negotiator.d.ts +67 -0
  27. package/dist/capability-negotiator.d.ts.map +1 -0
  28. package/dist/capability-negotiator.js +104 -0
  29. package/dist/capability-negotiator.js.map +1 -0
  30. package/dist/channel-manager.d.ts +122 -0
  31. package/dist/channel-manager.d.ts.map +1 -0
  32. package/dist/channel-manager.js +266 -0
  33. package/dist/channel-manager.js.map +1 -0
  34. package/dist/cli/commands/package.d.ts.map +1 -1
  35. package/dist/cli/commands/package.js +25 -7
  36. package/dist/cli/commands/package.js.map +1 -1
  37. package/dist/daemon/client.d.ts.map +1 -1
  38. package/dist/daemon/client.js +12 -0
  39. package/dist/daemon/client.js.map +1 -1
  40. package/dist/daemon/server.js +30 -49
  41. package/dist/daemon/server.js.map +1 -1
  42. package/dist/daemon/worker-manager.d.ts.map +1 -1
  43. package/dist/daemon/worker-manager.js +21 -7
  44. package/dist/daemon/worker-manager.js.map +1 -1
  45. package/dist/loader.d.ts +4 -1
  46. package/dist/loader.d.ts.map +1 -1
  47. package/dist/loader.js +73 -11
  48. package/dist/loader.js.map +1 -1
  49. package/dist/marketplace-manager.d.ts +6 -0
  50. package/dist/marketplace-manager.d.ts.map +1 -1
  51. package/dist/marketplace-manager.js +161 -58
  52. package/dist/marketplace-manager.js.map +1 -1
  53. package/dist/namespace-migration.d.ts +1 -0
  54. package/dist/namespace-migration.d.ts.map +1 -1
  55. package/dist/namespace-migration.js +86 -0
  56. package/dist/namespace-migration.js.map +1 -1
  57. package/dist/resource-server.d.ts +105 -0
  58. package/dist/resource-server.d.ts.map +1 -0
  59. package/dist/resource-server.js +723 -0
  60. package/dist/resource-server.js.map +1 -0
  61. package/dist/serv/auth/jwt.d.ts +2 -0
  62. package/dist/serv/auth/jwt.d.ts.map +1 -1
  63. package/dist/serv/auth/jwt.js +11 -5
  64. package/dist/serv/auth/jwt.js.map +1 -1
  65. package/dist/serv/vault/token-vault.d.ts +2 -0
  66. package/dist/serv/vault/token-vault.d.ts.map +1 -1
  67. package/dist/serv/vault/token-vault.js +6 -0
  68. package/dist/serv/vault/token-vault.js.map +1 -1
  69. package/dist/server.d.ts +20 -149
  70. package/dist/server.d.ts.map +1 -1
  71. package/dist/server.js +232 -1217
  72. package/dist/server.js.map +1 -1
  73. package/dist/shared/audit.d.ts.map +1 -1
  74. package/dist/shared/audit.js +7 -0
  75. package/dist/shared/audit.js.map +1 -1
  76. package/dist/shared/security.d.ts +10 -0
  77. package/dist/shared/security.d.ts.map +1 -1
  78. package/dist/shared/security.js +27 -0
  79. package/dist/shared/security.js.map +1 -1
  80. package/dist/task-executor.d.ts +69 -0
  81. package/dist/task-executor.d.ts.map +1 -0
  82. package/dist/task-executor.js +182 -0
  83. package/dist/task-executor.js.map +1 -0
  84. package/dist/types/photon-instance.d.ts +50 -0
  85. package/dist/types/photon-instance.d.ts.map +1 -0
  86. package/dist/types/photon-instance.js +9 -0
  87. package/dist/types/photon-instance.js.map +1 -0
  88. package/dist/types/server-types.d.ts +61 -0
  89. package/dist/types/server-types.d.ts.map +1 -0
  90. package/dist/types/server-types.js +8 -0
  91. package/dist/types/server-types.js.map +1 -0
  92. package/package.json +2 -2
@@ -14,7 +14,7 @@ import * as path from 'path';
14
14
  import * as os from 'os';
15
15
  import { fileURLToPath } from 'url';
16
16
  import { createHash } from 'crypto';
17
- import { setSecurityHeaders, SimpleRateLimiter, escapeHtml } from '../shared/security.js';
17
+ import { setSecurityHeaders, SimpleRateLimiter, escapeHtml, getCorsOrigin, } from '../shared/security.js';
18
18
  /**
19
19
  * Check if shell integration has been installed (photon init cli).
20
20
  * Cached at module load since it won't change during a Beam session.
@@ -100,7 +100,7 @@ import { ensurePhotonEditorDeclaration, writePhotonEditorDeclaration, } from '..
100
100
  import { ensureDaemon } from '../daemon/manager.js';
101
101
  import { SchemaExtractor } from '@portel/photon-core';
102
102
  import { generateServerCard } from '../server-card.js';
103
- import { handleStreamableHTTP, broadcastNotification, broadcastToBeam, } from './streamable-http-transport.js';
103
+ import { handleStreamableHTTP, broadcastNotification, broadcastToBeam, stopSessionCleanup, } from './streamable-http-transport.js';
104
104
  import { getBundledPhotonPath, BEAM_BUNDLED_PHOTONS } from '../shared-utils.js';
105
105
  // BUNDLED_PHOTONS and getBundledPhotonPath are imported from shared-utils.js
106
106
  // Extracted modules (Phase 5)
@@ -116,14 +116,40 @@ import { configurePhotonViaMCP, reloadPhotonViaMCP, removePhotonViaMCP, updateMe
116
116
  import { generateAgentCard } from '../a2a/card-generator.js';
117
117
  // Delegate to extracted module
118
118
  const getConfigFilePath = getConfigFilePathFromModule;
119
- // Module-level state for external MCPs (shared with transport handler)
120
- const externalMCPs = [];
121
- const externalMCPClients = new Map();
122
- const externalMCPSDKClients = new Map();
119
+ // ═══════════════════════════════════════════════════════════════════════════════
120
+ // BEAM CONTEXT — all module-level mutable state lives here
121
+ // ═══════════════════════════════════════════════════════════════════════════════
122
+ class BeamContext {
123
+ /** External MCP server metadata */
124
+ externalMCPs = [];
125
+ /** Transport-level clients for external MCPs */
126
+ externalMCPClients = new Map();
127
+ /** SDK Client instances for tool calls with structuredContent */
128
+ externalMCPSDKClients = new Map();
129
+ /**
130
+ * Notification subscriptions per photon.
131
+ * Key: photon name, Value: list of event types this photon cares about
132
+ * Example: { "chat": ["mentions", "direct-messages"], "tasks": ["deadline", "assigned-to-me"] }
133
+ */
134
+ photonNotificationSubscriptions = new Map();
135
+ /**
136
+ * Track which state-changed channels we've already subscribed to,
137
+ * so dynamically discovered photons can be subscribed without duplicates.
138
+ */
139
+ subscribedStateChannels = new Set();
140
+ /** Convenience accessor matching the shape expected by external-mcp module */
141
+ get externalMCPState() {
142
+ return {
143
+ externalMCPs: this.externalMCPs,
144
+ externalMCPClients: this.externalMCPClients,
145
+ externalMCPSDKClients: this.externalMCPSDKClients,
146
+ };
147
+ }
148
+ }
149
+ const ctx = new BeamContext();
123
150
  // Delegates — external MCP management now in beam/external-mcp.ts
124
- const externalMCPState = { externalMCPs, externalMCPClients, externalMCPSDKClients };
125
- const loadExternalMCPs = (config) => loadExternalMCPsFromModule(config, externalMCPState);
126
- const reconnectExternalMCP = (name) => reconnectExternalMCPFromModule(name, externalMCPState);
151
+ const loadExternalMCPs = (config) => loadExternalMCPsFromModule(config, ctx.externalMCPState);
152
+ const reconnectExternalMCP = (name) => reconnectExternalMCPFromModule(name, ctx.externalMCPState);
127
153
  // Delegates to extracted config module
128
154
  const migrateConfig = migrateConfigFromModule;
129
155
  const loadConfig = loadConfigFromModule;
@@ -135,19 +161,8 @@ const extractClassMetadataFromSource = extractClassMetadataFromModule;
135
161
  const applyMethodVisibility = applyMethodVisibilityFromModule;
136
162
  const extractCspFromSource = extractCspFromModule;
137
163
  // ═══════════════════════════════════════════════════════════════════════════════
138
- // NOTIFICATION SUBSCRIPTIONS
164
+ // NOTIFICATION SUBSCRIPTIONS (state lives in ctx: BeamContext)
139
165
  // ═══════════════════════════════════════════════════════════════════════════════
140
- /**
141
- * Map to store notification subscriptions per photon
142
- * Key: photon name, Value: list of event types this photon cares about
143
- * Example: { "chat": ["mentions", "direct-messages"], "tasks": ["deadline", "assigned-to-me"] }
144
- */
145
- const photonNotificationSubscriptions = new Map();
146
- /**
147
- * Track which state-changed channels we've already subscribed to,
148
- * so dynamically discovered photons can be subscribed without duplicates.
149
- */
150
- const subscribedStateChannels = new Set();
151
166
  /**
152
167
  * Generate the service worker JS that validates the Beam backend
153
168
  * on PWA launch and shows a diagnostic page if something is wrong.
@@ -460,8 +475,10 @@ const BOOT_PAGE = \`<!DOCTYPE html>
460
475
  export async function startBeam(rawWorkingDir, port) {
461
476
  const workingDir = path.resolve(rawWorkingDir);
462
477
  const { PHOTON_VERSION } = await import('../version.js');
463
- // Run data migration on first startup (fast no-op if already done)
478
+ // Run startup migrations (fast no-op when already applied)
464
479
  try {
480
+ const { runNamespaceMigration } = await import('../namespace-migration.js');
481
+ await runNamespaceMigration();
465
482
  const { runDataMigration } = await import('../data-migration.js');
466
483
  await runDataMigration();
467
484
  }
@@ -521,7 +538,7 @@ export async function startBeam(rawWorkingDir, port) {
521
538
  }
522
539
  // Build photon list with short names plus a numeric suffix for duplicates.
523
540
  // Also track resolved paths from namespace scan.
524
- const namespacePaths = new Map(); // displayName → filePath
541
+ const photonRouteMeta = new Map(); // displayName → route metadata
525
542
  const userPhotonList = [];
526
543
  const duplicateIndex = new Map();
527
544
  for (const p of userPhotonListDetailed) {
@@ -530,7 +547,12 @@ export async function startBeam(rawWorkingDir, port) {
530
547
  duplicateIndex.set(p.name, nextIndex);
531
548
  const displayName = duplicateCount > 1 ? `${p.name} (${nextIndex})` : p.name;
532
549
  userPhotonList.push(displayName);
533
- namespacePaths.set(displayName, p.filePath);
550
+ photonRouteMeta.set(displayName, {
551
+ filePath: p.filePath,
552
+ shortName: p.name,
553
+ namespace: p.namespace || undefined,
554
+ qualifiedName: p.qualifiedName || undefined,
555
+ });
534
556
  }
535
557
  // Add bundled photons with their paths
536
558
  const bundledPhotonPaths = new Map();
@@ -570,10 +592,11 @@ export async function startBeam(rawWorkingDir, port) {
570
592
  // Helper: load a single photon, returning the info to push into photons[]
571
593
  async function loadSinglePhoton(name) {
572
594
  const photonPath = bundledPhotonPaths.get(name) ||
573
- namespacePaths.get(name) ||
595
+ photonRouteMeta.get(name)?.filePath ||
574
596
  (await resolvePhotonPath(name, workingDir));
575
597
  if (!photonPath)
576
598
  return null;
599
+ const routeMeta = photonRouteMeta.get(name);
577
600
  // Apply saved config to environment before loading
578
601
  if (savedConfig.photons[name]) {
579
602
  for (const [key, value] of Object.entries(savedConfig.photons[name])) {
@@ -588,7 +611,9 @@ export async function startBeam(rawWorkingDir, port) {
588
611
  let isInternal;
589
612
  try {
590
613
  source = await readText(photonPath);
591
- await ensurePhotonEditorDeclaration(photonPath, source, workingDir).catch(() => { });
614
+ await ensurePhotonEditorDeclaration(photonPath, source, workingDir).catch((e) => {
615
+ logger.debug(`Failed to ensure editor declaration for ${photonPath}: ${e?.message || e}`);
616
+ });
592
617
  }
593
618
  catch {
594
619
  // Can't read source
@@ -670,11 +695,11 @@ export async function startBeam(rawWorkingDir, port) {
670
695
  mcp.schemas = schemas;
671
696
  // Store notification subscriptions per photon
672
697
  if (metadata.notificationSubscriptions?.watchFor) {
673
- photonNotificationSubscriptions.set(name, metadata.notificationSubscriptions.watchFor);
698
+ ctx.photonNotificationSubscriptions.set(name, metadata.notificationSubscriptions.watchFor);
674
699
  }
675
700
  else {
676
701
  // Clear previous subscription if photon no longer has @notify-on
677
- photonNotificationSubscriptions.delete(name);
702
+ ctx.photonNotificationSubscriptions.delete(name);
678
703
  }
679
704
  // Get UI assets for linking
680
705
  const uiAssets = mcp.assets?.ui || [];
@@ -812,6 +837,9 @@ export async function startBeam(rawWorkingDir, port) {
812
837
  return {
813
838
  id: generatePhotonId(photonPath),
814
839
  name,
840
+ ...(routeMeta?.shortName ? { shortName: routeMeta.shortName } : {}),
841
+ ...(routeMeta?.namespace ? { namespace: routeMeta.namespace } : {}),
842
+ ...(routeMeta?.qualifiedName ? { qualifiedName: routeMeta.qualifiedName } : {}),
815
843
  path: photonPath,
816
844
  configured: true,
817
845
  methods,
@@ -842,6 +870,9 @@ export async function startBeam(rawWorkingDir, port) {
842
870
  return {
843
871
  id: generatePhotonId(photonPath),
844
872
  name,
873
+ ...(routeMeta?.shortName ? { shortName: routeMeta.shortName } : {}),
874
+ ...(routeMeta?.namespace ? { namespace: routeMeta.namespace } : {}),
875
+ ...(routeMeta?.qualifiedName ? { qualifiedName: routeMeta.qualifiedName } : {}),
845
876
  path: photonPath,
846
877
  configured: false,
847
878
  label: prettifyName(name),
@@ -867,6 +898,7 @@ export async function startBeam(rawWorkingDir, port) {
867
898
  if (!photon || !photon.configured)
868
899
  return null;
869
900
  const photonDir = path.dirname(photon.path);
901
+ const photonBaseName = path.basename(photon.path, '.photon.ts');
870
902
  const asset = photon.assets?.ui?.find((u) => u.id === uiId);
871
903
  let uiPath;
872
904
  if (asset?.resolvedPath) {
@@ -874,8 +906,8 @@ export async function startBeam(rawWorkingDir, port) {
874
906
  }
875
907
  else {
876
908
  // Prefer .photon.html, then .photon.md, fall back to .html
877
- const photonHtmlPath = path.join(photonDir, photonName, 'ui', `${uiId}.photon.html`);
878
- const photonMdPath = path.join(photonDir, photonName, 'ui', `${uiId}.photon.md`);
909
+ const photonHtmlPath = path.join(photonDir, photonBaseName, 'ui', `${uiId}.photon.html`);
910
+ const photonMdPath = path.join(photonDir, photonBaseName, 'ui', `${uiId}.photon.md`);
879
911
  try {
880
912
  await fs.access(photonHtmlPath);
881
913
  uiPath = photonHtmlPath;
@@ -886,7 +918,7 @@ export async function startBeam(rawWorkingDir, port) {
886
918
  uiPath = photonMdPath;
887
919
  }
888
920
  catch {
889
- uiPath = path.join(photonDir, photonName, 'ui', `${uiId}.html`);
921
+ uiPath = path.join(photonDir, photonBaseName, 'ui', `${uiId}.html`);
890
922
  }
891
923
  }
892
924
  }
@@ -902,7 +934,7 @@ export async function startBeam(rawWorkingDir, port) {
902
934
  // Convention: format-<name> maps to assets/formats/<name>.html
903
935
  if (uiId.startsWith('format-')) {
904
936
  const formatName = uiId.slice('format-'.length);
905
- const formatPath = path.join(photonDir, photonName, 'assets', 'formats', `${formatName}.html`);
937
+ const formatPath = path.join(photonDir, photonBaseName, 'assets', 'formats', `${formatName}.html`);
906
938
  try {
907
939
  const content = await readText(formatPath);
908
940
  return { content, isPhotonTemplate: false };
@@ -939,9 +971,9 @@ export async function startBeam(rawWorkingDir, port) {
939
971
  savedConfig,
940
972
  photons,
941
973
  photonMCPs,
942
- externalMCPs,
943
- externalMCPClients,
944
- externalMCPSDKClients,
974
+ externalMCPs: ctx.externalMCPs,
975
+ externalMCPClients: ctx.externalMCPClients,
976
+ externalMCPSDKClients: ctx.externalMCPSDKClients,
945
977
  channelSubscriptions: new Map(),
946
978
  channelEventBuffers: new Map(),
947
979
  sessionViewState: new Map(),
@@ -991,10 +1023,11 @@ export async function startBeam(rawWorkingDir, port) {
991
1023
  baseUrl: `http://${req.headers.host}`,
992
1024
  version: PHOTON_VERSION,
993
1025
  });
994
- res.writeHead(200, {
995
- 'Content-Type': 'application/json',
996
- 'Access-Control-Allow-Origin': '*',
997
- });
1026
+ const cardHeaders = { 'Content-Type': 'application/json' };
1027
+ const cardCorsOrigin = getCorsOrigin(req);
1028
+ if (cardCorsOrigin)
1029
+ cardHeaders['Access-Control-Allow-Origin'] = cardCorsOrigin;
1030
+ res.writeHead(200, cardHeaders);
998
1031
  res.end(JSON.stringify(card));
999
1032
  return;
1000
1033
  }
@@ -1045,9 +1078,9 @@ export async function startBeam(rawWorkingDir, port) {
1045
1078
  const handled = await handleStreamableHTTP(req, res, {
1046
1079
  photons, // Pass all photons including unconfigured for configurationSchema
1047
1080
  photonMCPs,
1048
- externalMCPs,
1049
- externalMCPClients,
1050
- externalMCPSDKClients, // SDK clients for tool calls with structuredContent
1081
+ externalMCPs: ctx.externalMCPs,
1082
+ externalMCPClients: ctx.externalMCPClients,
1083
+ externalMCPSDKClients: ctx.externalMCPSDKClients, // SDK clients for tool calls with structuredContent
1051
1084
  reconnectExternalMCP,
1052
1085
  loadUIAsset,
1053
1086
  workingDir,
@@ -1057,8 +1090,12 @@ export async function startBeam(rawWorkingDir, port) {
1057
1090
  reloadPhoton: async (photonName) => {
1058
1091
  return reloadPhotonViaMCP(photonName, photons, photonMCPs, loader, savedConfig, broadcastPhotonChange, activeLoads, (name, path, isStateful) => {
1059
1092
  if (isStateful) {
1060
- subscribeStatefulPhoton(name).catch(() => { });
1061
- reloadDaemonPhoton(name, path, workingDir).catch(() => { });
1093
+ subscribeStatefulPhoton(name).catch((e) => {
1094
+ logger.debug(`Failed to subscribe stateful photon ${name}: ${e?.message || e}`);
1095
+ });
1096
+ reloadDaemonPhoton(name, path, workingDir).catch((e) => {
1097
+ logger.debug(`Failed to reload daemon photon ${name}: ${e?.message || e}`);
1098
+ });
1062
1099
  }
1063
1100
  });
1064
1101
  },
@@ -1482,9 +1519,27 @@ export async function startBeam(rawWorkingDir, port) {
1482
1519
  appEl.appendChild(iframe);
1483
1520
  initBridge(iframe, bridgeMethod);
1484
1521
  } catch (err) {
1485
- appEl.innerHTML = '<div class="status-page show"><div class="icon">⚠️</div>'
1486
- + '<h2>Failed to load</h2><p>' + err.message + '</p>'
1487
- + '<button class="retry-btn" onclick="checkAndLoad()">Retry</button></div>';
1522
+ appEl.innerHTML = '';
1523
+ const statusDiv = document.createElement('div');
1524
+ statusDiv.className = 'status-page show';
1525
+
1526
+ const icon = document.createElement('div');
1527
+ icon.className = 'icon';
1528
+ icon.textContent = '⚠️';
1529
+
1530
+ const h2 = document.createElement('h2');
1531
+ h2.textContent = 'Failed to load';
1532
+
1533
+ const p = document.createElement('p');
1534
+ p.textContent = err.message;
1535
+
1536
+ const btn = document.createElement('button');
1537
+ btn.className = 'retry-btn';
1538
+ btn.textContent = 'Retry';
1539
+ btn.addEventListener('click', () => checkAndLoad());
1540
+
1541
+ statusDiv.append(icon, h2, p, btn);
1542
+ appEl.appendChild(statusDiv);
1488
1543
  }
1489
1544
  }
1490
1545
 
@@ -1859,9 +1914,31 @@ export async function startBeam(rawWorkingDir, port) {
1859
1914
  const relativePath = path.relative(workingDir, changedPath);
1860
1915
  // React to .photon.ts file changes — both top-level and namespaced subdirectories.
1861
1916
  // Top-level: foo.photon.ts → "foo"
1862
- // Namespaced: portel/gitbox.photon.ts → "portel/gitbox"
1917
+ // Namespaced: Arul-/git-box.photon.ts → "git-box" (short name only)
1863
1918
  if (relativePath.endsWith('.photon.ts')) {
1864
- return relativePath.slice(0, -'.photon.ts'.length);
1919
+ // For namespaced paths, look up by file path first to respect disambiguated
1920
+ // names like "chat (1)" / "chat (2)" assigned at startup
1921
+ let resolvedPath;
1922
+ try {
1923
+ resolvedPath = realpathSync(changedPath);
1924
+ }
1925
+ catch {
1926
+ resolvedPath = changedPath;
1927
+ }
1928
+ const byPath = photons.find((p) => {
1929
+ try {
1930
+ return realpathSync(p.path) === resolvedPath;
1931
+ }
1932
+ catch {
1933
+ return p.path === changedPath;
1934
+ }
1935
+ });
1936
+ if (byPath)
1937
+ return byPath.name;
1938
+ // New photon — derive short name from filename
1939
+ const withoutExt = relativePath.slice(0, -'.photon.ts'.length);
1940
+ const slashIndex = withoutExt.lastIndexOf(path.sep);
1941
+ return slashIndex >= 0 ? withoutExt.slice(slashIndex + 1) : withoutExt;
1865
1942
  }
1866
1943
  // Detect asset changes for local (non-symlinked) photons.
1867
1944
  // Runtime data now lives in .data/ (filtered above), so any remaining
@@ -1893,9 +1970,37 @@ export async function startBeam(rawWorkingDir, port) {
1893
1970
  try {
1894
1971
  const photonIndex = photons.findIndex((p) => p.name === photonName);
1895
1972
  const isNewPhoton = photonIndex === -1;
1896
- const photonPath = isNewPhoton
1897
- ? path.join(workingDir, `${photonName}.photon.ts`)
1898
- : photons[photonIndex].path;
1973
+ let photonPath;
1974
+ if (!isNewPhoton) {
1975
+ photonPath = photons[photonIndex].path;
1976
+ }
1977
+ else {
1978
+ // Try flat path first, then search namespace subdirectories
1979
+ const flatPath = path.join(workingDir, `${photonName}.photon.ts`);
1980
+ if (existsSync(flatPath)) {
1981
+ photonPath = flatPath;
1982
+ }
1983
+ else {
1984
+ // Search namespace dirs (one level deep) for the photon file
1985
+ let found = null;
1986
+ try {
1987
+ const entries = await fs.readdir(workingDir, { withFileTypes: true });
1988
+ for (const entry of entries) {
1989
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
1990
+ continue;
1991
+ const candidate = path.join(workingDir, entry.name, `${photonName}.photon.ts`);
1992
+ if (existsSync(candidate)) {
1993
+ found = candidate;
1994
+ break;
1995
+ }
1996
+ }
1997
+ }
1998
+ catch {
1999
+ // readdir failed
2000
+ }
2001
+ photonPath = found || flatPath;
2002
+ }
2003
+ }
1899
2004
  const previouslyConfigured = !isNewPhoton && photons[photonIndex]?.configured === true;
1900
2005
  // Handle file deletion - if file no longer exists and photon is in list, remove it
1901
2006
  if (!isNewPhoton && photonPath && !existsSync(photonPath)) {
@@ -1968,7 +2073,9 @@ export async function startBeam(rawWorkingDir, port) {
1968
2073
  let constructorParams = [];
1969
2074
  try {
1970
2075
  const source = await readText(photonPath);
1971
- await writePhotonEditorDeclaration(photonPath, source, workingDir).catch(() => { });
2076
+ await writePhotonEditorDeclaration(photonPath, source, workingDir).catch((e) => {
2077
+ logger.debug(`Failed to write editor declaration for ${photonPath}: ${e?.message || e}`);
2078
+ });
1972
2079
  const params = extractor.extractConstructorParams(source);
1973
2080
  constructorParams = params
1974
2081
  .filter((p) => p.isPrimitive)
@@ -2023,10 +2130,10 @@ export async function startBeam(rawWorkingDir, port) {
2023
2130
  mcp.schemas = schemas; // Store schemas for result rendering
2024
2131
  // Update notification subscriptions for reloaded photon
2025
2132
  if (reloadMetadata.notificationSubscriptions?.watchFor) {
2026
- photonNotificationSubscriptions.set(photonName, reloadMetadata.notificationSubscriptions.watchFor);
2133
+ ctx.photonNotificationSubscriptions.set(photonName, reloadMetadata.notificationSubscriptions.watchFor);
2027
2134
  }
2028
2135
  else {
2029
- photonNotificationSubscriptions.delete(photonName);
2136
+ ctx.photonNotificationSubscriptions.delete(photonName);
2030
2137
  }
2031
2138
  const lifecycleMethods = ['onInitialize', 'onShutdown', 'constructor'];
2032
2139
  const uiAssets = mcp.assets?.ui || [];
@@ -2385,7 +2492,7 @@ export async function startBeam(rawWorkingDir, port) {
2385
2492
  }
2386
2493
  // Load external MCPs from config
2387
2494
  const externalMCPList = await loadExternalMCPs(savedConfig);
2388
- externalMCPs.push(...externalMCPList);
2495
+ ctx.externalMCPs.push(...externalMCPList);
2389
2496
  // Mark startup complete — flushes queued output and restores console
2390
2497
  startup.ready();
2391
2498
  // Notify connected clients that photon list is now available
@@ -2396,9 +2503,9 @@ export async function startBeam(rawWorkingDir, port) {
2396
2503
  const instanceNames = ['default'];
2397
2504
  for (const instanceName of instanceNames) {
2398
2505
  const channel = `${photonName}:${instanceName}:state-changed`;
2399
- if (subscribedStateChannels.has(channel))
2506
+ if (ctx.subscribedStateChannels.has(channel))
2400
2507
  continue;
2401
- subscribedStateChannels.add(channel);
2508
+ ctx.subscribedStateChannels.add(channel);
2402
2509
  subscribeChannel(photonName, channel, (message) => {
2403
2510
  // Sync Beam's local instance from the daemon-persisted state file BEFORE
2404
2511
  // notifying the frontend. The daemon persists state to disk after mutations,
@@ -2489,7 +2596,7 @@ export async function startBeam(rawWorkingDir, port) {
2489
2596
  // Subscribe to notifications channel (always-on, not just active)
2490
2597
  const notificationChannel = `${photonName}:${instanceName}:notifications`;
2491
2598
  // Get this photon's notification subscriptions from @notify-on tags
2492
- const watchFor = photonNotificationSubscriptions.get(photonName);
2599
+ const watchFor = ctx.photonNotificationSubscriptions.get(photonName);
2493
2600
  subscribeChannel(photonName, notificationChannel, (message) => {
2494
2601
  // Check if this photon cares about this notification type
2495
2602
  if (!watchFor || !watchFor.includes(message?.type)) {
@@ -2676,14 +2783,14 @@ export async function startBeam(rawWorkingDir, port) {
2676
2783
  // Remove MCPs — do all synchronous Map mutations first, then close async
2677
2784
  const removedSdkClients = [];
2678
2785
  for (const name of removed) {
2679
- const idx = externalMCPs.findIndex((m) => m.name === name);
2786
+ const idx = ctx.externalMCPs.findIndex((m) => m.name === name);
2680
2787
  if (idx !== -1)
2681
- externalMCPs.splice(idx, 1);
2682
- const sdkClient = externalMCPSDKClients.get(name);
2788
+ ctx.externalMCPs.splice(idx, 1);
2789
+ const sdkClient = ctx.externalMCPSDKClients.get(name);
2683
2790
  if (sdkClient)
2684
2791
  removedSdkClients.push({ name, client: sdkClient });
2685
- externalMCPSDKClients.delete(name);
2686
- externalMCPClients.delete(name);
2792
+ ctx.externalMCPSDKClients.delete(name);
2793
+ ctx.externalMCPClients.delete(name);
2687
2794
  logger.info(`🔌 Removed external MCP: ${name}`);
2688
2795
  }
2689
2796
  // Close SDK clients after all Maps are consistent
@@ -2702,7 +2809,7 @@ export async function startBeam(rawWorkingDir, port) {
2702
2809
  mcpServers: Object.fromEntries(added.map((k) => [k, newServers[k]])),
2703
2810
  };
2704
2811
  const newMCPs = await loadExternalMCPs(addConfig);
2705
- externalMCPs.push(...newMCPs);
2812
+ ctx.externalMCPs.push(...newMCPs);
2706
2813
  for (const m of newMCPs) {
2707
2814
  logger.info(`🔌 Added external MCP: ${m.name} (${m.connected ? m.methods.length + ' tools' : 'failed'})`);
2708
2815
  }
@@ -2710,14 +2817,14 @@ export async function startBeam(rawWorkingDir, port) {
2710
2817
  // Reconnect modified MCPs — synchronous cleanup first, then async reconnect
2711
2818
  const modifiedSdkClients = [];
2712
2819
  for (const name of modified) {
2713
- const idx = externalMCPs.findIndex((m) => m.name === name);
2820
+ const idx = ctx.externalMCPs.findIndex((m) => m.name === name);
2714
2821
  if (idx !== -1)
2715
- externalMCPs.splice(idx, 1);
2716
- const sdkClient = externalMCPSDKClients.get(name);
2822
+ ctx.externalMCPs.splice(idx, 1);
2823
+ const sdkClient = ctx.externalMCPSDKClients.get(name);
2717
2824
  if (sdkClient)
2718
2825
  modifiedSdkClients.push({ name, client: sdkClient });
2719
- externalMCPSDKClients.delete(name);
2720
- externalMCPClients.delete(name);
2826
+ ctx.externalMCPSDKClients.delete(name);
2827
+ ctx.externalMCPClients.delete(name);
2721
2828
  }
2722
2829
  // Close old SDK clients
2723
2830
  for (const { client } of modifiedSdkClients) {
@@ -2735,7 +2842,7 @@ export async function startBeam(rawWorkingDir, port) {
2735
2842
  mcpServers: { [name]: newServers[name] },
2736
2843
  };
2737
2844
  const reconnected = await loadExternalMCPs(modConfig);
2738
- externalMCPs.push(...reconnected);
2845
+ ctx.externalMCPs.push(...reconnected);
2739
2846
  logger.info(`🔌 Reconnected external MCP: ${name}`);
2740
2847
  }
2741
2848
  // Update savedConfig
@@ -2763,9 +2870,11 @@ export async function startBeam(rawWorkingDir, port) {
2763
2870
  * Closes all external MCP SDK clients to prevent ugly tracebacks on shutdown.
2764
2871
  */
2765
2872
  export async function stopBeam() {
2873
+ // Stop session cleanup timer
2874
+ stopSessionCleanup();
2766
2875
  // Close all SDK clients gracefully
2767
2876
  const closePromises = [];
2768
- for (const [, client] of externalMCPSDKClients) {
2877
+ for (const [, client] of ctx.externalMCPSDKClients) {
2769
2878
  closePromises.push(client.close().catch(() => {
2770
2879
  // Ignore close errors - process is exiting anyway
2771
2880
  }));
@@ -2774,7 +2883,7 @@ export async function stopBeam() {
2774
2883
  if (closePromises.length > 0) {
2775
2884
  await withTimeout(Promise.all(closePromises), 1000, 'MCP client close timeout').catch(() => { }); // Timeout during shutdown is expected
2776
2885
  }
2777
- externalMCPSDKClients.clear();
2778
- externalMCPClients.clear();
2886
+ ctx.externalMCPSDKClients.clear();
2887
+ ctx.externalMCPClients.clear();
2779
2888
  }
2780
2889
  //# sourceMappingURL=beam.js.map