@portel/photon 1.20.0 → 1.21.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 +5 -5
- package/dist/ag-ui/adapter.d.ts.map +1 -1
- package/dist/ag-ui/adapter.js +25 -0
- package/dist/ag-ui/adapter.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 +8 -49
- 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 +161 -20
- package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +24 -31
- 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 +107 -11
- package/dist/auto-ui/bridge/index.js.map +1 -1
- package/dist/auto-ui/bridge/renderers.d.ts +14 -0
- package/dist/auto-ui/bridge/renderers.d.ts.map +1 -1
- package/dist/auto-ui/bridge/renderers.js +692 -61
- package/dist/auto-ui/bridge/renderers.js.map +1 -1
- package/dist/auto-ui/frontend/index.html +3 -3
- package/dist/auto-ui/frontend/pure-view.html +19 -19
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +144 -28
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/ui-resolver.d.ts +25 -0
- package/dist/auto-ui/ui-resolver.d.ts.map +1 -0
- package/dist/auto-ui/ui-resolver.js +95 -0
- package/dist/auto-ui/ui-resolver.js.map +1 -0
- package/dist/beam-form.bundle.js +26 -189
- package/dist/beam-form.bundle.js.map +4 -4
- package/dist/beam.bundle.js +1646 -494
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli/commands/beam.d.ts.map +1 -1
- package/dist/cli/commands/beam.js +47 -30
- package/dist/cli/commands/beam.js.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +36 -7
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/daemon.d.ts.map +1 -1
- package/dist/cli/commands/daemon.js +12 -6
- package/dist/cli/commands/daemon.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +90 -50
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +18 -6
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/publish.d.ts +14 -0
- package/dist/cli/commands/publish.d.ts.map +1 -0
- package/dist/cli/commands/publish.js +126 -0
- package/dist/cli/commands/publish.js.map +1 -0
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +2 -0
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/serve.js +14 -2
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli-alias.d.ts.map +1 -1
- package/dist/cli-alias.js +2 -3
- package/dist/cli-alias.js.map +1 -1
- package/dist/context-store.d.ts +4 -4
- package/dist/context-store.d.ts.map +1 -1
- package/dist/context-store.js +18 -15
- package/dist/context-store.js.map +1 -1
- package/dist/context.d.ts +31 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +86 -9
- package/dist/context.js.map +1 -1
- package/dist/daemon/client.d.ts +9 -1
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +58 -2
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +5 -0
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +116 -34
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/ownership.d.ts +12 -0
- package/dist/daemon/ownership.d.ts.map +1 -0
- package/dist/daemon/ownership.js +55 -0
- package/dist/daemon/ownership.js.map +1 -0
- package/dist/daemon/protocol.d.ts +3 -1
- package/dist/daemon/protocol.d.ts.map +1 -1
- package/dist/daemon/protocol.js +14 -2
- package/dist/daemon/protocol.js.map +1 -1
- package/dist/daemon/server.js +587 -77
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/session-manager.d.ts +9 -1
- package/dist/daemon/session-manager.d.ts.map +1 -1
- package/dist/daemon/session-manager.js +54 -1
- package/dist/daemon/session-manager.js.map +1 -1
- package/dist/daemon/worker-host.js +7 -0
- package/dist/daemon/worker-host.js.map +1 -1
- package/dist/daemon/worker-manager.d.ts +12 -0
- package/dist/daemon/worker-manager.d.ts.map +1 -1
- package/dist/daemon/worker-manager.js +147 -16
- package/dist/daemon/worker-manager.js.map +1 -1
- package/dist/daemon/worker-protocol.d.ts +3 -0
- package/dist/daemon/worker-protocol.d.ts.map +1 -1
- package/dist/deploy/cloudflare.d.ts.map +1 -1
- package/dist/deploy/cloudflare.js +2 -4
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/loader.d.ts +10 -9
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +224 -115
- package/dist/loader.js.map +1 -1
- package/dist/marketplace-manager.d.ts +1 -1
- package/dist/marketplace-manager.d.ts.map +1 -1
- package/dist/marketplace-manager.js +5 -4
- package/dist/marketplace-manager.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +66 -23
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +59 -15
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/photons/canvas/ui/canvas.photon.html +1493 -0
- package/dist/photons/canvas.photon.d.ts +400 -0
- package/dist/photons/canvas.photon.d.ts.map +1 -0
- package/dist/photons/canvas.photon.js +662 -0
- package/dist/photons/canvas.photon.js.map +1 -0
- package/dist/photons/canvas.photon.ts +814 -0
- package/dist/photons/publish.photon.d.ts +97 -0
- package/dist/photons/publish.photon.d.ts.map +1 -0
- package/dist/photons/publish.photon.js +569 -0
- package/dist/photons/publish.photon.js.map +1 -0
- package/dist/photons/publish.photon.ts +683 -0
- package/dist/photons/ui/canvas.photon.html +624 -0
- package/dist/resource-server.d.ts.map +1 -1
- package/dist/resource-server.js +7 -1
- package/dist/resource-server.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +14 -16
- package/dist/server.js.map +1 -1
- package/dist/shared-utils.d.ts +4 -0
- package/dist/shared-utils.d.ts.map +1 -1
- package/dist/shared-utils.js +24 -2
- package/dist/shared-utils.js.map +1 -1
- package/dist/template-manager.d.ts.map +1 -1
- package/dist/template-manager.js +56 -234
- package/dist/template-manager.js.map +1 -1
- package/dist/tsx-compiler.d.ts +23 -0
- package/dist/tsx-compiler.d.ts.map +1 -0
- package/dist/tsx-compiler.js +221 -0
- package/dist/tsx-compiler.js.map +1 -0
- package/package.json +7 -7
package/dist/loader.js
CHANGED
|
@@ -8,7 +8,7 @@ import { realpathSync, existsSync, mkdirSync, symlinkSync, readFileSync } from '
|
|
|
8
8
|
import { readText, readJSON, writeText, writeJSON } from './shared/io.js';
|
|
9
9
|
import { createRequire } from 'module';
|
|
10
10
|
import * as path from 'path';
|
|
11
|
-
import { pathToFileURL } from 'url';
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
12
12
|
import * as crypto from 'crypto';
|
|
13
13
|
import { startToolSpan } from './telemetry/otel.js';
|
|
14
14
|
import { spawn } from 'child_process';
|
|
@@ -130,6 +130,38 @@ const cliRenderZone = new CLIRenderZone();
|
|
|
130
130
|
export function clearRenderZone() {
|
|
131
131
|
cliRenderZone.clear();
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Inject emit-based convenience helpers on a plain-class instance.
|
|
135
|
+
* Every helper is a thin wrapper around this.emit so generator-yield and
|
|
136
|
+
* imperative-call styles produce identical wire events.
|
|
137
|
+
*/
|
|
138
|
+
function injectEmitHelpers(instance) {
|
|
139
|
+
const emit = (data) => instance.emit(data);
|
|
140
|
+
// Mirrors photon-core base-class render(): UI-feedback formats route to their
|
|
141
|
+
// dedicated emit events; all other formats go through the render channel.
|
|
142
|
+
instance.render = (format, value) => {
|
|
143
|
+
if (format === undefined)
|
|
144
|
+
return emit({ emit: 'render:clear' });
|
|
145
|
+
if (format === 'status')
|
|
146
|
+
return emit(typeof value === 'string'
|
|
147
|
+
? { emit: 'status', message: value }
|
|
148
|
+
: { emit: 'status', ...value });
|
|
149
|
+
if (format === 'progress')
|
|
150
|
+
return emit(typeof value === 'number' ? { emit: 'progress', value } : { emit: 'progress', ...value });
|
|
151
|
+
if (format === 'toast')
|
|
152
|
+
return emit(typeof value === 'string' ? { emit: 'toast', message: value } : { emit: 'toast', ...value });
|
|
153
|
+
emit({ emit: 'render', format, value });
|
|
154
|
+
};
|
|
155
|
+
instance.toast = (message, opts = {}) => emit({ emit: 'toast', message, ...opts });
|
|
156
|
+
instance.log = (message, opts = {}) => emit({ emit: 'log', message, level: opts.level ?? 'info', data: opts.data });
|
|
157
|
+
instance.status = (message) => emit({ emit: 'status', message });
|
|
158
|
+
instance.progress = (value, message) => emit({ emit: 'progress', value, message });
|
|
159
|
+
instance.thinking = (active = true) => emit({ emit: 'thinking', active });
|
|
160
|
+
}
|
|
161
|
+
/** Extra regex checks that force 'emit' capability when helper methods are used. */
|
|
162
|
+
function detectEmitHelperUsage(source) {
|
|
163
|
+
return /this\.(toast|log|status|progress|thinking)\s*\(/.test(source);
|
|
164
|
+
}
|
|
133
165
|
/**
|
|
134
166
|
* Render a formatted value in the CLI using @portel/cli's formatOutput.
|
|
135
167
|
* Uses clear-and-replace semantics — each call overwrites the previous render.
|
|
@@ -225,6 +257,12 @@ export class PhotonLoader {
|
|
|
225
257
|
* without file I/O when running as a standalone binary.
|
|
226
258
|
*/
|
|
227
259
|
preloadedDependencies;
|
|
260
|
+
/**
|
|
261
|
+
* Optional progress callback — invoked during long-running init phases
|
|
262
|
+
* (dependency install, compilation, onInitialize). Used by worker-host
|
|
263
|
+
* to send keepalive signals so the spawn timeout resets.
|
|
264
|
+
*/
|
|
265
|
+
onProgress;
|
|
228
266
|
constructor(verbose = false, logger, baseDir) {
|
|
229
267
|
this.dependencyManager = new DependencyManager();
|
|
230
268
|
this.verbose = verbose;
|
|
@@ -332,7 +370,7 @@ export class PhotonLoader {
|
|
|
332
370
|
* Directory where MCP-specific dependencies are cached
|
|
333
371
|
*/
|
|
334
372
|
getDependencyCacheDir(cacheKey) {
|
|
335
|
-
return path.join(getCacheDir(), 'dependencies', cacheKey);
|
|
373
|
+
return path.join(getCacheDir(process.env.PHOTON_DIR), 'dependencies', cacheKey);
|
|
336
374
|
}
|
|
337
375
|
getBuildCacheDir(cacheKey) {
|
|
338
376
|
return path.join(this.getDependencyCacheDir(cacheKey), '.build');
|
|
@@ -425,6 +463,7 @@ export class PhotonLoader {
|
|
|
425
463
|
}
|
|
426
464
|
let nodeModules = null;
|
|
427
465
|
if (dependencies.length > 0) {
|
|
466
|
+
this.onProgress?.('installing dependencies');
|
|
428
467
|
nodeModules = await this.dependencyManager.ensureDependencies(cacheKey, dependencies);
|
|
429
468
|
if (nodeModules) {
|
|
430
469
|
this.log(`📦 Dependencies ready for ${mcpName}`, { nodeModules });
|
|
@@ -572,6 +611,7 @@ export class PhotonLoader {
|
|
|
572
611
|
}
|
|
573
612
|
const importModule = async () => {
|
|
574
613
|
if (tsContent) {
|
|
614
|
+
this.onProgress?.('compiling typescript');
|
|
575
615
|
const cachedJsPath = await this.compileTypeScript(absolutePath, cacheKey, tsContent);
|
|
576
616
|
const cachedJsUrl = pathToFileURL(cachedJsPath).href;
|
|
577
617
|
return await import(`${cachedJsUrl}?t=${Date.now()}`);
|
|
@@ -642,15 +682,12 @@ export class PhotonLoader {
|
|
|
642
682
|
// Auto-wire ReactiveArray/Map/Set properties for zero-boilerplate reactivity
|
|
643
683
|
// Developers just `import { Array } from '@portel/photon-core'` and use normally
|
|
644
684
|
this.wireReactiveCollections(instance);
|
|
685
|
+
// Inject format catalog — this.formats
|
|
686
|
+
this.injectFormatCatalog(instance);
|
|
645
687
|
// Inject @mcp dependencies from source (this.github, this.fs, etc.)
|
|
646
688
|
if (tsContent) {
|
|
647
689
|
await this.injectMCPDependencies(instance, tsContent, name);
|
|
648
690
|
}
|
|
649
|
-
// Auto-wrap public methods in @stateful classes to emit events
|
|
650
|
-
// All method calls automatically produce events with params, result, timestamp
|
|
651
|
-
if (tsContent) {
|
|
652
|
-
this.wrapStatefulMethods(instance, tsContent);
|
|
653
|
-
}
|
|
654
691
|
// Inject MCP client factory if available (enables this.mcp() calls)
|
|
655
692
|
const setMCPFactory = instance.setMCPFactory;
|
|
656
693
|
if (this.mcpClientFactory && typeof setMCPFactory === 'function') {
|
|
@@ -674,6 +711,8 @@ export class PhotonLoader {
|
|
|
674
711
|
// Photon base class already has these built-in, so only inject for plain classes
|
|
675
712
|
if (tsContent && typeof instance.executeTool !== 'function') {
|
|
676
713
|
const caps = detectCapabilities(tsContent);
|
|
714
|
+
if (detectEmitHelperUsage(tsContent))
|
|
715
|
+
caps.add('emit');
|
|
677
716
|
if (caps.size > 0) {
|
|
678
717
|
this.log(`🔍 Detected capabilities for ${name}: ${[...caps].join(', ')}`);
|
|
679
718
|
}
|
|
@@ -719,15 +758,8 @@ export class PhotonLoader {
|
|
|
719
758
|
});
|
|
720
759
|
}
|
|
721
760
|
};
|
|
722
|
-
//
|
|
723
|
-
instance
|
|
724
|
-
if (format === undefined) {
|
|
725
|
-
instance.emit({ emit: 'render:clear' });
|
|
726
|
-
}
|
|
727
|
-
else {
|
|
728
|
-
instance.emit({ emit: 'render', format, value });
|
|
729
|
-
}
|
|
730
|
-
};
|
|
761
|
+
// Inject convenience helpers (render, toast, log, status, progress, thinking)
|
|
762
|
+
injectEmitHelpers(instance);
|
|
731
763
|
}
|
|
732
764
|
if (caps.has('memory')) {
|
|
733
765
|
// Inject lazy memory provider — capture baseDir from loader context
|
|
@@ -978,6 +1010,7 @@ export class PhotonLoader {
|
|
|
978
1010
|
// instance after loadFile returns, then onInitialize is called manually.
|
|
979
1011
|
const onInitialize = instance.onInitialize;
|
|
980
1012
|
if (typeof onInitialize === 'function' && !options?.skipInitialize) {
|
|
1013
|
+
this.onProgress?.('running onInitialize');
|
|
981
1014
|
try {
|
|
982
1015
|
await onInitialize.call(instance);
|
|
983
1016
|
}
|
|
@@ -992,8 +1025,13 @@ export class PhotonLoader {
|
|
|
992
1025
|
throw initError;
|
|
993
1026
|
}
|
|
994
1027
|
}
|
|
1028
|
+
// Auto-wrap public methods in @stateful classes to emit events
|
|
1029
|
+
// Must happen AFTER capability injection so that emit() is available
|
|
1030
|
+
if (tsContent) {
|
|
1031
|
+
this.wrapStatefulMethods(instance, tsContent);
|
|
1032
|
+
}
|
|
995
1033
|
// Extract tools, templates, and statics (with schema override support)
|
|
996
|
-
const { tools, templates, statics, settingsSchema } = await this.extractTools(MCPClass, absolutePath);
|
|
1034
|
+
const { tools, templates, statics, settingsSchema, auth: extractedAuth, } = await this.extractTools(MCPClass, absolutePath);
|
|
997
1035
|
// ═══ SETTINGS INJECTION ═══
|
|
998
1036
|
// If the photon declared `protected settings = { ... }`, inject persistence + proxy
|
|
999
1037
|
if (settingsSchema?.hasSettings &&
|
|
@@ -1043,6 +1081,8 @@ export class PhotonLoader {
|
|
|
1043
1081
|
result.icon = classIcon;
|
|
1044
1082
|
if (isStateful)
|
|
1045
1083
|
result.stateful = true;
|
|
1084
|
+
if (extractedAuth)
|
|
1085
|
+
result.auth = extractedAuth;
|
|
1046
1086
|
// Store class constructor for static method access
|
|
1047
1087
|
result.classConstructor = MCPClass;
|
|
1048
1088
|
// Store settings schema for Beam UI
|
|
@@ -1114,6 +1154,8 @@ export class PhotonLoader {
|
|
|
1114
1154
|
instance._photonResolver = (photonName, instanceName) => {
|
|
1115
1155
|
return this.resolveAndLoadPhoton(photonName, absolutePath, instanceName);
|
|
1116
1156
|
};
|
|
1157
|
+
// Inject format catalog — this.formats
|
|
1158
|
+
this.injectFormatCatalog(instance);
|
|
1117
1159
|
// Wire reactive collections
|
|
1118
1160
|
this.wireReactiveCollections(instance);
|
|
1119
1161
|
// Inject @mcp dependencies
|
|
@@ -1143,6 +1185,8 @@ export class PhotonLoader {
|
|
|
1143
1185
|
// Detect and inject capabilities for plain classes
|
|
1144
1186
|
if (tsContent && typeof instance.executeTool !== 'function') {
|
|
1145
1187
|
const caps = detectCapabilities(tsContent);
|
|
1188
|
+
if (detectEmitHelperUsage(tsContent))
|
|
1189
|
+
caps.add('emit');
|
|
1146
1190
|
this.injectPathHelpers(instance, tsContent);
|
|
1147
1191
|
if (caps.has('emit')) {
|
|
1148
1192
|
instance.emit = (data) => {
|
|
@@ -1177,15 +1221,8 @@ export class PhotonLoader {
|
|
|
1177
1221
|
});
|
|
1178
1222
|
}
|
|
1179
1223
|
};
|
|
1180
|
-
//
|
|
1181
|
-
instance
|
|
1182
|
-
if (format === undefined) {
|
|
1183
|
-
instance.emit({ emit: 'render:clear' });
|
|
1184
|
-
}
|
|
1185
|
-
else {
|
|
1186
|
-
instance.emit({ emit: 'render', format, value });
|
|
1187
|
-
}
|
|
1188
|
-
};
|
|
1224
|
+
// Inject convenience helpers (render, toast, log, status, progress, thinking)
|
|
1225
|
+
injectEmitHelpers(instance);
|
|
1189
1226
|
}
|
|
1190
1227
|
if (caps.has('memory')) {
|
|
1191
1228
|
const memoryBaseDir = this.baseDir;
|
|
@@ -1276,6 +1313,7 @@ export class PhotonLoader {
|
|
|
1276
1313
|
// Call lifecycle hook
|
|
1277
1314
|
const onInitialize = instance.onInitialize;
|
|
1278
1315
|
if (typeof onInitialize === 'function' && !options?.skipInitialize) {
|
|
1316
|
+
this.onProgress?.('running onInitialize');
|
|
1279
1317
|
try {
|
|
1280
1318
|
await onInitialize.call(instance);
|
|
1281
1319
|
}
|
|
@@ -1286,7 +1324,7 @@ export class PhotonLoader {
|
|
|
1286
1324
|
}
|
|
1287
1325
|
}
|
|
1288
1326
|
// Extract tools and metadata from embedded source (no disk I/O)
|
|
1289
|
-
const { tools, templates, statics, settingsSchema } = await this.extractTools(MCPClass, absolutePath, tsContent);
|
|
1327
|
+
const { tools, templates, statics, settingsSchema, auth: extractedAuth, } = await this.extractTools(MCPClass, absolutePath, tsContent);
|
|
1290
1328
|
// Settings injection
|
|
1291
1329
|
if (settingsSchema?.hasSettings && instance.settings && typeof instance.settings === 'object') {
|
|
1292
1330
|
const instanceName = options?.instanceName || 'default';
|
|
@@ -1316,6 +1354,8 @@ export class PhotonLoader {
|
|
|
1316
1354
|
result.icon = classIcon;
|
|
1317
1355
|
if (isStateful)
|
|
1318
1356
|
result.stateful = true;
|
|
1357
|
+
if (extractedAuth)
|
|
1358
|
+
result.auth = extractedAuth;
|
|
1319
1359
|
result.classConstructor = MCPClass;
|
|
1320
1360
|
if (settingsSchema?.hasSettings) {
|
|
1321
1361
|
result.settingsSchema = settingsSchema;
|
|
@@ -1331,6 +1371,14 @@ export class PhotonLoader {
|
|
|
1331
1371
|
const match = source.match(/\/\*\*([\s\S]*?)\*\/\s*export\s+default\s+class\b/);
|
|
1332
1372
|
return match ? match[1] : '';
|
|
1333
1373
|
}
|
|
1374
|
+
extractAuthTag(source) {
|
|
1375
|
+
const docblock = this.extractClassDocblock(source);
|
|
1376
|
+
// Use \b to avoid matching @author, @authorize, etc.
|
|
1377
|
+
const match = docblock.match(/@auth\b(?:\s+(\S+))?/i);
|
|
1378
|
+
if (!match)
|
|
1379
|
+
return undefined;
|
|
1380
|
+
return match[1]?.trim() || 'required';
|
|
1381
|
+
}
|
|
1334
1382
|
/**
|
|
1335
1383
|
* Reload a Photon MCP file (for hot reload)
|
|
1336
1384
|
*/
|
|
@@ -1557,7 +1605,13 @@ export class PhotonLoader {
|
|
|
1557
1605
|
}
|
|
1558
1606
|
});
|
|
1559
1607
|
}
|
|
1560
|
-
return {
|
|
1608
|
+
return {
|
|
1609
|
+
tools,
|
|
1610
|
+
templates,
|
|
1611
|
+
statics,
|
|
1612
|
+
settingsSchema: metadata.settingsSchema,
|
|
1613
|
+
auth: this.extractAuthTag(source),
|
|
1614
|
+
};
|
|
1561
1615
|
}
|
|
1562
1616
|
throw jsonError;
|
|
1563
1617
|
}
|
|
@@ -2231,11 +2285,14 @@ export class PhotonLoader {
|
|
|
2231
2285
|
return;
|
|
2232
2286
|
if (path.dirname(realCurrentPhotonPath) !== path.dirname(resolvedPath))
|
|
2233
2287
|
return;
|
|
2234
|
-
|
|
2288
|
+
// Symlink into the directory where the current photon's symlink lives
|
|
2289
|
+
// (e.g. ~/.photon/), NOT this.baseDir which may be a different workspace.
|
|
2290
|
+
const symlinkDir = path.dirname(currentPhotonPath);
|
|
2291
|
+
const linkPath = path.join(symlinkDir, path.basename(resolvedPath));
|
|
2235
2292
|
await this.createSymlinkIfMissing(resolvedPath, linkPath);
|
|
2236
2293
|
const depName = path.basename(resolvedPath).replace(/\.photon\.(ts|js)$/, '');
|
|
2237
2294
|
const sourceAssetDir = path.join(path.dirname(resolvedPath), depName);
|
|
2238
|
-
const targetAssetDir = path.join(
|
|
2295
|
+
const targetAssetDir = path.join(symlinkDir, depName);
|
|
2239
2296
|
if (existsSync(sourceAssetDir)) {
|
|
2240
2297
|
await this.createSymlinkIfMissing(sourceAssetDir, targetAssetDir, 'dir');
|
|
2241
2298
|
}
|
|
@@ -2648,6 +2705,39 @@ Run: photon mcp ${mcpName} --config
|
|
|
2648
2705
|
if (mcp.instance._photonConfigError) {
|
|
2649
2706
|
throw new Error(mcp.instance._photonConfigError);
|
|
2650
2707
|
}
|
|
2708
|
+
// Enforce @auth at method level — works across ALL transports (CLI, STDIO, HTTP, daemon)
|
|
2709
|
+
const photonAuth = mcp.auth;
|
|
2710
|
+
if (photonAuth === 'required') {
|
|
2711
|
+
const caller = options?.caller;
|
|
2712
|
+
if (!caller || caller.anonymous) {
|
|
2713
|
+
const inputProvider = options?.inputProvider || this.createInputProvider();
|
|
2714
|
+
try {
|
|
2715
|
+
const token = await inputProvider({
|
|
2716
|
+
ask: 'password',
|
|
2717
|
+
message: `${mcp.name} requires authentication. Enter your access token:`,
|
|
2718
|
+
});
|
|
2719
|
+
if (token && typeof token === 'string') {
|
|
2720
|
+
options = {
|
|
2721
|
+
...options,
|
|
2722
|
+
caller: {
|
|
2723
|
+
id: token.slice(0, 8),
|
|
2724
|
+
name: 'authenticated-user',
|
|
2725
|
+
anonymous: false,
|
|
2726
|
+
claims: { token },
|
|
2727
|
+
},
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
else {
|
|
2731
|
+
throw new Error(`Authentication required: ${mcp.name} has @auth required but no credentials were provided`);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
catch (e) {
|
|
2735
|
+
if (e.message?.includes('Authentication required'))
|
|
2736
|
+
throw e;
|
|
2737
|
+
throw new Error(`Authentication required: ${mcp.name} has @auth required. Provide credentials via Authorization header, MCP elicitation, or CLI prompt.`);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2651
2741
|
// Intercept auto-generated settings tool
|
|
2652
2742
|
if (toolName === 'settings' && mcp.instance._settingsBacking) {
|
|
2653
2743
|
const result = await this.executeSettingsTool(mcp.instance, parameters, {
|
|
@@ -2659,7 +2749,10 @@ Run: photon mcp ${mcpName} --config
|
|
|
2659
2749
|
return result;
|
|
2660
2750
|
}
|
|
2661
2751
|
// Get tool metadata for functional tags
|
|
2662
|
-
const
|
|
2752
|
+
const rawToolMeta = mcp.tools.find((t) => t.name === toolName) || {};
|
|
2753
|
+
const normalized = this.normalizeNestedParamsTool(rawToolMeta, parameters);
|
|
2754
|
+
const toolMeta = normalized.toolMeta;
|
|
2755
|
+
parameters = normalized.parameters;
|
|
2663
2756
|
const hasFunctionalTags = toolMeta.middleware?.length > 0;
|
|
2664
2757
|
// Validate required parameters before execution
|
|
2665
2758
|
const requiredParams = toolMeta.inputSchema?.required || [];
|
|
@@ -2909,6 +3002,47 @@ Run: photon mcp ${mcpName} --config
|
|
|
2909
3002
|
span.end();
|
|
2910
3003
|
}
|
|
2911
3004
|
}
|
|
3005
|
+
normalizeNestedParamsTool(toolMeta, parameters) {
|
|
3006
|
+
const schema = toolMeta?.inputSchema;
|
|
3007
|
+
if (!schema?.properties || typeof schema.properties !== 'object') {
|
|
3008
|
+
return { toolMeta, parameters };
|
|
3009
|
+
}
|
|
3010
|
+
const propNames = Object.keys(schema.properties);
|
|
3011
|
+
if (propNames.length !== 1 || propNames[0] !== 'params') {
|
|
3012
|
+
return { toolMeta, parameters };
|
|
3013
|
+
}
|
|
3014
|
+
const nested = schema.properties.params;
|
|
3015
|
+
if (!nested ||
|
|
3016
|
+
nested.type !== 'object' ||
|
|
3017
|
+
!nested.properties ||
|
|
3018
|
+
typeof nested.properties !== 'object') {
|
|
3019
|
+
return { toolMeta, parameters };
|
|
3020
|
+
}
|
|
3021
|
+
const normalizedToolMeta = {
|
|
3022
|
+
...toolMeta,
|
|
3023
|
+
inputSchema: {
|
|
3024
|
+
type: 'object',
|
|
3025
|
+
properties: nested.properties,
|
|
3026
|
+
...(Array.isArray(nested.required) ? { required: nested.required } : {}),
|
|
3027
|
+
},
|
|
3028
|
+
// Named object params should stay as a single object argument at runtime.
|
|
3029
|
+
simpleParams: false,
|
|
3030
|
+
};
|
|
3031
|
+
if (parameters &&
|
|
3032
|
+
typeof parameters === 'object' &&
|
|
3033
|
+
'params' in parameters &&
|
|
3034
|
+
parameters.params &&
|
|
3035
|
+
typeof parameters.params === 'object') {
|
|
3036
|
+
return {
|
|
3037
|
+
toolMeta: normalizedToolMeta,
|
|
3038
|
+
parameters: parameters.params,
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
return {
|
|
3042
|
+
toolMeta: normalizedToolMeta,
|
|
3043
|
+
parameters,
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
2912
3046
|
/**
|
|
2913
3047
|
* Create an input provider for generator ask yields
|
|
2914
3048
|
* Supports the new ask/emit pattern from photon-core 1.2.0
|
|
@@ -2994,7 +3128,7 @@ Run: photon mcp ${mcpName} --config
|
|
|
2994
3128
|
: emit.type === 'success'
|
|
2995
3129
|
? '✅'
|
|
2996
3130
|
: 'ℹ';
|
|
2997
|
-
|
|
3131
|
+
process.stdout.write(`${icon} ${emit.message}\n`);
|
|
2998
3132
|
break;
|
|
2999
3133
|
case 'thinking':
|
|
3000
3134
|
if (emit.active) {
|
|
@@ -3061,6 +3195,30 @@ Run: photon mcp ${mcpName} --config
|
|
|
3061
3195
|
* }
|
|
3062
3196
|
* ```
|
|
3063
3197
|
*/
|
|
3198
|
+
_formatCatalogCache = null;
|
|
3199
|
+
injectFormatCatalog(instance) {
|
|
3200
|
+
if (instance.formats)
|
|
3201
|
+
return;
|
|
3202
|
+
// Eagerly load once per loader, cached across all photon instances
|
|
3203
|
+
if (!this._formatCatalogCache) {
|
|
3204
|
+
try {
|
|
3205
|
+
const loaderDir = path.dirname(fileURLToPath(import.meta.url));
|
|
3206
|
+
const renderersPath = path.join(loaderDir, 'auto-ui', 'bridge', 'renderers.js');
|
|
3207
|
+
const esmRequire = createRequire(import.meta.url);
|
|
3208
|
+
this._formatCatalogCache = esmRequire(renderersPath).FORMAT_CATALOG;
|
|
3209
|
+
}
|
|
3210
|
+
catch (e) {
|
|
3211
|
+
this.logger.debug('Failed to load format catalog', { error: e?.message });
|
|
3212
|
+
this._formatCatalogCache = {};
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
Object.defineProperty(instance, 'formats', {
|
|
3216
|
+
value: this._formatCatalogCache,
|
|
3217
|
+
configurable: true,
|
|
3218
|
+
enumerable: false,
|
|
3219
|
+
writable: false,
|
|
3220
|
+
});
|
|
3221
|
+
}
|
|
3064
3222
|
wireReactiveCollections(instance) {
|
|
3065
3223
|
// Get the emit function if available
|
|
3066
3224
|
const emit = typeof instance.emit === 'function'
|
|
@@ -3109,7 +3267,18 @@ Run: photon mcp ${mcpName} --config
|
|
|
3109
3267
|
// Get all public method names from the instance
|
|
3110
3268
|
// Skip runtime-injected methods (emit, render, push, ask) — these are
|
|
3111
3269
|
// capability methods injected by the loader, not user-defined tools
|
|
3112
|
-
const RUNTIME_METHODS = new Set([
|
|
3270
|
+
const RUNTIME_METHODS = new Set([
|
|
3271
|
+
'emit',
|
|
3272
|
+
'render',
|
|
3273
|
+
'channel',
|
|
3274
|
+
'ask',
|
|
3275
|
+
'call',
|
|
3276
|
+
'toast',
|
|
3277
|
+
'log',
|
|
3278
|
+
'status',
|
|
3279
|
+
'progress',
|
|
3280
|
+
'thinking',
|
|
3281
|
+
]);
|
|
3113
3282
|
const proto = Object.getPrototypeOf(instance);
|
|
3114
3283
|
const methodNames = Object.getOwnPropertyNames(proto).filter((name) => {
|
|
3115
3284
|
// Skip constructor and private/protected methods
|
|
@@ -3134,95 +3303,35 @@ Run: photon mcp ${mcpName} --config
|
|
|
3134
3303
|
if (typeof original !== 'function')
|
|
3135
3304
|
continue;
|
|
3136
3305
|
instance[methodName] = function (...args) {
|
|
3137
|
-
// Extract parameter names and map arguments to them
|
|
3138
|
-
const paramNames = PhotonLoader.extractParamNames(original);
|
|
3139
|
-
const params = Object.fromEntries(paramNames.map((name, i) => [name, args[i]]));
|
|
3140
|
-
// Call the original method
|
|
3141
3306
|
const result = original.apply(this, args);
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
},
|
|
3157
|
-
enumerable: false,
|
|
3158
|
-
writable: true,
|
|
3159
|
-
configurable: true,
|
|
3160
|
-
});
|
|
3161
|
-
}
|
|
3162
|
-
// Emit event with complete context
|
|
3163
|
-
const eventData = {
|
|
3164
|
-
method: methodName,
|
|
3165
|
-
params,
|
|
3166
|
-
result,
|
|
3167
|
-
timestamp: new Date().toISOString(),
|
|
3168
|
-
};
|
|
3169
|
-
if (this.instanceName) {
|
|
3170
|
-
eventData.instance = this.instanceName;
|
|
3171
|
-
}
|
|
3172
|
-
// Detect array mutations for range-based pagination support (Phase 5)
|
|
3173
|
-
// If result is an object from this.items, add index and array metadata
|
|
3174
|
-
if (result && typeof result === 'object' && Array.isArray(this.items)) {
|
|
3175
|
-
const index = this.items.findIndex((item) => item === result);
|
|
3176
|
-
if (index !== -1) {
|
|
3177
|
-
eventData.index = index;
|
|
3178
|
-
eventData.totalCount = this.items.length;
|
|
3179
|
-
// Affected range: just this item
|
|
3180
|
-
eventData.affectedRange = {
|
|
3181
|
-
start: index,
|
|
3182
|
-
end: index + 1,
|
|
3183
|
-
};
|
|
3307
|
+
const attachMeta = (obj) => {
|
|
3308
|
+
if (obj && typeof obj === 'object' && !Array.isArray(obj) && !obj.__meta) {
|
|
3309
|
+
Object.defineProperty(obj, '__meta', {
|
|
3310
|
+
value: {
|
|
3311
|
+
createdAt: new Date().toISOString(),
|
|
3312
|
+
createdBy: methodName,
|
|
3313
|
+
modifiedAt: null,
|
|
3314
|
+
modifiedBy: null,
|
|
3315
|
+
modifications: [],
|
|
3316
|
+
},
|
|
3317
|
+
enumerable: false,
|
|
3318
|
+
writable: true,
|
|
3319
|
+
configurable: true,
|
|
3320
|
+
});
|
|
3184
3321
|
}
|
|
3322
|
+
};
|
|
3323
|
+
// For async methods, attach __meta to the resolved value, not the Promise
|
|
3324
|
+
if (result && typeof result.then === 'function') {
|
|
3325
|
+
return result.then((resolved) => {
|
|
3326
|
+
attachMeta(resolved);
|
|
3327
|
+
return resolved;
|
|
3328
|
+
});
|
|
3185
3329
|
}
|
|
3186
|
-
|
|
3187
|
-
// (line 2362 via outputHandler). This method wrapper is only called during
|
|
3188
|
-
// direct instantiation testing, not in actual MCP execution paths where executeTool
|
|
3189
|
-
// is the proper routing point.
|
|
3190
|
-
//
|
|
3191
|
-
// If we emit here too, we get duplicate messages:
|
|
3192
|
-
// 1. This wrapper emits directly: emit(eventData)
|
|
3193
|
-
// 2. executeTool emits via outputHandler: outputHandler(eventData)
|
|
3194
|
-
// Both route to daemon, causing double notifications.
|
|
3330
|
+
attachMeta(result);
|
|
3195
3331
|
return result;
|
|
3196
3332
|
};
|
|
3197
3333
|
}
|
|
3198
3334
|
}
|
|
3199
|
-
/**
|
|
3200
|
-
* Extract parameter names from a function by parsing its signature
|
|
3201
|
-
*
|
|
3202
|
-
* Examples:
|
|
3203
|
-
* - (text, priority = 'medium') => ['text', 'priority']
|
|
3204
|
-
* - (id) => ['id']
|
|
3205
|
-
* - () => []
|
|
3206
|
-
*/
|
|
3207
|
-
static extractParamNames(fn) {
|
|
3208
|
-
const fnStr = fn.toString();
|
|
3209
|
-
// Match parameters inside parentheses: ( ... )
|
|
3210
|
-
const match = fnStr.match(/\(([^)]*)\)/);
|
|
3211
|
-
if (!match?.[1]) {
|
|
3212
|
-
return [];
|
|
3213
|
-
}
|
|
3214
|
-
return match[1]
|
|
3215
|
-
.split(',')
|
|
3216
|
-
.map((param) => {
|
|
3217
|
-
const cleaned = param
|
|
3218
|
-
.trim()
|
|
3219
|
-
.split('=')[0] // Remove default value
|
|
3220
|
-
.split(':')[0] // Remove type annotations
|
|
3221
|
-
.trim();
|
|
3222
|
-
return cleaned;
|
|
3223
|
-
})
|
|
3224
|
-
.filter((name) => name && name !== 'this');
|
|
3225
|
-
}
|
|
3226
3335
|
/**
|
|
3227
3336
|
* Extract @mcp dependencies from source and inject them as instance properties
|
|
3228
3337
|
*
|