@portel/photon 1.29.0 → 1.31.1
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 +41 -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 +162 -45
- 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.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +24 -14
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +15 -1
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +170 -22
- 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.js +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 +30 -9
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/server.js +34 -13
- 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 +129 -2
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/embedded-runtime.js.map +1 -1
- package/dist/loader.d.ts +43 -66
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +185 -305
- 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/resource-server.d.ts +3 -3
- package/dist/resource-server.d.ts.map +1 -1
- 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 +42 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +166 -14
- 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/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/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/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 -8
- package/dist/types/server-types.d.ts.map +1 -1
- package/package.json +11 -4
- package/templates/cloudflare/worker.ts.template +338 -11
- package/templates/cloudflare/wrangler.toml.template +1 -6
- package/templates/photon.template.ts +13 -0
package/dist/loader.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
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';
|
|
10
13
|
import { extractExposesFromSource } from './shared/expose-route-extractor.js';
|
|
11
14
|
import { createRequire } from 'module';
|
|
@@ -28,7 +31,7 @@ ProgressRenderer,
|
|
|
28
31
|
// CLI formatting
|
|
29
32
|
formatOutput as cliFormatOutput, createMCPProxy, MCPConfigurationError, SDKMCPClientFactory, resolveMCPSource,
|
|
30
33
|
// Photon runtime configuration
|
|
31
|
-
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,
|
|
32
35
|
// Execution audit trail
|
|
33
36
|
getAuditTrail,
|
|
34
37
|
// Capability detection for auto-injection
|
|
@@ -49,6 +52,8 @@ import { PHOTON_VERSION, getResolvedPhotonCoreVersion } from './version.js';
|
|
|
49
52
|
// Timeout for external fetch requests (marketplace, GitHub)
|
|
50
53
|
const FETCH_TIMEOUT_MS = 30 * 1000;
|
|
51
54
|
import { generateConfigErrorMessage, summarizeConstructorParams } from './shared/config-docs.js';
|
|
55
|
+
import { SettingsPersistence } from './settings-persistence.js';
|
|
56
|
+
import { AssetResolver } from './asset-resolver.js';
|
|
52
57
|
import { createLogger } from './shared/logger.js';
|
|
53
58
|
import { getErrorMessage } from './shared/error-handler.js';
|
|
54
59
|
import { validateOrThrow, assertString, notEmpty, hasExtension } from './shared/validation.js';
|
|
@@ -183,6 +188,31 @@ function injectEmitHelpers(instance) {
|
|
|
183
188
|
instance.thinking = (active = true) => emit({ emit: 'thinking', active });
|
|
184
189
|
}
|
|
185
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';
|
|
215
|
+
}
|
|
186
216
|
/** Extra regex checks that force 'emit' capability when helper methods are used. */
|
|
187
217
|
function detectEmitHelperUsage(source) {
|
|
188
218
|
return /this\.(toast|log|status|progress|thinking)\s*\(/.test(source);
|
|
@@ -369,6 +399,14 @@ export class PhotonLoader {
|
|
|
369
399
|
marketplaceManager;
|
|
370
400
|
marketplaceManagerPromise;
|
|
371
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();
|
|
372
410
|
/**
|
|
373
411
|
* Per-instance call queue. Two tool invocations targeting the same photon
|
|
374
412
|
* instance are serialized: the second starts only after the first returns.
|
|
@@ -430,6 +468,8 @@ export class PhotonLoader {
|
|
|
430
468
|
this.verbose = verbose;
|
|
431
469
|
this.logger = logger ?? createLogger({ component: 'photon-loader', minimal: true });
|
|
432
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));
|
|
433
473
|
}
|
|
434
474
|
/**
|
|
435
475
|
* Load MCP configuration from ~/.photon/config.json
|
|
@@ -451,6 +491,63 @@ export class PhotonLoader {
|
|
|
451
491
|
});
|
|
452
492
|
return this.mcpConfigPromise;
|
|
453
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
|
+
}
|
|
454
551
|
async getMarketplaceManager() {
|
|
455
552
|
if (this.marketplaceManager) {
|
|
456
553
|
return this.marketplaceManager;
|
|
@@ -558,6 +655,18 @@ export class PhotonLoader {
|
|
|
558
655
|
await this.clearDependencyCache(cacheKey);
|
|
559
656
|
await this.clearBuildCache(cacheKey);
|
|
560
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
|
+
}
|
|
561
670
|
/**
|
|
562
671
|
* Path to metadata file describing installed dependencies
|
|
563
672
|
*/
|
|
@@ -675,6 +784,11 @@ export class PhotonLoader {
|
|
|
675
784
|
message.includes('Cannot find module') ||
|
|
676
785
|
message.includes('require is not defined'));
|
|
677
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
|
+
}
|
|
678
792
|
static parseDependenciesFromSource(source) {
|
|
679
793
|
const deps = [];
|
|
680
794
|
// Only match @dependencies inside JSDoc blocks (/** ... */)
|
|
@@ -802,6 +916,12 @@ export class PhotonLoader {
|
|
|
802
916
|
module = await importModule();
|
|
803
917
|
}
|
|
804
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
|
+
}
|
|
805
925
|
throw error;
|
|
806
926
|
}
|
|
807
927
|
}
|
|
@@ -1088,6 +1208,12 @@ export class PhotonLoader {
|
|
|
1088
1208
|
configurable: true,
|
|
1089
1209
|
});
|
|
1090
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.
|
|
1091
1217
|
// Always-inject `this.callerCwd`. Reads the originating CLI cwd from
|
|
1092
1218
|
// the request context, falling back to `process.cwd()` when no context
|
|
1093
1219
|
// is active (direct in-process load with no caller info). Inside a
|
|
@@ -1376,15 +1502,33 @@ export class PhotonLoader {
|
|
|
1376
1502
|
}
|
|
1377
1503
|
// Extract tools, templates, and statics (with schema override support)
|
|
1378
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
|
+
}
|
|
1379
1523
|
// ═══ SETTINGS INJECTION ═══
|
|
1380
1524
|
// If the photon declared `protected settings = { ... }`, inject persistence + proxy
|
|
1381
1525
|
if (settingsSchema?.hasSettings &&
|
|
1382
1526
|
instance.settings &&
|
|
1383
1527
|
typeof instance.settings === 'object') {
|
|
1384
1528
|
const instanceName = options?.instanceName || 'default';
|
|
1385
|
-
await this.
|
|
1529
|
+
await this.settingsPersistence.inject(instance, name, instanceName, settingsSchema);
|
|
1386
1530
|
// Auto-generate the `settings` MCP tool from the schema
|
|
1387
|
-
const settingsTool = this.
|
|
1531
|
+
const settingsTool = this.settingsPersistence.generateTool(settingsSchema);
|
|
1388
1532
|
tools.push(settingsTool);
|
|
1389
1533
|
this.log(`⚙️ Settings tool auto-generated for ${name} (${settingsSchema.properties.length} props)`);
|
|
1390
1534
|
}
|
|
@@ -1393,8 +1537,8 @@ export class PhotonLoader {
|
|
|
1393
1537
|
this.logger.warn(`⚠️ ${name}: configure() method is deprecated. Use 'protected settings = { ... }' property instead.`);
|
|
1394
1538
|
}
|
|
1395
1539
|
// Extract assets from source and discover asset folder
|
|
1396
|
-
const assets = await this.
|
|
1397
|
-
this.
|
|
1540
|
+
const assets = await this.assetResolver.discover(absolutePath, tsContent || '');
|
|
1541
|
+
this.assetResolver.attachToInstance(instance, assets);
|
|
1398
1542
|
const counts = [
|
|
1399
1543
|
tools.length > 0 ? `${tools.length} tools` : null,
|
|
1400
1544
|
templates.length > 0 ? `${templates.length} templates` : null,
|
|
@@ -1736,15 +1880,25 @@ export class PhotonLoader {
|
|
|
1736
1880
|
await this.invokeInitialize(instance, name, options);
|
|
1737
1881
|
// Extract tools and metadata from embedded source (no disk I/O)
|
|
1738
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
|
+
}
|
|
1739
1893
|
// Settings injection
|
|
1740
1894
|
if (settingsSchema?.hasSettings && instance.settings && typeof instance.settings === 'object') {
|
|
1741
1895
|
const instanceName = options?.instanceName || 'default';
|
|
1742
|
-
await this.
|
|
1743
|
-
tools.push(this.
|
|
1896
|
+
await this.settingsPersistence.inject(instance, name, instanceName, settingsSchema);
|
|
1897
|
+
tools.push(this.settingsPersistence.generateTool(settingsSchema));
|
|
1744
1898
|
}
|
|
1745
1899
|
// Discover assets (for @ui)
|
|
1746
|
-
const assets = await this.
|
|
1747
|
-
this.
|
|
1900
|
+
const assets = await this.assetResolver.discover(absolutePath, tsContent);
|
|
1901
|
+
this.assetResolver.attachToInstance(instance, assets);
|
|
1748
1902
|
this.log(`✅ Loaded (preloaded): ${name} (${tools.length} tools)`);
|
|
1749
1903
|
// Extract class-level metadata from docblock (icon, description, stateful)
|
|
1750
1904
|
const classDocblock = this.extractClassDocblock(tsContent);
|
|
@@ -1819,7 +1973,10 @@ export class PhotonLoader {
|
|
|
1819
1973
|
const files = await fs.readdir(buildDir).catch(() => []);
|
|
1820
1974
|
for (const f of files) {
|
|
1821
1975
|
if (f.startsWith(fileName) && f.endsWith('.mjs')) {
|
|
1822
|
-
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
|
+
}));
|
|
1823
1980
|
}
|
|
1824
1981
|
}
|
|
1825
1982
|
}
|
|
@@ -2073,199 +2230,9 @@ export class PhotonLoader {
|
|
|
2073
2230
|
statics = statics.map((s) => ({ ...s, description: this.stripJSDocTags(s.description) }));
|
|
2074
2231
|
return { tools, templates, statics };
|
|
2075
2232
|
}
|
|
2076
|
-
//
|
|
2077
|
-
//
|
|
2078
|
-
//
|
|
2079
|
-
/**
|
|
2080
|
-
* Get the settings persistence path for a photon instance.
|
|
2081
|
-
* Co-located with state under Option B: the settings JSON lives next to
|
|
2082
|
-
* state.json inside the per-instance directory.
|
|
2083
|
-
*/
|
|
2084
|
-
getSettingsPath(photonName, instanceName) {
|
|
2085
|
-
// getInstanceStatePath returns `.../state/{instance}/state.json`; swap
|
|
2086
|
-
// the filename so settings land at `.../state/{instance}/settings.json`.
|
|
2087
|
-
// Keeps the source of truth for the layout in context-store.
|
|
2088
|
-
const statePath = getInstanceStatePath(photonName, instanceName, this.baseDir);
|
|
2089
|
-
return path.join(path.dirname(statePath), 'settings.json');
|
|
2090
|
-
}
|
|
2091
|
-
/**
|
|
2092
|
-
* Load persisted settings from disk
|
|
2093
|
-
*/
|
|
2094
|
-
async loadSettings(photonName, instanceName) {
|
|
2095
|
-
const settingsPath = this.getSettingsPath(photonName, instanceName);
|
|
2096
|
-
try {
|
|
2097
|
-
return await readJSON(settingsPath);
|
|
2098
|
-
}
|
|
2099
|
-
catch {
|
|
2100
|
-
return {};
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2103
|
-
/**
|
|
2104
|
-
* Persist settings to disk
|
|
2105
|
-
*/
|
|
2106
|
-
async persistSettings(photonName, instanceName, values) {
|
|
2107
|
-
const settingsPath = this.getSettingsPath(photonName, instanceName);
|
|
2108
|
-
const dir = path.dirname(settingsPath);
|
|
2109
|
-
await fs.mkdir(dir, { recursive: true });
|
|
2110
|
-
await writeJSON(settingsPath, values);
|
|
2111
|
-
}
|
|
2112
|
-
/**
|
|
2113
|
-
* Inject settings into a photon instance:
|
|
2114
|
-
* - Load persisted values (persisted wins over defaults)
|
|
2115
|
-
* - Replace instance.settings with a read-only Proxy
|
|
2116
|
-
* - Store writable backing object for the settings tool
|
|
2117
|
-
*/
|
|
2118
|
-
async injectSettings(instance, photonName, instanceName, schema) {
|
|
2119
|
-
const defaults = instance.settings;
|
|
2120
|
-
const persisted = await this.loadSettings(photonName, instanceName);
|
|
2121
|
-
// Merge: persisted values over defaults
|
|
2122
|
-
const backing = { ...defaults };
|
|
2123
|
-
for (const key of Object.keys(persisted)) {
|
|
2124
|
-
if (persisted[key] !== undefined) {
|
|
2125
|
-
backing[key] = persisted[key];
|
|
2126
|
-
}
|
|
2127
|
-
}
|
|
2128
|
-
// Store the writable backing object for the settings tool to update
|
|
2129
|
-
instance._settingsBacking = backing;
|
|
2130
|
-
instance._settingsPhotonName = photonName;
|
|
2131
|
-
instance._settingsInstanceName = instanceName;
|
|
2132
|
-
instance._settingsSchema = schema;
|
|
2133
|
-
// Replace with read-only Proxy
|
|
2134
|
-
instance.settings = new Proxy(backing, {
|
|
2135
|
-
get(target, prop) {
|
|
2136
|
-
if (typeof prop === 'string') {
|
|
2137
|
-
return target[prop];
|
|
2138
|
-
}
|
|
2139
|
-
return undefined;
|
|
2140
|
-
},
|
|
2141
|
-
set(_target, prop, _value) {
|
|
2142
|
-
throw new Error(`Cannot directly set settings.${String(prop)}. ` +
|
|
2143
|
-
`Use the 'settings' tool to change settings (e.g., settings({ ${String(prop)}: newValue })).`);
|
|
2144
|
-
},
|
|
2145
|
-
deleteProperty(_target, prop) {
|
|
2146
|
-
throw new Error(`Cannot delete settings.${String(prop)}. Use the 'settings' tool instead.`);
|
|
2147
|
-
},
|
|
2148
|
-
});
|
|
2149
|
-
}
|
|
2150
|
-
/**
|
|
2151
|
-
* Generate an MCP tool definition from a SettingsSchema
|
|
2152
|
-
*/
|
|
2153
|
-
generateSettingsTool(schema) {
|
|
2154
|
-
const properties = {};
|
|
2155
|
-
for (const prop of schema.properties) {
|
|
2156
|
-
const propSchema = { type: prop.type };
|
|
2157
|
-
if (prop.description) {
|
|
2158
|
-
propSchema.description = prop.description;
|
|
2159
|
-
}
|
|
2160
|
-
if (prop.default !== undefined) {
|
|
2161
|
-
propSchema.default = prop.default;
|
|
2162
|
-
}
|
|
2163
|
-
properties[prop.name] = propSchema;
|
|
2164
|
-
}
|
|
2165
|
-
return {
|
|
2166
|
-
name: 'settings',
|
|
2167
|
-
description: 'View or update photon settings. Call with no arguments to view current settings. Pass parameters to update specific settings.',
|
|
2168
|
-
inputSchema: {
|
|
2169
|
-
type: 'object',
|
|
2170
|
-
properties,
|
|
2171
|
-
// No required params — all settings are optional when calling the tool
|
|
2172
|
-
},
|
|
2173
|
-
};
|
|
2174
|
-
}
|
|
2175
|
-
/**
|
|
2176
|
-
* Execute the auto-generated settings tool:
|
|
2177
|
-
* - No params → return current settings
|
|
2178
|
-
* - Params with values → update those settings, persist, emit change
|
|
2179
|
-
* - Params with undefined + no default → trigger elicitation
|
|
2180
|
-
*/
|
|
2181
|
-
async executeSettingsTool(instance, parameters, options) {
|
|
2182
|
-
const backing = instance._settingsBacking;
|
|
2183
|
-
const photonName = instance._settingsPhotonName;
|
|
2184
|
-
const instanceName = instance._settingsInstanceName;
|
|
2185
|
-
const schema = instance._settingsSchema;
|
|
2186
|
-
if (!backing || !photonName || !schema) {
|
|
2187
|
-
throw new Error('Settings not initialized for this photon');
|
|
2188
|
-
}
|
|
2189
|
-
// No params or empty params → return current settings
|
|
2190
|
-
if (!parameters || Object.keys(parameters).length === 0) {
|
|
2191
|
-
// Check if any required settings (undefined defaults) need elicitation
|
|
2192
|
-
const needsElicitation = schema.properties.filter((p) => p.required && backing[p.name] === undefined);
|
|
2193
|
-
if (needsElicitation.length > 0 && options?.inputProvider) {
|
|
2194
|
-
for (const prop of needsElicitation) {
|
|
2195
|
-
const result = await options.inputProvider({
|
|
2196
|
-
ask: prop.type === 'number' ? 'number' : 'text',
|
|
2197
|
-
message: prop.description || `Enter value for ${prop.name}:`,
|
|
2198
|
-
});
|
|
2199
|
-
if (result !== undefined && result !== null) {
|
|
2200
|
-
const oldValue = backing[prop.name];
|
|
2201
|
-
backing[prop.name] = result;
|
|
2202
|
-
this.log(`⚙️ Settings: ${prop.name} = ${JSON.stringify(result)} (elicited)`);
|
|
2203
|
-
this.emitSettingsChange(instance, prop.name, oldValue, result);
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
await this.persistSettings(photonName, instanceName, backing);
|
|
2207
|
-
}
|
|
2208
|
-
return { ...backing };
|
|
2209
|
-
}
|
|
2210
|
-
// Update specified settings
|
|
2211
|
-
const changes = [];
|
|
2212
|
-
for (const [key, value] of Object.entries(parameters)) {
|
|
2213
|
-
// Verify this is a valid setting
|
|
2214
|
-
const prop = schema.properties.find((p) => p.name === key);
|
|
2215
|
-
if (!prop)
|
|
2216
|
-
continue;
|
|
2217
|
-
if (value === undefined && prop.required) {
|
|
2218
|
-
// Elicit value from user
|
|
2219
|
-
if (options?.inputProvider) {
|
|
2220
|
-
const result = await options.inputProvider({
|
|
2221
|
-
ask: prop.type === 'number' ? 'number' : 'text',
|
|
2222
|
-
message: prop.description || `Enter value for ${key}:`,
|
|
2223
|
-
});
|
|
2224
|
-
if (result !== undefined && result !== null) {
|
|
2225
|
-
const oldValue = backing[key];
|
|
2226
|
-
backing[key] = result;
|
|
2227
|
-
changes.push({ property: key, oldValue, newValue: result });
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
else {
|
|
2232
|
-
const oldValue = backing[key];
|
|
2233
|
-
backing[key] = value;
|
|
2234
|
-
changes.push({ property: key, oldValue, newValue: value });
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
// Persist if anything changed
|
|
2238
|
-
if (changes.length > 0) {
|
|
2239
|
-
await this.persistSettings(photonName, instanceName, backing);
|
|
2240
|
-
for (const change of changes) {
|
|
2241
|
-
this.log(`⚙️ Settings: ${change.property}: ${JSON.stringify(change.oldValue)} → ${JSON.stringify(change.newValue)}`);
|
|
2242
|
-
this.emitSettingsChange(instance, change.property, change.oldValue, change.newValue);
|
|
2243
|
-
}
|
|
2244
|
-
}
|
|
2245
|
-
// Re-apply proxy (backing object is already updated in place, proxy still references it)
|
|
2246
|
-
return { ...backing };
|
|
2247
|
-
}
|
|
2248
|
-
/**
|
|
2249
|
-
* Emit a settings:changed event through the photon's emit system
|
|
2250
|
-
*/
|
|
2251
|
-
emitSettingsChange(instance, property, oldValue, newValue) {
|
|
2252
|
-
if (typeof instance.emit === 'function') {
|
|
2253
|
-
try {
|
|
2254
|
-
instance.emit({
|
|
2255
|
-
event: 'settings:changed',
|
|
2256
|
-
data: {
|
|
2257
|
-
property,
|
|
2258
|
-
oldValue,
|
|
2259
|
-
newValue,
|
|
2260
|
-
timestamp: Date.now(),
|
|
2261
|
-
},
|
|
2262
|
-
});
|
|
2263
|
-
}
|
|
2264
|
-
catch {
|
|
2265
|
-
// Best-effort emit
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2233
|
+
// Settings persistence (load, persist, inject Proxy, generate tool, execute
|
|
2234
|
+
// updates) lives in `./settings-persistence.ts` and is reachable via
|
|
2235
|
+
// `this.settingsPersistence`.
|
|
2269
2236
|
/**
|
|
2270
2237
|
* Extract constructor parameters from source file
|
|
2271
2238
|
*/
|
|
@@ -3037,7 +3004,6 @@ Run: photon mcp ${mcpName} --config
|
|
|
3037
3004
|
* All middleware (built-in and custom) follows the same code path — no if-chain.
|
|
3038
3005
|
*/
|
|
3039
3006
|
applyMiddleware(execute, toolMeta, photonName, toolName, parameters, instanceName, outputHandler) {
|
|
3040
|
-
// Build middleware context
|
|
3041
3007
|
const store = executionContext.getStore();
|
|
3042
3008
|
const ctx = {
|
|
3043
3009
|
photon: photonName,
|
|
@@ -3046,7 +3012,7 @@ Run: photon mcp ${mcpName} --config
|
|
|
3046
3012
|
params: parameters,
|
|
3047
3013
|
caller: store?.caller,
|
|
3048
3014
|
outputHandler,
|
|
3049
|
-
};
|
|
3015
|
+
};
|
|
3050
3016
|
// Get declarations from the new middleware[] field
|
|
3051
3017
|
const declarations = toolMeta.middleware || [];
|
|
3052
3018
|
if (declarations.length === 0) {
|
|
@@ -3280,9 +3246,13 @@ Run: photon mcp ${mcpName} --config
|
|
|
3280
3246
|
parentTraceparent = metaPeek.traceparent;
|
|
3281
3247
|
}
|
|
3282
3248
|
}
|
|
3283
|
-
// Start OTel span for tool execution (no-op if SDK not installed)
|
|
3284
|
-
|
|
3285
|
-
|
|
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);
|
|
3286
3256
|
const span = startToolSpan(mcp.name, toolName, parameters, options?.traceId, isStateful, parentTraceparent);
|
|
3287
3257
|
if (mcp.instance?.instanceName) {
|
|
3288
3258
|
span.setAttribute('photon.instance', mcp.instance.instanceName);
|
|
@@ -3316,7 +3286,9 @@ Run: photon mcp ${mcpName} --config
|
|
|
3316
3286
|
if (mcp.instance._photonConfigError) {
|
|
3317
3287
|
throw new Error(mcp.instance._photonConfigError);
|
|
3318
3288
|
}
|
|
3319
|
-
// 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.
|
|
3320
3292
|
const photonAuth = mcp.auth;
|
|
3321
3293
|
if (photonAuth === 'required') {
|
|
3322
3294
|
const caller = options?.caller;
|
|
@@ -3371,7 +3343,7 @@ Run: photon mcp ${mcpName} --config
|
|
|
3371
3343
|
}
|
|
3372
3344
|
// Intercept auto-generated settings tool
|
|
3373
3345
|
if (toolName === 'settings' && mcp.instance._settingsBacking) {
|
|
3374
|
-
const result = await this.
|
|
3346
|
+
const result = await this.settingsPersistence.execute(mcp.instance, parameters, {
|
|
3375
3347
|
outputHandler: options?.outputHandler,
|
|
3376
3348
|
inputProvider: options?.inputProvider,
|
|
3377
3349
|
});
|
|
@@ -3673,7 +3645,7 @@ Run: photon mcp ${mcpName} --config
|
|
|
3673
3645
|
instance: mcp.instance?.instanceName,
|
|
3674
3646
|
});
|
|
3675
3647
|
}
|
|
3676
|
-
this.logger.
|
|
3648
|
+
this.logger.debug(`Tool execution failed: ${toolName} - ${getErrorMessage(error)}`);
|
|
3677
3649
|
throw error;
|
|
3678
3650
|
}
|
|
3679
3651
|
finally {
|
|
@@ -4183,57 +4155,8 @@ Run: photon mcp ${mcpName} --config
|
|
|
4183
4155
|
throw initError;
|
|
4184
4156
|
}
|
|
4185
4157
|
}
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
* Uses shared discoverAssets from photon-core for core logic,
|
|
4189
|
-
* then applies photon-specific extensions (method UI links, URI generation).
|
|
4190
|
-
*/
|
|
4191
|
-
async discoverAssets(photonPath, source) {
|
|
4192
|
-
const basename = path.basename(photonPath, '.photon.ts');
|
|
4193
|
-
// Use shared discovery from photon-core
|
|
4194
|
-
const assets = await sharedDiscoverAssets(photonPath, source);
|
|
4195
|
-
if (!assets) {
|
|
4196
|
-
return undefined;
|
|
4197
|
-
}
|
|
4198
|
-
// Apply method-level @ui links AFTER auto-discovery
|
|
4199
|
-
this.applyMethodUILinks(source, assets);
|
|
4200
|
-
// Generate ui:// URIs for MCP Apps Extension support (SEP-1865)
|
|
4201
|
-
this.generateAssetURIs(basename, assets);
|
|
4202
|
-
return assets;
|
|
4203
|
-
}
|
|
4204
|
-
/**
|
|
4205
|
-
* Expose discovered asset metadata on the instance without breaking Photon.assets().
|
|
4206
|
-
*
|
|
4207
|
-
* Photon subclasses inherit an `assets(subpath)` method from photon-core. We bind that
|
|
4208
|
-
* method and decorate the function object with discovered metadata so both of these work:
|
|
4209
|
-
* - `this.assets('templates')`
|
|
4210
|
-
* - `this.assets.ui`
|
|
4211
|
-
*
|
|
4212
|
-
* Plain classes don't have the inherited method, so they receive the metadata object directly.
|
|
4213
|
-
*/
|
|
4214
|
-
attachAssetsToInstance(instance, assets) {
|
|
4215
|
-
if (!assets) {
|
|
4216
|
-
return;
|
|
4217
|
-
}
|
|
4218
|
-
const existingAssets = instance.assets;
|
|
4219
|
-
if (typeof existingAssets === 'function') {
|
|
4220
|
-
const boundAssets = existingAssets.bind(instance);
|
|
4221
|
-
Object.assign(boundAssets, assets);
|
|
4222
|
-
Object.defineProperty(instance, 'assets', {
|
|
4223
|
-
value: boundAssets,
|
|
4224
|
-
configurable: true,
|
|
4225
|
-
enumerable: false,
|
|
4226
|
-
writable: false,
|
|
4227
|
-
});
|
|
4228
|
-
return;
|
|
4229
|
-
}
|
|
4230
|
-
Object.defineProperty(instance, 'assets', {
|
|
4231
|
-
value: assets,
|
|
4232
|
-
configurable: true,
|
|
4233
|
-
enumerable: false,
|
|
4234
|
-
writable: false,
|
|
4235
|
-
});
|
|
4236
|
-
}
|
|
4158
|
+
// Asset discovery + binding lives in `./asset-resolver.ts` and is reachable
|
|
4159
|
+
// via `this.assetResolver`.
|
|
4237
4160
|
/**
|
|
4238
4161
|
* Inject Photon path helpers for plain classes that use them without extending Photon.
|
|
4239
4162
|
*/
|
|
@@ -4290,48 +4213,5 @@ Run: photon mcp ${mcpName} --config
|
|
|
4290
4213
|
};
|
|
4291
4214
|
}
|
|
4292
4215
|
}
|
|
4293
|
-
/**
|
|
4294
|
-
* Generate ui:// URIs for all UI assets (MCP Apps Extension support)
|
|
4295
|
-
* URI format: ui://<photon-name>/<asset-id>
|
|
4296
|
-
*/
|
|
4297
|
-
generateAssetURIs(photonName, assets) {
|
|
4298
|
-
for (const ui of assets.ui) {
|
|
4299
|
-
// Add uri field for MCP Apps compatibility
|
|
4300
|
-
ui.uri = `ui://${photonName}/${ui.id}`;
|
|
4301
|
-
this.log(` 🔗 URI: ${ui.uri}`);
|
|
4302
|
-
}
|
|
4303
|
-
}
|
|
4304
|
-
/**
|
|
4305
|
-
* Apply method-level @ui annotations to link UI assets to tools
|
|
4306
|
-
* Called after auto-discovery so all UI assets are available
|
|
4307
|
-
*/
|
|
4308
|
-
applyMethodUILinks(source, assets) {
|
|
4309
|
-
// Match method JSDoc with @ui annotation: /** ... @ui <id> ... */ async methodName
|
|
4310
|
-
const methodUiRegex = /\/\*\*[\s\S]*?@ui\s+(\w[\w-]*)[\s\S]*?\*\/\s*(?:async\s+)?\*?\s*(\w+)/g;
|
|
4311
|
-
let match;
|
|
4312
|
-
while ((match = methodUiRegex.exec(source)) !== null) {
|
|
4313
|
-
const [, uiId, methodName] = match;
|
|
4314
|
-
const asset = assets.ui.find((u) => u.id === uiId);
|
|
4315
|
-
if (asset) {
|
|
4316
|
-
// First method wins as primary (used for app detection)
|
|
4317
|
-
if (!asset.linkedTool) {
|
|
4318
|
-
asset.linkedTool = methodName;
|
|
4319
|
-
this.log(` 🔗 UI ${uiId} → ${methodName}`);
|
|
4320
|
-
}
|
|
4321
|
-
// Track all methods that reference this UI
|
|
4322
|
-
if (!asset.linkedTools)
|
|
4323
|
-
asset.linkedTools = [];
|
|
4324
|
-
if (!asset.linkedTools.includes(methodName)) {
|
|
4325
|
-
asset.linkedTools.push(methodName);
|
|
4326
|
-
if (asset.linkedTools.length > 1) {
|
|
4327
|
-
this.log(` 🔗 UI ${uiId} → ${methodName} (shared)`);
|
|
4328
|
-
}
|
|
4329
|
-
}
|
|
4330
|
-
}
|
|
4331
|
-
else {
|
|
4332
|
-
this.log(` ⚠️ @ui ${uiId} on ${methodName}: asset not found (check file exists)`);
|
|
4333
|
-
}
|
|
4334
|
-
}
|
|
4335
|
-
}
|
|
4336
4216
|
}
|
|
4337
4217
|
//# sourceMappingURL=loader.js.map
|