@portel/photon 1.28.2 → 1.31.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 +42 -11
- package/dist/asset-resolver.d.ts +44 -0
- package/dist/asset-resolver.d.ts.map +1 -0
- package/dist/asset-resolver.js +105 -0
- package/dist/asset-resolver.js.map +1 -0
- package/dist/auto-ui/beam/external-mcp-manager.d.ts +73 -0
- package/dist/auto-ui/beam/external-mcp-manager.d.ts.map +1 -0
- package/dist/auto-ui/beam/external-mcp-manager.js +65 -0
- package/dist/auto-ui/beam/external-mcp-manager.js.map +1 -0
- package/dist/auto-ui/beam/external-mcp.d.ts.map +1 -1
- package/dist/auto-ui/beam/external-mcp.js +25 -1
- package/dist/auto-ui/beam/external-mcp.js.map +1 -1
- package/dist/auto-ui/beam/photon-management.d.ts.map +1 -1
- package/dist/auto-ui/beam/photon-management.js +11 -8
- package/dist/auto-ui/beam/photon-management.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.js +7 -4
- 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 +3 -2
- package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-marketplace.js +6 -2
- package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
- package/dist/auto-ui/beam/startup.js.map +1 -1
- package/dist/auto-ui/beam/types.d.ts +5 -2
- package/dist/auto-ui/beam/types.d.ts.map +1 -1
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +239 -88
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/bridge/index.d.ts.map +1 -1
- package/dist/auto-ui/bridge/index.js +11 -0
- package/dist/auto-ui/bridge/index.js.map +1 -1
- package/dist/auto-ui/bridge/types.d.ts +2 -0
- package/dist/auto-ui/bridge/types.d.ts.map +1 -1
- package/dist/auto-ui/openapi-generator.js +1 -4
- package/dist/auto-ui/openapi-generator.js.map +1 -1
- package/dist/auto-ui/photon-bridge.d.ts +4 -0
- package/dist/auto-ui/photon-bridge.d.ts.map +1 -1
- package/dist/auto-ui/photon-bridge.js.map +1 -1
- package/dist/auto-ui/photon-host.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +7 -0
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +252 -43
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +24 -2
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +202 -24
- package/dist/beam.bundle.js.map +3 -3
- package/dist/capability-negotiator.d.ts +39 -1
- package/dist/capability-negotiator.d.ts.map +1 -1
- package/dist/capability-negotiator.js +5 -0
- package/dist/capability-negotiator.js.map +1 -1
- package/dist/cf-bindings-parser.d.ts +15 -0
- package/dist/cf-bindings-parser.d.ts.map +1 -0
- package/dist/cf-bindings-parser.js +98 -0
- package/dist/cf-bindings-parser.js.map +1 -0
- package/dist/cf-usage-scanner.d.ts +76 -0
- package/dist/cf-usage-scanner.d.ts.map +1 -0
- package/dist/cf-usage-scanner.js +179 -0
- package/dist/cf-usage-scanner.js.map +1 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +124 -16
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/cf.d.ts +18 -0
- package/dist/cli/commands/cf.d.ts.map +1 -0
- package/dist/cli/commands/cf.js +207 -0
- package/dist/cli/commands/cf.js.map +1 -0
- package/dist/cli/commands/info.js +1 -1
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +59 -46
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +3 -0
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +43 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +40 -33
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +6 -2
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +75 -20
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/server.js +69 -11
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/worker-host.js.map +1 -1
- package/dist/deploy/cloudflare.d.ts +27 -0
- package/dist/deploy/cloudflare.d.ts.map +1 -1
- package/dist/deploy/cloudflare.js +210 -3
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/editor-support/docblock-tag-catalog.d.ts.map +1 -1
- package/dist/editor-support/docblock-tag-catalog.js +32 -2
- package/dist/editor-support/docblock-tag-catalog.js.map +1 -1
- package/dist/embedded-runtime.js.map +1 -1
- package/dist/format/registry.d.ts +83 -0
- package/dist/format/registry.d.ts.map +1 -0
- package/dist/format/registry.js +139 -0
- package/dist/format/registry.js.map +1 -0
- package/dist/format/seed.d.ts +18 -0
- package/dist/format/seed.d.ts.map +1 -0
- package/dist/format/seed.js +246 -0
- package/dist/format/seed.js.map +1 -0
- package/dist/loader.d.ts +61 -66
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +315 -327
- package/dist/loader.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +20 -11
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photons/maker.photon.d.ts +2 -2
- package/dist/photons/maker.photon.d.ts.map +1 -1
- package/dist/photons/maker.photon.js +5 -6
- package/dist/photons/maker.photon.js.map +1 -1
- package/dist/photons/maker.photon.ts +5 -6
- package/dist/resource-server.d.ts +55 -15
- package/dist/resource-server.d.ts.map +1 -1
- package/dist/resource-server.js +205 -50
- package/dist/resource-server.js.map +1 -1
- package/dist/runtime/cf-local.d.ts +157 -0
- package/dist/runtime/cf-local.d.ts.map +1 -0
- package/dist/runtime/cf-local.js +406 -0
- package/dist/runtime/cf-local.js.map +1 -0
- package/dist/server.d.ts +117 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +681 -67
- package/dist/server.js.map +1 -1
- package/dist/settings-persistence.d.ts +50 -0
- package/dist/settings-persistence.d.ts.map +1 -0
- package/dist/settings-persistence.js +188 -0
- package/dist/settings-persistence.js.map +1 -0
- package/dist/shared/asset-encoding.d.ts +30 -0
- package/dist/shared/asset-encoding.d.ts.map +1 -0
- package/dist/shared/asset-encoding.js +0 -0
- package/dist/shared/asset-encoding.js.map +1 -0
- package/dist/shared/audit-sqlite.d.ts.map +1 -1
- package/dist/shared/audit-sqlite.js +0 -1
- package/dist/shared/audit-sqlite.js.map +1 -1
- package/dist/shared/cross-origin-headers.d.ts +47 -0
- package/dist/shared/cross-origin-headers.d.ts.map +1 -0
- package/dist/shared/cross-origin-headers.js +61 -0
- package/dist/shared/cross-origin-headers.js.map +1 -0
- package/dist/shared/error-handler.d.ts.map +1 -1
- package/dist/shared/error-handler.js +3 -1
- package/dist/shared/error-handler.js.map +1 -1
- package/dist/shared/expose-route-extractor.d.ts +36 -0
- package/dist/shared/expose-route-extractor.d.ts.map +1 -0
- package/dist/shared/expose-route-extractor.js +64 -0
- package/dist/shared/expose-route-extractor.js.map +1 -0
- package/dist/shared/extract-claims.d.ts +33 -0
- package/dist/shared/extract-claims.d.ts.map +1 -0
- package/dist/shared/extract-claims.js +60 -0
- package/dist/shared/extract-claims.js.map +1 -0
- package/dist/shared/http-route-extractor.d.ts +6 -0
- package/dist/shared/http-route-extractor.d.ts.map +1 -1
- package/dist/shared/http-route-extractor.js +29 -5
- package/dist/shared/http-route-extractor.js.map +1 -1
- package/dist/shared/instance-binding.d.ts +53 -0
- package/dist/shared/instance-binding.d.ts.map +1 -0
- package/dist/shared/instance-binding.js +85 -0
- package/dist/shared/instance-binding.js.map +1 -0
- package/dist/shared/io.d.ts.map +1 -1
- package/dist/shared/io.js +5 -2
- package/dist/shared/io.js.map +1 -1
- package/dist/shared/logger.js.map +1 -1
- package/dist/shared/sqlite-runtime.d.ts.map +1 -1
- package/dist/shared/sqlite-runtime.js +0 -1
- package/dist/shared/sqlite-runtime.js.map +1 -1
- package/dist/task-executor.js.map +1 -1
- package/dist/telemetry/sdk.d.ts.map +1 -1
- package/dist/telemetry/sdk.js +0 -1
- package/dist/telemetry/sdk.js.map +1 -1
- package/dist/test-runner.d.ts.map +1 -1
- package/dist/test-runner.js.map +1 -1
- package/dist/types/server-types.d.ts +16 -7
- package/dist/types/server-types.d.ts.map +1 -1
- package/package.json +14 -4
- package/templates/cloudflare/worker.ts.template +428 -14
- package/templates/cloudflare/wrangler.toml.template +2 -7
- package/templates/photon.template.ts +13 -0
package/dist/loader.js
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
import * as fs from 'fs/promises';
|
|
7
7
|
import { realpathSync, existsSync, mkdirSync, symlinkSync, readFileSync, statSync, } from 'fs';
|
|
8
8
|
import { readText, readJSON, writeText, writeJSON } from './shared/io.js';
|
|
9
|
+
import { parseCfBindings } from './cf-bindings-parser.js';
|
|
10
|
+
import { CFLocalRuntime, mergeBindings } from './runtime/cf-local.js';
|
|
11
|
+
import { scanCfUsage } from './cf-usage-scanner.js';
|
|
9
12
|
import { extractHttpRoutesFromSource } from './shared/http-route-extractor.js';
|
|
13
|
+
import { extractExposesFromSource } from './shared/expose-route-extractor.js';
|
|
10
14
|
import { createRequire } from 'module';
|
|
11
15
|
import * as path from 'path';
|
|
12
16
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
@@ -27,7 +31,7 @@ ProgressRenderer,
|
|
|
27
31
|
// CLI formatting
|
|
28
32
|
formatOutput as cliFormatOutput, createMCPProxy, MCPConfigurationError, SDKMCPClientFactory, resolveMCPSource,
|
|
29
33
|
// Photon runtime configuration
|
|
30
|
-
loadPhotonMCPConfig, resolveEnvVars, isClass as sharedIsClass, findPhotonClass as sharedFindPhotonClass, parseEnvValue as sharedParseEnvValue, compilePhotonTS, parseRuntimeRequirement, checkRuntimeCompatibility,
|
|
34
|
+
loadPhotonMCPConfig, resolveEnvVars, isClass as sharedIsClass, findPhotonClass as sharedFindPhotonClass, parseEnvValue as sharedParseEnvValue, compilePhotonTS, parseRuntimeRequirement, checkRuntimeCompatibility,
|
|
31
35
|
// Execution audit trail
|
|
32
36
|
getAuditTrail,
|
|
33
37
|
// Capability detection for auto-injection
|
|
@@ -48,6 +52,8 @@ import { PHOTON_VERSION, getResolvedPhotonCoreVersion } from './version.js';
|
|
|
48
52
|
// Timeout for external fetch requests (marketplace, GitHub)
|
|
49
53
|
const FETCH_TIMEOUT_MS = 30 * 1000;
|
|
50
54
|
import { generateConfigErrorMessage, summarizeConstructorParams } from './shared/config-docs.js';
|
|
55
|
+
import { SettingsPersistence } from './settings-persistence.js';
|
|
56
|
+
import { AssetResolver } from './asset-resolver.js';
|
|
51
57
|
import { createLogger } from './shared/logger.js';
|
|
52
58
|
import { getErrorMessage } from './shared/error-handler.js';
|
|
53
59
|
import { validateOrThrow, assertString, notEmpty, hasExtension } from './shared/validation.js';
|
|
@@ -141,26 +147,71 @@ export function clearRenderZone() {
|
|
|
141
147
|
*/
|
|
142
148
|
function injectEmitHelpers(instance) {
|
|
143
149
|
const emit = (data) => instance.emit(data);
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
150
|
+
// User-declared methods always win — these helpers only fill in when absent.
|
|
151
|
+
// `in` walks the prototype chain so user methods on the class aren't shadowed
|
|
152
|
+
// by injected closures. Without the guards, a photon that declares `@get
|
|
153
|
+
// /status` (or any method named `render`/`toast`/`log`/`progress`/`thinking`)
|
|
154
|
+
// sees its handler clobbered and the dispatcher invokes the emit closure
|
|
155
|
+
// instead, which returns undefined and produces a hung response.
|
|
156
|
+
if (!('render' in instance)) {
|
|
157
|
+
// Mirrors photon-core base-class render(): UI-feedback formats route to
|
|
158
|
+
// their dedicated emit events; all other formats go through the render channel.
|
|
159
|
+
instance.render = (format, value) => {
|
|
160
|
+
if (format === undefined)
|
|
161
|
+
return emit({ emit: 'render:clear' });
|
|
162
|
+
if (format === 'status')
|
|
163
|
+
return emit(typeof value === 'string'
|
|
164
|
+
? { emit: 'status', message: value }
|
|
165
|
+
: { emit: 'status', ...value });
|
|
166
|
+
if (format === 'progress')
|
|
167
|
+
return emit(typeof value === 'number' ? { emit: 'progress', value } : { emit: 'progress', ...value });
|
|
168
|
+
if (format === 'toast')
|
|
169
|
+
return emit(typeof value === 'string'
|
|
170
|
+
? { emit: 'toast', message: value }
|
|
171
|
+
: { emit: 'toast', ...value });
|
|
172
|
+
emit({ emit: 'render', format, value });
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (!('toast' in instance)) {
|
|
176
|
+
instance.toast = (message, opts = {}) => emit({ emit: 'toast', message, ...opts });
|
|
177
|
+
}
|
|
178
|
+
if (!('log' in instance)) {
|
|
179
|
+
instance.log = (message, opts = {}) => emit({ emit: 'log', message, level: opts.level ?? 'info', data: opts.data });
|
|
180
|
+
}
|
|
181
|
+
if (!('status' in instance)) {
|
|
182
|
+
instance.status = (message) => emit({ emit: 'status', message });
|
|
183
|
+
}
|
|
184
|
+
if (!('progress' in instance)) {
|
|
185
|
+
instance.progress = (value, message) => emit({ emit: 'progress', value, message });
|
|
186
|
+
}
|
|
187
|
+
if (!('thinking' in instance)) {
|
|
188
|
+
instance.thinking = (active = true) => emit({ emit: 'thinking', active });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** True when the scanner detected any CF usage that warrants attaching a runtime. */
|
|
192
|
+
function cfUsageNonEmpty(usage) {
|
|
193
|
+
for (const cat of ['kv', 'r2', 'd1', 'queue', 'vectorize']) {
|
|
194
|
+
if (usage.qualifiers[cat].size > 0)
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
return usage.shared.ai || usage.shared.images || usage.shared.browser;
|
|
198
|
+
}
|
|
199
|
+
/** Short, log-friendly summary of which CF surfaces a photon reaches for. */
|
|
200
|
+
function describeUsage(usage, declared) {
|
|
201
|
+
const parts = [];
|
|
202
|
+
for (const cat of ['kv', 'r2', 'd1', 'queue', 'vectorize']) {
|
|
203
|
+
if (usage.qualifiers[cat].size > 0)
|
|
204
|
+
parts.push(`${cat}×${usage.qualifiers[cat].size}`);
|
|
205
|
+
}
|
|
206
|
+
if (usage.shared.ai)
|
|
207
|
+
parts.push('ai');
|
|
208
|
+
if (usage.shared.images)
|
|
209
|
+
parts.push('images');
|
|
210
|
+
if (usage.shared.browser)
|
|
211
|
+
parts.push('browser');
|
|
212
|
+
if (parts.length === 0 && declared)
|
|
213
|
+
parts.push('overrides-only');
|
|
214
|
+
return parts.join(', ') || 'cf';
|
|
164
215
|
}
|
|
165
216
|
/** Extra regex checks that force 'emit' capability when helper methods are used. */
|
|
166
217
|
function detectEmitHelperUsage(source) {
|
|
@@ -217,6 +268,15 @@ function injectSamplingAndElicitation(instance) {
|
|
|
217
268
|
return await store.inputProvider(params);
|
|
218
269
|
};
|
|
219
270
|
}
|
|
271
|
+
if (!Object.getOwnPropertyDescriptor(Object.getPrototypeOf(instance), 'roots')) {
|
|
272
|
+
Object.defineProperty(instance, 'roots', {
|
|
273
|
+
get() {
|
|
274
|
+
const store = executionContext.getStore();
|
|
275
|
+
return store?.roots ?? [];
|
|
276
|
+
},
|
|
277
|
+
configurable: true,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
220
280
|
if (!instance.sample) {
|
|
221
281
|
instance.sample = async (params) => {
|
|
222
282
|
const store = executionContext.getStore();
|
|
@@ -242,6 +302,37 @@ function injectSamplingAndElicitation(instance) {
|
|
|
242
302
|
};
|
|
243
303
|
}
|
|
244
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Inject `this.notifyResourceUpdated(uri)` so `@resource` photon authors can
|
|
307
|
+
* fan out `notifications/resources/updated` to subscribed clients.
|
|
308
|
+
*
|
|
309
|
+
* Unlike sample/elicit/confirm — which fire only inside a tool call and so can
|
|
310
|
+
* read their provider from `executionContext.getStore()` — resource updates
|
|
311
|
+
* commonly fire from setInterval, event handlers, or async paths that resolved
|
|
312
|
+
* after the triggering tool returned. We close over a notifier passed in by
|
|
313
|
+
* the loader (sourced from PhotonServer's SubscriptionRegistry) so the helper
|
|
314
|
+
* keeps working outside the ALS context.
|
|
315
|
+
*
|
|
316
|
+
* No-op if the runtime has no notifier wired (e.g. CLI execution outside an
|
|
317
|
+
* MCP server). User-defined methods on the class win.
|
|
318
|
+
*/
|
|
319
|
+
function injectResourceNotifier(instance, notifier) {
|
|
320
|
+
// Only skip if the instance itself (own property, not via prototype/base
|
|
321
|
+
// class) has a user-defined override. Otherwise install the wired version
|
|
322
|
+
// on the instance, shadowing any base-class default.
|
|
323
|
+
if (Object.prototype.hasOwnProperty.call(instance, 'notifyResourceUpdated'))
|
|
324
|
+
return;
|
|
325
|
+
instance.notifyResourceUpdated = (uri) => {
|
|
326
|
+
if (!notifier)
|
|
327
|
+
return;
|
|
328
|
+
try {
|
|
329
|
+
void notifier(uri);
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
// Notifier failures must never propagate into photon code.
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
245
336
|
/**
|
|
246
337
|
* Render a formatted value in the CLI using @portel/cli's formatOutput.
|
|
247
338
|
* Uses clear-and-replace semantics — each call overwrites the previous render.
|
|
@@ -308,6 +399,14 @@ export class PhotonLoader {
|
|
|
308
399
|
marketplaceManager;
|
|
309
400
|
marketplaceManagerPromise;
|
|
310
401
|
logger;
|
|
402
|
+
settingsPersistence;
|
|
403
|
+
assetResolver;
|
|
404
|
+
/**
|
|
405
|
+
* One CFLocalRuntime per photon name. Boot is lazy inside the runtime
|
|
406
|
+
* itself; this cache just ensures every instance of a given photon
|
|
407
|
+
* shares a single miniflare sandbox.
|
|
408
|
+
*/
|
|
409
|
+
cfRuntimes = new Map();
|
|
311
410
|
/**
|
|
312
411
|
* Per-instance call queue. Two tool invocations targeting the same photon
|
|
313
412
|
* instance are serialized: the second starts only after the first returns.
|
|
@@ -369,6 +468,8 @@ export class PhotonLoader {
|
|
|
369
468
|
this.verbose = verbose;
|
|
370
469
|
this.logger = logger ?? createLogger({ component: 'photon-loader', minimal: true });
|
|
371
470
|
this.baseDir = baseDir || getDefaultContext().baseDir;
|
|
471
|
+
this.settingsPersistence = new SettingsPersistence(this.baseDir, (msg, meta) => this.log(msg, meta));
|
|
472
|
+
this.assetResolver = new AssetResolver((msg, meta) => this.log(msg, meta));
|
|
372
473
|
}
|
|
373
474
|
/**
|
|
374
475
|
* Load MCP configuration from ~/.photon/config.json
|
|
@@ -390,6 +491,63 @@ export class PhotonLoader {
|
|
|
390
491
|
});
|
|
391
492
|
return this.mcpConfigPromise;
|
|
392
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* Resolve the path where a photon's CF binding override JSON lives.
|
|
496
|
+
* The override layers on top of `protected cfBindings` so users can
|
|
497
|
+
* repoint a binding (e.g., point `kv: cache` at a different namespace)
|
|
498
|
+
* without editing source.
|
|
499
|
+
*/
|
|
500
|
+
getCfOverridePath(photonName) {
|
|
501
|
+
return path.join(this.baseDir, '.data', 'cf-overrides', `${photonName}.json`);
|
|
502
|
+
}
|
|
503
|
+
/** Load the per-photon CF override JSON. Returns null if missing. */
|
|
504
|
+
async loadCfOverride(photonName) {
|
|
505
|
+
try {
|
|
506
|
+
return await readJSON(this.getCfOverridePath(photonName));
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Persist the CF override for a photon. Creates parent directories as
|
|
514
|
+
* needed; called by the `photon cf set` CLI command.
|
|
515
|
+
*/
|
|
516
|
+
async saveCfOverride(photonName, override) {
|
|
517
|
+
const overridePath = this.getCfOverridePath(photonName);
|
|
518
|
+
await fs.mkdir(path.dirname(overridePath), { recursive: true });
|
|
519
|
+
await writeJSON(overridePath, override);
|
|
520
|
+
return overridePath;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Build (or fetch the cached) CFLocalRuntime for `photonName`. Uses
|
|
524
|
+
* the new options-form constructor so auto-naming applies and
|
|
525
|
+
* miniflare seeds every literal qualifier the scanner found, plus
|
|
526
|
+
* any override-declared bindings.
|
|
527
|
+
*/
|
|
528
|
+
async attachCfRuntime(photonName, usage, declared) {
|
|
529
|
+
let runtime = this.cfRuntimes.get(photonName);
|
|
530
|
+
if (runtime)
|
|
531
|
+
return runtime;
|
|
532
|
+
const overrideJson = await this.loadCfOverride(photonName);
|
|
533
|
+
const overrides = mergeBindings(declared, overrideJson);
|
|
534
|
+
runtime = new CFLocalRuntime({
|
|
535
|
+
photonName,
|
|
536
|
+
baseDir: this.baseDir,
|
|
537
|
+
usage,
|
|
538
|
+
overrides,
|
|
539
|
+
});
|
|
540
|
+
this.cfRuntimes.set(photonName, runtime);
|
|
541
|
+
return runtime;
|
|
542
|
+
}
|
|
543
|
+
/** Read the effective bindings for a photon: declared (from source) plus override. */
|
|
544
|
+
async getEffectiveCfBindings(photonName, tsContent) {
|
|
545
|
+
const declared = parseCfBindings(tsContent);
|
|
546
|
+
if (!declared)
|
|
547
|
+
return { declared: null, override: null, effective: null };
|
|
548
|
+
const override = await this.loadCfOverride(photonName);
|
|
549
|
+
return { declared, override, effective: mergeBindings(declared, override) };
|
|
550
|
+
}
|
|
393
551
|
async getMarketplaceManager() {
|
|
394
552
|
if (this.marketplaceManager) {
|
|
395
553
|
return this.marketplaceManager;
|
|
@@ -411,6 +569,16 @@ export class PhotonLoader {
|
|
|
411
569
|
setMCPClientFactory(factory) {
|
|
412
570
|
this.mcpClientFactory = factory;
|
|
413
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* Notifier wired by PhotonServer so `this.notifyResourceUpdated(uri)`
|
|
574
|
+
* fans out to all subscribed MCP clients (STDIO + SSE sessions).
|
|
575
|
+
* `undefined` outside the MCP-server runtime, in which case the helper is
|
|
576
|
+
* a no-op.
|
|
577
|
+
*/
|
|
578
|
+
resourceUpdateNotifier;
|
|
579
|
+
setResourceUpdateNotifier(notifier) {
|
|
580
|
+
this.resourceUpdateNotifier = notifier;
|
|
581
|
+
}
|
|
414
582
|
/**
|
|
415
583
|
* Log message only if verbose mode is enabled
|
|
416
584
|
*/
|
|
@@ -487,6 +655,18 @@ export class PhotonLoader {
|
|
|
487
655
|
await this.clearDependencyCache(cacheKey);
|
|
488
656
|
await this.clearBuildCache(cacheKey);
|
|
489
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Clear all caches for a photon identified by file path.
|
|
660
|
+
* Called by Beam when a photon fails to load, so the next reload
|
|
661
|
+
* recompiles from source rather than using stale cached artifacts.
|
|
662
|
+
*/
|
|
663
|
+
async clearCacheForFile(filePath) {
|
|
664
|
+
const absolutePath = path.resolve(filePath);
|
|
665
|
+
const mcpName = path.basename(absolutePath, '.ts').replace('.photon', '');
|
|
666
|
+
const cacheKey = this.getCacheKey(mcpName, absolutePath);
|
|
667
|
+
await this.clearAllCaches(cacheKey);
|
|
668
|
+
this.log(`🗑️ Cache cleared for ${mcpName} (auto-retry after load failure)`);
|
|
669
|
+
}
|
|
490
670
|
/**
|
|
491
671
|
* Path to metadata file describing installed dependencies
|
|
492
672
|
*/
|
|
@@ -604,6 +784,11 @@ export class PhotonLoader {
|
|
|
604
784
|
message.includes('Cannot find module') ||
|
|
605
785
|
message.includes('require is not defined'));
|
|
606
786
|
}
|
|
787
|
+
isCompilationServiceError(error) {
|
|
788
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
789
|
+
return (message.includes('The service was stopped') ||
|
|
790
|
+
message.includes('The service is no longer running'));
|
|
791
|
+
}
|
|
607
792
|
static parseDependenciesFromSource(source) {
|
|
608
793
|
const deps = [];
|
|
609
794
|
// Only match @dependencies inside JSDoc blocks (/** ... */)
|
|
@@ -731,6 +916,12 @@ export class PhotonLoader {
|
|
|
731
916
|
module = await importModule();
|
|
732
917
|
}
|
|
733
918
|
else {
|
|
919
|
+
if (this.isCompilationServiceError(error) && cacheKey) {
|
|
920
|
+
// Compiler process crashed — clear the build cache so the next startup
|
|
921
|
+
// recompiles from source instead of finding a stale or missing artifact.
|
|
922
|
+
await this.clearBuildCache(cacheKey);
|
|
923
|
+
this.log(`⚠️ Compiler service crashed for ${mcpName || 'unknown'}, build cache cleared. Restart to recover.`);
|
|
924
|
+
}
|
|
734
925
|
throw error;
|
|
735
926
|
}
|
|
736
927
|
}
|
|
@@ -1017,6 +1208,12 @@ export class PhotonLoader {
|
|
|
1017
1208
|
configurable: true,
|
|
1018
1209
|
});
|
|
1019
1210
|
}
|
|
1211
|
+
// `this.cf` is no longer always-injected. The CF runtime
|
|
1212
|
+
// injection block above sets `instance.cf` directly when the
|
|
1213
|
+
// photon either references the surface (scanner detection) or
|
|
1214
|
+
// declares `protected cfBindings`. Plain photons that never
|
|
1215
|
+
// touch CF leave `this.cf` undefined — a plain runtime error
|
|
1216
|
+
// beats a throwing-Proxy stub for the diagnostic.
|
|
1020
1217
|
// Always-inject `this.callerCwd`. Reads the originating CLI cwd from
|
|
1021
1218
|
// the request context, falling back to `process.cwd()` when no context
|
|
1022
1219
|
// is active (direct in-process load with no caller info). Inside a
|
|
@@ -1074,6 +1271,7 @@ export class PhotonLoader {
|
|
|
1074
1271
|
// philosophy: detection-gate misses mean silent breakage, so don't
|
|
1075
1272
|
// gate cheap capabilities. User-defined methods win.
|
|
1076
1273
|
injectSamplingAndElicitation(instance);
|
|
1274
|
+
injectResourceNotifier(instance, this.resourceUpdateNotifier);
|
|
1077
1275
|
// Always-inject caller getter. The prototype check preserves any
|
|
1078
1276
|
// user-defined getter on the class (e.g. Photon base class has its
|
|
1079
1277
|
// own) so we don't clobber.
|
|
@@ -1303,16 +1501,34 @@ export class PhotonLoader {
|
|
|
1303
1501
|
this.wrapStatefulMethods(instance, tsContent);
|
|
1304
1502
|
}
|
|
1305
1503
|
// Extract tools, templates, and statics (with schema override support)
|
|
1306
|
-
const { tools, templates, statics, settingsSchema, auth: extractedAuth, httpRoutes: extractedHttpRoutes, } = await this.extractTools(MCPClass, absolutePath);
|
|
1504
|
+
const { tools, templates, statics, settingsSchema, auth: extractedAuth, httpRoutes: extractedHttpRoutes, exposes: extractedExposes, } = await this.extractTools(MCPClass, absolutePath);
|
|
1505
|
+
// ═══ CF RUNTIME INJECTION ═══
|
|
1506
|
+
// The photon's CF surface comes from the new `Cloudflare` injection
|
|
1507
|
+
// (constructor param typed `Cloudflare`, or the forgiving `this.cf.*`
|
|
1508
|
+
// path on plain classes). The scanner discovers literal qualifiers
|
|
1509
|
+
// so miniflare seeds them up front; `protected cfBindings` is now
|
|
1510
|
+
// an optional override layer for repointing bindings at pre-existing
|
|
1511
|
+
// CF resources.
|
|
1512
|
+
if (tsContent) {
|
|
1513
|
+
const usage = scanCfUsage(tsContent);
|
|
1514
|
+
const overrides = parseCfBindings(tsContent);
|
|
1515
|
+
const photonUsesCf = cfUsageNonEmpty(usage) || overrides !== null;
|
|
1516
|
+
if (photonUsesCf) {
|
|
1517
|
+
const runtime = await this.attachCfRuntime(name, usage, overrides);
|
|
1518
|
+
instance.cf = runtime;
|
|
1519
|
+
const what = describeUsage(usage, overrides);
|
|
1520
|
+
this.log(`☁️ CF runtime attached to ${name} (${what})`);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1307
1523
|
// ═══ SETTINGS INJECTION ═══
|
|
1308
1524
|
// If the photon declared `protected settings = { ... }`, inject persistence + proxy
|
|
1309
1525
|
if (settingsSchema?.hasSettings &&
|
|
1310
1526
|
instance.settings &&
|
|
1311
1527
|
typeof instance.settings === 'object') {
|
|
1312
1528
|
const instanceName = options?.instanceName || 'default';
|
|
1313
|
-
await this.
|
|
1529
|
+
await this.settingsPersistence.inject(instance, name, instanceName, settingsSchema);
|
|
1314
1530
|
// Auto-generate the `settings` MCP tool from the schema
|
|
1315
|
-
const settingsTool = this.
|
|
1531
|
+
const settingsTool = this.settingsPersistence.generateTool(settingsSchema);
|
|
1316
1532
|
tools.push(settingsTool);
|
|
1317
1533
|
this.log(`⚙️ Settings tool auto-generated for ${name} (${settingsSchema.properties.length} props)`);
|
|
1318
1534
|
}
|
|
@@ -1321,8 +1537,8 @@ export class PhotonLoader {
|
|
|
1321
1537
|
this.logger.warn(`⚠️ ${name}: configure() method is deprecated. Use 'protected settings = { ... }' property instead.`);
|
|
1322
1538
|
}
|
|
1323
1539
|
// Extract assets from source and discover asset folder
|
|
1324
|
-
const assets = await this.
|
|
1325
|
-
this.
|
|
1540
|
+
const assets = await this.assetResolver.discover(absolutePath, tsContent || '');
|
|
1541
|
+
this.assetResolver.attachToInstance(instance, assets);
|
|
1326
1542
|
const counts = [
|
|
1327
1543
|
tools.length > 0 ? `${tools.length} tools` : null,
|
|
1328
1544
|
templates.length > 0 ? `${templates.length} templates` : null,
|
|
@@ -1357,6 +1573,8 @@ export class PhotonLoader {
|
|
|
1357
1573
|
result.auth = extractedAuth;
|
|
1358
1574
|
if (extractedHttpRoutes?.length)
|
|
1359
1575
|
result._httpRoutes = extractedHttpRoutes;
|
|
1576
|
+
if (extractedExposes?.length)
|
|
1577
|
+
result._exposes = extractedExposes;
|
|
1360
1578
|
// Store class constructor for static method access
|
|
1361
1579
|
result.classConstructor = MCPClass;
|
|
1362
1580
|
// Store settings schema for Beam UI
|
|
@@ -1595,6 +1813,7 @@ export class PhotonLoader {
|
|
|
1595
1813
|
}
|
|
1596
1814
|
// Always-inject sample/confirm/elicit (see primary load path).
|
|
1597
1815
|
injectSamplingAndElicitation(instance);
|
|
1816
|
+
injectResourceNotifier(instance, this.resourceUpdateNotifier);
|
|
1598
1817
|
}
|
|
1599
1818
|
// Channel event capability: inject on()/off()/_dispatch()/_matchesFilter()
|
|
1600
1819
|
// when source uses this._dispatch( — the universal channel dispatch pattern.
|
|
@@ -1660,16 +1879,26 @@ export class PhotonLoader {
|
|
|
1660
1879
|
// Call lifecycle hook
|
|
1661
1880
|
await this.invokeInitialize(instance, name, options);
|
|
1662
1881
|
// Extract tools and metadata from embedded source (no disk I/O)
|
|
1663
|
-
const { tools, templates, statics, settingsSchema, auth: extractedAuth, httpRoutes: extractedHttpRoutes, } = await this.extractTools(MCPClass, absolutePath, tsContent);
|
|
1882
|
+
const { tools, templates, statics, settingsSchema, auth: extractedAuth, httpRoutes: extractedHttpRoutes, exposes: extractedExposes, } = await this.extractTools(MCPClass, absolutePath, tsContent);
|
|
1883
|
+
// CF runtime injection (mirrors the loadFile path above)
|
|
1884
|
+
if (tsContent) {
|
|
1885
|
+
const usage = scanCfUsage(tsContent);
|
|
1886
|
+
const overrides = parseCfBindings(tsContent);
|
|
1887
|
+
const photonUsesCf = cfUsageNonEmpty(usage) || overrides !== null;
|
|
1888
|
+
if (photonUsesCf) {
|
|
1889
|
+
const runtime = await this.attachCfRuntime(name, usage, overrides);
|
|
1890
|
+
instance.cf = runtime;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1664
1893
|
// Settings injection
|
|
1665
1894
|
if (settingsSchema?.hasSettings && instance.settings && typeof instance.settings === 'object') {
|
|
1666
1895
|
const instanceName = options?.instanceName || 'default';
|
|
1667
|
-
await this.
|
|
1668
|
-
tools.push(this.
|
|
1896
|
+
await this.settingsPersistence.inject(instance, name, instanceName, settingsSchema);
|
|
1897
|
+
tools.push(this.settingsPersistence.generateTool(settingsSchema));
|
|
1669
1898
|
}
|
|
1670
1899
|
// Discover assets (for @ui)
|
|
1671
|
-
const assets = await this.
|
|
1672
|
-
this.
|
|
1900
|
+
const assets = await this.assetResolver.discover(absolutePath, tsContent);
|
|
1901
|
+
this.assetResolver.attachToInstance(instance, assets);
|
|
1673
1902
|
this.log(`✅ Loaded (preloaded): ${name} (${tools.length} tools)`);
|
|
1674
1903
|
// Extract class-level metadata from docblock (icon, description, stateful)
|
|
1675
1904
|
const classDocblock = this.extractClassDocblock(tsContent);
|
|
@@ -1694,6 +1923,8 @@ export class PhotonLoader {
|
|
|
1694
1923
|
result.auth = extractedAuth;
|
|
1695
1924
|
if (extractedHttpRoutes?.length)
|
|
1696
1925
|
result._httpRoutes = extractedHttpRoutes;
|
|
1926
|
+
if (extractedExposes?.length)
|
|
1927
|
+
result._exposes = extractedExposes;
|
|
1697
1928
|
result.classConstructor = MCPClass;
|
|
1698
1929
|
if (settingsSchema?.hasSettings) {
|
|
1699
1930
|
result.settingsSchema = settingsSchema;
|
|
@@ -1742,7 +1973,10 @@ export class PhotonLoader {
|
|
|
1742
1973
|
const files = await fs.readdir(buildDir).catch(() => []);
|
|
1743
1974
|
for (const f of files) {
|
|
1744
1975
|
if (f.startsWith(fileName) && f.endsWith('.mjs')) {
|
|
1745
|
-
await fs.unlink(path.join(buildDir, f)).catch(() => {
|
|
1976
|
+
await fs.unlink(path.join(buildDir, f)).catch((err) => this.logger.debug('Failed to remove stale cache file', {
|
|
1977
|
+
file: f,
|
|
1978
|
+
error: String(err),
|
|
1979
|
+
}));
|
|
1746
1980
|
}
|
|
1747
1981
|
}
|
|
1748
1982
|
}
|
|
@@ -1954,6 +2188,15 @@ export class PhotonLoader {
|
|
|
1954
2188
|
}
|
|
1955
2189
|
});
|
|
1956
2190
|
}
|
|
2191
|
+
// Track C: capture @expose'd methods so the runtime dispatcher
|
|
2192
|
+
// can auto-bind them at /api/<kebab>. We only honour declarations
|
|
2193
|
+
// for methods the class actually defines (mirrors the tool/route
|
|
2194
|
+
// filter above) so a stray comment never opens a phantom
|
|
2195
|
+
// endpoint. Methods that already carry an explicit @get/@post are
|
|
2196
|
+
// excluded so the user's path/verb wins — the auto-RPC slot only
|
|
2197
|
+
// fills *unclaimed* spots.
|
|
2198
|
+
const httpRouteHandlers = new Set(httpRoutesFromSource.map((r) => r.handler));
|
|
2199
|
+
const exposesFromSource = extractExposesFromSource(source).filter((e) => methodNames.includes(e.handler) && !httpRouteHandlers.has(e.handler));
|
|
1957
2200
|
return {
|
|
1958
2201
|
tools,
|
|
1959
2202
|
templates,
|
|
@@ -1961,6 +2204,7 @@ export class PhotonLoader {
|
|
|
1961
2204
|
settingsSchema: metadata.settingsSchema,
|
|
1962
2205
|
auth: this.extractAuthTag(source),
|
|
1963
2206
|
httpRoutes: httpRoutesFromSource.length ? httpRoutesFromSource : undefined,
|
|
2207
|
+
exposes: exposesFromSource.length ? exposesFromSource : undefined,
|
|
1964
2208
|
};
|
|
1965
2209
|
}
|
|
1966
2210
|
throw jsonError;
|
|
@@ -1986,199 +2230,9 @@ export class PhotonLoader {
|
|
|
1986
2230
|
statics = statics.map((s) => ({ ...s, description: this.stripJSDocTags(s.description) }));
|
|
1987
2231
|
return { tools, templates, statics };
|
|
1988
2232
|
}
|
|
1989
|
-
//
|
|
1990
|
-
//
|
|
1991
|
-
//
|
|
1992
|
-
/**
|
|
1993
|
-
* Get the settings persistence path for a photon instance.
|
|
1994
|
-
* Co-located with state under Option B: the settings JSON lives next to
|
|
1995
|
-
* state.json inside the per-instance directory.
|
|
1996
|
-
*/
|
|
1997
|
-
getSettingsPath(photonName, instanceName) {
|
|
1998
|
-
// getInstanceStatePath returns `.../state/{instance}/state.json`; swap
|
|
1999
|
-
// the filename so settings land at `.../state/{instance}/settings.json`.
|
|
2000
|
-
// Keeps the source of truth for the layout in context-store.
|
|
2001
|
-
const statePath = getInstanceStatePath(photonName, instanceName, this.baseDir);
|
|
2002
|
-
return path.join(path.dirname(statePath), 'settings.json');
|
|
2003
|
-
}
|
|
2004
|
-
/**
|
|
2005
|
-
* Load persisted settings from disk
|
|
2006
|
-
*/
|
|
2007
|
-
async loadSettings(photonName, instanceName) {
|
|
2008
|
-
const settingsPath = this.getSettingsPath(photonName, instanceName);
|
|
2009
|
-
try {
|
|
2010
|
-
return await readJSON(settingsPath);
|
|
2011
|
-
}
|
|
2012
|
-
catch {
|
|
2013
|
-
return {};
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
/**
|
|
2017
|
-
* Persist settings to disk
|
|
2018
|
-
*/
|
|
2019
|
-
async persistSettings(photonName, instanceName, values) {
|
|
2020
|
-
const settingsPath = this.getSettingsPath(photonName, instanceName);
|
|
2021
|
-
const dir = path.dirname(settingsPath);
|
|
2022
|
-
await fs.mkdir(dir, { recursive: true });
|
|
2023
|
-
await writeJSON(settingsPath, values);
|
|
2024
|
-
}
|
|
2025
|
-
/**
|
|
2026
|
-
* Inject settings into a photon instance:
|
|
2027
|
-
* - Load persisted values (persisted wins over defaults)
|
|
2028
|
-
* - Replace instance.settings with a read-only Proxy
|
|
2029
|
-
* - Store writable backing object for the settings tool
|
|
2030
|
-
*/
|
|
2031
|
-
async injectSettings(instance, photonName, instanceName, schema) {
|
|
2032
|
-
const defaults = instance.settings;
|
|
2033
|
-
const persisted = await this.loadSettings(photonName, instanceName);
|
|
2034
|
-
// Merge: persisted values over defaults
|
|
2035
|
-
const backing = { ...defaults };
|
|
2036
|
-
for (const key of Object.keys(persisted)) {
|
|
2037
|
-
if (persisted[key] !== undefined) {
|
|
2038
|
-
backing[key] = persisted[key];
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
// Store the writable backing object for the settings tool to update
|
|
2042
|
-
instance._settingsBacking = backing;
|
|
2043
|
-
instance._settingsPhotonName = photonName;
|
|
2044
|
-
instance._settingsInstanceName = instanceName;
|
|
2045
|
-
instance._settingsSchema = schema;
|
|
2046
|
-
// Replace with read-only Proxy
|
|
2047
|
-
instance.settings = new Proxy(backing, {
|
|
2048
|
-
get(target, prop) {
|
|
2049
|
-
if (typeof prop === 'string') {
|
|
2050
|
-
return target[prop];
|
|
2051
|
-
}
|
|
2052
|
-
return undefined;
|
|
2053
|
-
},
|
|
2054
|
-
set(_target, prop, _value) {
|
|
2055
|
-
throw new Error(`Cannot directly set settings.${String(prop)}. ` +
|
|
2056
|
-
`Use the 'settings' tool to change settings (e.g., settings({ ${String(prop)}: newValue })).`);
|
|
2057
|
-
},
|
|
2058
|
-
deleteProperty(_target, prop) {
|
|
2059
|
-
throw new Error(`Cannot delete settings.${String(prop)}. Use the 'settings' tool instead.`);
|
|
2060
|
-
},
|
|
2061
|
-
});
|
|
2062
|
-
}
|
|
2063
|
-
/**
|
|
2064
|
-
* Generate an MCP tool definition from a SettingsSchema
|
|
2065
|
-
*/
|
|
2066
|
-
generateSettingsTool(schema) {
|
|
2067
|
-
const properties = {};
|
|
2068
|
-
for (const prop of schema.properties) {
|
|
2069
|
-
const propSchema = { type: prop.type };
|
|
2070
|
-
if (prop.description) {
|
|
2071
|
-
propSchema.description = prop.description;
|
|
2072
|
-
}
|
|
2073
|
-
if (prop.default !== undefined) {
|
|
2074
|
-
propSchema.default = prop.default;
|
|
2075
|
-
}
|
|
2076
|
-
properties[prop.name] = propSchema;
|
|
2077
|
-
}
|
|
2078
|
-
return {
|
|
2079
|
-
name: 'settings',
|
|
2080
|
-
description: 'View or update photon settings. Call with no arguments to view current settings. Pass parameters to update specific settings.',
|
|
2081
|
-
inputSchema: {
|
|
2082
|
-
type: 'object',
|
|
2083
|
-
properties,
|
|
2084
|
-
// No required params — all settings are optional when calling the tool
|
|
2085
|
-
},
|
|
2086
|
-
};
|
|
2087
|
-
}
|
|
2088
|
-
/**
|
|
2089
|
-
* Execute the auto-generated settings tool:
|
|
2090
|
-
* - No params → return current settings
|
|
2091
|
-
* - Params with values → update those settings, persist, emit change
|
|
2092
|
-
* - Params with undefined + no default → trigger elicitation
|
|
2093
|
-
*/
|
|
2094
|
-
async executeSettingsTool(instance, parameters, options) {
|
|
2095
|
-
const backing = instance._settingsBacking;
|
|
2096
|
-
const photonName = instance._settingsPhotonName;
|
|
2097
|
-
const instanceName = instance._settingsInstanceName;
|
|
2098
|
-
const schema = instance._settingsSchema;
|
|
2099
|
-
if (!backing || !photonName || !schema) {
|
|
2100
|
-
throw new Error('Settings not initialized for this photon');
|
|
2101
|
-
}
|
|
2102
|
-
// No params or empty params → return current settings
|
|
2103
|
-
if (!parameters || Object.keys(parameters).length === 0) {
|
|
2104
|
-
// Check if any required settings (undefined defaults) need elicitation
|
|
2105
|
-
const needsElicitation = schema.properties.filter((p) => p.required && backing[p.name] === undefined);
|
|
2106
|
-
if (needsElicitation.length > 0 && options?.inputProvider) {
|
|
2107
|
-
for (const prop of needsElicitation) {
|
|
2108
|
-
const result = await options.inputProvider({
|
|
2109
|
-
ask: prop.type === 'number' ? 'number' : 'text',
|
|
2110
|
-
message: prop.description || `Enter value for ${prop.name}:`,
|
|
2111
|
-
});
|
|
2112
|
-
if (result !== undefined && result !== null) {
|
|
2113
|
-
const oldValue = backing[prop.name];
|
|
2114
|
-
backing[prop.name] = result;
|
|
2115
|
-
this.log(`⚙️ Settings: ${prop.name} = ${JSON.stringify(result)} (elicited)`);
|
|
2116
|
-
this.emitSettingsChange(instance, prop.name, oldValue, result);
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
await this.persistSettings(photonName, instanceName, backing);
|
|
2120
|
-
}
|
|
2121
|
-
return { ...backing };
|
|
2122
|
-
}
|
|
2123
|
-
// Update specified settings
|
|
2124
|
-
const changes = [];
|
|
2125
|
-
for (const [key, value] of Object.entries(parameters)) {
|
|
2126
|
-
// Verify this is a valid setting
|
|
2127
|
-
const prop = schema.properties.find((p) => p.name === key);
|
|
2128
|
-
if (!prop)
|
|
2129
|
-
continue;
|
|
2130
|
-
if (value === undefined && prop.required) {
|
|
2131
|
-
// Elicit value from user
|
|
2132
|
-
if (options?.inputProvider) {
|
|
2133
|
-
const result = await options.inputProvider({
|
|
2134
|
-
ask: prop.type === 'number' ? 'number' : 'text',
|
|
2135
|
-
message: prop.description || `Enter value for ${key}:`,
|
|
2136
|
-
});
|
|
2137
|
-
if (result !== undefined && result !== null) {
|
|
2138
|
-
const oldValue = backing[key];
|
|
2139
|
-
backing[key] = result;
|
|
2140
|
-
changes.push({ property: key, oldValue, newValue: result });
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
else {
|
|
2145
|
-
const oldValue = backing[key];
|
|
2146
|
-
backing[key] = value;
|
|
2147
|
-
changes.push({ property: key, oldValue, newValue: value });
|
|
2148
|
-
}
|
|
2149
|
-
}
|
|
2150
|
-
// Persist if anything changed
|
|
2151
|
-
if (changes.length > 0) {
|
|
2152
|
-
await this.persistSettings(photonName, instanceName, backing);
|
|
2153
|
-
for (const change of changes) {
|
|
2154
|
-
this.log(`⚙️ Settings: ${change.property}: ${JSON.stringify(change.oldValue)} → ${JSON.stringify(change.newValue)}`);
|
|
2155
|
-
this.emitSettingsChange(instance, change.property, change.oldValue, change.newValue);
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
// Re-apply proxy (backing object is already updated in place, proxy still references it)
|
|
2159
|
-
return { ...backing };
|
|
2160
|
-
}
|
|
2161
|
-
/**
|
|
2162
|
-
* Emit a settings:changed event through the photon's emit system
|
|
2163
|
-
*/
|
|
2164
|
-
emitSettingsChange(instance, property, oldValue, newValue) {
|
|
2165
|
-
if (typeof instance.emit === 'function') {
|
|
2166
|
-
try {
|
|
2167
|
-
instance.emit({
|
|
2168
|
-
event: 'settings:changed',
|
|
2169
|
-
data: {
|
|
2170
|
-
property,
|
|
2171
|
-
oldValue,
|
|
2172
|
-
newValue,
|
|
2173
|
-
timestamp: Date.now(),
|
|
2174
|
-
},
|
|
2175
|
-
});
|
|
2176
|
-
}
|
|
2177
|
-
catch {
|
|
2178
|
-
// Best-effort emit
|
|
2179
|
-
}
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2233
|
+
// Settings persistence (load, persist, inject Proxy, generate tool, execute
|
|
2234
|
+
// updates) lives in `./settings-persistence.ts` and is reachable via
|
|
2235
|
+
// `this.settingsPersistence`.
|
|
2182
2236
|
/**
|
|
2183
2237
|
* Extract constructor parameters from source file
|
|
2184
2238
|
*/
|
|
@@ -2950,7 +3004,6 @@ Run: photon mcp ${mcpName} --config
|
|
|
2950
3004
|
* All middleware (built-in and custom) follows the same code path — no if-chain.
|
|
2951
3005
|
*/
|
|
2952
3006
|
applyMiddleware(execute, toolMeta, photonName, toolName, parameters, instanceName, outputHandler) {
|
|
2953
|
-
// Build middleware context
|
|
2954
3007
|
const store = executionContext.getStore();
|
|
2955
3008
|
const ctx = {
|
|
2956
3009
|
photon: photonName,
|
|
@@ -2959,7 +3012,7 @@ Run: photon mcp ${mcpName} --config
|
|
|
2959
3012
|
params: parameters,
|
|
2960
3013
|
caller: store?.caller,
|
|
2961
3014
|
outputHandler,
|
|
2962
|
-
};
|
|
3015
|
+
};
|
|
2963
3016
|
// Get declarations from the new middleware[] field
|
|
2964
3017
|
const declarations = toolMeta.middleware || [];
|
|
2965
3018
|
if (declarations.length === 0) {
|
|
@@ -3193,9 +3246,13 @@ Run: photon mcp ${mcpName} --config
|
|
|
3193
3246
|
parentTraceparent = metaPeek.traceparent;
|
|
3194
3247
|
}
|
|
3195
3248
|
}
|
|
3196
|
-
// Start OTel span for tool execution (no-op if SDK not installed)
|
|
3197
|
-
|
|
3198
|
-
|
|
3249
|
+
// Start OTel span for tool execution (no-op if SDK not installed).
|
|
3250
|
+
// The `mcp.meta` shape is an internal extension some callers stamp on the
|
|
3251
|
+
// loaded photon — it isn't on PhotonClass so we type the lookup as a
|
|
3252
|
+
// narrow structural shape rather than reach for `as any`.
|
|
3253
|
+
const mcpMeta = mcp.meta;
|
|
3254
|
+
const toolMetaForSpan = mcpMeta?.tools?.[toolName];
|
|
3255
|
+
const isStateful = Boolean(toolMetaForSpan?.stateful ?? mcpMeta?.stateful);
|
|
3199
3256
|
const span = startToolSpan(mcp.name, toolName, parameters, options?.traceId, isStateful, parentTraceparent);
|
|
3200
3257
|
if (mcp.instance?.instanceName) {
|
|
3201
3258
|
span.setAttribute('photon.instance', mcp.instance.instanceName);
|
|
@@ -3229,7 +3286,9 @@ Run: photon mcp ${mcpName} --config
|
|
|
3229
3286
|
if (mcp.instance._photonConfigError) {
|
|
3230
3287
|
throw new Error(mcp.instance._photonConfigError);
|
|
3231
3288
|
}
|
|
3232
|
-
// Enforce @auth at method level — works across ALL transports (CLI, STDIO, HTTP, daemon)
|
|
3289
|
+
// Enforce @auth at method level — works across ALL transports (CLI, STDIO, HTTP, daemon).
|
|
3290
|
+
// `auth` is a runtime extension stamped by the loader on top of the published
|
|
3291
|
+
// PhotonClass type; reach via the narrower PhotonClassWithMeta interface.
|
|
3233
3292
|
const photonAuth = mcp.auth;
|
|
3234
3293
|
if (photonAuth === 'required') {
|
|
3235
3294
|
const caller = options?.caller;
|
|
@@ -3262,9 +3321,29 @@ Run: photon mcp ${mcpName} --config
|
|
|
3262
3321
|
}
|
|
3263
3322
|
}
|
|
3264
3323
|
}
|
|
3324
|
+
// Track C: auth → instance binding. For `@stateful` photons with an
|
|
3325
|
+
// `@auth` scheme, derive `_targetInstance` from the caller's claims
|
|
3326
|
+
// so each authenticated user lands on a disjoint per-user instance.
|
|
3327
|
+
// Existing `_targetInstance` on the call wins (explicit overrides
|
|
3328
|
+
// implicit). Standalone `photon mcp` is single-tenant — this only
|
|
3329
|
+
// takes effect via the daemon and streamable-HTTP transport, which
|
|
3330
|
+
// both consume `_targetInstance` to route the call.
|
|
3331
|
+
if (photonAuth &&
|
|
3332
|
+
isStateful &&
|
|
3333
|
+
options?.caller?.claims &&
|
|
3334
|
+
parameters &&
|
|
3335
|
+
typeof parameters === 'object' &&
|
|
3336
|
+
!('_targetInstance' in parameters)) {
|
|
3337
|
+
const { resolveInstanceFromClaims, parseAuthDirective } = await import('./shared/instance-binding.js');
|
|
3338
|
+
const { scheme, claim } = parseAuthDirective(photonAuth);
|
|
3339
|
+
const bound = resolveInstanceFromClaims(scheme, options.caller.claims, claim);
|
|
3340
|
+
if (bound) {
|
|
3341
|
+
parameters._targetInstance = bound;
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3265
3344
|
// Intercept auto-generated settings tool
|
|
3266
3345
|
if (toolName === 'settings' && mcp.instance._settingsBacking) {
|
|
3267
|
-
const result = await this.
|
|
3346
|
+
const result = await this.settingsPersistence.execute(mcp.instance, parameters, {
|
|
3268
3347
|
outputHandler: options?.outputHandler,
|
|
3269
3348
|
inputProvider: options?.inputProvider,
|
|
3270
3349
|
});
|
|
@@ -3482,6 +3561,7 @@ Run: photon mcp ${mcpName} --config
|
|
|
3482
3561
|
caller: options?.caller,
|
|
3483
3562
|
inputProvider,
|
|
3484
3563
|
samplingProvider: options?.samplingProvider,
|
|
3564
|
+
roots: options?.roots,
|
|
3485
3565
|
}, () => {
|
|
3486
3566
|
return method.call(mcp.instance, ...args);
|
|
3487
3567
|
});
|
|
@@ -3565,7 +3645,7 @@ Run: photon mcp ${mcpName} --config
|
|
|
3565
3645
|
instance: mcp.instance?.instanceName,
|
|
3566
3646
|
});
|
|
3567
3647
|
}
|
|
3568
|
-
this.logger.
|
|
3648
|
+
this.logger.debug(`Tool execution failed: ${toolName} - ${getErrorMessage(error)}`);
|
|
3569
3649
|
throw error;
|
|
3570
3650
|
}
|
|
3571
3651
|
finally {
|
|
@@ -4075,57 +4155,8 @@ Run: photon mcp ${mcpName} --config
|
|
|
4075
4155
|
throw initError;
|
|
4076
4156
|
}
|
|
4077
4157
|
}
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
* Uses shared discoverAssets from photon-core for core logic,
|
|
4081
|
-
* then applies photon-specific extensions (method UI links, URI generation).
|
|
4082
|
-
*/
|
|
4083
|
-
async discoverAssets(photonPath, source) {
|
|
4084
|
-
const basename = path.basename(photonPath, '.photon.ts');
|
|
4085
|
-
// Use shared discovery from photon-core
|
|
4086
|
-
const assets = await sharedDiscoverAssets(photonPath, source);
|
|
4087
|
-
if (!assets) {
|
|
4088
|
-
return undefined;
|
|
4089
|
-
}
|
|
4090
|
-
// Apply method-level @ui links AFTER auto-discovery
|
|
4091
|
-
this.applyMethodUILinks(source, assets);
|
|
4092
|
-
// Generate ui:// URIs for MCP Apps Extension support (SEP-1865)
|
|
4093
|
-
this.generateAssetURIs(basename, assets);
|
|
4094
|
-
return assets;
|
|
4095
|
-
}
|
|
4096
|
-
/**
|
|
4097
|
-
* Expose discovered asset metadata on the instance without breaking Photon.assets().
|
|
4098
|
-
*
|
|
4099
|
-
* Photon subclasses inherit an `assets(subpath)` method from photon-core. We bind that
|
|
4100
|
-
* method and decorate the function object with discovered metadata so both of these work:
|
|
4101
|
-
* - `this.assets('templates')`
|
|
4102
|
-
* - `this.assets.ui`
|
|
4103
|
-
*
|
|
4104
|
-
* Plain classes don't have the inherited method, so they receive the metadata object directly.
|
|
4105
|
-
*/
|
|
4106
|
-
attachAssetsToInstance(instance, assets) {
|
|
4107
|
-
if (!assets) {
|
|
4108
|
-
return;
|
|
4109
|
-
}
|
|
4110
|
-
const existingAssets = instance.assets;
|
|
4111
|
-
if (typeof existingAssets === 'function') {
|
|
4112
|
-
const boundAssets = existingAssets.bind(instance);
|
|
4113
|
-
Object.assign(boundAssets, assets);
|
|
4114
|
-
Object.defineProperty(instance, 'assets', {
|
|
4115
|
-
value: boundAssets,
|
|
4116
|
-
configurable: true,
|
|
4117
|
-
enumerable: false,
|
|
4118
|
-
writable: false,
|
|
4119
|
-
});
|
|
4120
|
-
return;
|
|
4121
|
-
}
|
|
4122
|
-
Object.defineProperty(instance, 'assets', {
|
|
4123
|
-
value: assets,
|
|
4124
|
-
configurable: true,
|
|
4125
|
-
enumerable: false,
|
|
4126
|
-
writable: false,
|
|
4127
|
-
});
|
|
4128
|
-
}
|
|
4158
|
+
// Asset discovery + binding lives in `./asset-resolver.ts` and is reachable
|
|
4159
|
+
// via `this.assetResolver`.
|
|
4129
4160
|
/**
|
|
4130
4161
|
* Inject Photon path helpers for plain classes that use them without extending Photon.
|
|
4131
4162
|
*/
|
|
@@ -4182,48 +4213,5 @@ Run: photon mcp ${mcpName} --config
|
|
|
4182
4213
|
};
|
|
4183
4214
|
}
|
|
4184
4215
|
}
|
|
4185
|
-
/**
|
|
4186
|
-
* Generate ui:// URIs for all UI assets (MCP Apps Extension support)
|
|
4187
|
-
* URI format: ui://<photon-name>/<asset-id>
|
|
4188
|
-
*/
|
|
4189
|
-
generateAssetURIs(photonName, assets) {
|
|
4190
|
-
for (const ui of assets.ui) {
|
|
4191
|
-
// Add uri field for MCP Apps compatibility
|
|
4192
|
-
ui.uri = `ui://${photonName}/${ui.id}`;
|
|
4193
|
-
this.log(` 🔗 URI: ${ui.uri}`);
|
|
4194
|
-
}
|
|
4195
|
-
}
|
|
4196
|
-
/**
|
|
4197
|
-
* Apply method-level @ui annotations to link UI assets to tools
|
|
4198
|
-
* Called after auto-discovery so all UI assets are available
|
|
4199
|
-
*/
|
|
4200
|
-
applyMethodUILinks(source, assets) {
|
|
4201
|
-
// Match method JSDoc with @ui annotation: /** ... @ui <id> ... */ async methodName
|
|
4202
|
-
const methodUiRegex = /\/\*\*[\s\S]*?@ui\s+(\w[\w-]*)[\s\S]*?\*\/\s*(?:async\s+)?\*?\s*(\w+)/g;
|
|
4203
|
-
let match;
|
|
4204
|
-
while ((match = methodUiRegex.exec(source)) !== null) {
|
|
4205
|
-
const [, uiId, methodName] = match;
|
|
4206
|
-
const asset = assets.ui.find((u) => u.id === uiId);
|
|
4207
|
-
if (asset) {
|
|
4208
|
-
// First method wins as primary (used for app detection)
|
|
4209
|
-
if (!asset.linkedTool) {
|
|
4210
|
-
asset.linkedTool = methodName;
|
|
4211
|
-
this.log(` 🔗 UI ${uiId} → ${methodName}`);
|
|
4212
|
-
}
|
|
4213
|
-
// Track all methods that reference this UI
|
|
4214
|
-
if (!asset.linkedTools)
|
|
4215
|
-
asset.linkedTools = [];
|
|
4216
|
-
if (!asset.linkedTools.includes(methodName)) {
|
|
4217
|
-
asset.linkedTools.push(methodName);
|
|
4218
|
-
if (asset.linkedTools.length > 1) {
|
|
4219
|
-
this.log(` 🔗 UI ${uiId} → ${methodName} (shared)`);
|
|
4220
|
-
}
|
|
4221
|
-
}
|
|
4222
|
-
}
|
|
4223
|
-
else {
|
|
4224
|
-
this.log(` ⚠️ @ui ${uiId} on ${methodName}: asset not found (check file exists)`);
|
|
4225
|
-
}
|
|
4226
|
-
}
|
|
4227
|
-
}
|
|
4228
4216
|
}
|
|
4229
4217
|
//# sourceMappingURL=loader.js.map
|