@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.
- package/README.md +1 -1
- package/dist/beam-form.bundle.js +11 -1
- package/dist/beam-form.bundle.js.map +2 -2
- package/dist/beam.bundle.js +80 -55
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli/commands/host.d.ts.map +1 -1
- package/dist/cli/commands/host.js +2 -0
- package/dist/cli/commands/host.js.map +1 -1
- package/dist/cli/commands/ps.d.ts.map +1 -1
- package/dist/cli/commands/ps.js +15 -0
- package/dist/cli/commands/ps.js.map +1 -1
- package/dist/daemon/client.d.ts +6 -0
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/server.js +57 -1
- package/dist/daemon/server.js.map +1 -1
- package/dist/deploy/cloudflare.d.ts +7 -0
- package/dist/deploy/cloudflare.d.ts.map +1 -1
- package/dist/deploy/cloudflare.js +420 -35
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/loader.d.ts +5 -0
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +122 -28
- package/dist/loader.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +15 -2
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +40 -0
- package/dist/server.js.map +1 -1
- package/dist/telemetry/context.d.ts +9 -0
- package/dist/telemetry/context.d.ts.map +1 -1
- package/dist/telemetry/context.js.map +1 -1
- package/dist/types/server-types.d.ts +6 -0
- package/dist/types/server-types.d.ts.map +1 -1
- package/package.json +3 -3
- package/templates/cloudflare/worker.ts.template +932 -131
- 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
|
|
877
|
-
// downstream span chains as a child of the current span
|
|
878
|
-
//
|
|
879
|
-
//
|
|
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
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
1825
|
-
|
|
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
|
|
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
|
-
|
|
3014
|
-
|
|
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
|
|
3019
|
-
resolvedParentTraceparent
|
|
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,
|