@portel/photon 1.24.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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/dist/beam-form.bundle.js +11 -1
  3. package/dist/beam-form.bundle.js.map +2 -2
  4. package/dist/beam.bundle.js +80 -55
  5. package/dist/beam.bundle.js.map +4 -4
  6. package/dist/cli/commands/host.d.ts.map +1 -1
  7. package/dist/cli/commands/host.js +2 -0
  8. package/dist/cli/commands/host.js.map +1 -1
  9. package/dist/cli/commands/ps.d.ts.map +1 -1
  10. package/dist/cli/commands/ps.js +15 -0
  11. package/dist/cli/commands/ps.js.map +1 -1
  12. package/dist/daemon/client.d.ts +6 -0
  13. package/dist/daemon/client.d.ts.map +1 -1
  14. package/dist/daemon/client.js.map +1 -1
  15. package/dist/daemon/server.js +57 -1
  16. package/dist/daemon/server.js.map +1 -1
  17. package/dist/deploy/cloudflare.d.ts +7 -0
  18. package/dist/deploy/cloudflare.d.ts.map +1 -1
  19. package/dist/deploy/cloudflare.js +420 -35
  20. package/dist/deploy/cloudflare.js.map +1 -1
  21. package/dist/loader.d.ts +5 -0
  22. package/dist/loader.d.ts.map +1 -1
  23. package/dist/loader.js +122 -28
  24. package/dist/loader.js.map +1 -1
  25. package/dist/photon-cli-runner.d.ts.map +1 -1
  26. package/dist/photon-cli-runner.js +15 -2
  27. package/dist/photon-cli-runner.js.map +1 -1
  28. package/dist/server.d.ts.map +1 -1
  29. package/dist/server.js +40 -0
  30. package/dist/server.js.map +1 -1
  31. package/dist/telemetry/context.d.ts +9 -0
  32. package/dist/telemetry/context.d.ts.map +1 -1
  33. package/dist/telemetry/context.js.map +1 -1
  34. package/dist/types/server-types.d.ts +6 -0
  35. package/dist/types/server-types.d.ts.map +1 -1
  36. package/package.json +3 -3
  37. package/templates/cloudflare/worker.ts.template +932 -131
  38. package/templates/cloudflare/wrangler.toml.template +27 -1
package/dist/loader.js CHANGED
@@ -12,7 +12,7 @@ import { fileURLToPath, pathToFileURL } from 'url';
12
12
  import * as crypto from 'crypto';
13
13
  import { startToolSpan } from './telemetry/otel.js';
14
14
  import { recordToolCall, recordCircuitStateChange, recordRateLimitRejection, recordBulkheadRejection, } from './telemetry/metrics.js';
15
- import { runWithRequestContext } from './telemetry/context.js';
15
+ import { runWithRequestContext, getRequestContext } from './telemetry/context.js';
16
16
  import { spawn, execSync } from 'child_process';
17
17
  import { SchemaExtractor, DependencyManager,
18
18
  // Generator utilities (ask/emit pattern from 1.2.0)
@@ -873,27 +873,33 @@ export class PhotonLoader {
873
873
  if (typeof instance._callHandler === 'undefined' || instance._callHandler === undefined) {
874
874
  const callBaseDir = this.baseDir;
875
875
  instance._callHandler = async (photonName, method, params, targetInstance) => {
876
- // Propagate trace context to nested photon-to-photon calls so the
877
- // downstream span chains as a child of the current span. Uses
878
- // in-band `_meta.traceparent` which executeTool peeks at before
879
- // schema validation works through worker threads transparently.
876
+ // Propagate trace context and caller cwd to nested photon-to-photon
877
+ // calls so the downstream span chains as a child of the current span
878
+ // and the callee can resolve relative paths against the originating
879
+ // CLI directory. Uses in-band `_meta` which executeTool peeks at
880
+ // before schema validation: works through worker threads transparently.
880
881
  const ctxMod = await import('./telemetry/context.js');
881
882
  const ctx = ctxMod.getRequestContext();
882
883
  let forwardedParams = params;
884
+ const metaAdditions = {};
883
885
  if (ctx) {
884
886
  const traceparent = ctx.parentTraceparent ||
885
887
  (ctx.traceId
886
888
  ? `00-${ctx.traceId}-${crypto.randomBytes(8).toString('hex')}-01`
887
889
  : undefined);
888
- if (traceparent) {
889
- const existingMeta = params?._meta;
890
- forwardedParams = {
891
- ...(params || {}),
892
- _meta: existingMeta && typeof existingMeta === 'object'
893
- ? { traceparent, ...existingMeta }
894
- : { traceparent },
895
- };
896
- }
890
+ if (traceparent)
891
+ metaAdditions.traceparent = traceparent;
892
+ if (ctx.cwd)
893
+ metaAdditions.callerCwd = ctx.cwd;
894
+ }
895
+ if (Object.keys(metaAdditions).length > 0) {
896
+ const existingMeta = params?._meta;
897
+ forwardedParams = {
898
+ ...(params || {}),
899
+ _meta: existingMeta && typeof existingMeta === 'object'
900
+ ? { ...metaAdditions, ...existingMeta }
901
+ : metaAdditions,
902
+ };
897
903
  }
898
904
  // Dynamic import to avoid circular dependency
899
905
  const { sendCommand } = await import('./daemon/client.js');
@@ -984,6 +990,20 @@ export class PhotonLoader {
984
990
  configurable: true,
985
991
  });
986
992
  }
993
+ // Always-inject `this.callerCwd`. Reads the originating CLI cwd from
994
+ // the request context, falling back to `process.cwd()` when no context
995
+ // is active (direct in-process load with no caller info). Inside a
996
+ // daemon worker `process.cwd()` is the daemon's cwd, so photons that
997
+ // resolve defaults relative to the user's invocation directory must
998
+ // prefer `this.callerCwd`.
999
+ if (!('callerCwd' in instance)) {
1000
+ Object.defineProperty(instance, 'callerCwd', {
1001
+ get() {
1002
+ return getRequestContext()?.cwd ?? process.cwd();
1003
+ },
1004
+ configurable: true,
1005
+ });
1006
+ }
987
1007
  // Inject call() for cross-photon communication. Always-inject (no
988
1008
  // capability-detection gate) because the underlying _callHandler
989
1009
  // is ALWAYS wired above. Gating on a regex that matches literal
@@ -1229,7 +1249,17 @@ export class PhotonLoader {
1229
1249
  permissionHandler?.(request);
1230
1250
  },
1231
1251
  });
1232
- 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.
1233
1263
  }
1234
1264
  // Check @cli dependencies (required system CLI tools)
1235
1265
  if (tsContent) {
@@ -1246,7 +1276,7 @@ export class PhotonLoader {
1246
1276
  this.wrapStatefulMethods(instance, tsContent);
1247
1277
  }
1248
1278
  // Extract tools, templates, and statics (with schema override support)
1249
- 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);
1250
1280
  // ═══ SETTINGS INJECTION ═══
1251
1281
  // If the photon declared `protected settings = { ... }`, inject persistence + proxy
1252
1282
  if (settingsSchema?.hasSettings &&
@@ -1298,6 +1328,8 @@ export class PhotonLoader {
1298
1328
  result.stateful = true;
1299
1329
  if (extractedAuth)
1300
1330
  result.auth = extractedAuth;
1331
+ if (extractedHttpRoutes?.length)
1332
+ result._httpRoutes = extractedHttpRoutes;
1301
1333
  // Store class constructor for static method access
1302
1334
  result.classConstructor = MCPClass;
1303
1335
  // Store settings schema for Beam UI
@@ -1423,8 +1455,34 @@ export class PhotonLoader {
1423
1455
  if (typeof instance._callHandler === 'undefined' || instance._callHandler === undefined) {
1424
1456
  const callBaseDir = this.baseDir;
1425
1457
  instance._callHandler = async (photonName, method, params, targetInstance) => {
1458
+ // Mirror the primary path: propagate trace context and caller cwd via
1459
+ // in-band `_meta` so the callee resolves relative paths against the
1460
+ // originating CLI directory and traces chain across worker boundaries.
1461
+ const ctxMod = await import('./telemetry/context.js');
1462
+ const ctx = ctxMod.getRequestContext();
1463
+ let forwardedParams = params;
1464
+ const metaAdditions = {};
1465
+ if (ctx) {
1466
+ const traceparent = ctx.parentTraceparent ||
1467
+ (ctx.traceId
1468
+ ? `00-${ctx.traceId}-${crypto.randomBytes(8).toString('hex')}-01`
1469
+ : undefined);
1470
+ if (traceparent)
1471
+ metaAdditions.traceparent = traceparent;
1472
+ if (ctx.cwd)
1473
+ metaAdditions.callerCwd = ctx.cwd;
1474
+ }
1475
+ if (Object.keys(metaAdditions).length > 0) {
1476
+ const existingMeta = params?._meta;
1477
+ forwardedParams = {
1478
+ ...(params || {}),
1479
+ _meta: existingMeta && typeof existingMeta === 'object'
1480
+ ? { ...metaAdditions, ...existingMeta }
1481
+ : metaAdditions,
1482
+ };
1483
+ }
1426
1484
  const { sendCommand } = await import('./daemon/client.js');
1427
- return sendCommand(photonName, method, params, {
1485
+ return sendCommand(photonName, method, forwardedParams, {
1428
1486
  workingDir: callBaseDir,
1429
1487
  targetInstance,
1430
1488
  });
@@ -1486,6 +1544,15 @@ export class PhotonLoader {
1486
1544
  configurable: true,
1487
1545
  });
1488
1546
  }
1547
+ // Always-inject callerCwd (see primary load path for rationale).
1548
+ if (!('callerCwd' in instance)) {
1549
+ Object.defineProperty(instance, 'callerCwd', {
1550
+ get() {
1551
+ return getRequestContext()?.cwd ?? process.cwd();
1552
+ },
1553
+ configurable: true,
1554
+ });
1555
+ }
1489
1556
  // Always-inject call() (see the primary load path for rationale).
1490
1557
  if (!instance.call) {
1491
1558
  instance.call = async (target, params = {}, opts) => {
@@ -1566,7 +1633,7 @@ export class PhotonLoader {
1566
1633
  // Call lifecycle hook
1567
1634
  await this.invokeInitialize(instance, name, options);
1568
1635
  // Extract tools and metadata from embedded source (no disk I/O)
1569
- 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);
1570
1637
  // Settings injection
1571
1638
  if (settingsSchema?.hasSettings && instance.settings && typeof instance.settings === 'object') {
1572
1639
  const instanceName = options?.instanceName || 'default';
@@ -1598,6 +1665,8 @@ export class PhotonLoader {
1598
1665
  result.stateful = true;
1599
1666
  if (extractedAuth)
1600
1667
  result.auth = extractedAuth;
1668
+ if (extractedHttpRoutes?.length)
1669
+ result._httpRoutes = extractedHttpRoutes;
1601
1670
  result.classConstructor = MCPClass;
1602
1671
  if (settingsSchema?.hasSettings) {
1603
1672
  result.settingsSchema = settingsSchema;
@@ -1751,6 +1820,21 @@ export class PhotonLoader {
1751
1820
  }
1752
1821
  return cleaned;
1753
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
+ }
1754
1838
  /**
1755
1839
  * Extract tools, templates, and statics from a class
1756
1840
  */
@@ -1821,8 +1905,11 @@ export class PhotonLoader {
1821
1905
  const extractor = new SchemaExtractor();
1822
1906
  const source = sourceContent || (await readText(sourceFilePath));
1823
1907
  const metadata = extractor.extractAllFromSource(source);
1824
- // Filter by method names that exist in the class
1825
- 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));
1826
1913
  templates = metadata.templates.filter((t) => methodNames.includes(t.name));
1827
1914
  statics = metadata.statics.filter((s) => methodNames.includes(s.name));
1828
1915
  this.log(`Extracted ${tools.length} tools, ${templates.length} templates, ${statics.length} statics from source`);
@@ -1853,6 +1940,7 @@ export class PhotonLoader {
1853
1940
  statics,
1854
1941
  settingsSchema: metadata.settingsSchema,
1855
1942
  auth: this.extractAuthTag(source),
1943
+ httpRoutes: httpRoutesFromSource.length ? httpRoutesFromSource : undefined,
1856
1944
  };
1857
1945
  }
1858
1946
  throw jsonError;
@@ -3007,16 +3095,21 @@ Run: photon mcp ${mcpName} --config
3007
3095
  * @returns Tool result, or wrapped result with runId for stateful workflows
3008
3096
  */
3009
3097
  async executeTool(mcp, toolName, parameters, options) {
3010
- // Resolve parentTraceparent from options or in-band `_meta.traceparent` so
3011
- // the ALS context reflects the true parent for any nested `this.call()`.
3098
+ // Resolve parentTraceparent and callerCwd from options or in-band `_meta`
3099
+ // so the ALS context reflects the true parent for any nested `this.call()`
3100
+ // and the originating CLI invocation directory propagates through worker
3101
+ // boundaries.
3012
3102
  let resolvedParentTraceparent = options?.parentTraceparent;
3013
- if (!resolvedParentTraceparent &&
3014
- parameters &&
3015
- typeof parameters === 'object' &&
3016
- '_meta' in parameters) {
3103
+ let resolvedCallerCwd;
3104
+ if (parameters && typeof parameters === 'object' && '_meta' in parameters) {
3017
3105
  const metaPeek = parameters._meta;
3018
- if (metaPeek && typeof metaPeek.traceparent === 'string') {
3019
- resolvedParentTraceparent = metaPeek.traceparent;
3106
+ if (metaPeek) {
3107
+ if (!resolvedParentTraceparent && typeof metaPeek.traceparent === 'string') {
3108
+ resolvedParentTraceparent = metaPeek.traceparent;
3109
+ }
3110
+ if (typeof metaPeek.callerCwd === 'string') {
3111
+ resolvedCallerCwd = metaPeek.callerCwd;
3112
+ }
3020
3113
  }
3021
3114
  }
3022
3115
  const run = () => runWithRequestContext({
@@ -3025,6 +3118,7 @@ Run: photon mcp ${mcpName} --config
3025
3118
  traceId: options?.traceId,
3026
3119
  parentTraceparent: resolvedParentTraceparent,
3027
3120
  caller: options?.caller,
3121
+ cwd: resolvedCallerCwd,
3028
3122
  startedAt: Date.now(),
3029
3123
  }, () => this._executeToolInner(mcp, toolName, parameters, {
3030
3124
  ...options,