@portel/photon 1.20.1 → 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.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +23 -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 +680 -57
- 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 +29 -0
- 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 +7 -7
- package/dist/beam-form.bundle.js.map +1 -1
- package/dist/beam.bundle.js +905 -185
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +9 -5
- package/dist/cli/commands/build.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/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/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +17 -5
- 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 +54 -1
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +3 -0
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +88 -38
- 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 +549 -83
- 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-manager.d.ts +12 -0
- package/dist/daemon/worker-manager.d.ts.map +1 -1
- package/dist/daemon/worker-manager.js +89 -6
- package/dist/daemon/worker-manager.js.map +1 -1
- package/dist/loader.d.ts +3 -9
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +168 -113
- package/dist/loader.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +26 -2
- package/dist/photon-cli-runner.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/shared-utils.d.ts.map +1 -1
- package/dist/shared-utils.js +2 -2
- package/dist/shared-utils.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.
|
|
@@ -650,15 +682,12 @@ export class PhotonLoader {
|
|
|
650
682
|
// Auto-wire ReactiveArray/Map/Set properties for zero-boilerplate reactivity
|
|
651
683
|
// Developers just `import { Array } from '@portel/photon-core'` and use normally
|
|
652
684
|
this.wireReactiveCollections(instance);
|
|
685
|
+
// Inject format catalog — this.formats
|
|
686
|
+
this.injectFormatCatalog(instance);
|
|
653
687
|
// Inject @mcp dependencies from source (this.github, this.fs, etc.)
|
|
654
688
|
if (tsContent) {
|
|
655
689
|
await this.injectMCPDependencies(instance, tsContent, name);
|
|
656
690
|
}
|
|
657
|
-
// Auto-wrap public methods in @stateful classes to emit events
|
|
658
|
-
// All method calls automatically produce events with params, result, timestamp
|
|
659
|
-
if (tsContent) {
|
|
660
|
-
this.wrapStatefulMethods(instance, tsContent);
|
|
661
|
-
}
|
|
662
691
|
// Inject MCP client factory if available (enables this.mcp() calls)
|
|
663
692
|
const setMCPFactory = instance.setMCPFactory;
|
|
664
693
|
if (this.mcpClientFactory && typeof setMCPFactory === 'function') {
|
|
@@ -682,6 +711,8 @@ export class PhotonLoader {
|
|
|
682
711
|
// Photon base class already has these built-in, so only inject for plain classes
|
|
683
712
|
if (tsContent && typeof instance.executeTool !== 'function') {
|
|
684
713
|
const caps = detectCapabilities(tsContent);
|
|
714
|
+
if (detectEmitHelperUsage(tsContent))
|
|
715
|
+
caps.add('emit');
|
|
685
716
|
if (caps.size > 0) {
|
|
686
717
|
this.log(`🔍 Detected capabilities for ${name}: ${[...caps].join(', ')}`);
|
|
687
718
|
}
|
|
@@ -727,15 +758,8 @@ export class PhotonLoader {
|
|
|
727
758
|
});
|
|
728
759
|
}
|
|
729
760
|
};
|
|
730
|
-
//
|
|
731
|
-
instance
|
|
732
|
-
if (format === undefined) {
|
|
733
|
-
instance.emit({ emit: 'render:clear' });
|
|
734
|
-
}
|
|
735
|
-
else {
|
|
736
|
-
instance.emit({ emit: 'render', format, value });
|
|
737
|
-
}
|
|
738
|
-
};
|
|
761
|
+
// Inject convenience helpers (render, toast, log, status, progress, thinking)
|
|
762
|
+
injectEmitHelpers(instance);
|
|
739
763
|
}
|
|
740
764
|
if (caps.has('memory')) {
|
|
741
765
|
// Inject lazy memory provider — capture baseDir from loader context
|
|
@@ -1001,8 +1025,13 @@ export class PhotonLoader {
|
|
|
1001
1025
|
throw initError;
|
|
1002
1026
|
}
|
|
1003
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
|
+
}
|
|
1004
1033
|
// Extract tools, templates, and statics (with schema override support)
|
|
1005
|
-
const { tools, templates, statics, settingsSchema } = await this.extractTools(MCPClass, absolutePath);
|
|
1034
|
+
const { tools, templates, statics, settingsSchema, auth: extractedAuth, } = await this.extractTools(MCPClass, absolutePath);
|
|
1006
1035
|
// ═══ SETTINGS INJECTION ═══
|
|
1007
1036
|
// If the photon declared `protected settings = { ... }`, inject persistence + proxy
|
|
1008
1037
|
if (settingsSchema?.hasSettings &&
|
|
@@ -1052,6 +1081,8 @@ export class PhotonLoader {
|
|
|
1052
1081
|
result.icon = classIcon;
|
|
1053
1082
|
if (isStateful)
|
|
1054
1083
|
result.stateful = true;
|
|
1084
|
+
if (extractedAuth)
|
|
1085
|
+
result.auth = extractedAuth;
|
|
1055
1086
|
// Store class constructor for static method access
|
|
1056
1087
|
result.classConstructor = MCPClass;
|
|
1057
1088
|
// Store settings schema for Beam UI
|
|
@@ -1123,6 +1154,8 @@ export class PhotonLoader {
|
|
|
1123
1154
|
instance._photonResolver = (photonName, instanceName) => {
|
|
1124
1155
|
return this.resolveAndLoadPhoton(photonName, absolutePath, instanceName);
|
|
1125
1156
|
};
|
|
1157
|
+
// Inject format catalog — this.formats
|
|
1158
|
+
this.injectFormatCatalog(instance);
|
|
1126
1159
|
// Wire reactive collections
|
|
1127
1160
|
this.wireReactiveCollections(instance);
|
|
1128
1161
|
// Inject @mcp dependencies
|
|
@@ -1152,6 +1185,8 @@ export class PhotonLoader {
|
|
|
1152
1185
|
// Detect and inject capabilities for plain classes
|
|
1153
1186
|
if (tsContent && typeof instance.executeTool !== 'function') {
|
|
1154
1187
|
const caps = detectCapabilities(tsContent);
|
|
1188
|
+
if (detectEmitHelperUsage(tsContent))
|
|
1189
|
+
caps.add('emit');
|
|
1155
1190
|
this.injectPathHelpers(instance, tsContent);
|
|
1156
1191
|
if (caps.has('emit')) {
|
|
1157
1192
|
instance.emit = (data) => {
|
|
@@ -1186,15 +1221,8 @@ export class PhotonLoader {
|
|
|
1186
1221
|
});
|
|
1187
1222
|
}
|
|
1188
1223
|
};
|
|
1189
|
-
//
|
|
1190
|
-
instance
|
|
1191
|
-
if (format === undefined) {
|
|
1192
|
-
instance.emit({ emit: 'render:clear' });
|
|
1193
|
-
}
|
|
1194
|
-
else {
|
|
1195
|
-
instance.emit({ emit: 'render', format, value });
|
|
1196
|
-
}
|
|
1197
|
-
};
|
|
1224
|
+
// Inject convenience helpers (render, toast, log, status, progress, thinking)
|
|
1225
|
+
injectEmitHelpers(instance);
|
|
1198
1226
|
}
|
|
1199
1227
|
if (caps.has('memory')) {
|
|
1200
1228
|
const memoryBaseDir = this.baseDir;
|
|
@@ -1296,7 +1324,7 @@ export class PhotonLoader {
|
|
|
1296
1324
|
}
|
|
1297
1325
|
}
|
|
1298
1326
|
// Extract tools and metadata from embedded source (no disk I/O)
|
|
1299
|
-
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);
|
|
1300
1328
|
// Settings injection
|
|
1301
1329
|
if (settingsSchema?.hasSettings && instance.settings && typeof instance.settings === 'object') {
|
|
1302
1330
|
const instanceName = options?.instanceName || 'default';
|
|
@@ -1326,6 +1354,8 @@ export class PhotonLoader {
|
|
|
1326
1354
|
result.icon = classIcon;
|
|
1327
1355
|
if (isStateful)
|
|
1328
1356
|
result.stateful = true;
|
|
1357
|
+
if (extractedAuth)
|
|
1358
|
+
result.auth = extractedAuth;
|
|
1329
1359
|
result.classConstructor = MCPClass;
|
|
1330
1360
|
if (settingsSchema?.hasSettings) {
|
|
1331
1361
|
result.settingsSchema = settingsSchema;
|
|
@@ -1341,6 +1371,14 @@ export class PhotonLoader {
|
|
|
1341
1371
|
const match = source.match(/\/\*\*([\s\S]*?)\*\/\s*export\s+default\s+class\b/);
|
|
1342
1372
|
return match ? match[1] : '';
|
|
1343
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
|
+
}
|
|
1344
1382
|
/**
|
|
1345
1383
|
* Reload a Photon MCP file (for hot reload)
|
|
1346
1384
|
*/
|
|
@@ -1567,7 +1605,13 @@ export class PhotonLoader {
|
|
|
1567
1605
|
}
|
|
1568
1606
|
});
|
|
1569
1607
|
}
|
|
1570
|
-
return {
|
|
1608
|
+
return {
|
|
1609
|
+
tools,
|
|
1610
|
+
templates,
|
|
1611
|
+
statics,
|
|
1612
|
+
settingsSchema: metadata.settingsSchema,
|
|
1613
|
+
auth: this.extractAuthTag(source),
|
|
1614
|
+
};
|
|
1571
1615
|
}
|
|
1572
1616
|
throw jsonError;
|
|
1573
1617
|
}
|
|
@@ -2241,11 +2285,14 @@ export class PhotonLoader {
|
|
|
2241
2285
|
return;
|
|
2242
2286
|
if (path.dirname(realCurrentPhotonPath) !== path.dirname(resolvedPath))
|
|
2243
2287
|
return;
|
|
2244
|
-
|
|
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));
|
|
2245
2292
|
await this.createSymlinkIfMissing(resolvedPath, linkPath);
|
|
2246
2293
|
const depName = path.basename(resolvedPath).replace(/\.photon\.(ts|js)$/, '');
|
|
2247
2294
|
const sourceAssetDir = path.join(path.dirname(resolvedPath), depName);
|
|
2248
|
-
const targetAssetDir = path.join(
|
|
2295
|
+
const targetAssetDir = path.join(symlinkDir, depName);
|
|
2249
2296
|
if (existsSync(sourceAssetDir)) {
|
|
2250
2297
|
await this.createSymlinkIfMissing(sourceAssetDir, targetAssetDir, 'dir');
|
|
2251
2298
|
}
|
|
@@ -2658,6 +2705,39 @@ Run: photon mcp ${mcpName} --config
|
|
|
2658
2705
|
if (mcp.instance._photonConfigError) {
|
|
2659
2706
|
throw new Error(mcp.instance._photonConfigError);
|
|
2660
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
|
+
}
|
|
2661
2741
|
// Intercept auto-generated settings tool
|
|
2662
2742
|
if (toolName === 'settings' && mcp.instance._settingsBacking) {
|
|
2663
2743
|
const result = await this.executeSettingsTool(mcp.instance, parameters, {
|
|
@@ -3048,7 +3128,7 @@ Run: photon mcp ${mcpName} --config
|
|
|
3048
3128
|
: emit.type === 'success'
|
|
3049
3129
|
? '✅'
|
|
3050
3130
|
: 'ℹ';
|
|
3051
|
-
|
|
3131
|
+
process.stdout.write(`${icon} ${emit.message}\n`);
|
|
3052
3132
|
break;
|
|
3053
3133
|
case 'thinking':
|
|
3054
3134
|
if (emit.active) {
|
|
@@ -3115,6 +3195,30 @@ Run: photon mcp ${mcpName} --config
|
|
|
3115
3195
|
* }
|
|
3116
3196
|
* ```
|
|
3117
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
|
+
}
|
|
3118
3222
|
wireReactiveCollections(instance) {
|
|
3119
3223
|
// Get the emit function if available
|
|
3120
3224
|
const emit = typeof instance.emit === 'function'
|
|
@@ -3163,7 +3267,18 @@ Run: photon mcp ${mcpName} --config
|
|
|
3163
3267
|
// Get all public method names from the instance
|
|
3164
3268
|
// Skip runtime-injected methods (emit, render, push, ask) — these are
|
|
3165
3269
|
// capability methods injected by the loader, not user-defined tools
|
|
3166
|
-
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
|
+
]);
|
|
3167
3282
|
const proto = Object.getPrototypeOf(instance);
|
|
3168
3283
|
const methodNames = Object.getOwnPropertyNames(proto).filter((name) => {
|
|
3169
3284
|
// Skip constructor and private/protected methods
|
|
@@ -3188,95 +3303,35 @@ Run: photon mcp ${mcpName} --config
|
|
|
3188
3303
|
if (typeof original !== 'function')
|
|
3189
3304
|
continue;
|
|
3190
3305
|
instance[methodName] = function (...args) {
|
|
3191
|
-
// Extract parameter names and map arguments to them
|
|
3192
|
-
const paramNames = PhotonLoader.extractParamNames(original);
|
|
3193
|
-
const params = Object.fromEntries(paramNames.map((name, i) => [name, args[i]]));
|
|
3194
|
-
// Call the original method
|
|
3195
3306
|
const result = original.apply(this, args);
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
},
|
|
3211
|
-
enumerable: false,
|
|
3212
|
-
writable: true,
|
|
3213
|
-
configurable: true,
|
|
3214
|
-
});
|
|
3215
|
-
}
|
|
3216
|
-
// Emit event with complete context
|
|
3217
|
-
const eventData = {
|
|
3218
|
-
method: methodName,
|
|
3219
|
-
params,
|
|
3220
|
-
result,
|
|
3221
|
-
timestamp: new Date().toISOString(),
|
|
3222
|
-
};
|
|
3223
|
-
if (this.instanceName) {
|
|
3224
|
-
eventData.instance = this.instanceName;
|
|
3225
|
-
}
|
|
3226
|
-
// Detect array mutations for range-based pagination support (Phase 5)
|
|
3227
|
-
// If result is an object from this.items, add index and array metadata
|
|
3228
|
-
if (result && typeof result === 'object' && Array.isArray(this.items)) {
|
|
3229
|
-
const index = this.items.findIndex((item) => item === result);
|
|
3230
|
-
if (index !== -1) {
|
|
3231
|
-
eventData.index = index;
|
|
3232
|
-
eventData.totalCount = this.items.length;
|
|
3233
|
-
// Affected range: just this item
|
|
3234
|
-
eventData.affectedRange = {
|
|
3235
|
-
start: index,
|
|
3236
|
-
end: index + 1,
|
|
3237
|
-
};
|
|
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
|
+
});
|
|
3238
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
|
+
});
|
|
3239
3329
|
}
|
|
3240
|
-
|
|
3241
|
-
// (line 2362 via outputHandler). This method wrapper is only called during
|
|
3242
|
-
// direct instantiation testing, not in actual MCP execution paths where executeTool
|
|
3243
|
-
// is the proper routing point.
|
|
3244
|
-
//
|
|
3245
|
-
// If we emit here too, we get duplicate messages:
|
|
3246
|
-
// 1. This wrapper emits directly: emit(eventData)
|
|
3247
|
-
// 2. executeTool emits via outputHandler: outputHandler(eventData)
|
|
3248
|
-
// Both route to daemon, causing double notifications.
|
|
3330
|
+
attachMeta(result);
|
|
3249
3331
|
return result;
|
|
3250
3332
|
};
|
|
3251
3333
|
}
|
|
3252
3334
|
}
|
|
3253
|
-
/**
|
|
3254
|
-
* Extract parameter names from a function by parsing its signature
|
|
3255
|
-
*
|
|
3256
|
-
* Examples:
|
|
3257
|
-
* - (text, priority = 'medium') => ['text', 'priority']
|
|
3258
|
-
* - (id) => ['id']
|
|
3259
|
-
* - () => []
|
|
3260
|
-
*/
|
|
3261
|
-
static extractParamNames(fn) {
|
|
3262
|
-
const fnStr = fn.toString();
|
|
3263
|
-
// Match parameters inside parentheses: ( ... )
|
|
3264
|
-
const match = fnStr.match(/\(([^)]*)\)/);
|
|
3265
|
-
if (!match?.[1]) {
|
|
3266
|
-
return [];
|
|
3267
|
-
}
|
|
3268
|
-
return match[1]
|
|
3269
|
-
.split(',')
|
|
3270
|
-
.map((param) => {
|
|
3271
|
-
const cleaned = param
|
|
3272
|
-
.trim()
|
|
3273
|
-
.split('=')[0] // Remove default value
|
|
3274
|
-
.split(':')[0] // Remove type annotations
|
|
3275
|
-
.trim();
|
|
3276
|
-
return cleaned;
|
|
3277
|
-
})
|
|
3278
|
-
.filter((name) => name && name !== 'this');
|
|
3279
|
-
}
|
|
3280
3335
|
/**
|
|
3281
3336
|
* Extract @mcp dependencies from source and inject them as instance properties
|
|
3282
3337
|
*
|