@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.
- package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.js +16 -4
- package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-config.js +4 -4
- 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 +14 -1
- package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +183 -74
- 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 +17 -0
- package/dist/auto-ui/bridge/index.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +1 -0
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +64 -16
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +12 -0
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam-form.bundle.js +44 -3
- package/dist/beam-form.bundle.js.map +2 -2
- package/dist/beam.bundle.js +1404 -482
- package/dist/beam.bundle.js.map +4 -4
- package/dist/capability-negotiator.d.ts +67 -0
- package/dist/capability-negotiator.d.ts.map +1 -0
- package/dist/capability-negotiator.js +104 -0
- package/dist/capability-negotiator.js.map +1 -0
- package/dist/channel-manager.d.ts +122 -0
- package/dist/channel-manager.d.ts.map +1 -0
- package/dist/channel-manager.js +266 -0
- package/dist/channel-manager.js.map +1 -0
- package/dist/cli/commands/package.d.ts.map +1 -1
- package/dist/cli/commands/package.js +25 -7
- package/dist/cli/commands/package.js.map +1 -1
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +12 -0
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/server.js +30 -49
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/worker-manager.d.ts.map +1 -1
- package/dist/daemon/worker-manager.js +21 -7
- package/dist/daemon/worker-manager.js.map +1 -1
- package/dist/loader.d.ts +4 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +73 -11
- package/dist/loader.js.map +1 -1
- package/dist/marketplace-manager.d.ts +6 -0
- package/dist/marketplace-manager.d.ts.map +1 -1
- package/dist/marketplace-manager.js +161 -58
- package/dist/marketplace-manager.js.map +1 -1
- package/dist/namespace-migration.d.ts +1 -0
- package/dist/namespace-migration.d.ts.map +1 -1
- package/dist/namespace-migration.js +86 -0
- package/dist/namespace-migration.js.map +1 -1
- package/dist/resource-server.d.ts +105 -0
- package/dist/resource-server.d.ts.map +1 -0
- package/dist/resource-server.js +723 -0
- package/dist/resource-server.js.map +1 -0
- package/dist/serv/auth/jwt.d.ts +2 -0
- package/dist/serv/auth/jwt.d.ts.map +1 -1
- package/dist/serv/auth/jwt.js +11 -5
- package/dist/serv/auth/jwt.js.map +1 -1
- package/dist/serv/vault/token-vault.d.ts +2 -0
- package/dist/serv/vault/token-vault.d.ts.map +1 -1
- package/dist/serv/vault/token-vault.js +6 -0
- package/dist/serv/vault/token-vault.js.map +1 -1
- package/dist/server.d.ts +20 -149
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +232 -1217
- package/dist/server.js.map +1 -1
- package/dist/shared/audit.d.ts.map +1 -1
- package/dist/shared/audit.js +7 -0
- package/dist/shared/audit.js.map +1 -1
- package/dist/shared/security.d.ts +10 -0
- package/dist/shared/security.d.ts.map +1 -1
- package/dist/shared/security.js +27 -0
- package/dist/shared/security.js.map +1 -1
- package/dist/task-executor.d.ts +69 -0
- package/dist/task-executor.d.ts.map +1 -0
- package/dist/task-executor.js +182 -0
- package/dist/task-executor.js.map +1 -0
- package/dist/types/photon-instance.d.ts +50 -0
- package/dist/types/photon-instance.d.ts.map +1 -0
- package/dist/types/photon-instance.js +9 -0
- package/dist/types/photon-instance.js.map +1 -0
- package/dist/types/server-types.d.ts +61 -0
- package/dist/types/server-types.d.ts.map +1 -0
- package/dist/types/server-types.js +8 -0
- package/dist/types/server-types.js.map +1 -0
- package/package.json +2 -2
package/dist/auto-ui/beam.js
CHANGED
|
@@ -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
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
125
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
878
|
-
const photonMdPath = path.join(photonDir,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
-
|
|
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 = '
|
|
1486
|
-
|
|
1487
|
-
|
|
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:
|
|
1917
|
+
// Namespaced: Arul-/git-box.photon.ts → "git-box" (short name only)
|
|
1863
1918
|
if (relativePath.endsWith('.photon.ts')) {
|
|
1864
|
-
|
|
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
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
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
|