@portel/photon 1.25.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/loader.js CHANGED
@@ -1249,7 +1249,17 @@ export class PhotonLoader {
1249
1249
  permissionHandler?.(request);
1250
1250
  },
1251
1251
  });
1252
- instance.channel = channelFn;
1252
+ // Only inject if 'channel' is not already defined as a getter on the instance/prototype.
1253
+ // Some photons define a private `get channel()` getter for their own internal use —
1254
+ // attempting a plain assignment onto a getter-only property throws in strict mode.
1255
+ const channelDescriptor = Object.getOwnPropertyDescriptor(instance, 'channel') ??
1256
+ Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), 'channel');
1257
+ const channelIsGetter = channelDescriptor?.get !== undefined;
1258
+ if (!channelIsGetter) {
1259
+ instance.channel = channelFn;
1260
+ }
1261
+ // If the photon already has its own getter for 'channel', skip injection —
1262
+ // the photon is using the name for its own purpose and likely calls this.emit() directly.
1253
1263
  }
1254
1264
  // Check @cli dependencies (required system CLI tools)
1255
1265
  if (tsContent) {
@@ -1266,7 +1276,7 @@ export class PhotonLoader {
1266
1276
  this.wrapStatefulMethods(instance, tsContent);
1267
1277
  }
1268
1278
  // Extract tools, templates, and statics (with schema override support)
1269
- const { tools, templates, statics, settingsSchema, auth: extractedAuth, } = await this.extractTools(MCPClass, absolutePath);
1279
+ const { tools, templates, statics, settingsSchema, auth: extractedAuth, httpRoutes: extractedHttpRoutes, } = await this.extractTools(MCPClass, absolutePath);
1270
1280
  // ═══ SETTINGS INJECTION ═══
1271
1281
  // If the photon declared `protected settings = { ... }`, inject persistence + proxy
1272
1282
  if (settingsSchema?.hasSettings &&
@@ -1318,6 +1328,8 @@ export class PhotonLoader {
1318
1328
  result.stateful = true;
1319
1329
  if (extractedAuth)
1320
1330
  result.auth = extractedAuth;
1331
+ if (extractedHttpRoutes?.length)
1332
+ result._httpRoutes = extractedHttpRoutes;
1321
1333
  // Store class constructor for static method access
1322
1334
  result.classConstructor = MCPClass;
1323
1335
  // Store settings schema for Beam UI
@@ -1621,7 +1633,7 @@ export class PhotonLoader {
1621
1633
  // Call lifecycle hook
1622
1634
  await this.invokeInitialize(instance, name, options);
1623
1635
  // Extract tools and metadata from embedded source (no disk I/O)
1624
- const { tools, templates, statics, settingsSchema, auth: extractedAuth, } = await this.extractTools(MCPClass, absolutePath, tsContent);
1636
+ const { tools, templates, statics, settingsSchema, auth: extractedAuth, httpRoutes: extractedHttpRoutes, } = await this.extractTools(MCPClass, absolutePath, tsContent);
1625
1637
  // Settings injection
1626
1638
  if (settingsSchema?.hasSettings && instance.settings && typeof instance.settings === 'object') {
1627
1639
  const instanceName = options?.instanceName || 'default';
@@ -1653,6 +1665,8 @@ export class PhotonLoader {
1653
1665
  result.stateful = true;
1654
1666
  if (extractedAuth)
1655
1667
  result.auth = extractedAuth;
1668
+ if (extractedHttpRoutes?.length)
1669
+ result._httpRoutes = extractedHttpRoutes;
1656
1670
  result.classConstructor = MCPClass;
1657
1671
  if (settingsSchema?.hasSettings) {
1658
1672
  result.settingsSchema = settingsSchema;
@@ -1806,6 +1820,21 @@ export class PhotonLoader {
1806
1820
  }
1807
1821
  return cleaned;
1808
1822
  }
1823
+ /**
1824
+ * Extract @get and @post HTTP route declarations from photon source.
1825
+ * Methods tagged with @get or @post are HTTP-only and must NOT appear as MCP tools.
1826
+ */
1827
+ extractHttpRoutesFromSource(source) {
1828
+ const routes = [];
1829
+ // Match JSDoc blocks that contain @get or @post followed by the async method name.
1830
+ // Pattern: /** ... @get /path ... */ async methodName(
1831
+ const routeRe = /\/\*\*[\s\S]*?@(get|post)\s+(\/[^\s*]*)[\s\S]*?\*\/\s*(?:async\s+)?(\w+)\s*\(/gi;
1832
+ let m;
1833
+ while ((m = routeRe.exec(source)) !== null) {
1834
+ routes.push({ method: m[1].toUpperCase(), path: m[2], handler: m[3] });
1835
+ }
1836
+ return routes;
1837
+ }
1809
1838
  /**
1810
1839
  * Extract tools, templates, and statics from a class
1811
1840
  */
@@ -1876,8 +1905,11 @@ export class PhotonLoader {
1876
1905
  const extractor = new SchemaExtractor();
1877
1906
  const source = sourceContent || (await readText(sourceFilePath));
1878
1907
  const metadata = extractor.extractAllFromSource(source);
1879
- // Filter by method names that exist in the class
1880
- tools = metadata.tools.filter((t) => methodNames.includes(t.name));
1908
+ // Extract @get/@post HTTP routes from source (not in photon-core SchemaExtractor)
1909
+ const httpRoutesFromSource = this.extractHttpRoutesFromSource(source);
1910
+ const routeHandlerNames = new Set(httpRoutesFromSource.map((r) => r.handler));
1911
+ // Filter by method names that exist in the class; exclude HTTP-route methods from tools
1912
+ tools = metadata.tools.filter((t) => methodNames.includes(t.name) && !routeHandlerNames.has(t.name));
1881
1913
  templates = metadata.templates.filter((t) => methodNames.includes(t.name));
1882
1914
  statics = metadata.statics.filter((s) => methodNames.includes(s.name));
1883
1915
  this.log(`Extracted ${tools.length} tools, ${templates.length} templates, ${statics.length} statics from source`);
@@ -1908,6 +1940,7 @@ export class PhotonLoader {
1908
1940
  statics,
1909
1941
  settingsSchema: metadata.settingsSchema,
1910
1942
  auth: this.extractAuthTag(source),
1943
+ httpRoutes: httpRoutesFromSource.length ? httpRoutesFromSource : undefined,
1911
1944
  };
1912
1945
  }
1913
1946
  throw jsonError;