@portel/photon 1.28.2 → 1.31.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.
- package/README.md +42 -11
- package/dist/asset-resolver.d.ts +44 -0
- package/dist/asset-resolver.d.ts.map +1 -0
- package/dist/asset-resolver.js +105 -0
- package/dist/asset-resolver.js.map +1 -0
- package/dist/auto-ui/beam/external-mcp-manager.d.ts +73 -0
- package/dist/auto-ui/beam/external-mcp-manager.d.ts.map +1 -0
- package/dist/auto-ui/beam/external-mcp-manager.js +65 -0
- package/dist/auto-ui/beam/external-mcp-manager.js.map +1 -0
- package/dist/auto-ui/beam/external-mcp.d.ts.map +1 -1
- package/dist/auto-ui/beam/external-mcp.js +25 -1
- package/dist/auto-ui/beam/external-mcp.js.map +1 -1
- package/dist/auto-ui/beam/photon-management.d.ts.map +1 -1
- package/dist/auto-ui/beam/photon-management.js +11 -8
- package/dist/auto-ui/beam/photon-management.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.js +7 -4
- package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-config.js +3 -2
- package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-marketplace.js +6 -2
- package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
- package/dist/auto-ui/beam/startup.js.map +1 -1
- package/dist/auto-ui/beam/types.d.ts +5 -2
- package/dist/auto-ui/beam/types.d.ts.map +1 -1
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +239 -88
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/bridge/index.d.ts.map +1 -1
- package/dist/auto-ui/bridge/index.js +11 -0
- package/dist/auto-ui/bridge/index.js.map +1 -1
- package/dist/auto-ui/bridge/types.d.ts +2 -0
- package/dist/auto-ui/bridge/types.d.ts.map +1 -1
- package/dist/auto-ui/openapi-generator.js +1 -4
- package/dist/auto-ui/openapi-generator.js.map +1 -1
- package/dist/auto-ui/photon-bridge.d.ts +4 -0
- package/dist/auto-ui/photon-bridge.d.ts.map +1 -1
- package/dist/auto-ui/photon-bridge.js.map +1 -1
- package/dist/auto-ui/photon-host.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +7 -0
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +252 -43
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +24 -2
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +202 -24
- package/dist/beam.bundle.js.map +3 -3
- package/dist/capability-negotiator.d.ts +39 -1
- package/dist/capability-negotiator.d.ts.map +1 -1
- package/dist/capability-negotiator.js +5 -0
- package/dist/capability-negotiator.js.map +1 -1
- package/dist/cf-bindings-parser.d.ts +15 -0
- package/dist/cf-bindings-parser.d.ts.map +1 -0
- package/dist/cf-bindings-parser.js +98 -0
- package/dist/cf-bindings-parser.js.map +1 -0
- package/dist/cf-usage-scanner.d.ts +76 -0
- package/dist/cf-usage-scanner.d.ts.map +1 -0
- package/dist/cf-usage-scanner.js +179 -0
- package/dist/cf-usage-scanner.js.map +1 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +124 -16
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/cf.d.ts +18 -0
- package/dist/cli/commands/cf.d.ts.map +1 -0
- package/dist/cli/commands/cf.js +207 -0
- package/dist/cli/commands/cf.js.map +1 -0
- package/dist/cli/commands/info.js +1 -1
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +59 -46
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +3 -0
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +43 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +40 -33
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +6 -2
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +75 -20
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/server.js +69 -11
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/worker-host.js.map +1 -1
- package/dist/deploy/cloudflare.d.ts +27 -0
- package/dist/deploy/cloudflare.d.ts.map +1 -1
- package/dist/deploy/cloudflare.js +210 -3
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/editor-support/docblock-tag-catalog.d.ts.map +1 -1
- package/dist/editor-support/docblock-tag-catalog.js +32 -2
- package/dist/editor-support/docblock-tag-catalog.js.map +1 -1
- package/dist/embedded-runtime.js.map +1 -1
- package/dist/format/registry.d.ts +83 -0
- package/dist/format/registry.d.ts.map +1 -0
- package/dist/format/registry.js +139 -0
- package/dist/format/registry.js.map +1 -0
- package/dist/format/seed.d.ts +18 -0
- package/dist/format/seed.d.ts.map +1 -0
- package/dist/format/seed.js +246 -0
- package/dist/format/seed.js.map +1 -0
- package/dist/loader.d.ts +61 -66
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +315 -327
- package/dist/loader.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +20 -11
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photons/maker.photon.d.ts +2 -2
- package/dist/photons/maker.photon.d.ts.map +1 -1
- package/dist/photons/maker.photon.js +5 -6
- package/dist/photons/maker.photon.js.map +1 -1
- package/dist/photons/maker.photon.ts +5 -6
- package/dist/resource-server.d.ts +55 -15
- package/dist/resource-server.d.ts.map +1 -1
- package/dist/resource-server.js +205 -50
- package/dist/resource-server.js.map +1 -1
- package/dist/runtime/cf-local.d.ts +157 -0
- package/dist/runtime/cf-local.d.ts.map +1 -0
- package/dist/runtime/cf-local.js +406 -0
- package/dist/runtime/cf-local.js.map +1 -0
- package/dist/server.d.ts +117 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +681 -67
- package/dist/server.js.map +1 -1
- package/dist/settings-persistence.d.ts +50 -0
- package/dist/settings-persistence.d.ts.map +1 -0
- package/dist/settings-persistence.js +188 -0
- package/dist/settings-persistence.js.map +1 -0
- package/dist/shared/asset-encoding.d.ts +30 -0
- package/dist/shared/asset-encoding.d.ts.map +1 -0
- package/dist/shared/asset-encoding.js +0 -0
- package/dist/shared/asset-encoding.js.map +1 -0
- package/dist/shared/audit-sqlite.d.ts.map +1 -1
- package/dist/shared/audit-sqlite.js +0 -1
- package/dist/shared/audit-sqlite.js.map +1 -1
- package/dist/shared/cross-origin-headers.d.ts +47 -0
- package/dist/shared/cross-origin-headers.d.ts.map +1 -0
- package/dist/shared/cross-origin-headers.js +61 -0
- package/dist/shared/cross-origin-headers.js.map +1 -0
- package/dist/shared/error-handler.d.ts.map +1 -1
- package/dist/shared/error-handler.js +3 -1
- package/dist/shared/error-handler.js.map +1 -1
- package/dist/shared/expose-route-extractor.d.ts +36 -0
- package/dist/shared/expose-route-extractor.d.ts.map +1 -0
- package/dist/shared/expose-route-extractor.js +64 -0
- package/dist/shared/expose-route-extractor.js.map +1 -0
- package/dist/shared/extract-claims.d.ts +33 -0
- package/dist/shared/extract-claims.d.ts.map +1 -0
- package/dist/shared/extract-claims.js +60 -0
- package/dist/shared/extract-claims.js.map +1 -0
- package/dist/shared/http-route-extractor.d.ts +6 -0
- package/dist/shared/http-route-extractor.d.ts.map +1 -1
- package/dist/shared/http-route-extractor.js +29 -5
- package/dist/shared/http-route-extractor.js.map +1 -1
- package/dist/shared/instance-binding.d.ts +53 -0
- package/dist/shared/instance-binding.d.ts.map +1 -0
- package/dist/shared/instance-binding.js +85 -0
- package/dist/shared/instance-binding.js.map +1 -0
- package/dist/shared/io.d.ts.map +1 -1
- package/dist/shared/io.js +5 -2
- package/dist/shared/io.js.map +1 -1
- package/dist/shared/logger.js.map +1 -1
- package/dist/shared/sqlite-runtime.d.ts.map +1 -1
- package/dist/shared/sqlite-runtime.js +0 -1
- package/dist/shared/sqlite-runtime.js.map +1 -1
- package/dist/task-executor.js.map +1 -1
- package/dist/telemetry/sdk.d.ts.map +1 -1
- package/dist/telemetry/sdk.js +0 -1
- package/dist/telemetry/sdk.js.map +1 -1
- package/dist/test-runner.d.ts.map +1 -1
- package/dist/test-runner.js.map +1 -1
- package/dist/types/server-types.d.ts +16 -7
- package/dist/types/server-types.d.ts.map +1 -1
- package/package.json +14 -4
- package/templates/cloudflare/worker.ts.template +428 -14
- package/templates/cloudflare/wrangler.toml.template +2 -7
- package/templates/photon.template.ts +13 -0
package/dist/auto-ui/beam.js
CHANGED
|
@@ -102,7 +102,7 @@ import { ensureDaemon } from '../daemon/manager.js';
|
|
|
102
102
|
import { SchemaExtractor } from '@portel/photon-core';
|
|
103
103
|
import { generateServerCard } from '../server-card.js';
|
|
104
104
|
import { resolveUIAssetPath, readUIContent } from './ui-resolver.js';
|
|
105
|
-
import { handleStreamableHTTP, broadcastNotification, broadcastToBeam, stopSessionCleanup, } from './streamable-http-transport.js';
|
|
105
|
+
import { handleStreamableHTTP, broadcastNotification, broadcastToBeam, stopSessionCleanup, attachLoaderForResourceUpdates, } from './streamable-http-transport.js';
|
|
106
106
|
import { getBundledPhotonPath, BEAM_BUNDLED_PHOTONS } from '../shared-utils.js';
|
|
107
107
|
// BUNDLED_PHOTONS and getBundledPhotonPath are imported from shared-utils.js
|
|
108
108
|
// Extracted modules (Phase 5)
|
|
@@ -110,6 +110,7 @@ import { loadConfig as loadConfigFromModule, saveConfig as saveConfigFromModule,
|
|
|
110
110
|
import { extractClassMetadataFromSource as extractClassMetadataFromModule, applyMethodVisibility as applyMethodVisibilityFromModule, extractCspFromSource as extractCspFromModule, prettifyName as prettifyNameFromModule, backfillEnvDefaults as backfillEnvDefaultsFromModule, } from './beam/class-metadata.js';
|
|
111
111
|
import { StartupSequencer } from './beam/startup.js';
|
|
112
112
|
import { SubscriptionManager } from './beam/subscription.js';
|
|
113
|
+
import { ExternalMCPManager } from './beam/external-mcp-manager.js';
|
|
113
114
|
import { handleMarketplaceRoutes } from './beam/routes/api-marketplace.js';
|
|
114
115
|
import { handleBrowseRoutes } from './beam/routes/api-browse.js';
|
|
115
116
|
import { handleConfigRoutes } from './beam/routes/api-config.js';
|
|
@@ -123,12 +124,8 @@ const getConfigFilePath = getConfigFilePathFromModule;
|
|
|
123
124
|
// BEAM CONTEXT — all module-level mutable state lives here
|
|
124
125
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
125
126
|
class BeamContext {
|
|
126
|
-
/** External MCP
|
|
127
|
-
|
|
128
|
-
/** Transport-level clients for external MCPs */
|
|
129
|
-
externalMCPClients = new Map();
|
|
130
|
-
/** SDK Client instances for tool calls with structuredContent */
|
|
131
|
-
externalMCPSDKClients = new Map();
|
|
127
|
+
/** External MCP lifecycle: list, transport clients, SDK clients, add/remove. */
|
|
128
|
+
mcp = new ExternalMCPManager();
|
|
132
129
|
/**
|
|
133
130
|
* Notification subscriptions per photon.
|
|
134
131
|
* Key: photon name, Value: list of event types this photon cares about
|
|
@@ -140,12 +137,24 @@ class BeamContext {
|
|
|
140
137
|
* so dynamically discovered photons can be subscribed without duplicates.
|
|
141
138
|
*/
|
|
142
139
|
subscribedStateChannels = new Set();
|
|
140
|
+
// Backward-compat field-name accessors. The 24 callsites that read
|
|
141
|
+
// ctx.externalMCPs / ctx.externalMCPClients / ctx.externalMCPSDKClients
|
|
142
|
+
// continue to work; they now flow through the manager.
|
|
143
|
+
get externalMCPs() {
|
|
144
|
+
return this.mcp.externalMCPs;
|
|
145
|
+
}
|
|
146
|
+
get externalMCPClients() {
|
|
147
|
+
return this.mcp.externalMCPClients;
|
|
148
|
+
}
|
|
149
|
+
get externalMCPSDKClients() {
|
|
150
|
+
return this.mcp.externalMCPSDKClients;
|
|
151
|
+
}
|
|
143
152
|
/** Convenience accessor matching the shape expected by external-mcp module */
|
|
144
153
|
get externalMCPState() {
|
|
145
154
|
return {
|
|
146
|
-
externalMCPs: this.externalMCPs,
|
|
147
|
-
externalMCPClients: this.externalMCPClients,
|
|
148
|
-
externalMCPSDKClients: this.externalMCPSDKClients,
|
|
155
|
+
externalMCPs: this.mcp.externalMCPs,
|
|
156
|
+
externalMCPClients: this.mcp.externalMCPClients,
|
|
157
|
+
externalMCPSDKClients: this.mcp.externalMCPSDKClients,
|
|
149
158
|
};
|
|
150
159
|
}
|
|
151
160
|
}
|
|
@@ -598,6 +607,9 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
598
607
|
// Beam handles config errors gracefully via UI forms, but we still want to see actual errors
|
|
599
608
|
const errorOnlyLogger = createLogger({ level: 'error' });
|
|
600
609
|
const loader = new PhotonLoader(false, errorOnlyLogger, workingDir);
|
|
610
|
+
// Bridge `this.notifyResourceUpdated(uri)` from photon authors into the
|
|
611
|
+
// streamable-HTTP subscription registry so SSE clients get fanout.
|
|
612
|
+
attachLoaderForResourceUpdates(loader);
|
|
601
613
|
// Check for placeholder defaults or localhost URLs (which need local services running)
|
|
602
614
|
const isPlaceholderOrLocalDefault = (value) => {
|
|
603
615
|
if (value.includes('<') || value.includes('your-'))
|
|
@@ -750,7 +762,10 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
750
762
|
// Check if method has @ui tag matching this id
|
|
751
763
|
const methodSource = schemaSource.match(new RegExp(`@ui\\s+${uiId}[\\s\\n]*\\*/[\\s\\n]*(?:async\\s+)?${schema.name}\\s*\\(`, 'm'));
|
|
752
764
|
if (methodSource) {
|
|
753
|
-
|
|
765
|
+
// Synthetic record for sidebar linking — `path` isn't available
|
|
766
|
+
// here (source-only inference), but downstream readers only
|
|
767
|
+
// touch id/linkedTool. Empty string keeps the UIAsset contract.
|
|
768
|
+
uiAssets.push({ id: uiId, path: '', linkedTool: schema.name });
|
|
754
769
|
}
|
|
755
770
|
});
|
|
756
771
|
}
|
|
@@ -833,7 +848,6 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
833
848
|
const mainMethod = methods.find((m) => m.name === 'main');
|
|
834
849
|
// Extract class-level metadata — reuse source already read
|
|
835
850
|
const classMetadata = extractClassMetadataFromSource(schemaSource);
|
|
836
|
-
// Extract class-level @csp metadata and apply to all UI assets
|
|
837
851
|
const cspData = extractCspFromSource(schemaSource);
|
|
838
852
|
if (cspData['__class__'] && mcp.assets?.ui) {
|
|
839
853
|
for (const uiAsset of mcp.assets.ui) {
|
|
@@ -934,7 +948,8 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
934
948
|
return null;
|
|
935
949
|
const photonDir = path.dirname(photon.path);
|
|
936
950
|
const photonBaseName = path.basename(photon.path, '.photon.ts');
|
|
937
|
-
|
|
951
|
+
// assets only live on configured photons
|
|
952
|
+
const asset = photon.configured ? photon.assets?.ui?.find((u) => u.id === uiId) : undefined;
|
|
938
953
|
let resolved;
|
|
939
954
|
if (asset?.resolvedPath) {
|
|
940
955
|
resolved = {
|
|
@@ -1194,54 +1209,85 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1194
1209
|
res.end('Photon not found');
|
|
1195
1210
|
return;
|
|
1196
1211
|
}
|
|
1197
|
-
//
|
|
1198
|
-
//
|
|
1199
|
-
// {photonDir}/{photonBaseName}/
|
|
1212
|
+
// Dual-layout asset resolution (v1.29 Track E):
|
|
1213
|
+
// 1. {photonDir}/{photonBaseName}/assets/{assetPath} — canonical
|
|
1214
|
+
// 2. {photonDir}/{photonBaseName}/{assetPath} — legacy
|
|
1215
|
+
// The new convention wins; legacy only fires when nothing matches
|
|
1216
|
+
// under assets/ (preserves byte-compat for fixtures without an
|
|
1217
|
+
// assets/ wrapper).
|
|
1200
1218
|
const realPath = realpathSync(photon.path);
|
|
1201
1219
|
const photonDir = path.dirname(realPath);
|
|
1202
1220
|
const baseName = path.basename(realPath).replace(/\.photon\.(ts|js)$/, '');
|
|
1203
|
-
const
|
|
1204
|
-
const
|
|
1205
|
-
const
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
});
|
|
1239
|
-
res.end(data);
|
|
1240
|
-
}
|
|
1241
|
-
catch {
|
|
1221
|
+
const legacyRoot = path.join(photonDir, baseName);
|
|
1222
|
+
const canonicalRoot = path.join(legacyRoot, 'assets');
|
|
1223
|
+
const candidateRoots = [canonicalRoot, legacyRoot];
|
|
1224
|
+
const tryRoots = async (subPath) => {
|
|
1225
|
+
for (const root of candidateRoots) {
|
|
1226
|
+
const candidate = path.join(root, subPath);
|
|
1227
|
+
const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;
|
|
1228
|
+
if (!candidate.startsWith(rootWithSep) && candidate !== root) {
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
try {
|
|
1232
|
+
const stat = await fs.stat(candidate);
|
|
1233
|
+
if (stat.isDirectory()) {
|
|
1234
|
+
// Directory-style serve: if the dir contains index.html,
|
|
1235
|
+
// serve it. Otherwise fall through to the next root.
|
|
1236
|
+
const indexCandidate = path.join(candidate, 'index.html');
|
|
1237
|
+
try {
|
|
1238
|
+
const data = await fs.readFile(indexCandidate);
|
|
1239
|
+
return { data, resolved: indexCandidate };
|
|
1240
|
+
}
|
|
1241
|
+
catch {
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const data = await fs.readFile(candidate);
|
|
1246
|
+
return { data, resolved: candidate };
|
|
1247
|
+
}
|
|
1248
|
+
catch {
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return null;
|
|
1253
|
+
};
|
|
1254
|
+
const found = await tryRoots(assetPath);
|
|
1255
|
+
if (!found) {
|
|
1242
1256
|
res.writeHead(404);
|
|
1243
1257
|
res.end('Asset not found');
|
|
1258
|
+
return;
|
|
1244
1259
|
}
|
|
1260
|
+
const ext = path.extname(found.resolved).toLowerCase();
|
|
1261
|
+
const mimeTypes = {
|
|
1262
|
+
'.html': 'text/html; charset=utf-8',
|
|
1263
|
+
'.png': 'image/png',
|
|
1264
|
+
'.jpg': 'image/jpeg',
|
|
1265
|
+
'.jpeg': 'image/jpeg',
|
|
1266
|
+
'.gif': 'image/gif',
|
|
1267
|
+
'.svg': 'image/svg+xml',
|
|
1268
|
+
'.webp': 'image/webp',
|
|
1269
|
+
'.ico': 'image/x-icon',
|
|
1270
|
+
'.mp4': 'video/mp4',
|
|
1271
|
+
'.webm': 'video/webm',
|
|
1272
|
+
'.pdf': 'application/pdf',
|
|
1273
|
+
'.woff2': 'font/woff2',
|
|
1274
|
+
'.woff': 'font/woff',
|
|
1275
|
+
'.ttf': 'font/ttf',
|
|
1276
|
+
'.css': 'text/css',
|
|
1277
|
+
'.js': 'text/javascript',
|
|
1278
|
+
'.mjs': 'text/javascript',
|
|
1279
|
+
'.json': 'application/json',
|
|
1280
|
+
'.txt': 'text/plain',
|
|
1281
|
+
'.wasm': 'application/wasm',
|
|
1282
|
+
};
|
|
1283
|
+
res.writeHead(200, {
|
|
1284
|
+
'Content-Type': mimeTypes[ext] || 'application/octet-stream',
|
|
1285
|
+
'Cache-Control': 'public, max-age=3600',
|
|
1286
|
+
// Track D2: CORP same-origin keeps assets fetchable from a
|
|
1287
|
+
// standalone parent page that asserts COEP `require-corp`.
|
|
1288
|
+
'Cross-Origin-Resource-Policy': 'same-origin',
|
|
1289
|
+
});
|
|
1290
|
+
res.end(found.data);
|
|
1245
1291
|
return;
|
|
1246
1292
|
}
|
|
1247
1293
|
// ══════════════════════════════════════════════════════════════════════════
|
|
@@ -1353,10 +1399,12 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1353
1399
|
res.end(`Photon not found: ${photonName}`);
|
|
1354
1400
|
return;
|
|
1355
1401
|
}
|
|
1356
|
-
const label = photon
|
|
1402
|
+
const label = photon.label ||
|
|
1357
1403
|
photonName.charAt(0).toUpperCase() + photonName.slice(1).replace(/-/g, ' ');
|
|
1358
|
-
|
|
1359
|
-
|
|
1404
|
+
// `description` and `icon` only live on configured photons; an unconfigured
|
|
1405
|
+
// entry falls back to the auto-built defaults below.
|
|
1406
|
+
const description = (photon.configured && photon.description) || `${label} - Photon App`;
|
|
1407
|
+
const iconValue = (photon.configured && photon.icon) || '📦';
|
|
1360
1408
|
const encodedName = encodeURIComponent(photonName);
|
|
1361
1409
|
// Sanitize strings for safe embedding in HTML
|
|
1362
1410
|
const safeLabel = label.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] || c);
|
|
@@ -1840,6 +1888,94 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1840
1888
|
}
|
|
1841
1889
|
return;
|
|
1842
1890
|
}
|
|
1891
|
+
// Web route proxy: /web/{photonName}/{...path} dispatches to the photon's
|
|
1892
|
+
// @get/@post handlers. The prefix is stripped before dispatch so that the
|
|
1893
|
+
// photon's routes look like they're running at the root. HTML responses
|
|
1894
|
+
// get a fetch interceptor injected so that absolute fetch('/api/foo')
|
|
1895
|
+
// calls inside the photon's UI are transparently rewritten to
|
|
1896
|
+
// /web/{photonName}/api/foo by the browser.
|
|
1897
|
+
if (url.pathname.startsWith('/web/')) {
|
|
1898
|
+
const [, , photonName, ...pathParts] = url.pathname.split('/');
|
|
1899
|
+
const photonPath = '/' + pathParts.join('/') || '/';
|
|
1900
|
+
const photonClass = photonName ? photonMCPs.get(photonName) : undefined;
|
|
1901
|
+
const httpRoutes = photonClass?._httpRoutes;
|
|
1902
|
+
const route = httpRoutes?.find((r) => r.method === (req.method || 'GET') && r.path === photonPath);
|
|
1903
|
+
if (route && photonClass?.instance) {
|
|
1904
|
+
const fn = photonClass.instance[route.handler];
|
|
1905
|
+
if (typeof fn === 'function') {
|
|
1906
|
+
try {
|
|
1907
|
+
let bodyBuffer = Buffer.alloc(0);
|
|
1908
|
+
await new Promise((resolve) => {
|
|
1909
|
+
req.on('data', (chunk) => {
|
|
1910
|
+
bodyBuffer = Buffer.concat([bodyBuffer, chunk]);
|
|
1911
|
+
});
|
|
1912
|
+
req.on('end', resolve);
|
|
1913
|
+
});
|
|
1914
|
+
const internalUrl = new URL(photonPath + (url.search || ''), `http://${req.headers.host || 'localhost'}`);
|
|
1915
|
+
const webReq = new Request(internalUrl.toString(), {
|
|
1916
|
+
method: req.method,
|
|
1917
|
+
headers: req.headers,
|
|
1918
|
+
...(req.method !== 'GET' && bodyBuffer.length > 0 ? { body: bodyBuffer } : {}),
|
|
1919
|
+
});
|
|
1920
|
+
const result = await fn.call(photonClass.instance, webReq);
|
|
1921
|
+
if (result instanceof Response) {
|
|
1922
|
+
const contentType = result.headers.get('content-type') || '';
|
|
1923
|
+
const responseHeaders = {};
|
|
1924
|
+
result.headers.forEach((value, key) => {
|
|
1925
|
+
responseHeaders[key] = value;
|
|
1926
|
+
});
|
|
1927
|
+
let body = Buffer.from(await result.arrayBuffer());
|
|
1928
|
+
// Inject fetch interceptor into HTML responses so relative API
|
|
1929
|
+
// calls inside the photon UI resolve via the /web/ prefix.
|
|
1930
|
+
if (contentType.includes('text/html')) {
|
|
1931
|
+
const prefix = `/web/${photonName}`;
|
|
1932
|
+
const interceptor = `<script>
|
|
1933
|
+
(function(){
|
|
1934
|
+
const _prefix="${prefix}";
|
|
1935
|
+
const _origFetch=window.fetch;
|
|
1936
|
+
window.fetch=function(input,init){
|
|
1937
|
+
if(typeof input==='string'&&input.startsWith('/')&&!input.startsWith(_prefix))
|
|
1938
|
+
input=_prefix+input;
|
|
1939
|
+
return _origFetch(input,init);
|
|
1940
|
+
};
|
|
1941
|
+
const _origOpen=XMLHttpRequest.prototype.open;
|
|
1942
|
+
XMLHttpRequest.prototype.open=function(m,u,...a){
|
|
1943
|
+
if(typeof u==='string'&&u.startsWith('/')&&!u.startsWith(_prefix))u=_prefix+u;
|
|
1944
|
+
return _origOpen.call(this,m,u,...a);
|
|
1945
|
+
};
|
|
1946
|
+
})();
|
|
1947
|
+
</script>`;
|
|
1948
|
+
const bodyStr = body.toString('utf-8').replace('<head>', '<head>' + interceptor);
|
|
1949
|
+
body = Buffer.from(bodyStr, 'utf-8');
|
|
1950
|
+
responseHeaders['content-length'] = String(body.length);
|
|
1951
|
+
}
|
|
1952
|
+
res.writeHead(result.status, responseHeaders);
|
|
1953
|
+
res.end(body);
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
// Plain value — negotiate content type and render
|
|
1957
|
+
const { negotiateAccept } = await import('../format/registry.js');
|
|
1958
|
+
const { getDefaultRegistry } = await import('../format/seed.js');
|
|
1959
|
+
const acceptHeader = req.headers['accept'];
|
|
1960
|
+
const rendered = negotiateAccept({
|
|
1961
|
+
accept: typeof acceptHeader === 'string' ? acceptHeader : undefined,
|
|
1962
|
+
declaredFormat: route.format,
|
|
1963
|
+
value: result,
|
|
1964
|
+
registry: getDefaultRegistry(),
|
|
1965
|
+
});
|
|
1966
|
+
res.writeHead(200, { 'Content-Type': rendered.mime });
|
|
1967
|
+
res.end(typeof rendered.body === 'string' ? rendered.body : Buffer.from(rendered.body));
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
catch (err) {
|
|
1971
|
+
res.writeHead(500).end(err?.message ?? 'Internal Server Error');
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
res.writeHead(404).end('Not Found');
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1843
1979
|
// Default route: Serve Lit App
|
|
1844
1980
|
if (url.pathname === '/' || !url.pathname.startsWith('/api')) {
|
|
1845
1981
|
try {
|
|
@@ -1877,6 +2013,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1877
2013
|
const activeLoads = new Set(); // Photons currently being loaded (prevents concurrent duplicate loads)
|
|
1878
2014
|
const pendingAfterLoad = new Set(); // File changes that arrived while a load was active; re-triggered after
|
|
1879
2015
|
const symlinkWatchedDirs = new Set(); // Track which source dirs already have watchers (prevents duplicates on re-setup)
|
|
2016
|
+
const autoRetried = new Set(); // Photons that have already had one auto-retry after a load failure
|
|
1880
2017
|
// Set up file watchers for a symlinked photon's real source directory and asset folder.
|
|
1881
2018
|
// Called both at startup and after a previously-errored symlinked photon recovers.
|
|
1882
2019
|
const setupSymlinkWatcher = (photonName, photonPath) => {
|
|
@@ -2135,12 +2272,13 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2135
2272
|
}
|
|
2136
2273
|
try {
|
|
2137
2274
|
// Load or reload the photon
|
|
2138
|
-
const mcp = isNewPhoton
|
|
2275
|
+
const mcp = (isNewPhoton
|
|
2139
2276
|
? await loader.loadFile(photonPath)
|
|
2140
|
-
: await loader.reloadFile(photonPath);
|
|
2277
|
+
: await loader.reloadFile(photonPath));
|
|
2141
2278
|
if (!mcp.instance)
|
|
2142
2279
|
throw new Error('Failed to create instance');
|
|
2143
2280
|
photonMCPs.set(photonName, mcp);
|
|
2281
|
+
autoRetried.delete(photonName); // Clear retry flag on success
|
|
2144
2282
|
// Re-extract schema - use extractAllFromSource to get both tools and templates
|
|
2145
2283
|
const extractor = new SchemaExtractor();
|
|
2146
2284
|
const reloadSource = await readText(photonPath);
|
|
@@ -2339,6 +2477,40 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2339
2477
|
}
|
|
2340
2478
|
return;
|
|
2341
2479
|
}
|
|
2480
|
+
// esbuild subprocess crash: the in-process service is permanently
|
|
2481
|
+
// dead — cache clears don't help. Respawn Beam so the next
|
|
2482
|
+
// file-save gets a fresh compiler. The browser's SSE reconnect
|
|
2483
|
+
// loop handles the brief disconnect transparently.
|
|
2484
|
+
const isServiceCrash = errorMsg.includes('The service was stopped') ||
|
|
2485
|
+
errorMsg.includes('The service is no longer running');
|
|
2486
|
+
if (isServiceCrash) {
|
|
2487
|
+
logger.warn(`Compiler service crashed — restarting Beam (${process.argv.slice(1).join(' ')})`);
|
|
2488
|
+
const { spawn } = await import('child_process');
|
|
2489
|
+
spawn(process.execPath, process.argv.slice(1), {
|
|
2490
|
+
stdio: 'inherit',
|
|
2491
|
+
env: process.env,
|
|
2492
|
+
detached: false,
|
|
2493
|
+
});
|
|
2494
|
+
process.exit(0);
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
// On the first failure, clear the build cache and retry automatically.
|
|
2498
|
+
// This recovers from stale artifacts or dependency issues without
|
|
2499
|
+
// requiring the user to touch the file or restart Beam.
|
|
2500
|
+
if (!autoRetried.has(photonName)) {
|
|
2501
|
+
autoRetried.add(photonName);
|
|
2502
|
+
logger.info(`🔄 ${photonName} failed to load, clearing cache and retrying...`);
|
|
2503
|
+
try {
|
|
2504
|
+
const retryPath = photons.find((p) => p.name === photonName)?.path || photonPath;
|
|
2505
|
+
await loader.clearCacheForFile(retryPath);
|
|
2506
|
+
}
|
|
2507
|
+
catch {
|
|
2508
|
+
// best-effort
|
|
2509
|
+
}
|
|
2510
|
+
setTimeout(() => void handleFileChange(photonName), 500);
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
// Second failure — give up and surface the error
|
|
2342
2514
|
logger.error(`Hot reload failed for ${photonName}: ${errorMsg}`);
|
|
2343
2515
|
broadcastToBeam('beam/error', {
|
|
2344
2516
|
type: 'hot-reload-error',
|
|
@@ -2499,7 +2671,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2499
2671
|
}
|
|
2500
2672
|
// Load external MCPs from config
|
|
2501
2673
|
const externalMCPList = await loadExternalMCPs(savedConfig);
|
|
2502
|
-
ctx.
|
|
2674
|
+
ctx.mcp.addAll(externalMCPList);
|
|
2503
2675
|
// Mark startup complete — flushes queued output and restores console
|
|
2504
2676
|
startup.ready();
|
|
2505
2677
|
// Notify connected clients that photon list is now available
|
|
@@ -2789,14 +2961,9 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2789
2961
|
// Remove MCPs — do all synchronous Map mutations first, then close async
|
|
2790
2962
|
const removedSdkClients = [];
|
|
2791
2963
|
for (const name of removed) {
|
|
2792
|
-
const
|
|
2793
|
-
if (idx !== -1)
|
|
2794
|
-
ctx.externalMCPs.splice(idx, 1);
|
|
2795
|
-
const sdkClient = ctx.externalMCPSDKClients.get(name);
|
|
2964
|
+
const { sdkClient } = ctx.mcp.removeByName(name);
|
|
2796
2965
|
if (sdkClient)
|
|
2797
2966
|
removedSdkClients.push({ name, client: sdkClient });
|
|
2798
|
-
ctx.externalMCPSDKClients.delete(name);
|
|
2799
|
-
ctx.externalMCPClients.delete(name);
|
|
2800
2967
|
logger.info(`🔌 Removed external MCP: ${name}`);
|
|
2801
2968
|
}
|
|
2802
2969
|
// Close SDK clients after all Maps are consistent
|
|
@@ -2815,7 +2982,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2815
2982
|
mcpServers: Object.fromEntries(added.map((k) => [k, newServers[k]])),
|
|
2816
2983
|
};
|
|
2817
2984
|
const newMCPs = await loadExternalMCPs(addConfig);
|
|
2818
|
-
ctx.
|
|
2985
|
+
ctx.mcp.addAll(newMCPs);
|
|
2819
2986
|
for (const m of newMCPs) {
|
|
2820
2987
|
logger.info(`🔌 Added external MCP: ${m.name} (${m.connected ? m.methods.length + ' tools' : 'failed'})`);
|
|
2821
2988
|
}
|
|
@@ -2823,14 +2990,9 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2823
2990
|
// Reconnect modified MCPs — synchronous cleanup first, then async reconnect
|
|
2824
2991
|
const modifiedSdkClients = [];
|
|
2825
2992
|
for (const name of modified) {
|
|
2826
|
-
const
|
|
2827
|
-
if (idx !== -1)
|
|
2828
|
-
ctx.externalMCPs.splice(idx, 1);
|
|
2829
|
-
const sdkClient = ctx.externalMCPSDKClients.get(name);
|
|
2993
|
+
const { sdkClient } = ctx.mcp.removeByName(name);
|
|
2830
2994
|
if (sdkClient)
|
|
2831
2995
|
modifiedSdkClients.push({ name, client: sdkClient });
|
|
2832
|
-
ctx.externalMCPSDKClients.delete(name);
|
|
2833
|
-
ctx.externalMCPClients.delete(name);
|
|
2834
2996
|
}
|
|
2835
2997
|
// Close old SDK clients
|
|
2836
2998
|
for (const { client } of modifiedSdkClients) {
|
|
@@ -2848,7 +3010,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2848
3010
|
mcpServers: { [name]: newServers[name] },
|
|
2849
3011
|
};
|
|
2850
3012
|
const reconnected = await loadExternalMCPs(modConfig);
|
|
2851
|
-
ctx.
|
|
3013
|
+
ctx.mcp.addAll(reconnected);
|
|
2852
3014
|
logger.info(`🔌 Reconnected external MCP: ${name}`);
|
|
2853
3015
|
}
|
|
2854
3016
|
// Update savedConfig
|
|
@@ -2878,18 +3040,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2878
3040
|
export async function stopBeam() {
|
|
2879
3041
|
// Stop session cleanup timer
|
|
2880
3042
|
stopSessionCleanup();
|
|
2881
|
-
// Close
|
|
2882
|
-
|
|
2883
|
-
for (const [, client] of ctx.externalMCPSDKClients) {
|
|
2884
|
-
closePromises.push(client.close().catch(() => {
|
|
2885
|
-
// Ignore close errors - process is exiting anyway
|
|
2886
|
-
}));
|
|
2887
|
-
}
|
|
2888
|
-
// Wait for all clients to close (with timeout)
|
|
2889
|
-
if (closePromises.length > 0) {
|
|
2890
|
-
await withTimeout(Promise.all(closePromises), 1000, 'MCP client close timeout').catch(() => { }); // Timeout during shutdown is expected
|
|
2891
|
-
}
|
|
2892
|
-
ctx.externalMCPSDKClients.clear();
|
|
2893
|
-
ctx.externalMCPClients.clear();
|
|
3043
|
+
// Close every external MCP SDK client gracefully and clear the maps.
|
|
3044
|
+
await ctx.mcp.closeAllSDKClients();
|
|
2894
3045
|
}
|
|
2895
3046
|
//# sourceMappingURL=beam.js.map
|