@portel/photon 1.16.0 → 1.17.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 (155) hide show
  1. package/dist/auto-ui/beam/external-mcp.d.ts.map +1 -1
  2. package/dist/auto-ui/beam/photon-management.d.ts +1 -1
  3. package/dist/auto-ui/beam/photon-management.d.ts.map +1 -1
  4. package/dist/auto-ui/beam/photon-management.js +5 -1
  5. package/dist/auto-ui/beam/photon-management.js.map +1 -1
  6. package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
  7. package/dist/auto-ui/beam/routes/api-browse.js +28 -7
  8. package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
  9. package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
  10. package/dist/auto-ui/beam/routes/api-config.js +2 -1
  11. package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
  12. package/dist/auto-ui/beam/types.d.ts.map +1 -1
  13. package/dist/auto-ui/beam.d.ts.map +1 -1
  14. package/dist/auto-ui/beam.js +74 -51
  15. package/dist/auto-ui/beam.js.map +1 -1
  16. package/dist/auto-ui/bridge/index.d.ts.map +1 -1
  17. package/dist/auto-ui/bridge/renderers.d.ts.map +1 -1
  18. package/dist/auto-ui/bridge/renderers.js +202 -13
  19. package/dist/auto-ui/bridge/renderers.js.map +1 -1
  20. package/dist/auto-ui/components/checklist.d.ts +13 -0
  21. package/dist/auto-ui/components/checklist.d.ts.map +1 -0
  22. package/dist/auto-ui/components/checklist.js +48 -0
  23. package/dist/auto-ui/components/checklist.js.map +1 -0
  24. package/dist/auto-ui/photon-host.d.ts +0 -1
  25. package/dist/auto-ui/photon-host.d.ts.map +1 -1
  26. package/dist/auto-ui/photon-host.js +0 -3
  27. package/dist/auto-ui/photon-host.js.map +1 -1
  28. package/dist/auto-ui/platform-compat.js.map +1 -1
  29. package/dist/auto-ui/playground-html.js +1 -1
  30. package/dist/auto-ui/playground-html.js.map +1 -1
  31. package/dist/auto-ui/registry.d.ts.map +1 -1
  32. package/dist/auto-ui/registry.js +2 -0
  33. package/dist/auto-ui/registry.js.map +1 -1
  34. package/dist/auto-ui/rendering/template-engine.d.ts.map +1 -1
  35. package/dist/auto-ui/rendering/template-engine.js +0 -3
  36. package/dist/auto-ui/rendering/template-engine.js.map +1 -1
  37. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  38. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  39. package/dist/auto-ui/types.d.ts +1 -1
  40. package/dist/auto-ui/types.d.ts.map +1 -1
  41. package/dist/auto-ui/types.js.map +1 -1
  42. package/dist/beam-form.bundle.js +131 -0
  43. package/dist/beam-form.bundle.js.map +2 -2
  44. package/dist/beam.bundle.js +4946 -498
  45. package/dist/beam.bundle.js.map +4 -4
  46. package/dist/claude-code-plugin.js +11 -3
  47. package/dist/claude-code-plugin.js.map +1 -1
  48. package/dist/cli/commands/build.js +1 -1
  49. package/dist/cli/commands/build.js.map +1 -1
  50. package/dist/cli/commands/config.js +2 -2
  51. package/dist/cli/commands/config.js.map +1 -1
  52. package/dist/cli/commands/daemon.js +4 -4
  53. package/dist/cli/commands/daemon.js.map +1 -1
  54. package/dist/cli/commands/doctor.d.ts.map +1 -1
  55. package/dist/cli/commands/doctor.js +7 -5
  56. package/dist/cli/commands/doctor.js.map +1 -1
  57. package/dist/cli/commands/info.d.ts.map +1 -1
  58. package/dist/cli/commands/info.js +3 -2
  59. package/dist/cli/commands/info.js.map +1 -1
  60. package/dist/cli/commands/init.d.ts.map +1 -1
  61. package/dist/cli/commands/init.js +0 -1
  62. package/dist/cli/commands/init.js.map +1 -1
  63. package/dist/cli/commands/mcp.d.ts.map +1 -1
  64. package/dist/cli/commands/mcp.js +2 -2
  65. package/dist/cli/commands/mcp.js.map +1 -1
  66. package/dist/cli/commands/package.d.ts.map +1 -1
  67. package/dist/cli/commands/package.js +10 -3
  68. package/dist/cli/commands/package.js.map +1 -1
  69. package/dist/cli/commands/run.d.ts.map +1 -1
  70. package/dist/cli/commands/run.js.map +1 -1
  71. package/dist/cli/commands/update.d.ts.map +1 -1
  72. package/dist/cli/commands/update.js +5 -2
  73. package/dist/cli/commands/update.js.map +1 -1
  74. package/dist/cli-alias.d.ts.map +1 -1
  75. package/dist/cli-alias.js +3 -2
  76. package/dist/cli-alias.js.map +1 -1
  77. package/dist/daemon/client.d.ts.map +1 -1
  78. package/dist/daemon/client.js +0 -2
  79. package/dist/daemon/client.js.map +1 -1
  80. package/dist/daemon/server.js +321 -22
  81. package/dist/daemon/server.js.map +1 -1
  82. package/dist/daemon/worker-manager.js +1 -1
  83. package/dist/daemon/worker-manager.js.map +1 -1
  84. package/dist/deploy/cloudflare.d.ts.map +1 -1
  85. package/dist/deploy/cloudflare.js +10 -16
  86. package/dist/deploy/cloudflare.js.map +1 -1
  87. package/dist/loader.d.ts +0 -2
  88. package/dist/loader.d.ts.map +1 -1
  89. package/dist/loader.js +18 -36
  90. package/dist/loader.js.map +1 -1
  91. package/dist/marketplace-manager.d.ts +14 -0
  92. package/dist/marketplace-manager.d.ts.map +1 -1
  93. package/dist/marketplace-manager.js +146 -26
  94. package/dist/marketplace-manager.js.map +1 -1
  95. package/dist/namespace-migration.d.ts.map +1 -1
  96. package/dist/namespace-migration.js +0 -12
  97. package/dist/namespace-migration.js.map +1 -1
  98. package/dist/path-resolver.js.map +1 -1
  99. package/dist/photon-cli-runner.d.ts.map +1 -1
  100. package/dist/photon-cli-runner.js +18 -24
  101. package/dist/photon-cli-runner.js.map +1 -1
  102. package/dist/photon-doc-extractor.d.ts.map +1 -1
  103. package/dist/photon-doc-extractor.js +0 -1
  104. package/dist/photon-doc-extractor.js.map +1 -1
  105. package/dist/photons/builder-compass.photon.d.ts +167 -0
  106. package/dist/photons/builder-compass.photon.d.ts.map +1 -0
  107. package/dist/photons/builder-compass.photon.js +816 -0
  108. package/dist/photons/builder-compass.photon.js.map +1 -0
  109. package/dist/photons/builder-compass.photon.ts +1129 -0
  110. package/dist/photons/maker.photon.d.ts +0 -1
  111. package/dist/photons/maker.photon.d.ts.map +1 -1
  112. package/dist/photons/maker.photon.js +19 -5
  113. package/dist/photons/maker.photon.js.map +1 -1
  114. package/dist/photons/maker.photon.ts +18 -6
  115. package/dist/photons/marketplace.photon.d.ts +1 -2
  116. package/dist/photons/marketplace.photon.d.ts.map +1 -1
  117. package/dist/photons/marketplace.photon.js +2 -3
  118. package/dist/photons/marketplace.photon.js.map +1 -1
  119. package/dist/photons/marketplace.photon.ts +2 -5
  120. package/dist/photons/ui/builder-compass.html +1199 -0
  121. package/dist/photons/ui/builder-compass.photon.html +380 -0
  122. package/dist/serv/auth/oauth.d.ts.map +1 -1
  123. package/dist/serv/auth/oauth.js.map +1 -1
  124. package/dist/serv/local.d.ts.map +1 -1
  125. package/dist/serv/local.js +1 -1
  126. package/dist/serv/local.js.map +1 -1
  127. package/dist/serv/middleware/auth.d.ts.map +1 -1
  128. package/dist/serv/middleware/auth.js.map +1 -1
  129. package/dist/serv/runtime/oauth-context.d.ts +0 -1
  130. package/dist/serv/runtime/oauth-context.d.ts.map +1 -1
  131. package/dist/serv/runtime/oauth-context.js +0 -2
  132. package/dist/serv/runtime/oauth-context.js.map +1 -1
  133. package/dist/serv/vault/token-vault.d.ts +0 -1
  134. package/dist/serv/vault/token-vault.d.ts.map +1 -1
  135. package/dist/serv/vault/token-vault.js +0 -2
  136. package/dist/serv/vault/token-vault.js.map +1 -1
  137. package/dist/server.d.ts.map +1 -1
  138. package/dist/server.js +20 -6
  139. package/dist/server.js.map +1 -1
  140. package/dist/shared/io.d.ts +33 -0
  141. package/dist/shared/io.d.ts.map +1 -0
  142. package/dist/shared/io.js +88 -0
  143. package/dist/shared/io.js.map +1 -0
  144. package/dist/shared-utils.d.ts +13 -0
  145. package/dist/shared-utils.d.ts.map +1 -1
  146. package/dist/shared-utils.js +33 -0
  147. package/dist/shared-utils.js.map +1 -1
  148. package/dist/tasks/store.d.ts.map +1 -1
  149. package/dist/tasks/store.js +7 -6
  150. package/dist/tasks/store.js.map +1 -1
  151. package/dist/template-manager.d.ts +0 -1
  152. package/dist/template-manager.d.ts.map +1 -1
  153. package/dist/template-manager.js +2 -3
  154. package/dist/template-manager.js.map +1 -1
  155. package/package.json +3 -2
@@ -8,6 +8,7 @@
8
8
  import * as http from 'http';
9
9
  import * as net from 'net';
10
10
  import * as fs from 'fs/promises';
11
+ import { readText, readJSON as readJSONFile, writeText } from '../shared/io.js';
11
12
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, watch, } from 'fs';
12
13
  import * as path from 'path';
13
14
  import * as os from 'os';
@@ -97,14 +98,14 @@ import { MarketplaceManager } from '../marketplace-manager.js';
97
98
  import { subscribeChannel, reloadDaemonPhoton } from '../daemon/client.js';
98
99
  import { ensurePhotonEditorDeclaration, writePhotonEditorDeclaration, } from '../photon-editor-declarations.js';
99
100
  import { ensureDaemon } from '../daemon/manager.js';
100
- import { SchemaExtractor, } from '@portel/photon-core';
101
+ import { SchemaExtractor } from '@portel/photon-core';
101
102
  import { generateServerCard } from '../server-card.js';
102
103
  import { handleStreamableHTTP, broadcastNotification, broadcastToBeam, } from './streamable-http-transport.js';
103
104
  import { getBundledPhotonPath, BEAM_BUNDLED_PHOTONS } from '../shared-utils.js';
104
105
  // BUNDLED_PHOTONS and getBundledPhotonPath are imported from shared-utils.js
105
106
  // Extracted modules (Phase 5)
106
107
  import { loadConfig as loadConfigFromModule, saveConfig as saveConfigFromModule, migrateConfig as migrateConfigFromModule, getConfigFilePath as getConfigFilePathFromModule, } from './beam/config.js';
107
- import { extractClassMetadataFromSource as extractClassMetadataFromModule, applyMethodVisibility as applyMethodVisibilityFromModule, extractCspFromSource as extractCspFromModule, prettifyName as prettifyNameFromModule, prettifyToolName as prettifyToolNameFromModule, backfillEnvDefaults as backfillEnvDefaultsFromModule, } from './beam/class-metadata.js';
108
+ import { extractClassMetadataFromSource as extractClassMetadataFromModule, applyMethodVisibility as applyMethodVisibilityFromModule, extractCspFromSource as extractCspFromModule, prettifyName as prettifyNameFromModule, backfillEnvDefaults as backfillEnvDefaultsFromModule, } from './beam/class-metadata.js';
108
109
  import { StartupSequencer } from './beam/startup.js';
109
110
  import { SubscriptionManager } from './beam/subscription.js';
110
111
  import { handleMarketplaceRoutes } from './beam/routes/api-marketplace.js';
@@ -119,8 +120,6 @@ const getConfigFilePath = getConfigFilePathFromModule;
119
120
  const externalMCPs = [];
120
121
  const externalMCPClients = new Map();
121
122
  const externalMCPSDKClients = new Map();
122
- // Delegate to extracted module
123
- const prettifyToolName = prettifyToolNameFromModule;
124
123
  // Delegates — external MCP management now in beam/external-mcp.ts
125
124
  const externalMCPState = { externalMCPs, externalMCPClients, externalMCPSDKClients };
126
125
  const loadExternalMCPs = (config) => loadExternalMCPsFromModule(config, externalMCPState);
@@ -485,18 +484,43 @@ export async function startBeam(rawWorkingDir, port) {
485
484
  logger.warn(`Asset repair check failed: ${getErrorMessage(error)}`);
486
485
  }
487
486
  // Discover all photons with namespace metadata (user photons + bundled photons)
488
- const userPhotonListDetailed = await listPhotonFilesWithNamespace(workingDir);
489
- // Detect name collisions to decide sidebar display names
487
+ const scannedPhotonList = await listPhotonFilesWithNamespace(workingDir);
488
+ // Deduplicate aliases that resolve to the same underlying file (for example,
489
+ // a marketplace photon and a local alias/symlink that both point at the same
490
+ // source). Beam should not show both copies in the sidebar.
491
+ const userPhotonListDetailed = [];
492
+ const seenPhotonKeys = new Set();
493
+ for (const photon of scannedPhotonList) {
494
+ let realPath = photon.filePath;
495
+ try {
496
+ realPath = realpathSync(photon.filePath);
497
+ }
498
+ catch {
499
+ // Fall back to the discovered path if realpath resolution fails.
500
+ }
501
+ const dedupeKey = `${photon.name}::${realPath}`;
502
+ if (seenPhotonKeys.has(dedupeKey))
503
+ continue;
504
+ seenPhotonKeys.add(dedupeKey);
505
+ userPhotonListDetailed.push(photon);
506
+ }
507
+ // Detect name collisions and generate friendly duplicate labels. We should
508
+ // never leak namespace/owner names into the sidebar just because there are
509
+ // multiple copies of a photon with the same short name.
490
510
  const nameOccurrences = new Map();
491
511
  for (const p of userPhotonListDetailed) {
492
512
  nameOccurrences.set(p.name, (nameOccurrences.get(p.name) || 0) + 1);
493
513
  }
494
- // Build photon list: use qualifiedName when collision, short name when unique
495
- // Also track resolved paths from namespace scan
514
+ // Build photon list with short names plus a numeric suffix for duplicates.
515
+ // Also track resolved paths from namespace scan.
496
516
  const namespacePaths = new Map(); // displayName → filePath
497
517
  const userPhotonList = [];
518
+ const duplicateIndex = new Map();
498
519
  for (const p of userPhotonListDetailed) {
499
- const displayName = (nameOccurrences.get(p.name) || 0) > 1 ? p.qualifiedName : p.name;
520
+ const duplicateCount = nameOccurrences.get(p.name) || 0;
521
+ const nextIndex = (duplicateIndex.get(p.name) || 0) + 1;
522
+ duplicateIndex.set(p.name, nextIndex);
523
+ const displayName = duplicateCount > 1 ? `${p.name} (${nextIndex})` : p.name;
500
524
  userPhotonList.push(displayName);
501
525
  namespacePaths.set(displayName, p.filePath);
502
526
  }
@@ -527,9 +551,6 @@ export async function startBeam(rawWorkingDir, port) {
527
551
  // Beam handles config errors gracefully via UI forms, but we still want to see actual errors
528
552
  const errorOnlyLogger = createLogger({ level: 'error' });
529
553
  const loader = new PhotonLoader(false, errorOnlyLogger, workingDir);
530
- // Counts updated after photon loading
531
- let configuredCount = 0;
532
- let unconfiguredCount = 0;
533
554
  // Check for placeholder defaults or localhost URLs (which need local services running)
534
555
  const isPlaceholderOrLocalDefault = (value) => {
535
556
  if (value.includes('<') || value.includes('your-'))
@@ -558,7 +579,7 @@ export async function startBeam(rawWorkingDir, port) {
558
579
  let source;
559
580
  let isInternal;
560
581
  try {
561
- source = await fs.readFile(photonPath, 'utf-8');
582
+ source = await readText(photonPath);
562
583
  await ensurePhotonEditorDeclaration(photonPath, source, workingDir).catch(() => { });
563
584
  }
564
585
  catch {
@@ -634,7 +655,7 @@ export async function startBeam(rawWorkingDir, port) {
634
655
  photonMCPs.set(name, mcp);
635
656
  backfillEnvDefaults(instance, constructorParams);
636
657
  // Extract schema for UI — reuse source read from above
637
- const schemaSource = source || (await fs.readFile(photonPath, 'utf-8'));
658
+ const schemaSource = source || (await readText(photonPath));
638
659
  const metadata = extractor.extractAllFromSource(schemaSource);
639
660
  const schemas = metadata.tools;
640
661
  const templates = metadata.templates;
@@ -652,8 +673,6 @@ export async function startBeam(rawWorkingDir, port) {
652
673
  // If loader didn't resolve UI assets but source has @ui tags,
653
674
  // extract them from source to populate linkedUi for sidebar
654
675
  if (uiAssets.length === 0 && schemaSource) {
655
- const uiTagRegex = /^\s*\*\s*@ui\s+(\S+)(?:\s+(\S+))?/gm;
656
- let uiMatch;
657
676
  const classUiMatch = schemaSource.match(/^\s*\*\s*@ui\s+(\S+)\s+(\.\/\S+)/m);
658
677
  if (classUiMatch) {
659
678
  // Class-level @ui tag with file path: @ui dashboard ./ui/dashboard.html
@@ -777,8 +796,10 @@ export async function startBeam(rawWorkingDir, port) {
777
796
  catch {
778
797
  // No install metadata - that's fine
779
798
  }
780
- const isStateful = schemaSource ? /@stateful\b/.test(schemaSource) : false;
781
- const authMatch = schemaSource?.match(/@auth\b(?:\s+(\S+))?/i);
799
+ // Extract class-level JSDoc block to avoid matching @tags inside template literals
800
+ const classDocblock = schemaSource?.match(/\/\*\*[\s\S]*?\*\/\s*(?:export\s+)?(?:default\s+)?class\b/)?.[0] ?? '';
801
+ const isStateful = classDocblock ? /@stateful\b/.test(classDocblock) : false;
802
+ const authMatch = classDocblock?.match(/@auth\b(?:\s+(\S+))?/i);
782
803
  const authValue = authMatch ? authMatch[1]?.trim() || 'required' : undefined;
783
804
  return {
784
805
  id: generatePhotonId(photonPath),
@@ -844,19 +865,26 @@ export async function startBeam(rawWorkingDir, port) {
844
865
  uiPath = asset.resolvedPath;
845
866
  }
846
867
  else {
847
- // Prefer .photon.html (declarative mode) over .html (full control)
868
+ // Prefer .photon.html, then .photon.md, fall back to .html
848
869
  const photonHtmlPath = path.join(photonDir, photonName, 'ui', `${uiId}.photon.html`);
870
+ const photonMdPath = path.join(photonDir, photonName, 'ui', `${uiId}.photon.md`);
849
871
  try {
850
872
  await fs.access(photonHtmlPath);
851
873
  uiPath = photonHtmlPath;
852
874
  }
853
875
  catch {
854
- uiPath = path.join(photonDir, photonName, 'ui', `${uiId}.html`);
876
+ try {
877
+ await fs.access(photonMdPath);
878
+ uiPath = photonMdPath;
879
+ }
880
+ catch {
881
+ uiPath = path.join(photonDir, photonName, 'ui', `${uiId}.html`);
882
+ }
855
883
  }
856
884
  }
857
- const isPhotonTemplate = uiPath.endsWith('.photon.html');
885
+ const isPhotonTemplate = uiPath.endsWith('.photon.html') || uiPath.endsWith('.photon.md');
858
886
  try {
859
- const content = await fs.readFile(uiPath, 'utf-8');
887
+ const content = await readText(uiPath);
860
888
  return { content, isPhotonTemplate };
861
889
  }
862
890
  catch {
@@ -868,7 +896,7 @@ export async function startBeam(rawWorkingDir, port) {
868
896
  const formatName = uiId.slice('format-'.length);
869
897
  const formatPath = path.join(photonDir, photonName, 'assets', 'formats', `${formatName}.html`);
870
898
  try {
871
- const content = await fs.readFile(formatPath, 'utf-8');
899
+ const content = await readText(formatPath);
872
900
  return { content, isPhotonTemplate: false };
873
901
  }
874
902
  catch {
@@ -1019,7 +1047,12 @@ export async function startBeam(rawWorkingDir, port) {
1019
1047
  return configurePhotonViaMCP(photonName, config, photons, photonMCPs, loader, savedConfig, workingDir, activeLoads);
1020
1048
  },
1021
1049
  reloadPhoton: async (photonName) => {
1022
- return reloadPhotonViaMCP(photonName, photons, photonMCPs, loader, savedConfig, broadcastPhotonChange, activeLoads);
1050
+ return reloadPhotonViaMCP(photonName, photons, photonMCPs, loader, savedConfig, broadcastPhotonChange, activeLoads, (name, path, isStateful) => {
1051
+ if (isStateful) {
1052
+ subscribeStatefulPhoton(name).catch(() => { });
1053
+ reloadDaemonPhoton(name, path, workingDir).catch(() => { });
1054
+ }
1055
+ });
1023
1056
  },
1024
1057
  removePhoton: async (photonName) => {
1025
1058
  return removePhotonViaMCP(photonName, photons, photonMCPs, savedConfig, broadcastPhotonChange, workingDir);
@@ -1190,7 +1223,7 @@ export async function startBeam(rawWorkingDir, port) {
1190
1223
  if (url.pathname === '/beam.bundle.js') {
1191
1224
  try {
1192
1225
  const bundlePath = path.join(__dirname, '../../dist/beam.bundle.js');
1193
- const content = await fs.readFile(bundlePath, 'utf-8');
1226
+ const content = await readText(bundlePath);
1194
1227
  res.writeHead(200, {
1195
1228
  'Content-Type': 'text/javascript',
1196
1229
  'Cache-Control': 'no-cache',
@@ -1199,7 +1232,7 @@ export async function startBeam(rawWorkingDir, port) {
1199
1232
  }
1200
1233
  catch {
1201
1234
  res.writeHead(404);
1202
- res.end('Bundle not found. Run npm run build:beam first.');
1235
+ res.end('Bundle not found. Run the beam build script first.');
1203
1236
  }
1204
1237
  return;
1205
1238
  }
@@ -1207,7 +1240,7 @@ export async function startBeam(rawWorkingDir, port) {
1207
1240
  if (url.pathname === '/beam-form.bundle.js') {
1208
1241
  try {
1209
1242
  const formBundlePath = path.join(__dirname, '../../dist/beam-form.bundle.js');
1210
- const content = await fs.readFile(formBundlePath, 'utf-8');
1243
+ const content = await readText(formBundlePath);
1211
1244
  res.writeHead(200, {
1212
1245
  'Content-Type': 'text/javascript',
1213
1246
  'Cache-Control': 'no-cache',
@@ -1216,14 +1249,14 @@ export async function startBeam(rawWorkingDir, port) {
1216
1249
  }
1217
1250
  catch {
1218
1251
  res.writeHead(404);
1219
- res.end('Form bundle not found. Run npm run build:beam first.');
1252
+ res.end('Form bundle not found. Run the beam build script first.');
1220
1253
  }
1221
1254
  return;
1222
1255
  }
1223
1256
  if (url.pathname === '/beam-ts-worker.js') {
1224
1257
  try {
1225
1258
  const workerPath = path.join(__dirname, '../../dist/beam-ts-worker.js');
1226
- const content = await fs.readFile(workerPath, 'utf-8');
1259
+ const content = await readText(workerPath);
1227
1260
  res.writeHead(200, {
1228
1261
  'Content-Type': 'text/javascript',
1229
1262
  'Cache-Control': 'no-cache',
@@ -1232,7 +1265,7 @@ export async function startBeam(rawWorkingDir, port) {
1232
1265
  }
1233
1266
  catch {
1234
1267
  res.writeHead(404);
1235
- res.end('TS worker not found. Run npm run build:beam first.');
1268
+ res.end('TS worker not found. Run the beam build script first.');
1236
1269
  }
1237
1270
  return;
1238
1271
  }
@@ -1696,7 +1729,7 @@ export async function startBeam(rawWorkingDir, port) {
1696
1729
  params[k] = v;
1697
1730
  }
1698
1731
  const pureViewPath = path.join(__dirname, 'frontend/pure-view.html');
1699
- let html = await fs.readFile(pureViewPath, 'utf-8');
1732
+ let html = await readText(pureViewPath);
1700
1733
  // Replace placeholders
1701
1734
  const argsJson = escapeHtml(JSON.stringify(params));
1702
1735
  html = html
@@ -1720,7 +1753,7 @@ export async function startBeam(rawWorkingDir, port) {
1720
1753
  if (url.pathname === '/' || !url.pathname.startsWith('/api')) {
1721
1754
  try {
1722
1755
  const indexPath = path.join(__dirname, 'frontend/index.html');
1723
- let content = await fs.readFile(indexPath, 'utf-8');
1756
+ let content = await readText(indexPath);
1724
1757
  // Inject shell integration flag so frontend can strip CLI prefix
1725
1758
  if (_shellIntegrationInstalled) {
1726
1759
  content = content.replace('</head>', '<script>window.__PHOTON_SHELL_INIT=true</script></head>');
@@ -1905,14 +1938,14 @@ export async function startBeam(rawWorkingDir, port) {
1905
1938
  // Auto-scaffold empty photon files with a starter template
1906
1939
  if (isNewPhoton) {
1907
1940
  try {
1908
- const rawContent = await fs.readFile(photonPath, 'utf-8');
1941
+ const rawContent = await readText(photonPath);
1909
1942
  if (rawContent.trim().length === 0) {
1910
1943
  const className = photonName
1911
1944
  .split(/[-_]/)
1912
1945
  .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1913
1946
  .join('');
1914
1947
  const scaffold = `/**\n * ${className} Photon\n */\n\nexport default class ${className} {\n /**\n * Example tool\n * @param message Message to echo\n */\n async echo(params: { message: string }) {\n return \`Echo: \${params.message}\`;\n }\n}\n`;
1915
- await fs.writeFile(photonPath, scaffold, 'utf-8');
1948
+ await writeText(photonPath, scaffold);
1916
1949
  logger.info(`📝 Scaffolded empty file: ${photonName}.photon.ts`);
1917
1950
  // The write triggers another watcher event which will load the scaffolded photon
1918
1951
  return;
@@ -1927,7 +1960,7 @@ export async function startBeam(rawWorkingDir, port) {
1927
1960
  const extractor = new SchemaExtractor();
1928
1961
  let constructorParams = [];
1929
1962
  try {
1930
- const source = await fs.readFile(photonPath, 'utf-8');
1963
+ const source = await readText(photonPath);
1931
1964
  await writePhotonEditorDeclaration(photonPath, source, workingDir).catch(() => { });
1932
1965
  const params = extractor.extractConstructorParams(source);
1933
1966
  constructorParams = params
@@ -1975,7 +2008,7 @@ export async function startBeam(rawWorkingDir, port) {
1975
2008
  photonMCPs.set(photonName, mcp);
1976
2009
  // Re-extract schema - use extractAllFromSource to get both tools and templates
1977
2010
  const extractor = new SchemaExtractor();
1978
- const reloadSource = await fs.readFile(photonPath, 'utf-8');
2011
+ const reloadSource = await readText(photonPath);
1979
2012
  await writePhotonEditorDeclaration(photonPath, reloadSource, workingDir).catch(() => { });
1980
2013
  const reloadMetadata = extractor.extractAllFromSource(reloadSource);
1981
2014
  const schemas = reloadMetadata.tools;
@@ -2153,7 +2186,7 @@ export async function startBeam(rawWorkingDir, port) {
2153
2186
  const extractor = new SchemaExtractor();
2154
2187
  let constructorParams = [];
2155
2188
  try {
2156
- const source = await fs.readFile(photonPath, 'utf-8');
2189
+ const source = await readText(photonPath);
2157
2190
  const params = extractor.extractConstructorParams(source);
2158
2191
  constructorParams = params
2159
2192
  .filter((p) => p.isPrimitive)
@@ -2340,18 +2373,9 @@ export async function startBeam(rawWorkingDir, port) {
2340
2373
  }
2341
2374
  }
2342
2375
  }
2343
- configuredCount = photons.filter((p) => p.configured).length;
2344
- unconfiguredCount = photons.filter((p) => !p.configured).length;
2345
2376
  // Load external MCPs from config
2346
2377
  const externalMCPList = await loadExternalMCPs(savedConfig);
2347
2378
  externalMCPs.push(...externalMCPList);
2348
- const connectedMCPs = externalMCPList.filter((m) => m.connected).length;
2349
- const failedMCPs = externalMCPList.length - connectedMCPs;
2350
- const photonStatus = unconfiguredCount > 0
2351
- ? `${configuredCount} ready, ${unconfiguredCount} need setup`
2352
- : `${configuredCount} photon${configuredCount !== 1 ? 's' : ''} ready`;
2353
- const mcpStatus = externalMCPList.length > 0 ? `, ${connectedMCPs}/${externalMCPList.length} MCPs` : '';
2354
- const url = `http://localhost:${process.env.BEAM_PORT || port}`;
2355
2379
  // Mark startup complete — flushes queued output and restores console
2356
2380
  startup.ready();
2357
2381
  // Notify connected clients that photon list is now available
@@ -2376,8 +2400,7 @@ export async function startBeam(rawWorkingDir, port) {
2376
2400
  const instName = instanceName || 'default';
2377
2401
  const stateFile = path.join(workingDir, 'state', photonName, `${instName}.json`);
2378
2402
  try {
2379
- const json = await fs.readFile(stateFile, 'utf-8');
2380
- const state = JSON.parse(json);
2403
+ const state = await readJSONFile(stateFile);
2381
2404
  for (const [key, value] of Object.entries(state)) {
2382
2405
  const target = mcp.instance[key];
2383
2406
  if (target && typeof target.splice === 'function' && Array.isArray(value)) {
@@ -2619,8 +2642,8 @@ export async function startBeam(rawWorkingDir, port) {
2619
2642
  configDebounce = null;
2620
2643
  let newConfig;
2621
2644
  try {
2622
- const data = await fs.readFile(configFile, 'utf-8');
2623
- newConfig = migrateConfig(JSON.parse(data));
2645
+ const data = await readJSONFile(configFile);
2646
+ newConfig = migrateConfig(data);
2624
2647
  }
2625
2648
  catch (err) {
2626
2649
  logger.warn(`⚠️ Failed to parse config.json: ${err instanceof Error ? err.message : String(err)}`);
@@ -2654,7 +2677,7 @@ export async function startBeam(rawWorkingDir, port) {
2654
2677
  logger.info(`🔌 Removed external MCP: ${name}`);
2655
2678
  }
2656
2679
  // Close SDK clients after all Maps are consistent
2657
- for (const { name, client } of removedSdkClients) {
2680
+ for (const { name: _name, client } of removedSdkClients) {
2658
2681
  try {
2659
2682
  await client.close();
2660
2683
  }