/** * Photon MCP Loader * * Loads a single .photon.ts file and extracts tools */ import { type CfBindingsConfig } from './runtime/cf-local.js'; import { type PhotonExecutionRequestContext } from './telemetry/context.js'; import { ConstructorParam, PhotonClass, PhotonClassExtended, type InputProvider, type OutputHandler, type SamplingProvider, type MCPClientFactory, type CallerInfo } from '@portel/photon-core'; export interface ConstructorEnvReplayOptions { currentEnv?: Record; resolve(envVarName: string): string | undefined | Promise; capture(values: Record): void | Promise; } interface LoadOptions { instanceName?: string; skipInitialize?: boolean; constructorEnvReplay?: ConstructorEnvReplayOptions; } import { Logger } from './shared/logger.js'; /** * Clear any intermediate render output before printing final results. * Called by CLI runner before rendering the method's return value. */ export declare function clearRenderZone(): void; export declare class PhotonLoader { private dependencyManager; private verbose; private mcpClientFactory?; /** Cache of loaded Photon instances by source path */ private loadedPhotons; /** Get all loaded photon instances (including sub-photons loaded via @photon). */ getLoadedPhotons(): Map; private attachRuntimeConfigApi; /** In-flight Photon load promises — dedup concurrent loads of the same path */ private loadedPhotonPromises; /** MCP clients cache - reuse connections */ private mcpClients; /** In-flight MCP client creation promises — dedup concurrent creates for the same dep */ private mcpClientPromises; /** Cached MCP config from ~/.photon/config.json */ private mcpConfig?; /** Inflight config load promise (dedup concurrent calls) */ private mcpConfigPromise?; /** Progress renderer for inline CLI animation */ private progressRenderer; /** Marketplace manager for resolving remote Photons */ private marketplaceManager?; private marketplaceManagerPromise?; private logger; private settingsPersistence; private assetResolver; /** * One CFLocalRuntime per photon name. Boot is lazy inside the runtime * itself; this cache just ensures every instance of a given photon * shares a single miniflare sandbox. */ private cfRuntimes; /** * Per-instance call queue. Two tool invocations targeting the same photon * instance are serialized: the second starts only after the first returns. * Prevents lost-update and read-after-write races between methods that share * state via `this.memory` or instance fields. Keyed on the instance object * via WeakMap so hot-reload drops the queue automatically. */ private _instanceCallTails; /** Per-middleware-name state stores (caches, throttle windows, queues, etc.) */ private middlewareStates; /** Per-photon custom middleware definitions discovered from module exports */ private photonMiddleware; /** Shadow registry of circuit breaker states, keyed by `${photon}:${instance}:${tool}` */ private circuitHealthTracker; /** * Returns all tracked circuit breaker states across all photons. * Each entry uses the key format `${photon}:${instance}:${tool}`. */ getCircuitHealth(): Record; /** Base directory for state/config/cache (defaults to ~/.photon) */ baseDir: string; /** * Optional resolver for @photon dependencies. * When set (by the daemon), the loader asks the daemon for an existing shared instance * instead of creating a new isolated one. This ensures injected photons share the * same instance as the one the daemon manages (e.g., WhatsApp socket reuse). */ photonInstanceResolver?: (photonName: string, photonPath: string, callerInstanceName?: string) => Promise; /** * Optional resolver for this.instance(name) — same-photon cross-instance access. * When set (by the daemon), allows a photon to get another instance of itself * in-process without daemon round-trips. Used for instance-per-group patterns * where the default instance routes to named sub-instances. */ instanceResolver?: (instanceName: string) => Promise; /** * Pre-loaded dependency modules for compiled binaries. * Maps dependency name → { module, source } so @photon deps can be resolved * without file I/O when running as a standalone binary. */ preloadedDependencies?: Map; /** * Optional progress callback — invoked during long-running init phases * (dependency install, compilation, onInitialize). Used by worker-host * to send keepalive signals so the spawn timeout resets. */ onProgress?: (phase: string) => void; constructor(verbose?: boolean, logger?: Logger, baseDir?: string); /** * Load MCP configuration from ~/.photon/config.json * Called lazily on first MCP injection */ private ensureMCPConfig; /** * Resolve the path where a photon's CF binding override JSON lives. * The override layers on top of `protected cfBindings` so users can * repoint a binding (e.g., point `kv: cache` at a different namespace) * without editing source. */ getCfOverridePath(photonName: string): string; /** Load the per-photon CF override JSON. Returns null if missing. */ private loadCfOverride; /** * Persist the CF override for a photon. Creates parent directories as * needed; called by the `photon cf set` CLI command. */ saveCfOverride(photonName: string, override: CfBindingsConfig): Promise; /** * Build (or fetch the cached) CFLocalRuntime for `photonName`. Uses * the new options-form constructor so auto-naming applies and * miniflare seeds every literal qualifier the scanner found, plus * any override-declared bindings. */ private attachCfRuntime; /** Read the effective bindings for a photon: declared (from source) plus override. */ getEffectiveCfBindings(photonName: string, tsContent: string): Promise<{ declared: CfBindingsConfig | null; override: CfBindingsConfig | null; effective: CfBindingsConfig | null; }>; private getMarketplaceManager; /** * Set MCP client factory for enabling this.mcp() in Photons */ setMCPClientFactory(factory: MCPClientFactory): void; /** * Notifier wired by PhotonServer so `this.notifyResourceUpdated(uri)` * fans out to all subscribed MCP clients (STDIO + SSE sessions). * `undefined` outside the MCP-server runtime, in which case the helper is * a no-op. */ private resourceUpdateNotifier?; setResourceUpdateNotifier(notifier: (uri: string) => void | Promise): void; /** * Log message only if verbose mode is enabled */ private log; /** * Generate deterministic cache key for an MCP + photon path */ private getCacheKey; private getPhotonCacheDir; private sanitizeCacheLabel; private writePhotonCacheFile; private runCommand; /** * Directory where MCP-specific dependencies are cached */ private getDependencyCacheDir; private getBuildCacheDir; private clearBuildCache; private clearDependencyCache; private clearAllCaches; /** * Clear all caches for a photon identified by file path. * Called by Beam when a photon fails to load, so the next reload * recompiles from source rather than using stale cached artifacts. */ clearCacheForFile(filePath: string): Promise; /** * Path to metadata file describing installed dependencies */ private getDependencyMetadataPath; private readDependencyMetadata; private pathExists; private dependenciesEqual; private writeDependencyMetadata; private ensureDependenciesWithHash; private shouldRetryInstall; private isCompilationServiceError; private static parseDependenciesFromSource; private static mergeDependencySpecs; /** * Load a single Photon MCP file */ loadFile(filePath: string, options?: LoadOptions): Promise; /** * Load a photon from a pre-imported module (for compiled binaries). * Skips file I/O, compilation, and dependency installation — the module is * already bundled. Still does class extraction, constructor injection, * capability wiring, middleware, and metadata extraction from embedded source. */ loadFromModule(module: { default: any; middleware?: any[]; }, filePath: string, sourceCode: string, options?: LoadOptions): Promise; /** * Extract the class-level JSDoc comment block from source code. * Returns the raw docblock content (between the last comment before `class` and the class keyword). */ private extractClassDocblock; private extractAuthTag; private extractToolScopesFromSource; private extractScopesFromDocblock; private inferToolScopes; /** * Drop circuit-breaker state for every instance/tool of a photon. * Called on hot reload so the reloaded code starts with closed circuits * and long-running daemons don't accumulate entries for retired versions. */ private clearCircuitState; /** * Reload a Photon MCP file (for hot reload) */ reloadFile(filePath: string, options?: LoadOptions): Promise; /** * Compile TypeScript file to JavaScript and cache it * Delegates to shared compilePhotonTS from photon-core */ private compileTypeScript; private compileTypeScriptWithLocalImports; private resolveLocalTypeScriptImports; private resolveLocalTypeScriptImport; private rewriteCompiledLocalImports; /** * Find the MCP class in a module * Delegates to shared findPhotonClass from photon-core */ private findMCPClass; /** * Check if a function is a class constructor * Delegates to shared isClass from photon-core */ private isClass; /** * Get MCP name from class */ private getMCPName; /** * Get tool methods from class */ private getToolMethods; /** * Strip JSDoc tags from descriptions (e.g., @emits, @internal, @deprecated) */ private stripJSDocTags; /** * Extract @get and @post HTTP route declarations from photon source. * Methods tagged with @get or @post are HTTP-only and must NOT appear as MCP tools. * Delegates to the shared module so the cf deploy path uses the same regex. */ private extractHttpRoutesFromSource; /** * Extract tools, templates, and statics from a class */ private extractTools; /** * Extract constructor parameters from source file */ extractConstructorParams(filePath: string): Promise; /** * Resolve all constructor injections using type-based detection * - Primitives (string, number, boolean) → env var * - Non-primitives matching @mcp → MCP client * - Non-primitives matching @photon → Photon instance * * Throws MCPConfigurationError if MCP dependencies are missing */ private resolveAllInjections; private captureConstructorEnvReplay; /** * Get or create an MCP client for a dependency * * Resolution order: * 1. Check ~/.photon/config.json for configured server * 2. Fall back to resolving from @mcp declaration source * * Validates connection on first use - throws MCPConfigurationError if connection fails */ private getMCPClient; private _createMCPClient; /** * Get or load a Photon instance for a dependency. * When dep.instanceName is set, loads a named instance (separate state). * When callerInstanceName is set and dep has no explicit instanceName, * the caller's instance name is inherited (multi-tenancy support). */ private getPhotonInstance; /** * Dynamic photon resolution for this.photon.use(). * * Supports: * - Short names: 'whatsapp' → resolves via marketplace path * - Namespace-qualified: 'portel-dev:whatsapp' → resolves in specific namespace dir * * Uses the same resolution pipeline as @photon DI but with dynamic names. */ resolveAndLoadPhoton(name: string, callerPhotonPath: string, instanceName?: string): Promise; /** * Resolve Photon dependency path based on source type */ private resolvePhotonPath; private resolveMarketplacePhoton; private resolveRealPhotonPath; private materializeSiblingDependencySymlink; private createSymlinkIfMissing; private normalizeMarketplaceSource; private fetchPhotonFromMarketplace; private fetchPhotonFromSpecificMarketplace; private fetchGithubPhoton; private parseGithubSource; private resolveNpmPhoton; private parseNpmSource; private extractPackageName; private getPackageInstallPath; private ensureNpmPackageInstalled; private findPhotonFile; /** * Parse environment variable value based on TypeScript type * Delegates to shared parseEnvValue from photon-core */ private parseEnvValue; /** * Enhance constructor error with configuration guidance */ private enhanceConstructorError; /** * Apply functional tag middleware to an execution function. * Tags become composable wrappers applied in the correct order via phase-sorted middleware. * All middleware (built-in and custom) follows the same code path — no if-chain. */ private applyMiddleware; /** * Execute a tool on the loaded MCP instance * Handles both regular async methods and async generators * * For generators with checkpoint yields, automatically uses stateful execution * with JSONL persistence. The run ID is returned in the result for stateful workflows. * * @param mcp - The loaded Photon MCP * @param toolName - Name of the tool to execute * @param parameters - Input parameters for the tool * @param options - Optional execution options * @returns Tool result, or wrapped result with runId for stateful workflows */ executeTool(mcp: PhotonClass, toolName: string, parameters: any, options?: { resumeRunId?: string; outputHandler?: OutputHandler; inputProvider?: InputProvider; /** * MCP sampling provider — the runtime attaches this when the * client supports `sampling`. Photons read it from the execution * context via `this.sample()`. */ samplingProvider?: SamplingProvider; /** * MCP roots reported by the connected client. The runtime caches * the result of `roots/list` per server and threads the snapshot * here so `this.roots` resolves synchronously inside photon code. * Refreshed on `notifications/roots/list_changed`. */ roots?: Array<{ uri: string; name?: string; }>; caller?: CallerInfo; requestContext?: PhotonExecutionRequestContext; traceId?: string; parentTraceparent?: string; signal?: AbortSignal; }): Promise; /** * Long-lived subscription tools are async generators. They must not hold * the stateful instance gate for their entire stream lifetime, or every * ordinary request to the same photon queues behind the subscription. */ private _isAsyncGeneratorTool; /** * Serialize calls targeting the same photon instance. Concurrent callers * queue behind the current in-flight call; each invocation gets an exclusive * slot for the duration of its method body (including all awaits), so two * methods that touch shared state via `this.memory` cannot interleave. */ private _withInstanceGate; private _executeToolInner; private normalizeNestedParamsTool; /** * Create an input provider for generator ask yields * Supports the new ask/emit pattern from photon-core 1.2.0 */ private createInputProvider; /** * Create an output handler for generator emit yields * Supports the new ask/emit pattern from photon-core 1.2.0 * Uses inline progress animation for CLI */ private createOutputHandler; /** * Auto-wire ReactiveArray/Map/Set properties for zero-boilerplate reactivity * * When a photon developer imports { Array } from '@portel/photon-core', * it shadows the global Array. The runtime then auto-wires these properties: * - Sets _propertyName to the property key (e.g., 'items') * - Sets _emitter to instance.emit.bind(instance) * * This enables the pattern: * ```typescript * import { Array } from '@portel/photon-core'; * * export default class TodoList { * items: Array = []; // Just use it normally * * add(text: string) { * this.items.push({ id: crypto.randomUUID(), text }); * // Auto-emits 'items:added' - no manual wiring needed! * } * } * ``` */ private _formatCatalogCache; private injectFormatCatalog; private wireReactiveCollections; /** * Wrap all public methods in @stateful classes to automatically emit events * * Event structure: { method, params, result, timestamp, instance } * Every public method call produces an event with the method name, input parameters, * return value, and timestamp. This enables real-time UI sync and event-driven architectures. */ private wrapStatefulMethods; /** * Extract @mcp dependencies from source and inject them as instance properties * * This enables the pattern: * ```typescript * /** * * @mcp github anthropics/mcp-server-github * *\/ * export default class MyPhoton extends Photon { * async doSomething() { * const issues = await this.github.list_issues({ repo: 'owner/repo' }); * } * } * ``` */ private injectMCPDependencies; /** * Check CLI dependencies declared via @cli tags * * Validates that required command-line tools are available on the system. * Throws a helpful error with install URLs if any are missing. * * Format: @cli - * * Example: * ```typescript * /** * * @cli git - https://git-scm.com/downloads * * @cli ffmpeg - https://ffmpeg.org/download.html * *\/ * ``` */ private checkCLIDependencies; /** * Check if a CLI command exists on the system */ private checkCLIExists; /** * Resolve the namespace for a photon based purely on its directory * position relative to baseDir. See docs/internals/PHOTON-DIR-AND-NAMESPACE.md. * * {baseDir}/foo.photon.ts → '' (flat at root) * {baseDir}/alice/foo.photon.ts → 'alice' * {baseDir}/org/team/foo.photon.ts → 'org/team' * * The runtime never consults git state. PHOTON_DIR is the outer boundary; * the file's position within it is the only namespace signal. */ private resolveNamespace; /** * Invoke the `onInitialize` lifecycle hook on a loaded photon instance, * honoring the `skipInitialize` option and wrapping any thrown error in * a PhotonInitializationError so the caller can surface hook failures * distinctly from runtime errors. */ private invokeInitialize; /** * Inject Photon path helpers for plain classes that use them without extending Photon. */ private injectPathHelpers; } export {}; //# sourceMappingURL=loader.d.ts.map