@portel/photon 1.29.0 → 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 +41 -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 +162 -45
- 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.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +24 -14
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +15 -1
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +170 -22
- 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.js +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 +30 -9
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/server.js +28 -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 +129 -2
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/embedded-runtime.js.map +1 -1
- package/dist/loader.d.ts +43 -66
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +185 -305
- 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/resource-server.d.ts +3 -3
- package/dist/resource-server.d.ts.map +1 -1
- 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 +42 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +166 -14
- 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/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/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/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 -8
- package/dist/types/server-types.d.ts.map +1 -1
- package/package.json +11 -4
- package/templates/cloudflare/worker.ts.template +338 -11
- package/templates/cloudflare/wrangler.toml.template +1 -6
- package/templates/photon.template.ts +13 -0
package/dist/auto-ui/beam.js
CHANGED
|
@@ -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
|
}
|
|
@@ -753,7 +762,10 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
753
762
|
// Check if method has @ui tag matching this id
|
|
754
763
|
const methodSource = schemaSource.match(new RegExp(`@ui\\s+${uiId}[\\s\\n]*\\*/[\\s\\n]*(?:async\\s+)?${schema.name}\\s*\\(`, 'm'));
|
|
755
764
|
if (methodSource) {
|
|
756
|
-
|
|
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 });
|
|
757
769
|
}
|
|
758
770
|
});
|
|
759
771
|
}
|
|
@@ -836,7 +848,6 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
836
848
|
const mainMethod = methods.find((m) => m.name === 'main');
|
|
837
849
|
// Extract class-level metadata — reuse source already read
|
|
838
850
|
const classMetadata = extractClassMetadataFromSource(schemaSource);
|
|
839
|
-
// Extract class-level @csp metadata and apply to all UI assets
|
|
840
851
|
const cspData = extractCspFromSource(schemaSource);
|
|
841
852
|
if (cspData['__class__'] && mcp.assets?.ui) {
|
|
842
853
|
for (const uiAsset of mcp.assets.ui) {
|
|
@@ -937,7 +948,8 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
937
948
|
return null;
|
|
938
949
|
const photonDir = path.dirname(photon.path);
|
|
939
950
|
const photonBaseName = path.basename(photon.path, '.photon.ts');
|
|
940
|
-
|
|
951
|
+
// assets only live on configured photons
|
|
952
|
+
const asset = photon.configured ? photon.assets?.ui?.find((u) => u.id === uiId) : undefined;
|
|
941
953
|
let resolved;
|
|
942
954
|
if (asset?.resolvedPath) {
|
|
943
955
|
resolved = {
|
|
@@ -1387,10 +1399,12 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1387
1399
|
res.end(`Photon not found: ${photonName}`);
|
|
1388
1400
|
return;
|
|
1389
1401
|
}
|
|
1390
|
-
const label = photon
|
|
1402
|
+
const label = photon.label ||
|
|
1391
1403
|
photonName.charAt(0).toUpperCase() + photonName.slice(1).replace(/-/g, ' ');
|
|
1392
|
-
|
|
1393
|
-
|
|
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) || '📦';
|
|
1394
1408
|
const encodedName = encodeURIComponent(photonName);
|
|
1395
1409
|
// Sanitize strings for safe embedding in HTML
|
|
1396
1410
|
const safeLabel = label.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] || c);
|
|
@@ -1874,6 +1888,94 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1874
1888
|
}
|
|
1875
1889
|
return;
|
|
1876
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
|
+
}
|
|
1877
1979
|
// Default route: Serve Lit App
|
|
1878
1980
|
if (url.pathname === '/' || !url.pathname.startsWith('/api')) {
|
|
1879
1981
|
try {
|
|
@@ -1911,6 +2013,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1911
2013
|
const activeLoads = new Set(); // Photons currently being loaded (prevents concurrent duplicate loads)
|
|
1912
2014
|
const pendingAfterLoad = new Set(); // File changes that arrived while a load was active; re-triggered after
|
|
1913
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
|
|
1914
2017
|
// Set up file watchers for a symlinked photon's real source directory and asset folder.
|
|
1915
2018
|
// Called both at startup and after a previously-errored symlinked photon recovers.
|
|
1916
2019
|
const setupSymlinkWatcher = (photonName, photonPath) => {
|
|
@@ -2169,12 +2272,13 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2169
2272
|
}
|
|
2170
2273
|
try {
|
|
2171
2274
|
// Load or reload the photon
|
|
2172
|
-
const mcp = isNewPhoton
|
|
2275
|
+
const mcp = (isNewPhoton
|
|
2173
2276
|
? await loader.loadFile(photonPath)
|
|
2174
|
-
: await loader.reloadFile(photonPath);
|
|
2277
|
+
: await loader.reloadFile(photonPath));
|
|
2175
2278
|
if (!mcp.instance)
|
|
2176
2279
|
throw new Error('Failed to create instance');
|
|
2177
2280
|
photonMCPs.set(photonName, mcp);
|
|
2281
|
+
autoRetried.delete(photonName); // Clear retry flag on success
|
|
2178
2282
|
// Re-extract schema - use extractAllFromSource to get both tools and templates
|
|
2179
2283
|
const extractor = new SchemaExtractor();
|
|
2180
2284
|
const reloadSource = await readText(photonPath);
|
|
@@ -2373,6 +2477,40 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2373
2477
|
}
|
|
2374
2478
|
return;
|
|
2375
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
|
|
2376
2514
|
logger.error(`Hot reload failed for ${photonName}: ${errorMsg}`);
|
|
2377
2515
|
broadcastToBeam('beam/error', {
|
|
2378
2516
|
type: 'hot-reload-error',
|
|
@@ -2533,7 +2671,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2533
2671
|
}
|
|
2534
2672
|
// Load external MCPs from config
|
|
2535
2673
|
const externalMCPList = await loadExternalMCPs(savedConfig);
|
|
2536
|
-
ctx.
|
|
2674
|
+
ctx.mcp.addAll(externalMCPList);
|
|
2537
2675
|
// Mark startup complete — flushes queued output and restores console
|
|
2538
2676
|
startup.ready();
|
|
2539
2677
|
// Notify connected clients that photon list is now available
|
|
@@ -2823,14 +2961,9 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2823
2961
|
// Remove MCPs — do all synchronous Map mutations first, then close async
|
|
2824
2962
|
const removedSdkClients = [];
|
|
2825
2963
|
for (const name of removed) {
|
|
2826
|
-
const
|
|
2827
|
-
if (idx !== -1)
|
|
2828
|
-
ctx.externalMCPs.splice(idx, 1);
|
|
2829
|
-
const sdkClient = ctx.externalMCPSDKClients.get(name);
|
|
2964
|
+
const { sdkClient } = ctx.mcp.removeByName(name);
|
|
2830
2965
|
if (sdkClient)
|
|
2831
2966
|
removedSdkClients.push({ name, client: sdkClient });
|
|
2832
|
-
ctx.externalMCPSDKClients.delete(name);
|
|
2833
|
-
ctx.externalMCPClients.delete(name);
|
|
2834
2967
|
logger.info(`🔌 Removed external MCP: ${name}`);
|
|
2835
2968
|
}
|
|
2836
2969
|
// Close SDK clients after all Maps are consistent
|
|
@@ -2849,7 +2982,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2849
2982
|
mcpServers: Object.fromEntries(added.map((k) => [k, newServers[k]])),
|
|
2850
2983
|
};
|
|
2851
2984
|
const newMCPs = await loadExternalMCPs(addConfig);
|
|
2852
|
-
ctx.
|
|
2985
|
+
ctx.mcp.addAll(newMCPs);
|
|
2853
2986
|
for (const m of newMCPs) {
|
|
2854
2987
|
logger.info(`🔌 Added external MCP: ${m.name} (${m.connected ? m.methods.length + ' tools' : 'failed'})`);
|
|
2855
2988
|
}
|
|
@@ -2857,14 +2990,9 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2857
2990
|
// Reconnect modified MCPs — synchronous cleanup first, then async reconnect
|
|
2858
2991
|
const modifiedSdkClients = [];
|
|
2859
2992
|
for (const name of modified) {
|
|
2860
|
-
const
|
|
2861
|
-
if (idx !== -1)
|
|
2862
|
-
ctx.externalMCPs.splice(idx, 1);
|
|
2863
|
-
const sdkClient = ctx.externalMCPSDKClients.get(name);
|
|
2993
|
+
const { sdkClient } = ctx.mcp.removeByName(name);
|
|
2864
2994
|
if (sdkClient)
|
|
2865
2995
|
modifiedSdkClients.push({ name, client: sdkClient });
|
|
2866
|
-
ctx.externalMCPSDKClients.delete(name);
|
|
2867
|
-
ctx.externalMCPClients.delete(name);
|
|
2868
2996
|
}
|
|
2869
2997
|
// Close old SDK clients
|
|
2870
2998
|
for (const { client } of modifiedSdkClients) {
|
|
@@ -2882,7 +3010,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2882
3010
|
mcpServers: { [name]: newServers[name] },
|
|
2883
3011
|
};
|
|
2884
3012
|
const reconnected = await loadExternalMCPs(modConfig);
|
|
2885
|
-
ctx.
|
|
3013
|
+
ctx.mcp.addAll(reconnected);
|
|
2886
3014
|
logger.info(`🔌 Reconnected external MCP: ${name}`);
|
|
2887
3015
|
}
|
|
2888
3016
|
// Update savedConfig
|
|
@@ -2912,18 +3040,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2912
3040
|
export async function stopBeam() {
|
|
2913
3041
|
// Stop session cleanup timer
|
|
2914
3042
|
stopSessionCleanup();
|
|
2915
|
-
// Close
|
|
2916
|
-
|
|
2917
|
-
for (const [, client] of ctx.externalMCPSDKClients) {
|
|
2918
|
-
closePromises.push(client.close().catch(() => {
|
|
2919
|
-
// Ignore close errors - process is exiting anyway
|
|
2920
|
-
}));
|
|
2921
|
-
}
|
|
2922
|
-
// Wait for all clients to close (with timeout)
|
|
2923
|
-
if (closePromises.length > 0) {
|
|
2924
|
-
await withTimeout(Promise.all(closePromises), 1000, 'MCP client close timeout').catch(() => { }); // Timeout during shutdown is expected
|
|
2925
|
-
}
|
|
2926
|
-
ctx.externalMCPSDKClients.clear();
|
|
2927
|
-
ctx.externalMCPClients.clear();
|
|
3043
|
+
// Close every external MCP SDK client gracefully and clear the maps.
|
|
3044
|
+
await ctx.mcp.closeAllSDKClients();
|
|
2928
3045
|
}
|
|
2929
3046
|
//# sourceMappingURL=beam.js.map
|