@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.
Files changed (56) hide show
  1. package/README.md +81 -72
  2. package/dist/auto-ui/beam/photon-management.d.ts.map +1 -1
  3. package/dist/auto-ui/beam/photon-management.js +5 -0
  4. package/dist/auto-ui/beam/photon-management.js.map +1 -1
  5. package/dist/auto-ui/beam/routes/api-browse.d.ts +1 -2
  6. package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
  7. package/dist/auto-ui/beam/routes/api-browse.js +140 -191
  8. package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
  9. package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
  10. package/dist/auto-ui/beam/routes/api-config.js +44 -1
  11. package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
  12. package/dist/auto-ui/beam.d.ts.map +1 -1
  13. package/dist/auto-ui/beam.js +874 -20
  14. package/dist/auto-ui/beam.js.map +1 -1
  15. package/dist/auto-ui/frontend/index.html +83 -60
  16. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  17. package/dist/auto-ui/streamable-http-transport.js +16 -2
  18. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  19. package/dist/auto-ui/types.d.ts +1 -1
  20. package/dist/auto-ui/types.d.ts.map +1 -1
  21. package/dist/auto-ui/types.js.map +1 -1
  22. package/dist/beam.bundle.js +2836 -357
  23. package/dist/beam.bundle.js.map +4 -4
  24. package/dist/cli/commands/package-app.d.ts.map +1 -1
  25. package/dist/cli/commands/package-app.js +116 -35
  26. package/dist/cli/commands/package-app.js.map +1 -1
  27. package/dist/context-store.d.ts +5 -0
  28. package/dist/context-store.d.ts.map +1 -1
  29. package/dist/context-store.js +9 -0
  30. package/dist/context-store.js.map +1 -1
  31. package/dist/daemon/server.js +303 -6
  32. package/dist/daemon/server.js.map +1 -1
  33. package/dist/loader.d.ts +21 -0
  34. package/dist/loader.d.ts.map +1 -1
  35. package/dist/loader.js +277 -0
  36. package/dist/loader.js.map +1 -1
  37. package/dist/photon-cli-runner.d.ts.map +1 -1
  38. package/dist/photon-cli-runner.js +21 -1
  39. package/dist/photon-cli-runner.js.map +1 -1
  40. package/dist/photon-doc-extractor.d.ts +6 -0
  41. package/dist/photon-doc-extractor.d.ts.map +1 -1
  42. package/dist/photon-doc-extractor.js +22 -0
  43. package/dist/photon-doc-extractor.js.map +1 -1
  44. package/dist/photons/tunnel.photon.d.ts +5 -9
  45. package/dist/photons/tunnel.photon.d.ts.map +1 -1
  46. package/dist/photons/tunnel.photon.js +36 -96
  47. package/dist/photons/tunnel.photon.js.map +1 -1
  48. package/dist/photons/tunnel.photon.ts +40 -112
  49. package/dist/server.d.ts.map +1 -1
  50. package/dist/server.js +27 -2
  51. package/dist/server.js.map +1 -1
  52. package/dist/test-runner.d.ts +13 -1
  53. package/dist/test-runner.d.ts.map +1 -1
  54. package/dist/test-runner.js +529 -122
  55. package/dist/test-runner.js.map +1 -1
  56. 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
  *