@portel/photon 1.10.0 → 1.12.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 +81 -72
- package/dist/auto-ui/beam/photon-management.d.ts.map +1 -1
- package/dist/auto-ui/beam/photon-management.js +5 -0
- package/dist/auto-ui/beam/photon-management.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.d.ts +1 -2
- package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.js +140 -191
- 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 +44 -1
- 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 +874 -20
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/frontend/index.html +83 -60
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +16 -2
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +1 -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 +2836 -357
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli/commands/package-app.d.ts.map +1 -1
- package/dist/cli/commands/package-app.js +116 -35
- package/dist/cli/commands/package-app.js.map +1 -1
- package/dist/context-store.d.ts +5 -0
- package/dist/context-store.d.ts.map +1 -1
- package/dist/context-store.js +9 -0
- package/dist/context-store.js.map +1 -1
- package/dist/daemon/server.js +303 -6
- package/dist/daemon/server.js.map +1 -1
- package/dist/loader.d.ts +21 -0
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +277 -0
- package/dist/loader.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +21 -1
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts +6 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +22 -0
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/photons/tunnel.photon.d.ts +5 -9
- package/dist/photons/tunnel.photon.d.ts.map +1 -1
- package/dist/photons/tunnel.photon.js +36 -96
- package/dist/photons/tunnel.photon.js.map +1 -1
- package/dist/photons/tunnel.photon.ts +40 -112
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +27 -2
- package/dist/server.js.map +1 -1
- package/dist/test-runner.d.ts +13 -1
- package/dist/test-runner.d.ts.map +1 -1
- package/dist/test-runner.js +529 -122
- package/dist/test-runner.js.map +1 -1
- package/package.json +22 -6
package/dist/loader.js
CHANGED
|
@@ -451,6 +451,11 @@ export class PhotonLoader {
|
|
|
451
451
|
if (tsContent) {
|
|
452
452
|
await this.injectMCPDependencies(instance, tsContent, name);
|
|
453
453
|
}
|
|
454
|
+
// Auto-wrap public methods in @stateful classes to emit events
|
|
455
|
+
// All method calls automatically produce events with params, result, timestamp
|
|
456
|
+
if (tsContent) {
|
|
457
|
+
this.wrapStatefulMethods(instance, tsContent);
|
|
458
|
+
}
|
|
454
459
|
// Inject MCP client factory if available (enables this.mcp() calls)
|
|
455
460
|
const setMCPFactory = instance.setMCPFactory;
|
|
456
461
|
if (this.mcpClientFactory && typeof setMCPFactory === 'function') {
|
|
@@ -796,6 +801,28 @@ export class PhotonLoader {
|
|
|
796
801
|
});
|
|
797
802
|
return methods;
|
|
798
803
|
}
|
|
804
|
+
/**
|
|
805
|
+
* Strip JSDoc tags from descriptions (e.g., @emits, @internal, @deprecated)
|
|
806
|
+
*/
|
|
807
|
+
stripJSDocTags(description) {
|
|
808
|
+
if (!description)
|
|
809
|
+
return '';
|
|
810
|
+
if (description.includes('@emits') && process.env.PHOTON_DEBUG_EXTRACT) {
|
|
811
|
+
console.log(`[stripJSDocTags] Input: "${description}"`);
|
|
812
|
+
}
|
|
813
|
+
// Remove lines that start with @ (full line removal)
|
|
814
|
+
let cleaned = description
|
|
815
|
+
.split('\n')
|
|
816
|
+
.filter((line) => !line.trim().startsWith('@'))
|
|
817
|
+
.join('\n')
|
|
818
|
+
.trim();
|
|
819
|
+
// Also remove inline @ tags (e.g., "text @emits ... " at end of line)
|
|
820
|
+
cleaned = cleaned.replace(/\s*@\w+.*$/gm, '').trim();
|
|
821
|
+
if (description.includes('@emits') && process.env.PHOTON_DEBUG_EXTRACT) {
|
|
822
|
+
console.log(`[stripJSDocTags] Output: "${cleaned}"`);
|
|
823
|
+
}
|
|
824
|
+
return cleaned;
|
|
825
|
+
}
|
|
799
826
|
/**
|
|
800
827
|
* Extract tools, templates, and statics from a class
|
|
801
828
|
*/
|
|
@@ -849,6 +876,13 @@ export class PhotonLoader {
|
|
|
849
876
|
}
|
|
850
877
|
}
|
|
851
878
|
this.log(`Loaded ${tools.length} tools, ${templates.length} templates, ${statics.length} statics from .schema.json override`);
|
|
879
|
+
// Clean JSDoc tags from descriptions
|
|
880
|
+
tools = tools.map((t) => ({ ...t, description: this.stripJSDocTags(t.description) }));
|
|
881
|
+
templates = templates.map((t) => ({
|
|
882
|
+
...t,
|
|
883
|
+
description: this.stripJSDocTags(t.description),
|
|
884
|
+
}));
|
|
885
|
+
statics = statics.map((s) => ({ ...s, description: this.stripJSDocTags(s.description) }));
|
|
852
886
|
return { tools, templates, statics };
|
|
853
887
|
}
|
|
854
888
|
catch (jsonError) {
|
|
@@ -865,6 +899,27 @@ export class PhotonLoader {
|
|
|
865
899
|
templates = metadata.templates.filter((t) => methodNames.includes(t.name));
|
|
866
900
|
statics = metadata.statics.filter((s) => methodNames.includes(s.name));
|
|
867
901
|
this.log(`Extracted ${tools.length} tools, ${templates.length} templates, ${statics.length} statics from source`);
|
|
902
|
+
// Clean JSDoc tags from descriptions
|
|
903
|
+
if (process.env.PHOTON_DEBUG_EXTRACT) {
|
|
904
|
+
tools.forEach((t) => {
|
|
905
|
+
if (t.description?.includes('@')) {
|
|
906
|
+
console.log(`[EXTRACTOR] Before clean: ${t.name}: "${t.description}"`);
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
tools = tools.map((t) => ({ ...t, description: this.stripJSDocTags(t.description) }));
|
|
911
|
+
templates = templates.map((t) => ({
|
|
912
|
+
...t,
|
|
913
|
+
description: this.stripJSDocTags(t.description),
|
|
914
|
+
}));
|
|
915
|
+
statics = statics.map((s) => ({ ...s, description: this.stripJSDocTags(s.description) }));
|
|
916
|
+
if (process.env.PHOTON_DEBUG_EXTRACT) {
|
|
917
|
+
tools.forEach((t) => {
|
|
918
|
+
if (t.name === 'clear') {
|
|
919
|
+
console.log(`[EXTRACTOR] After clean: ${t.name}: "${t.description}"`);
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
}
|
|
868
923
|
return { tools, templates, statics, settingsSchema: metadata.settingsSchema };
|
|
869
924
|
}
|
|
870
925
|
throw jsonError;
|
|
@@ -884,6 +939,10 @@ export class PhotonLoader {
|
|
|
884
939
|
});
|
|
885
940
|
}
|
|
886
941
|
}
|
|
942
|
+
// Clean JSDoc tags from all descriptions (final safety net)
|
|
943
|
+
tools = tools.map((t) => ({ ...t, description: this.stripJSDocTags(t.description) }));
|
|
944
|
+
templates = templates.map((t) => ({ ...t, description: this.stripJSDocTags(t.description) }));
|
|
945
|
+
statics = statics.map((s) => ({ ...s, description: this.stripJSDocTags(s.description) }));
|
|
887
946
|
return { tools, templates, statics };
|
|
888
947
|
}
|
|
889
948
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -1762,6 +1821,99 @@ Run: photon mcp ${mcpName} --config
|
|
|
1762
1821
|
const result = await executeBase();
|
|
1763
1822
|
this.progressRenderer.done();
|
|
1764
1823
|
auditFinish(result);
|
|
1824
|
+
// CRITICAL FIX: Emit @stateful events at executeTool level
|
|
1825
|
+
// The method wrapper approach is bypassed by Photon.executeTool(),
|
|
1826
|
+
// so we must emit events here where ALL tool executions are guaranteed to pass through
|
|
1827
|
+
if (mcp.instance && typeof mcp.instance.emit === 'function') {
|
|
1828
|
+
try {
|
|
1829
|
+
// Check if this is a @stateful photon by looking for emit method and @stateful tag
|
|
1830
|
+
const photonName = mcp.name;
|
|
1831
|
+
const hasStatefulTag = mcp.tools.some((t) => t.name === toolName); // Assume @stateful if loaded
|
|
1832
|
+
// Attach __meta to result if it's an object and doesn't already have it
|
|
1833
|
+
if (result && typeof result === 'object' && !Array.isArray(result) && !result.__meta) {
|
|
1834
|
+
const timestamp = new Date().toISOString();
|
|
1835
|
+
Object.defineProperty(result, '__meta', {
|
|
1836
|
+
value: {
|
|
1837
|
+
createdAt: timestamp,
|
|
1838
|
+
createdBy: toolName,
|
|
1839
|
+
modifiedAt: null,
|
|
1840
|
+
modifiedBy: null,
|
|
1841
|
+
modifications: [],
|
|
1842
|
+
},
|
|
1843
|
+
enumerable: false,
|
|
1844
|
+
writable: true,
|
|
1845
|
+
configurable: true,
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
// Construct event data with full context for transmission
|
|
1849
|
+
// CRITICAL: emit() expects { channel, event, data } structure for daemon pub/sub routing
|
|
1850
|
+
const eventPayload = {
|
|
1851
|
+
method: toolName,
|
|
1852
|
+
params: parameters,
|
|
1853
|
+
result,
|
|
1854
|
+
timestamp: new Date().toISOString(),
|
|
1855
|
+
};
|
|
1856
|
+
// Add instance name if available
|
|
1857
|
+
if (mcp.instance.instanceName) {
|
|
1858
|
+
eventPayload.instance = mcp.instance.instanceName;
|
|
1859
|
+
}
|
|
1860
|
+
// Add index/pagination info if result is from items array
|
|
1861
|
+
if (result && typeof result === 'object' && Array.isArray(mcp.instance.items)) {
|
|
1862
|
+
const index = mcp.instance.items.findIndex((item) => item === result);
|
|
1863
|
+
if (index !== -1) {
|
|
1864
|
+
eventPayload.index = index;
|
|
1865
|
+
eventPayload.totalCount = mcp.instance.items.length;
|
|
1866
|
+
eventPayload.affectedRange = {
|
|
1867
|
+
start: index,
|
|
1868
|
+
end: index + 1,
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
// Wrap in daemon pub/sub format: { channel, event, data }
|
|
1873
|
+
const eventData = {
|
|
1874
|
+
channel: `${photonName}:${toolName}`, // For daemon pub/sub routing
|
|
1875
|
+
event: 'tool-executed',
|
|
1876
|
+
data: eventPayload,
|
|
1877
|
+
};
|
|
1878
|
+
// Send event through outputHandler for daemon pub/sub routing
|
|
1879
|
+
// This is the critical path that routes events to subscribers through the daemon
|
|
1880
|
+
if (options?.outputHandler) {
|
|
1881
|
+
try {
|
|
1882
|
+
if (process.env.PHOTON_DEBUG_EMIT === '1') {
|
|
1883
|
+
console.error(`[EMIT-DEBUG] Sending event: method=${eventPayload.method}, channel=${eventData.channel}, hasMeta=${!!result?.__meta}`);
|
|
1884
|
+
}
|
|
1885
|
+
// Cast to any - outputHandler is flexible and routes any object with channel property
|
|
1886
|
+
void Promise.resolve(options.outputHandler(eventData)).catch(() => {
|
|
1887
|
+
// Ignore output handler errors - don't break tool execution
|
|
1888
|
+
});
|
|
1889
|
+
if (process.env.PHOTON_DEBUG_EMIT === '1') {
|
|
1890
|
+
console.error(`[EMIT-DEBUG] Event transmitted to outputHandler`);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
catch (e) {
|
|
1894
|
+
console.error(`[EMIT-ERROR] Failed to send event through outputHandler: ${e instanceof Error ? e.message : String(e)}`);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
else if (mcp.instance && typeof mcp.instance.emit === 'function') {
|
|
1898
|
+
// Fallback for cases where outputHandler isn't available
|
|
1899
|
+
// (this path won't route to daemon pub/sub, but at least calls emit)
|
|
1900
|
+
try {
|
|
1901
|
+
if (process.env.PHOTON_DEBUG_EMIT === '1') {
|
|
1902
|
+
console.error(`[EMIT-DEBUG] No outputHandler, falling back to instance.emit`);
|
|
1903
|
+
}
|
|
1904
|
+
mcp.instance.emit(eventData);
|
|
1905
|
+
}
|
|
1906
|
+
catch (e) {
|
|
1907
|
+
console.error(`[EMIT-ERROR] Failed to emit: ${e instanceof Error ? e.message : String(e)}`);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
catch (e) {
|
|
1912
|
+
// Log emit errors but don't break tool execution
|
|
1913
|
+
console.error(`[EMIT-ERROR] Failed to emit @stateful event: ${e instanceof Error ? e.message : String(e)}`);
|
|
1914
|
+
this.logger.debug(`Failed to emit @stateful event: ${e instanceof Error ? e.message : String(e)}`);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1765
1917
|
return result;
|
|
1766
1918
|
}
|
|
1767
1919
|
// Plain class - call method directly with implicit stateful support
|
|
@@ -2016,6 +2168,131 @@ Run: photon mcp ${mcpName} --config
|
|
|
2016
2168
|
}
|
|
2017
2169
|
}
|
|
2018
2170
|
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Wrap all public methods in @stateful classes to automatically emit events
|
|
2173
|
+
*
|
|
2174
|
+
* Event structure: { method, params, result, timestamp, instance }
|
|
2175
|
+
* Every public method call produces an event with the method name, input parameters,
|
|
2176
|
+
* return value, and timestamp. This enables real-time UI sync and event-driven architectures.
|
|
2177
|
+
*/
|
|
2178
|
+
wrapStatefulMethods(instance, source) {
|
|
2179
|
+
// Check if this class has @stateful decorator
|
|
2180
|
+
if (!/@stateful\b/i.test(source)) {
|
|
2181
|
+
return; // Not a @stateful class
|
|
2182
|
+
}
|
|
2183
|
+
// Get the emit function if available
|
|
2184
|
+
const emit = typeof instance.emit === 'function'
|
|
2185
|
+
? instance.emit.bind(instance)
|
|
2186
|
+
: null;
|
|
2187
|
+
if (!emit) {
|
|
2188
|
+
return; // No emit function, can't wrap methods
|
|
2189
|
+
}
|
|
2190
|
+
// Get all public method names from the instance
|
|
2191
|
+
const proto = Object.getPrototypeOf(instance);
|
|
2192
|
+
const methodNames = Object.getOwnPropertyNames(proto).filter((name) => {
|
|
2193
|
+
// Skip constructor and private/protected methods
|
|
2194
|
+
if (name === 'constructor' || name.startsWith('_')) {
|
|
2195
|
+
return false;
|
|
2196
|
+
}
|
|
2197
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, name);
|
|
2198
|
+
return descriptor && typeof descriptor.value === 'function';
|
|
2199
|
+
});
|
|
2200
|
+
if (methodNames.length === 0) {
|
|
2201
|
+
return; // No public methods to wrap
|
|
2202
|
+
}
|
|
2203
|
+
const photonName = instance._photonName || 'photon';
|
|
2204
|
+
this.log(`📡 Wrapping ${methodNames.length} methods in @stateful ${photonName}`);
|
|
2205
|
+
// Wrap each public method
|
|
2206
|
+
for (const methodName of methodNames) {
|
|
2207
|
+
const original = instance[methodName];
|
|
2208
|
+
if (typeof original !== 'function')
|
|
2209
|
+
continue;
|
|
2210
|
+
instance[methodName] = function (...args) {
|
|
2211
|
+
// Extract parameter names and map arguments to them
|
|
2212
|
+
const paramNames = PhotonLoader.extractParamNames(original);
|
|
2213
|
+
const params = Object.fromEntries(paramNames.map((name, i) => [name, args[i]]));
|
|
2214
|
+
// Call the original method
|
|
2215
|
+
const result = original.apply(this, args);
|
|
2216
|
+
// Attach __meta to returned objects for audit trail
|
|
2217
|
+
if (result && typeof result === 'object' && !Array.isArray(result) && !result.__meta) {
|
|
2218
|
+
const timestamp = new Date().toISOString();
|
|
2219
|
+
Object.defineProperty(result, '__meta', {
|
|
2220
|
+
value: {
|
|
2221
|
+
createdAt: timestamp,
|
|
2222
|
+
createdBy: methodName,
|
|
2223
|
+
modifiedAt: null,
|
|
2224
|
+
modifiedBy: null,
|
|
2225
|
+
modifications: [],
|
|
2226
|
+
},
|
|
2227
|
+
enumerable: false,
|
|
2228
|
+
writable: true,
|
|
2229
|
+
configurable: true,
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
// Emit event with complete context
|
|
2233
|
+
const eventData = {
|
|
2234
|
+
method: methodName,
|
|
2235
|
+
params,
|
|
2236
|
+
result,
|
|
2237
|
+
timestamp: new Date().toISOString(),
|
|
2238
|
+
};
|
|
2239
|
+
if (this.instanceName) {
|
|
2240
|
+
eventData.instance = this.instanceName;
|
|
2241
|
+
}
|
|
2242
|
+
// Detect array mutations for range-based pagination support (Phase 5)
|
|
2243
|
+
// If result is an object from this.items, add index and array metadata
|
|
2244
|
+
if (result && typeof result === 'object' && Array.isArray(this.items)) {
|
|
2245
|
+
const index = this.items.findIndex((item) => item === result);
|
|
2246
|
+
if (index !== -1) {
|
|
2247
|
+
eventData.index = index;
|
|
2248
|
+
eventData.totalCount = this.items.length;
|
|
2249
|
+
// Affected range: just this item
|
|
2250
|
+
eventData.affectedRange = {
|
|
2251
|
+
start: index,
|
|
2252
|
+
end: index + 1,
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
// NOTE: Don't emit here - the real emission happens at executeTool level
|
|
2257
|
+
// (line 2362 via outputHandler). This method wrapper is only called during
|
|
2258
|
+
// direct instantiation testing, not in actual MCP execution paths where executeTool
|
|
2259
|
+
// is the proper routing point.
|
|
2260
|
+
//
|
|
2261
|
+
// If we emit here too, we get duplicate messages:
|
|
2262
|
+
// 1. This wrapper emits directly: emit(eventData)
|
|
2263
|
+
// 2. executeTool emits via outputHandler: outputHandler(eventData)
|
|
2264
|
+
// Both route to daemon, causing double notifications.
|
|
2265
|
+
return result;
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Extract parameter names from a function by parsing its signature
|
|
2271
|
+
*
|
|
2272
|
+
* Examples:
|
|
2273
|
+
* - (text, priority = 'medium') => ['text', 'priority']
|
|
2274
|
+
* - (id) => ['id']
|
|
2275
|
+
* - () => []
|
|
2276
|
+
*/
|
|
2277
|
+
static extractParamNames(fn) {
|
|
2278
|
+
const fnStr = fn.toString();
|
|
2279
|
+
// Match parameters inside parentheses: ( ... )
|
|
2280
|
+
const match = fnStr.match(/\(([^)]*)\)/);
|
|
2281
|
+
if (!match?.[1]) {
|
|
2282
|
+
return [];
|
|
2283
|
+
}
|
|
2284
|
+
return match[1]
|
|
2285
|
+
.split(',')
|
|
2286
|
+
.map((param) => {
|
|
2287
|
+
const cleaned = param
|
|
2288
|
+
.trim()
|
|
2289
|
+
.split('=')[0] // Remove default value
|
|
2290
|
+
.split(':')[0] // Remove type annotations
|
|
2291
|
+
.trim();
|
|
2292
|
+
return cleaned;
|
|
2293
|
+
})
|
|
2294
|
+
.filter((name) => name && name !== 'this');
|
|
2295
|
+
}
|
|
2019
2296
|
/**
|
|
2020
2297
|
* Extract @mcp dependencies from source and inject them as instance properties
|
|
2021
2298
|
*
|