@portel/photon 1.4.1 → 1.6.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 +326 -1177
- package/dist/auto-ui/beam.d.ts +14 -0
- package/dist/auto-ui/beam.d.ts.map +1 -0
- package/dist/auto-ui/beam.js +3057 -0
- package/dist/auto-ui/beam.js.map +1 -0
- package/dist/auto-ui/bridge/index.d.ts +37 -0
- package/dist/auto-ui/bridge/index.d.ts.map +1 -0
- package/dist/auto-ui/bridge/index.js +555 -0
- package/dist/auto-ui/bridge/index.js.map +1 -0
- package/dist/auto-ui/bridge/openai-shim.d.ts +20 -0
- package/dist/auto-ui/bridge/openai-shim.d.ts.map +1 -0
- package/dist/auto-ui/bridge/openai-shim.js +231 -0
- package/dist/auto-ui/bridge/openai-shim.js.map +1 -0
- package/dist/auto-ui/bridge/photon-app.d.ts +162 -0
- package/dist/auto-ui/bridge/photon-app.d.ts.map +1 -0
- package/dist/auto-ui/bridge/photon-app.js +460 -0
- package/dist/auto-ui/bridge/photon-app.js.map +1 -0
- package/dist/auto-ui/bridge/types.d.ts +128 -0
- package/dist/auto-ui/bridge/types.d.ts.map +1 -0
- package/dist/auto-ui/bridge/types.js +7 -0
- package/dist/auto-ui/bridge/types.js.map +1 -0
- package/dist/auto-ui/components/card.d.ts +13 -0
- package/dist/auto-ui/components/card.d.ts.map +1 -0
- package/dist/auto-ui/components/card.js +64 -0
- package/dist/auto-ui/components/card.js.map +1 -0
- package/dist/auto-ui/components/form.d.ts +15 -0
- package/dist/auto-ui/components/form.d.ts.map +1 -0
- package/dist/auto-ui/components/form.js +72 -0
- package/dist/auto-ui/components/form.js.map +1 -0
- package/dist/auto-ui/components/list.d.ts +13 -0
- package/dist/auto-ui/components/list.d.ts.map +1 -0
- package/dist/auto-ui/components/list.js +58 -0
- package/dist/auto-ui/components/list.js.map +1 -0
- package/dist/auto-ui/components/progress.d.ts +18 -0
- package/dist/auto-ui/components/progress.d.ts.map +1 -0
- package/dist/auto-ui/components/progress.js +125 -0
- package/dist/auto-ui/components/progress.js.map +1 -0
- package/dist/auto-ui/components/table.d.ts +13 -0
- package/dist/auto-ui/components/table.d.ts.map +1 -0
- package/dist/auto-ui/components/table.js +82 -0
- package/dist/auto-ui/components/table.js.map +1 -0
- package/dist/auto-ui/components/tree.d.ts +13 -0
- package/dist/auto-ui/components/tree.d.ts.map +1 -0
- package/dist/auto-ui/components/tree.js +61 -0
- package/dist/auto-ui/components/tree.js.map +1 -0
- package/dist/auto-ui/daemon-tools.d.ts +45 -0
- package/dist/auto-ui/daemon-tools.d.ts.map +1 -0
- package/dist/auto-ui/daemon-tools.js +580 -0
- package/dist/auto-ui/daemon-tools.js.map +1 -0
- package/dist/auto-ui/design-system/index.d.ts +21 -0
- package/dist/auto-ui/design-system/index.d.ts.map +1 -0
- package/dist/auto-ui/design-system/index.js +27 -0
- package/dist/auto-ui/design-system/index.js.map +1 -0
- package/dist/auto-ui/design-system/tokens.d.ts +9 -0
- package/dist/auto-ui/design-system/tokens.d.ts.map +1 -0
- package/dist/auto-ui/design-system/tokens.js +27 -0
- package/dist/auto-ui/design-system/tokens.js.map +1 -0
- package/dist/auto-ui/design-system/transaction-ui.d.ts +70 -0
- package/dist/auto-ui/design-system/transaction-ui.d.ts.map +1 -0
- package/dist/auto-ui/design-system/transaction-ui.js +982 -0
- package/dist/auto-ui/design-system/transaction-ui.js.map +1 -0
- package/dist/auto-ui/frontend/index.html +84 -0
- package/dist/auto-ui/index.d.ts +23 -0
- package/dist/auto-ui/index.d.ts.map +1 -0
- package/dist/auto-ui/index.js +28 -0
- package/dist/auto-ui/index.js.map +1 -0
- package/dist/auto-ui/openapi-generator.d.ts +71 -0
- package/dist/auto-ui/openapi-generator.d.ts.map +1 -0
- package/dist/auto-ui/openapi-generator.js +223 -0
- package/dist/auto-ui/openapi-generator.js.map +1 -0
- package/dist/auto-ui/photon-bridge.d.ts +159 -0
- package/dist/auto-ui/photon-bridge.d.ts.map +1 -0
- package/dist/auto-ui/photon-bridge.js +262 -0
- package/dist/auto-ui/photon-bridge.js.map +1 -0
- package/dist/auto-ui/photon-host.d.ts +113 -0
- package/dist/auto-ui/photon-host.d.ts.map +1 -0
- package/dist/auto-ui/photon-host.js +284 -0
- package/dist/auto-ui/photon-host.js.map +1 -0
- package/dist/auto-ui/platform-compat.d.ts +71 -0
- package/dist/auto-ui/platform-compat.d.ts.map +1 -0
- package/dist/auto-ui/platform-compat.js +628 -0
- package/dist/auto-ui/platform-compat.js.map +1 -0
- package/dist/auto-ui/playground-html.d.ts +15 -0
- package/dist/auto-ui/playground-html.d.ts.map +1 -0
- package/dist/auto-ui/playground-html.js +1113 -0
- package/dist/auto-ui/playground-html.js.map +1 -0
- package/dist/auto-ui/playground-server.d.ts +7 -0
- package/dist/auto-ui/playground-server.d.ts.map +1 -0
- package/dist/auto-ui/playground-server.js +840 -0
- package/dist/auto-ui/playground-server.js.map +1 -0
- package/dist/auto-ui/registry.d.ts +13 -0
- package/dist/auto-ui/registry.d.ts.map +1 -0
- package/dist/auto-ui/registry.js +62 -0
- package/dist/auto-ui/registry.js.map +1 -0
- package/dist/auto-ui/renderer.d.ts +14 -0
- package/dist/auto-ui/renderer.d.ts.map +1 -0
- package/dist/auto-ui/renderer.js +88 -0
- package/dist/auto-ui/renderer.js.map +1 -0
- package/dist/auto-ui/rendering/components.d.ts +29 -0
- package/dist/auto-ui/rendering/components.d.ts.map +1 -0
- package/dist/auto-ui/rendering/components.js +773 -0
- package/dist/auto-ui/rendering/components.js.map +1 -0
- package/dist/auto-ui/rendering/field-analyzer.d.ts +48 -0
- package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -0
- package/dist/auto-ui/rendering/field-analyzer.js +270 -0
- package/dist/auto-ui/rendering/field-analyzer.js.map +1 -0
- package/dist/auto-ui/rendering/field-renderers.d.ts +64 -0
- package/dist/auto-ui/rendering/field-renderers.d.ts.map +1 -0
- package/dist/auto-ui/rendering/field-renderers.js +317 -0
- package/dist/auto-ui/rendering/field-renderers.js.map +1 -0
- package/dist/auto-ui/rendering/index.d.ts +28 -0
- package/dist/auto-ui/rendering/index.d.ts.map +1 -0
- package/dist/auto-ui/rendering/index.js +60 -0
- package/dist/auto-ui/rendering/index.js.map +1 -0
- package/dist/auto-ui/rendering/layout-selector.d.ts +48 -0
- package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -0
- package/dist/auto-ui/rendering/layout-selector.js +352 -0
- package/dist/auto-ui/rendering/layout-selector.js.map +1 -0
- package/dist/auto-ui/rendering/template-engine.d.ts +41 -0
- package/dist/auto-ui/rendering/template-engine.d.ts.map +1 -0
- package/dist/auto-ui/rendering/template-engine.js +238 -0
- package/dist/auto-ui/rendering/template-engine.js.map +1 -0
- package/dist/auto-ui/streamable-http-transport.d.ts +103 -0
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -0
- package/dist/auto-ui/streamable-http-transport.js +1875 -0
- package/dist/auto-ui/streamable-http-transport.js.map +1 -0
- package/dist/auto-ui/types.d.ts +384 -0
- package/dist/auto-ui/types.d.ts.map +1 -0
- package/dist/auto-ui/types.js +92 -0
- package/dist/auto-ui/types.js.map +1 -0
- package/dist/beam.bundle.js +63137 -0
- package/dist/beam.bundle.js.map +7 -0
- package/dist/claude-code-plugin.d.ts.map +1 -1
- package/dist/claude-code-plugin.js +30 -30
- package/dist/claude-code-plugin.js.map +1 -1
- package/dist/cli/commands/info.d.ts +11 -0
- package/dist/cli/commands/info.d.ts.map +1 -0
- package/dist/cli/commands/info.js +313 -0
- package/dist/cli/commands/info.js.map +1 -0
- package/dist/cli/commands/marketplace.d.ts +11 -0
- package/dist/cli/commands/marketplace.d.ts.map +1 -0
- package/dist/cli/commands/marketplace.js +198 -0
- package/dist/cli/commands/marketplace.js.map +1 -0
- package/dist/cli/commands/package-app.d.ts +9 -0
- package/dist/cli/commands/package-app.d.ts.map +1 -0
- package/dist/cli/commands/package-app.js +191 -0
- package/dist/cli/commands/package-app.js.map +1 -0
- package/dist/cli/commands/package.d.ts +11 -0
- package/dist/cli/commands/package.d.ts.map +1 -0
- package/dist/cli/commands/package.js +573 -0
- package/dist/cli/commands/package.js.map +1 -0
- package/dist/cli-alias.d.ts.map +1 -1
- package/dist/cli-alias.js +30 -28
- package/dist/cli-alias.js.map +1 -1
- package/dist/cli-formatter.d.ts +8 -24
- package/dist/cli-formatter.d.ts.map +1 -1
- package/dist/cli-formatter.js +8 -325
- package/dist/cli-formatter.js.map +1 -1
- package/dist/cli.d.ts +15 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1166 -1131
- package/dist/cli.js.map +1 -1
- package/dist/daemon/client.d.ts +84 -3
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +561 -11
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +51 -12
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +122 -61
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/protocol.d.ts +62 -6
- package/dist/daemon/protocol.d.ts.map +1 -1
- package/dist/daemon/protocol.js +76 -1
- package/dist/daemon/protocol.js.map +1 -1
- package/dist/daemon/server.d.ts +6 -6
- package/dist/daemon/server.js +743 -133
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/session-manager.d.ts +8 -1
- package/dist/daemon/session-manager.d.ts.map +1 -1
- package/dist/daemon/session-manager.js +32 -9
- package/dist/daemon/session-manager.js.map +1 -1
- package/dist/deploy/cloudflare.d.ts +12 -0
- package/dist/deploy/cloudflare.d.ts.map +1 -0
- package/dist/deploy/cloudflare.js +216 -0
- package/dist/deploy/cloudflare.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +191 -21
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +1186 -319
- package/dist/loader.js.map +1 -1
- package/dist/markdown-utils.d.ts +8 -0
- package/dist/markdown-utils.d.ts.map +1 -0
- package/dist/markdown-utils.js +63 -0
- package/dist/markdown-utils.js.map +1 -0
- package/dist/marketplace-manager.d.ts +10 -0
- package/dist/marketplace-manager.d.ts.map +1 -1
- package/dist/marketplace-manager.js +112 -28
- package/dist/marketplace-manager.js.map +1 -1
- package/dist/mcp-client.d.ts +9 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +11 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/mcp-elicitation.d.ts +32 -0
- package/dist/mcp-elicitation.d.ts.map +1 -0
- package/dist/mcp-elicitation.js +26 -0
- package/dist/mcp-elicitation.js.map +1 -0
- package/dist/path-resolver.d.ts +9 -12
- package/dist/path-resolver.d.ts.map +1 -1
- package/dist/path-resolver.js +13 -43
- package/dist/path-resolver.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +204 -77
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts +89 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +560 -32
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/photons/maker.photon.d.ts +182 -0
- package/dist/photons/maker.photon.d.ts.map +1 -0
- package/dist/photons/maker.photon.js +504 -0
- package/dist/photons/maker.photon.js.map +1 -0
- package/dist/photons/maker.photon.ts +626 -0
- package/dist/photons/marketplace.photon.d.ts +110 -0
- package/dist/photons/marketplace.photon.d.ts.map +1 -0
- package/dist/photons/marketplace.photon.js +260 -0
- package/dist/photons/marketplace.photon.js.map +1 -0
- package/dist/photons/marketplace.photon.ts +378 -0
- package/dist/photons/tunnel.photon.d.ts +80 -0
- package/dist/photons/tunnel.photon.d.ts.map +1 -0
- package/dist/photons/tunnel.photon.js +269 -0
- package/dist/photons/tunnel.photon.js.map +1 -0
- package/dist/photons/tunnel.photon.ts +345 -0
- package/dist/security-scanner.d.ts.map +1 -1
- package/dist/security-scanner.js +18 -15
- package/dist/security-scanner.js.map +1 -1
- package/dist/serv/auth/jwt.d.ts +89 -0
- package/dist/serv/auth/jwt.d.ts.map +1 -0
- package/dist/serv/auth/jwt.js +239 -0
- package/dist/serv/auth/jwt.js.map +1 -0
- package/dist/serv/auth/oauth.d.ts +117 -0
- package/dist/serv/auth/oauth.d.ts.map +1 -0
- package/dist/serv/auth/oauth.js +395 -0
- package/dist/serv/auth/oauth.js.map +1 -0
- package/dist/serv/auth/well-known.d.ts +60 -0
- package/dist/serv/auth/well-known.d.ts.map +1 -0
- package/dist/serv/auth/well-known.js +154 -0
- package/dist/serv/auth/well-known.js.map +1 -0
- package/dist/serv/db/d1-client.d.ts +65 -0
- package/dist/serv/db/d1-client.d.ts.map +1 -0
- package/dist/serv/db/d1-client.js +137 -0
- package/dist/serv/db/d1-client.js.map +1 -0
- package/dist/serv/db/d1-stores.d.ts +62 -0
- package/dist/serv/db/d1-stores.d.ts.map +1 -0
- package/dist/serv/db/d1-stores.js +307 -0
- package/dist/serv/db/d1-stores.js.map +1 -0
- package/dist/serv/index.d.ts +114 -0
- package/dist/serv/index.d.ts.map +1 -0
- package/dist/serv/index.js +172 -0
- package/dist/serv/index.js.map +1 -0
- package/dist/serv/local.d.ts +118 -0
- package/dist/serv/local.d.ts.map +1 -0
- package/dist/serv/local.js +392 -0
- package/dist/serv/local.js.map +1 -0
- package/dist/serv/middleware/auth.d.ts +66 -0
- package/dist/serv/middleware/auth.d.ts.map +1 -0
- package/dist/serv/middleware/auth.js +178 -0
- package/dist/serv/middleware/auth.js.map +1 -0
- package/dist/serv/middleware/tenant.d.ts +94 -0
- package/dist/serv/middleware/tenant.d.ts.map +1 -0
- package/dist/serv/middleware/tenant.js +152 -0
- package/dist/serv/middleware/tenant.js.map +1 -0
- package/dist/serv/runtime/executor.d.ts +76 -0
- package/dist/serv/runtime/executor.d.ts.map +1 -0
- package/dist/serv/runtime/executor.js +105 -0
- package/dist/serv/runtime/executor.js.map +1 -0
- package/dist/serv/runtime/index.d.ts +8 -0
- package/dist/serv/runtime/index.d.ts.map +1 -0
- package/dist/serv/runtime/index.js +10 -0
- package/dist/serv/runtime/index.js.map +1 -0
- package/dist/serv/runtime/oauth-context.d.ts +121 -0
- package/dist/serv/runtime/oauth-context.d.ts.map +1 -0
- package/dist/serv/runtime/oauth-context.js +153 -0
- package/dist/serv/runtime/oauth-context.js.map +1 -0
- package/dist/serv/session/kv-store.d.ts +54 -0
- package/dist/serv/session/kv-store.d.ts.map +1 -0
- package/dist/serv/session/kv-store.js +149 -0
- package/dist/serv/session/kv-store.js.map +1 -0
- package/dist/serv/session/store.d.ts +113 -0
- package/dist/serv/session/store.d.ts.map +1 -0
- package/dist/serv/session/store.js +284 -0
- package/dist/serv/session/store.js.map +1 -0
- package/dist/serv/types/index.d.ts +147 -0
- package/dist/serv/types/index.d.ts.map +1 -0
- package/dist/serv/types/index.js +8 -0
- package/dist/serv/types/index.js.map +1 -0
- package/dist/serv/vault/token-vault.d.ts +102 -0
- package/dist/serv/vault/token-vault.d.ts.map +1 -0
- package/dist/serv/vault/token-vault.js +177 -0
- package/dist/serv/vault/token-vault.js.map +1 -0
- package/dist/server.d.ts +184 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1995 -86
- package/dist/server.js.map +1 -1
- package/dist/shared/cli-sections.d.ts +6 -0
- package/dist/shared/cli-sections.d.ts.map +1 -0
- package/dist/shared/cli-sections.js +16 -0
- package/dist/shared/cli-sections.js.map +1 -0
- package/dist/shared/cli-utils.d.ts +81 -0
- package/dist/shared/cli-utils.d.ts.map +1 -0
- package/dist/shared/cli-utils.js +174 -0
- package/dist/shared/cli-utils.js.map +1 -0
- package/dist/shared/config-docs.d.ts +6 -0
- package/dist/shared/config-docs.d.ts.map +1 -0
- package/dist/shared/config-docs.js +6 -0
- package/dist/shared/config-docs.js.map +1 -0
- package/dist/shared/error-handler.d.ts +128 -0
- package/dist/shared/error-handler.d.ts.map +1 -0
- package/dist/shared/error-handler.js +342 -0
- package/dist/shared/error-handler.js.map +1 -0
- package/dist/shared/logger.d.ts +42 -0
- package/dist/shared/logger.d.ts.map +1 -0
- package/dist/shared/logger.js +123 -0
- package/dist/shared/logger.js.map +1 -0
- package/dist/shared/performance.d.ts +65 -0
- package/dist/shared/performance.d.ts.map +1 -0
- package/dist/shared/performance.js +136 -0
- package/dist/shared/performance.js.map +1 -0
- package/dist/shared/task-runner.d.ts +2 -0
- package/dist/shared/task-runner.d.ts.map +1 -0
- package/dist/shared/task-runner.js +16 -0
- package/dist/shared/task-runner.js.map +1 -0
- package/dist/shared/validation.d.ts +6 -0
- package/dist/shared/validation.d.ts.map +1 -0
- package/dist/shared/validation.js +6 -0
- package/dist/shared/validation.js.map +1 -0
- package/dist/shared-utils.d.ts +63 -0
- package/dist/shared-utils.d.ts.map +1 -0
- package/dist/shared-utils.js +123 -0
- package/dist/shared-utils.js.map +1 -0
- package/dist/template-manager.d.ts +23 -2
- package/dist/template-manager.d.ts.map +1 -1
- package/dist/template-manager.js +176 -87
- package/dist/template-manager.js.map +1 -1
- package/dist/test-client.d.ts.map +1 -1
- package/dist/test-client.js +10 -8
- package/dist/test-client.js.map +1 -1
- package/dist/test-runner.d.ts +52 -0
- package/dist/test-runner.d.ts.map +1 -0
- package/dist/test-runner.js +785 -0
- package/dist/test-runner.js.map +1 -0
- package/dist/testing.d.ts +103 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +163 -0
- package/dist/testing.js.map +1 -0
- package/dist/version-checker.d.ts.map +1 -1
- package/dist/version-checker.js +2 -2
- package/dist/version-checker.js.map +1 -1
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +21 -0
- package/dist/version.js.map +1 -0
- package/dist/watcher.d.ts +6 -3
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +49 -10
- package/dist/watcher.js.map +1 -1
- package/package.json +57 -7
- package/templates/cloudflare/worker.ts.template +381 -0
- package/templates/cloudflare/wrangler.toml.template +9 -0
- package/dist/base.d.ts +0 -58
- package/dist/base.d.ts.map +0 -1
- package/dist/base.js +0 -92
- package/dist/base.js.map +0 -1
- package/dist/dependency-manager.d.ts +0 -49
- package/dist/dependency-manager.d.ts.map +0 -1
- package/dist/dependency-manager.js +0 -165
- package/dist/dependency-manager.js.map +0 -1
- package/dist/registry-manager.d.ts +0 -76
- package/dist/registry-manager.d.ts.map +0 -1
- package/dist/registry-manager.js +0 -220
- package/dist/registry-manager.js.map +0 -1
- package/dist/schema-extractor.d.ts +0 -110
- package/dist/schema-extractor.d.ts.map +0 -1
- package/dist/schema-extractor.js +0 -727
- package/dist/schema-extractor.js.map +0 -1
- package/dist/test-marketplace-sources.d.ts +0 -5
- package/dist/test-marketplace-sources.d.ts.map +0 -1
- package/dist/test-marketplace-sources.js +0 -53
- package/dist/test-marketplace-sources.js.map +0 -1
- package/dist/types.d.ts +0 -109
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -12
- package/dist/types.js.map +0 -1
package/dist/loader.js
CHANGED
|
@@ -7,46 +7,180 @@ import * as fs from 'fs/promises';
|
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import { pathToFileURL } from 'url';
|
|
9
9
|
import * as crypto from 'crypto';
|
|
10
|
+
import { spawn } from 'child_process';
|
|
10
11
|
import { SchemaExtractor, DependencyManager,
|
|
11
|
-
// Generator utilities
|
|
12
|
-
isAsyncGenerator, executeGenerator,
|
|
12
|
+
// Generator utilities (ask/emit pattern from 1.2.0)
|
|
13
|
+
isAsyncGenerator, executeGenerator,
|
|
14
|
+
// Implicit stateful execution (auto-detect checkpoint yields)
|
|
15
|
+
maybeStatefulExecute,
|
|
13
16
|
// Elicit for fallback
|
|
14
|
-
prompt as elicitPrompt, confirm as elicitConfirm,
|
|
17
|
+
prompt as elicitPrompt, confirm as elicitConfirm,
|
|
18
|
+
// Progress rendering
|
|
19
|
+
ProgressRenderer, createMCPProxy, MCPConfigurationError, SDKMCPClientFactory, resolveMCPSource,
|
|
20
|
+
// Photon runtime configuration
|
|
21
|
+
loadPhotonMCPConfig, resolveEnvVars,
|
|
22
|
+
// Shared utilities (photon-core 2.4.0)
|
|
23
|
+
isClass as sharedIsClass, hasAsyncMethods as sharedHasAsyncMethods, findPhotonClass as sharedFindPhotonClass, parseEnvValue as sharedParseEnvValue, compilePhotonTS, getMimeType as sharedGetMimeType, parseRuntimeRequirement, checkRuntimeCompatibility, discoverAssets as sharedDiscoverAssets, } from '@portel/photon-core';
|
|
15
24
|
import * as os from 'os';
|
|
25
|
+
import { MarketplaceManager } from './marketplace-manager.js';
|
|
26
|
+
import { PHOTON_VERSION, PHOTON_CORE_VERSION, getResolvedPhotonCoreVersion } from './version.js';
|
|
27
|
+
// Timeout for external fetch requests (marketplace, GitHub)
|
|
28
|
+
const FETCH_TIMEOUT_MS = 30 * 1000;
|
|
29
|
+
import { generateConfigErrorMessage, summarizeConstructorParams, toEnvVarName, } from './shared/config-docs.js';
|
|
30
|
+
import { createLogger } from './shared/logger.js';
|
|
31
|
+
import { getErrorMessage } from './shared/error-handler.js';
|
|
32
|
+
import { validateOrThrow, assertString, notEmpty, hasExtension } from './shared/validation.js';
|
|
16
33
|
export class PhotonLoader {
|
|
17
34
|
dependencyManager;
|
|
18
35
|
verbose;
|
|
19
|
-
|
|
36
|
+
mcpClientFactory;
|
|
37
|
+
/** Cache of loaded Photon instances by source path */
|
|
38
|
+
loadedPhotons = new Map();
|
|
39
|
+
/** MCP clients cache - reuse connections */
|
|
40
|
+
mcpClients = new Map();
|
|
41
|
+
/** SDK factory for MCP connections */
|
|
42
|
+
sdkFactory;
|
|
43
|
+
/** Cached MCP config from ~/.photon/config.json */
|
|
44
|
+
mcpConfig;
|
|
45
|
+
/** Progress renderer for inline CLI animation */
|
|
46
|
+
progressRenderer = new ProgressRenderer();
|
|
47
|
+
/** Marketplace manager for resolving remote Photons */
|
|
48
|
+
marketplaceManager;
|
|
49
|
+
marketplaceManagerPromise;
|
|
50
|
+
logger;
|
|
51
|
+
constructor(verbose = false, logger) {
|
|
20
52
|
this.dependencyManager = new DependencyManager();
|
|
21
53
|
this.verbose = verbose;
|
|
54
|
+
this.logger = logger ?? createLogger({ component: 'photon-loader', minimal: true });
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Load MCP configuration from ~/.photon/config.json
|
|
58
|
+
* Called lazily on first MCP injection
|
|
59
|
+
*/
|
|
60
|
+
async ensureMCPConfig() {
|
|
61
|
+
if (!this.mcpConfig) {
|
|
62
|
+
this.mcpConfig = await loadPhotonMCPConfig();
|
|
63
|
+
const serverCount = Object.keys(this.mcpConfig.mcpServers).length;
|
|
64
|
+
if (serverCount > 0) {
|
|
65
|
+
this.log(`Loaded ${serverCount} MCP servers from config`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return this.mcpConfig;
|
|
69
|
+
}
|
|
70
|
+
async getMarketplaceManager() {
|
|
71
|
+
if (this.marketplaceManager) {
|
|
72
|
+
return this.marketplaceManager;
|
|
73
|
+
}
|
|
74
|
+
if (!this.marketplaceManagerPromise) {
|
|
75
|
+
this.marketplaceManagerPromise = (async () => {
|
|
76
|
+
const managerLogger = this.logger.child({ component: 'marketplace-manager' });
|
|
77
|
+
const manager = new MarketplaceManager(managerLogger);
|
|
78
|
+
await manager.initialize();
|
|
79
|
+
return manager;
|
|
80
|
+
})();
|
|
81
|
+
}
|
|
82
|
+
this.marketplaceManager = await this.marketplaceManagerPromise;
|
|
83
|
+
return this.marketplaceManager;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Set MCP client factory for enabling this.mcp() in Photons
|
|
87
|
+
*/
|
|
88
|
+
setMCPClientFactory(factory) {
|
|
89
|
+
this.mcpClientFactory = factory;
|
|
22
90
|
}
|
|
23
91
|
/**
|
|
24
92
|
* Log message only if verbose mode is enabled
|
|
25
93
|
*/
|
|
26
|
-
log(message) {
|
|
94
|
+
log(message, meta) {
|
|
27
95
|
if (this.verbose) {
|
|
28
|
-
|
|
96
|
+
this.logger.info(message, meta);
|
|
29
97
|
}
|
|
30
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Generate deterministic cache key for an MCP + photon path
|
|
101
|
+
*/
|
|
102
|
+
getCacheKey(mcpName, photonPath) {
|
|
103
|
+
const normalized = path.resolve(photonPath);
|
|
104
|
+
const hash = crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 8);
|
|
105
|
+
return `${mcpName}-${hash}`;
|
|
106
|
+
}
|
|
107
|
+
getPhotonCacheDir() {
|
|
108
|
+
return path.join(os.homedir(), '.photon', '.cache', 'photons');
|
|
109
|
+
}
|
|
110
|
+
sanitizeCacheLabel(label) {
|
|
111
|
+
return label.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
112
|
+
}
|
|
113
|
+
async writePhotonCacheFile(label, content, hashHint) {
|
|
114
|
+
const cacheDir = this.getPhotonCacheDir();
|
|
115
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
116
|
+
const hash = hashHint
|
|
117
|
+
? hashHint.replace(/^sha256:/, '').slice(0, 12)
|
|
118
|
+
: crypto.createHash('sha256').update(content).digest('hex').slice(0, 12);
|
|
119
|
+
const safeLabel = this.sanitizeCacheLabel(label);
|
|
120
|
+
const cachePath = path.join(cacheDir, `${safeLabel}.${hash}.photon.ts`);
|
|
121
|
+
await fs.writeFile(cachePath, content, 'utf-8');
|
|
122
|
+
return cachePath;
|
|
123
|
+
}
|
|
124
|
+
async runCommand(command, args, cwd) {
|
|
125
|
+
await new Promise((resolve, reject) => {
|
|
126
|
+
const child = spawn(command, args, {
|
|
127
|
+
cwd,
|
|
128
|
+
stdio: 'inherit',
|
|
129
|
+
});
|
|
130
|
+
child.on('error', reject);
|
|
131
|
+
child.on('exit', (code) => {
|
|
132
|
+
if (code === 0) {
|
|
133
|
+
resolve();
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
31
141
|
/**
|
|
32
142
|
* Directory where MCP-specific dependencies are cached
|
|
33
143
|
*/
|
|
34
|
-
getDependencyCacheDir(
|
|
35
|
-
return path.join(os.homedir(), '.cache', 'photon-mcp', 'dependencies',
|
|
144
|
+
getDependencyCacheDir(cacheKey) {
|
|
145
|
+
return path.join(os.homedir(), '.cache', 'photon-mcp', 'dependencies', cacheKey);
|
|
146
|
+
}
|
|
147
|
+
getBuildCacheDir(cacheKey) {
|
|
148
|
+
return path.join(this.getDependencyCacheDir(cacheKey), '.build');
|
|
149
|
+
}
|
|
150
|
+
async clearBuildCache(cacheKey) {
|
|
151
|
+
const buildDir = this.getBuildCacheDir(cacheKey);
|
|
152
|
+
await fs.rm(buildDir, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
async clearDependencyCache(cacheKey) {
|
|
155
|
+
await this.dependencyManager.clearCache(cacheKey);
|
|
156
|
+
}
|
|
157
|
+
async clearAllCaches(cacheKey) {
|
|
158
|
+
await this.clearDependencyCache(cacheKey);
|
|
159
|
+
await this.clearBuildCache(cacheKey);
|
|
36
160
|
}
|
|
37
161
|
/**
|
|
38
162
|
* Path to metadata file describing installed dependencies
|
|
39
163
|
*/
|
|
40
|
-
getDependencyMetadataPath(
|
|
41
|
-
return path.join(this.getDependencyCacheDir(
|
|
164
|
+
getDependencyMetadataPath(cacheKey) {
|
|
165
|
+
return path.join(this.getDependencyCacheDir(cacheKey), 'metadata.json');
|
|
42
166
|
}
|
|
43
|
-
async readDependencyMetadata(
|
|
167
|
+
async readDependencyMetadata(cacheKey) {
|
|
44
168
|
try {
|
|
45
|
-
const data = await fs.readFile(this.getDependencyMetadataPath(
|
|
169
|
+
const data = await fs.readFile(this.getDependencyMetadataPath(cacheKey), 'utf-8');
|
|
46
170
|
return JSON.parse(data);
|
|
47
171
|
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
this.logger.debug('Failed to read dependency metadata', { error });
|
|
174
|
+
return null; // cache miss
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async pathExists(targetPath) {
|
|
178
|
+
try {
|
|
179
|
+
await fs.access(targetPath);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
48
182
|
catch {
|
|
49
|
-
return
|
|
183
|
+
return false; // path does not exist
|
|
50
184
|
}
|
|
51
185
|
}
|
|
52
186
|
dependenciesEqual(a, b) {
|
|
@@ -56,32 +190,57 @@ export class PhotonLoader {
|
|
|
56
190
|
if (a.length !== b.length) {
|
|
57
191
|
return false;
|
|
58
192
|
}
|
|
59
|
-
const normalize = (deps) => deps
|
|
193
|
+
const normalize = (deps) => deps
|
|
194
|
+
.map((d) => `${d.name}@${d.version}`)
|
|
195
|
+
.sort()
|
|
196
|
+
.join('|');
|
|
60
197
|
return normalize(a) === normalize(b);
|
|
61
198
|
}
|
|
62
|
-
async writeDependencyMetadata(
|
|
63
|
-
const metadataPath = this.getDependencyMetadataPath(
|
|
199
|
+
async writeDependencyMetadata(cacheKey, hash, dependencies, photonPath, photonCoreVersion) {
|
|
200
|
+
const metadataPath = this.getDependencyMetadataPath(cacheKey);
|
|
64
201
|
await fs.mkdir(path.dirname(metadataPath), { recursive: true });
|
|
65
|
-
await fs.writeFile(metadataPath, JSON.stringify({
|
|
202
|
+
await fs.writeFile(metadataPath, JSON.stringify({
|
|
203
|
+
hash,
|
|
204
|
+
dependencies,
|
|
205
|
+
photonPath: photonPath ? path.resolve(photonPath) : undefined,
|
|
206
|
+
photonCoreVersion,
|
|
207
|
+
}, null, 2), 'utf-8');
|
|
66
208
|
}
|
|
67
|
-
async ensureDependenciesWithHash(mcpName, dependencies, sourceHash) {
|
|
68
|
-
const metadata = await this.readDependencyMetadata(
|
|
209
|
+
async ensureDependenciesWithHash(cacheKey, mcpName, dependencies, sourceHash, photonPath) {
|
|
210
|
+
const metadata = await this.readDependencyMetadata(cacheKey);
|
|
69
211
|
const hashMatches = metadata?.hash === sourceHash;
|
|
70
|
-
const depsMatch = metadata
|
|
71
|
-
|
|
212
|
+
const depsMatch = metadata
|
|
213
|
+
? this.dependenciesEqual(metadata.dependencies, dependencies)
|
|
214
|
+
: false;
|
|
215
|
+
const resolvedCoreVersion = getResolvedPhotonCoreVersion();
|
|
216
|
+
const coreVersionChanged = metadata?.photonCoreVersion !== resolvedCoreVersion;
|
|
217
|
+
const needsClear = Boolean(metadata && (!hashMatches || !depsMatch || coreVersionChanged));
|
|
72
218
|
if (needsClear) {
|
|
73
|
-
this.
|
|
74
|
-
|
|
219
|
+
const depDir = this.getDependencyCacheDir(cacheKey);
|
|
220
|
+
const buildDir = this.getBuildCacheDir(cacheKey);
|
|
221
|
+
if (coreVersionChanged) {
|
|
222
|
+
this.log(`🔄 photon-core version changed (${metadata?.photonCoreVersion} → ${resolvedCoreVersion}), clearing cache for ${mcpName}`);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
this.log(`🔄 Dependencies changed for ${mcpName} (${cacheKey}), clearing caches`, {
|
|
226
|
+
dependencyCache: depDir,
|
|
227
|
+
buildCache: buildDir,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
await this.clearAllCaches(cacheKey);
|
|
75
231
|
}
|
|
76
232
|
let nodeModules = null;
|
|
77
233
|
if (dependencies.length > 0) {
|
|
78
|
-
nodeModules = await this.dependencyManager.ensureDependencies(
|
|
234
|
+
nodeModules = await this.dependencyManager.ensureDependencies(cacheKey, dependencies);
|
|
235
|
+
if (nodeModules) {
|
|
236
|
+
this.log(`📦 Dependencies ready for ${mcpName}`, { nodeModules });
|
|
237
|
+
}
|
|
79
238
|
}
|
|
80
|
-
await this.writeDependencyMetadata(
|
|
239
|
+
await this.writeDependencyMetadata(cacheKey, sourceHash, dependencies, photonPath, resolvedCoreVersion);
|
|
81
240
|
return nodeModules;
|
|
82
241
|
}
|
|
83
242
|
shouldRetryInstall(error) {
|
|
84
|
-
const message = error
|
|
243
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
85
244
|
return (message.includes('Cannot find package') ||
|
|
86
245
|
message.includes('ERR_MODULE_NOT_FOUND') ||
|
|
87
246
|
message.includes('Cannot find module'));
|
|
@@ -91,7 +250,10 @@ export class PhotonLoader {
|
|
|
91
250
|
const regex = /@dependencies\s+([^\r\n]+)/g;
|
|
92
251
|
let match;
|
|
93
252
|
while ((match = regex.exec(source)) !== null) {
|
|
94
|
-
const entries = match[1]
|
|
253
|
+
const entries = match[1]
|
|
254
|
+
.split(',')
|
|
255
|
+
.map((entry) => entry.trim())
|
|
256
|
+
.filter(Boolean);
|
|
95
257
|
for (const entry of entries) {
|
|
96
258
|
const atIndex = entry.lastIndexOf('@');
|
|
97
259
|
if (atIndex <= 0) {
|
|
@@ -116,10 +278,17 @@ export class PhotonLoader {
|
|
|
116
278
|
}
|
|
117
279
|
return Array.from(map.entries()).map(([name, version]) => ({ name, version }));
|
|
118
280
|
}
|
|
281
|
+
// parseRuntimeRequirement and checkRuntimeCompatibility are now imported from photon-core
|
|
119
282
|
/**
|
|
120
283
|
* Load a single Photon MCP file
|
|
121
284
|
*/
|
|
122
285
|
async loadFile(filePath) {
|
|
286
|
+
// Validate input
|
|
287
|
+
assertString(filePath, 'filePath');
|
|
288
|
+
validateOrThrow(filePath, [
|
|
289
|
+
notEmpty('Photon file path'),
|
|
290
|
+
hasExtension('Photon file', ['ts', 'js']),
|
|
291
|
+
]);
|
|
123
292
|
try {
|
|
124
293
|
// Resolve to absolute path
|
|
125
294
|
const absolutePath = path.resolve(filePath);
|
|
@@ -131,21 +300,39 @@ export class PhotonLoader {
|
|
|
131
300
|
let dependencies = [];
|
|
132
301
|
let sourceHash;
|
|
133
302
|
let mcpName = '';
|
|
303
|
+
let cacheKey = null;
|
|
134
304
|
if (absolutePath.endsWith('.ts')) {
|
|
135
305
|
tsContent = await fs.readFile(absolutePath, 'utf-8');
|
|
306
|
+
// Check runtime version compatibility
|
|
307
|
+
const requiredRuntime = parseRuntimeRequirement(tsContent);
|
|
308
|
+
if (requiredRuntime) {
|
|
309
|
+
const check = checkRuntimeCompatibility(requiredRuntime, PHOTON_VERSION);
|
|
310
|
+
if (!check.compatible) {
|
|
311
|
+
throw new Error(check.message);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
136
314
|
const extracted = await this.dependencyManager.extractDependencies(absolutePath);
|
|
137
315
|
const parsed = PhotonLoader.parseDependenciesFromSource(tsContent);
|
|
138
316
|
dependencies = PhotonLoader.mergeDependencySpecs(extracted, parsed);
|
|
317
|
+
// Auto-include @portel/photon-core if imported (it's the runtime, always available)
|
|
318
|
+
if (tsContent.includes('@portel/photon-core') &&
|
|
319
|
+
!dependencies.some((d) => d.name === '@portel/photon-core')) {
|
|
320
|
+
dependencies.push({
|
|
321
|
+
name: '@portel/photon-core',
|
|
322
|
+
version: PHOTON_CORE_VERSION,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
139
325
|
mcpName = path.basename(absolutePath, '.ts').replace('.photon', '');
|
|
326
|
+
cacheKey = this.getCacheKey(mcpName, absolutePath);
|
|
140
327
|
sourceHash = crypto.createHash('sha256').update(tsContent).digest('hex');
|
|
141
328
|
if (dependencies.length > 0) {
|
|
142
329
|
this.log(`📦 Found ${dependencies.length} dependencies`);
|
|
143
330
|
}
|
|
144
|
-
await this.ensureDependenciesWithHash(mcpName, dependencies, sourceHash);
|
|
331
|
+
await this.ensureDependenciesWithHash(cacheKey, mcpName, dependencies, sourceHash, absolutePath);
|
|
145
332
|
}
|
|
146
333
|
const importModule = async () => {
|
|
147
334
|
if (tsContent) {
|
|
148
|
-
const cachedJsPath = await this.compileTypeScript(absolutePath,
|
|
335
|
+
const cachedJsPath = await this.compileTypeScript(absolutePath, cacheKey, tsContent);
|
|
149
336
|
const cachedJsUrl = pathToFileURL(cachedJsPath).href;
|
|
150
337
|
return await import(`${cachedJsUrl}?t=${Date.now()}`);
|
|
151
338
|
}
|
|
@@ -156,10 +343,10 @@ export class PhotonLoader {
|
|
|
156
343
|
module = await importModule();
|
|
157
344
|
}
|
|
158
345
|
catch (error) {
|
|
159
|
-
if (this.shouldRetryInstall(error) && tsContent && sourceHash && mcpName) {
|
|
346
|
+
if (this.shouldRetryInstall(error) && tsContent && sourceHash && mcpName && cacheKey) {
|
|
160
347
|
this.log(`⚠️ Missing dependency detected, reinstalling dependencies for ${mcpName}`);
|
|
161
|
-
await this.
|
|
162
|
-
await this.ensureDependenciesWithHash(mcpName, dependencies, sourceHash);
|
|
348
|
+
await this.clearAllCaches(cacheKey);
|
|
349
|
+
await this.ensureDependenciesWithHash(cacheKey, mcpName, dependencies, sourceHash, absolutePath);
|
|
163
350
|
module = await importModule();
|
|
164
351
|
}
|
|
165
352
|
else {
|
|
@@ -168,66 +355,97 @@ export class PhotonLoader {
|
|
|
168
355
|
}
|
|
169
356
|
// Find the exported class
|
|
170
357
|
const MCPClass = this.findMCPClass(module);
|
|
171
|
-
if (!MCPClass) {
|
|
358
|
+
if (!MCPClass || !this.isClass(MCPClass)) {
|
|
172
359
|
throw new Error('No MCP class found in file. Expected a class with async methods.');
|
|
173
360
|
}
|
|
174
361
|
// Get MCP name
|
|
175
362
|
const name = this.getMCPName(MCPClass);
|
|
176
|
-
//
|
|
177
|
-
const
|
|
178
|
-
//
|
|
179
|
-
const { values, configError } = this.resolveConstructorArgs(constructorParams, name);
|
|
180
|
-
// Create instance with injected config
|
|
363
|
+
// Resolve all constructor injections using type-based detection
|
|
364
|
+
const { values, configError, injectedPhotonNames } = await this.resolveAllInjections(tsContent || '', name, absolutePath);
|
|
365
|
+
// Create instance with injected dependencies
|
|
181
366
|
let instance;
|
|
182
367
|
try {
|
|
183
368
|
instance = new MCPClass(...values);
|
|
184
369
|
}
|
|
185
370
|
catch (error) {
|
|
186
371
|
// Constructor threw an error (likely validation failure)
|
|
187
|
-
|
|
188
|
-
const enhancedError = this.enhanceConstructorError(error, name, constructorParams, configError);
|
|
372
|
+
const constructorParams = await this.extractConstructorParams(absolutePath);
|
|
373
|
+
const enhancedError = this.enhanceConstructorError(error instanceof Error ? error : new Error(String(error)), name, constructorParams, configError);
|
|
189
374
|
throw enhancedError;
|
|
190
375
|
}
|
|
191
376
|
// Store config warning for later if there were missing params
|
|
192
|
-
// (constructor didn't throw, but params were missing - they had defaults)
|
|
193
377
|
if (configError) {
|
|
194
378
|
instance._photonConfigError = configError;
|
|
195
|
-
|
|
196
|
-
|
|
379
|
+
this.logger.warn(`⚠️ ${name} loaded with configuration warnings:`);
|
|
380
|
+
this.logger.warn(String(configError));
|
|
381
|
+
}
|
|
382
|
+
// Set photon name for event source identification
|
|
383
|
+
instance._photonName = name;
|
|
384
|
+
// Auto-wire ReactiveArray/Map/Set properties for zero-boilerplate reactivity
|
|
385
|
+
// Developers just `import { Array } from '@portel/photon-core'` and use normally
|
|
386
|
+
this.wireReactiveCollections(instance);
|
|
387
|
+
// Inject @mcp dependencies from source (this.github, this.fs, etc.)
|
|
388
|
+
if (tsContent) {
|
|
389
|
+
await this.injectMCPDependencies(instance, tsContent, name);
|
|
390
|
+
}
|
|
391
|
+
// Inject MCP client factory if available (enables this.mcp() calls)
|
|
392
|
+
const setMCPFactory = instance.setMCPFactory;
|
|
393
|
+
if (this.mcpClientFactory && typeof setMCPFactory === 'function') {
|
|
394
|
+
setMCPFactory.call(instance, this.mcpClientFactory);
|
|
395
|
+
this.log(`Injected MCP factory into ${name}`);
|
|
396
|
+
}
|
|
397
|
+
// Check @cli dependencies (required system CLI tools)
|
|
398
|
+
if (tsContent) {
|
|
399
|
+
await this.checkCLIDependencies(tsContent, name);
|
|
197
400
|
}
|
|
198
401
|
// Call lifecycle hook if present with error handling
|
|
199
|
-
|
|
402
|
+
const onInitialize = instance.onInitialize;
|
|
403
|
+
if (typeof onInitialize === 'function') {
|
|
200
404
|
try {
|
|
201
|
-
await
|
|
405
|
+
await onInitialize.call(instance);
|
|
202
406
|
}
|
|
203
407
|
catch (error) {
|
|
204
|
-
const initError = new Error(`Initialization failed for ${name}: ${error
|
|
408
|
+
const initError = new Error(`Initialization failed for ${name}: ${getErrorMessage(error)}\n` +
|
|
205
409
|
`\nThe onInitialize() lifecycle hook threw an error.\n` +
|
|
206
410
|
`Check your constructor configuration and initialization logic.`);
|
|
207
411
|
initError.name = 'PhotonInitializationError';
|
|
208
|
-
|
|
412
|
+
if (error instanceof Error && error.stack) {
|
|
413
|
+
initError.stack = error.stack;
|
|
414
|
+
}
|
|
209
415
|
throw initError;
|
|
210
416
|
}
|
|
211
417
|
}
|
|
212
418
|
// Extract tools, templates, and statics (with schema override support)
|
|
213
419
|
const { tools, templates, statics } = await this.extractTools(MCPClass, absolutePath);
|
|
420
|
+
// Extract assets from source and discover asset folder
|
|
421
|
+
const assets = await this.discoverAssets(absolutePath, tsContent || '');
|
|
214
422
|
const counts = [
|
|
215
423
|
tools.length > 0 ? `${tools.length} tools` : null,
|
|
216
424
|
templates.length > 0 ? `${templates.length} templates` : null,
|
|
217
425
|
statics.length > 0 ? `${statics.length} statics` : null,
|
|
218
|
-
|
|
426
|
+
assets && (assets.ui.length > 0 || assets.prompts.length > 0 || assets.resources.length > 0)
|
|
427
|
+
? `${assets.ui.length + assets.prompts.length + assets.resources.length} assets`
|
|
428
|
+
: null,
|
|
429
|
+
]
|
|
430
|
+
.filter(Boolean)
|
|
431
|
+
.join(', ');
|
|
219
432
|
this.log(`✅ Loaded: ${name} (${counts})`);
|
|
220
|
-
|
|
433
|
+
const result = {
|
|
221
434
|
name,
|
|
222
435
|
description: `${name} MCP`,
|
|
223
436
|
tools,
|
|
224
437
|
templates,
|
|
225
438
|
statics,
|
|
226
439
|
instance,
|
|
440
|
+
assets,
|
|
441
|
+
injectedPhotons: injectedPhotonNames.length > 0 ? injectedPhotonNames : undefined,
|
|
227
442
|
};
|
|
443
|
+
// Store class constructor for static method access
|
|
444
|
+
result.classConstructor = MCPClass;
|
|
445
|
+
return result;
|
|
228
446
|
}
|
|
229
447
|
catch (error) {
|
|
230
|
-
|
|
448
|
+
this.logger.error(`❌ Failed to load ${filePath}: ${getErrorMessage(error)}`);
|
|
231
449
|
throw error;
|
|
232
450
|
}
|
|
233
451
|
}
|
|
@@ -242,9 +460,10 @@ export class PhotonLoader {
|
|
|
242
460
|
const tsContent = await fs.readFile(absolutePath, 'utf-8');
|
|
243
461
|
const hash = crypto.createHash('sha256').update(tsContent).digest('hex').slice(0, 16);
|
|
244
462
|
const mcpName = path.basename(absolutePath, '.ts').replace('.photon', '');
|
|
245
|
-
const
|
|
463
|
+
const cacheKey = this.getCacheKey(mcpName, absolutePath);
|
|
464
|
+
const buildDir = this.getBuildCacheDir(cacheKey);
|
|
246
465
|
const fileName = path.basename(absolutePath, '.ts');
|
|
247
|
-
const cachedJsPath = path.join(
|
|
466
|
+
const cachedJsPath = path.join(buildDir, `${fileName}.${hash}.mjs`);
|
|
248
467
|
try {
|
|
249
468
|
await fs.unlink(cachedJsPath);
|
|
250
469
|
}
|
|
@@ -256,94 +475,47 @@ export class PhotonLoader {
|
|
|
256
475
|
}
|
|
257
476
|
/**
|
|
258
477
|
* Compile TypeScript file to JavaScript and cache it
|
|
478
|
+
* Delegates to shared compilePhotonTS from photon-core
|
|
259
479
|
*/
|
|
260
|
-
async compileTypeScript(tsFilePath,
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const fileName = path.basename(tsFilePath, '.ts');
|
|
266
|
-
const cachedJsPath = path.join(cacheDir, `${fileName}.${hash}.mjs`);
|
|
267
|
-
// Check if cached version exists
|
|
268
|
-
try {
|
|
269
|
-
await fs.access(cachedJsPath);
|
|
270
|
-
this.log(`Using cached compiled version`);
|
|
271
|
-
return cachedJsPath;
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
// Cache miss - compile it
|
|
275
|
-
}
|
|
276
|
-
// Compile TypeScript to JavaScript
|
|
277
|
-
this.log(`Compiling ${path.basename(tsFilePath)} with esbuild...`);
|
|
278
|
-
const esbuild = await import('esbuild');
|
|
279
|
-
const result = await esbuild.transform(source, {
|
|
280
|
-
loader: 'ts',
|
|
281
|
-
format: 'esm',
|
|
282
|
-
target: 'es2022',
|
|
283
|
-
sourcemap: 'inline'
|
|
284
|
-
});
|
|
285
|
-
// Ensure cache directory exists
|
|
286
|
-
await fs.mkdir(cacheDir, { recursive: true });
|
|
287
|
-
// Write compiled JavaScript to cache
|
|
288
|
-
await fs.writeFile(cachedJsPath, result.code, 'utf-8');
|
|
289
|
-
this.log(`Compiled and cached`);
|
|
290
|
-
return cachedJsPath;
|
|
480
|
+
async compileTypeScript(tsFilePath, cacheKey, tsContent) {
|
|
481
|
+
const cacheDir = this.getBuildCacheDir(cacheKey);
|
|
482
|
+
const result = await compilePhotonTS(tsFilePath, { cacheDir, content: tsContent });
|
|
483
|
+
this.log(`Compiled: ${path.basename(tsFilePath)}`, { cached: result });
|
|
484
|
+
return result;
|
|
291
485
|
}
|
|
292
486
|
/**
|
|
293
487
|
* Find the MCP class in a module
|
|
294
|
-
*
|
|
488
|
+
* Delegates to shared findPhotonClass from photon-core
|
|
295
489
|
*/
|
|
296
490
|
findMCPClass(module) {
|
|
297
|
-
|
|
298
|
-
if (module.default && this.isClass(module.default)) {
|
|
299
|
-
if (this.hasAsyncMethods(module.default)) {
|
|
300
|
-
return module.default;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
// Try named exports
|
|
304
|
-
for (const exportedItem of Object.values(module)) {
|
|
305
|
-
if (this.isClass(exportedItem) && this.hasAsyncMethods(exportedItem)) {
|
|
306
|
-
return exportedItem;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
return null;
|
|
491
|
+
return sharedFindPhotonClass(module);
|
|
310
492
|
}
|
|
311
493
|
/**
|
|
312
494
|
* Check if a function is a class constructor
|
|
495
|
+
* Delegates to shared isClass from photon-core
|
|
313
496
|
*/
|
|
314
497
|
isClass(fn) {
|
|
315
|
-
return
|
|
498
|
+
return sharedIsClass(fn);
|
|
316
499
|
}
|
|
317
500
|
/**
|
|
318
501
|
* Check if a class has async methods
|
|
502
|
+
* Delegates to shared hasAsyncMethods from photon-core
|
|
319
503
|
*/
|
|
320
504
|
hasAsyncMethods(ClassConstructor) {
|
|
321
|
-
|
|
322
|
-
for (const key of Object.getOwnPropertyNames(prototype)) {
|
|
323
|
-
if (key === 'constructor')
|
|
324
|
-
continue;
|
|
325
|
-
const descriptor = Object.getOwnPropertyDescriptor(prototype, key);
|
|
326
|
-
if (descriptor && typeof descriptor.value === 'function') {
|
|
327
|
-
// Check if it's an async function
|
|
328
|
-
const fn = descriptor.value;
|
|
329
|
-
if (fn.constructor.name === 'AsyncFunction') {
|
|
330
|
-
return true;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
return false;
|
|
505
|
+
return sharedHasAsyncMethods(ClassConstructor);
|
|
335
506
|
}
|
|
336
507
|
/**
|
|
337
508
|
* Get MCP name from class
|
|
338
509
|
*/
|
|
339
510
|
getMCPName(mcpClass) {
|
|
340
511
|
// Try to use PhotonMCP's method if available
|
|
341
|
-
|
|
342
|
-
|
|
512
|
+
const classWithStatic = mcpClass;
|
|
513
|
+
if (typeof classWithStatic.getMCPName === 'function') {
|
|
514
|
+
return classWithStatic.getMCPName();
|
|
343
515
|
}
|
|
344
516
|
// Fallback: implement convention for plain classes
|
|
345
517
|
// Convert PascalCase to kebab-case (e.g., MyAwesomeMCP → my-awesome-mcp)
|
|
346
|
-
return
|
|
518
|
+
return classWithStatic.name
|
|
347
519
|
.replace(/MCP$/, '') // Remove "MCP" suffix if present
|
|
348
520
|
.replace(/([A-Z])/g, '-$1')
|
|
349
521
|
.toLowerCase()
|
|
@@ -354,22 +526,39 @@ export class PhotonLoader {
|
|
|
354
526
|
*/
|
|
355
527
|
getToolMethods(mcpClass) {
|
|
356
528
|
// Try to use PhotonMCP's method if available
|
|
357
|
-
|
|
358
|
-
|
|
529
|
+
const classWithStatic = mcpClass;
|
|
530
|
+
if (typeof classWithStatic.getToolMethods === 'function') {
|
|
531
|
+
return classWithStatic.getToolMethods();
|
|
359
532
|
}
|
|
360
533
|
// Fallback: implement convention for plain classes
|
|
361
|
-
const prototype =
|
|
534
|
+
const prototype = classWithStatic.prototype;
|
|
362
535
|
const methods = [];
|
|
536
|
+
const conventionMethods = new Set([
|
|
537
|
+
'constructor',
|
|
538
|
+
'onInitialize',
|
|
539
|
+
'onShutdown',
|
|
540
|
+
'configure',
|
|
541
|
+
'getConfig',
|
|
542
|
+
]);
|
|
543
|
+
const builtInStatics = new Set(['length', 'name', 'prototype', 'getToolMethods']);
|
|
544
|
+
// Get instance methods from prototype
|
|
363
545
|
Object.getOwnPropertyNames(prototype).forEach((name) => {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
!name.startsWith('_') &&
|
|
367
|
-
name !== 'onInitialize' &&
|
|
368
|
-
name !== 'onShutdown' &&
|
|
546
|
+
if (!name.startsWith('_') &&
|
|
547
|
+
!conventionMethods.has(name) &&
|
|
369
548
|
typeof prototype[name] === 'function') {
|
|
370
549
|
methods.push(name);
|
|
371
550
|
}
|
|
372
551
|
});
|
|
552
|
+
// Get static methods from class constructor
|
|
553
|
+
const classAsRecord = mcpClass;
|
|
554
|
+
Object.getOwnPropertyNames(mcpClass).forEach((name) => {
|
|
555
|
+
if (!name.startsWith('_') &&
|
|
556
|
+
!builtInStatics.has(name) &&
|
|
557
|
+
!conventionMethods.has(name) &&
|
|
558
|
+
typeof classAsRecord[name] === 'function') {
|
|
559
|
+
methods.push(name);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
373
562
|
return methods;
|
|
374
563
|
}
|
|
375
564
|
/**
|
|
@@ -429,14 +618,17 @@ export class PhotonLoader {
|
|
|
429
618
|
}
|
|
430
619
|
catch (jsonError) {
|
|
431
620
|
// .schema.json doesn't exist, try extracting from .ts source
|
|
432
|
-
|
|
621
|
+
const isNotFound = jsonError instanceof Error &&
|
|
622
|
+
'code' in jsonError &&
|
|
623
|
+
jsonError.code === 'ENOENT';
|
|
624
|
+
if (isNotFound) {
|
|
433
625
|
const extractor = new SchemaExtractor();
|
|
434
626
|
const source = await fs.readFile(sourceFilePath, 'utf-8');
|
|
435
627
|
const metadata = extractor.extractAllFromSource(source);
|
|
436
628
|
// Filter by method names that exist in the class
|
|
437
|
-
tools = metadata.tools.filter(t => methodNames.includes(t.name));
|
|
438
|
-
templates = metadata.templates.filter(t => methodNames.includes(t.name));
|
|
439
|
-
statics = metadata.statics.filter(s => methodNames.includes(s.name));
|
|
629
|
+
tools = metadata.tools.filter((t) => methodNames.includes(t.name));
|
|
630
|
+
templates = metadata.templates.filter((t) => methodNames.includes(t.name));
|
|
631
|
+
statics = metadata.statics.filter((s) => methodNames.includes(s.name));
|
|
440
632
|
this.log(`Extracted ${tools.length} tools, ${templates.length} templates, ${statics.length} statics from source`);
|
|
441
633
|
return { tools, templates, statics };
|
|
442
634
|
}
|
|
@@ -444,7 +636,7 @@ export class PhotonLoader {
|
|
|
444
636
|
}
|
|
445
637
|
}
|
|
446
638
|
catch (error) {
|
|
447
|
-
|
|
639
|
+
this.logger.warn(`⚠️ Failed to extract schemas: ${getErrorMessage(error)}. Using basic tools.`);
|
|
448
640
|
// Fallback: create basic tools without detailed schemas
|
|
449
641
|
for (const methodName of methodNames) {
|
|
450
642
|
tools.push({
|
|
@@ -469,18 +661,473 @@ export class PhotonLoader {
|
|
|
469
661
|
return extractor.extractConstructorParams(source);
|
|
470
662
|
}
|
|
471
663
|
catch (error) {
|
|
472
|
-
|
|
664
|
+
this.logger.warn(`Failed to extract constructor params: ${getErrorMessage(error)}`);
|
|
473
665
|
return [];
|
|
474
666
|
}
|
|
475
667
|
}
|
|
668
|
+
/**
|
|
669
|
+
* Resolve all constructor injections using type-based detection
|
|
670
|
+
* - Primitives (string, number, boolean) → env var
|
|
671
|
+
* - Non-primitives matching @mcp → MCP client
|
|
672
|
+
* - Non-primitives matching @photon → Photon instance
|
|
673
|
+
*
|
|
674
|
+
* Throws MCPConfigurationError if MCP dependencies are missing
|
|
675
|
+
*/
|
|
676
|
+
async resolveAllInjections(source, mcpName, photonPath) {
|
|
677
|
+
const extractor = new SchemaExtractor();
|
|
678
|
+
const injections = extractor.resolveInjections(source, mcpName);
|
|
679
|
+
const values = [];
|
|
680
|
+
const missingEnvVars = [];
|
|
681
|
+
const missingMCPs = [];
|
|
682
|
+
const missingPhotons = [];
|
|
683
|
+
const injectedPhotonNames = [];
|
|
684
|
+
for (const injection of injections) {
|
|
685
|
+
const { param, injectionType } = injection;
|
|
686
|
+
switch (injectionType) {
|
|
687
|
+
case 'env': {
|
|
688
|
+
// Inject from environment variable
|
|
689
|
+
const envVarName = injection.envVarName;
|
|
690
|
+
const envValue = process.env[envVarName];
|
|
691
|
+
if (envValue !== undefined) {
|
|
692
|
+
values.push(this.parseEnvValue(envValue, param.type));
|
|
693
|
+
}
|
|
694
|
+
else if (param.hasDefault || param.isOptional) {
|
|
695
|
+
values.push(undefined);
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
missingEnvVars.push({
|
|
699
|
+
paramName: param.name,
|
|
700
|
+
envVarName,
|
|
701
|
+
type: param.type,
|
|
702
|
+
});
|
|
703
|
+
values.push(undefined);
|
|
704
|
+
}
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
case 'mcp': {
|
|
708
|
+
// Inject MCP client
|
|
709
|
+
const mcpDep = injection.mcpDependency;
|
|
710
|
+
try {
|
|
711
|
+
const client = await this.getMCPClient(mcpDep);
|
|
712
|
+
values.push(client);
|
|
713
|
+
this.log(` ✅ Injected MCP: ${mcpDep.name} (${mcpDep.source})`);
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
// If it's already an MCPConfigurationError, re-throw it directly
|
|
717
|
+
if (error instanceof MCPConfigurationError) {
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
this.log(` ⚠️ Failed to create MCP client for ${mcpDep.name}: ${getErrorMessage(error)}`);
|
|
721
|
+
missingMCPs.push({
|
|
722
|
+
name: mcpDep.name,
|
|
723
|
+
source: mcpDep.source,
|
|
724
|
+
sourceType: mcpDep.sourceType,
|
|
725
|
+
declaredIn: path.basename(photonPath),
|
|
726
|
+
originalError: getErrorMessage(error),
|
|
727
|
+
});
|
|
728
|
+
values.push(undefined);
|
|
729
|
+
}
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
case 'photon': {
|
|
733
|
+
// Inject Photon instance
|
|
734
|
+
const photonDep = injection.photonDependency;
|
|
735
|
+
try {
|
|
736
|
+
const photonInstance = await this.getPhotonInstance(photonDep, photonPath);
|
|
737
|
+
values.push(photonInstance);
|
|
738
|
+
injectedPhotonNames.push(photonDep.name);
|
|
739
|
+
this.log(` ✅ Injected Photon: ${photonDep.name} (${photonDep.source})`);
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
this.log(` ⚠️ Failed to load Photon ${photonDep.name}: ${getErrorMessage(error)}`);
|
|
743
|
+
missingPhotons.push(`@photon ${photonDep.name} ${photonDep.source}: ${getErrorMessage(error)}`);
|
|
744
|
+
values.push(undefined);
|
|
745
|
+
}
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
// Throw MCPConfigurationError immediately if MCP dependencies are missing
|
|
751
|
+
// This provides actionable guidance to users
|
|
752
|
+
if (missingMCPs.length > 0) {
|
|
753
|
+
throw new MCPConfigurationError(missingMCPs);
|
|
754
|
+
}
|
|
755
|
+
// Build config error for env vars and photon dependencies
|
|
756
|
+
let configError = null;
|
|
757
|
+
if (missingEnvVars.length > 0 || missingPhotons.length > 0) {
|
|
758
|
+
const parts = [];
|
|
759
|
+
if (missingEnvVars.length > 0) {
|
|
760
|
+
parts.push(generateConfigErrorMessage(mcpName, missingEnvVars));
|
|
761
|
+
}
|
|
762
|
+
if (missingPhotons.length > 0) {
|
|
763
|
+
parts.push(`Missing Photon dependencies:\n` +
|
|
764
|
+
missingPhotons.map((d) => ` • ${d}`).join('\n') +
|
|
765
|
+
`\n\nEnsure these Photons are installed and accessible.`);
|
|
766
|
+
}
|
|
767
|
+
configError = parts.join('\n\n');
|
|
768
|
+
}
|
|
769
|
+
return { values, configError, injectedPhotonNames };
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Get or create an MCP client for a dependency
|
|
773
|
+
*
|
|
774
|
+
* Resolution order:
|
|
775
|
+
* 1. Check ~/.photon/config.json for configured server
|
|
776
|
+
* 2. Fall back to resolving from @mcp declaration source
|
|
777
|
+
*
|
|
778
|
+
* Validates connection on first use - throws MCPConfigurationError if connection fails
|
|
779
|
+
*/
|
|
780
|
+
async getMCPClient(dep) {
|
|
781
|
+
// Check cache first
|
|
782
|
+
if (this.mcpClients.has(dep.name)) {
|
|
783
|
+
return this.mcpClients.get(dep.name);
|
|
784
|
+
}
|
|
785
|
+
// Try to get config from ~/.photon/config.json first
|
|
786
|
+
const photonConfig = await this.ensureMCPConfig();
|
|
787
|
+
let serverConfig;
|
|
788
|
+
let isFromConfig = false;
|
|
789
|
+
if (photonConfig.mcpServers[dep.name]) {
|
|
790
|
+
// Use pre-configured server from config.json
|
|
791
|
+
serverConfig = resolveEnvVars(photonConfig.mcpServers[dep.name]);
|
|
792
|
+
isFromConfig = true;
|
|
793
|
+
this.log(` Using configured MCP: ${dep.name} from config.json`);
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
// Fall back to resolving from @mcp declaration
|
|
797
|
+
serverConfig = resolveMCPSource(dep.name, dep.source, dep.sourceType);
|
|
798
|
+
this.log(` Resolving MCP: ${dep.name} from @mcp declaration (${dep.source})`);
|
|
799
|
+
}
|
|
800
|
+
// Build config with this MCP
|
|
801
|
+
const mcpConfig = {
|
|
802
|
+
mcpServers: {
|
|
803
|
+
[dep.name]: serverConfig,
|
|
804
|
+
},
|
|
805
|
+
};
|
|
806
|
+
// Create a factory for this MCP
|
|
807
|
+
// Note: Each MCP gets its own factory for isolation
|
|
808
|
+
const factory = new SDKMCPClientFactory(mcpConfig, this.verbose);
|
|
809
|
+
// Create client and proxy
|
|
810
|
+
const client = factory.create(dep.name);
|
|
811
|
+
const proxy = createMCPProxy(client);
|
|
812
|
+
// Validate connection by attempting to list tools
|
|
813
|
+
// This catches configuration errors early (missing env vars, wrong command, etc.)
|
|
814
|
+
try {
|
|
815
|
+
this.log(` Connecting to MCP: ${dep.name}...`);
|
|
816
|
+
await client.list();
|
|
817
|
+
this.log(` ✅ Connected to MCP: ${dep.name}`);
|
|
818
|
+
}
|
|
819
|
+
catch (error) {
|
|
820
|
+
const errorMsg = getErrorMessage(error) || 'Unknown connection error';
|
|
821
|
+
// If not configured in mcp-servers.json, throw configuration error
|
|
822
|
+
if (!isFromConfig) {
|
|
823
|
+
throw new MCPConfigurationError([
|
|
824
|
+
{
|
|
825
|
+
name: dep.name,
|
|
826
|
+
source: dep.source,
|
|
827
|
+
sourceType: dep.sourceType,
|
|
828
|
+
originalError: errorMsg,
|
|
829
|
+
},
|
|
830
|
+
]);
|
|
831
|
+
}
|
|
832
|
+
// If configured but failed, provide more specific error
|
|
833
|
+
throw new Error(`MCP "${dep.name}" is configured but failed to connect: ${errorMsg}\n` +
|
|
834
|
+
`Check your ~/.photon/config.json configuration and ensure:\n` +
|
|
835
|
+
` • The command/URL is correct\n` +
|
|
836
|
+
` • Required environment variables are set\n` +
|
|
837
|
+
` • The MCP server is accessible`);
|
|
838
|
+
}
|
|
839
|
+
// Cache it
|
|
840
|
+
this.mcpClients.set(dep.name, proxy);
|
|
841
|
+
return proxy;
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Get or load a Photon instance for a dependency
|
|
845
|
+
*/
|
|
846
|
+
async getPhotonInstance(dep, currentPhotonPath) {
|
|
847
|
+
// Resolve the Photon path
|
|
848
|
+
const resolvedPath = await this.resolvePhotonPath(dep, currentPhotonPath);
|
|
849
|
+
// Check cache
|
|
850
|
+
if (this.loadedPhotons.has(resolvedPath)) {
|
|
851
|
+
return this.loadedPhotons.get(resolvedPath).instance;
|
|
852
|
+
}
|
|
853
|
+
// Load the Photon (recursive call)
|
|
854
|
+
this.log(` 📦 Loading Photon dependency: ${dep.name} from ${resolvedPath}`);
|
|
855
|
+
const loaded = await this.loadFile(resolvedPath);
|
|
856
|
+
// Cache it
|
|
857
|
+
this.loadedPhotons.set(resolvedPath, loaded);
|
|
858
|
+
return loaded.instance;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Resolve Photon dependency path based on source type
|
|
862
|
+
*/
|
|
863
|
+
async resolvePhotonPath(dep, currentPhotonPath) {
|
|
864
|
+
switch (dep.sourceType) {
|
|
865
|
+
case 'local':
|
|
866
|
+
if (dep.source.startsWith('./') || dep.source.startsWith('../')) {
|
|
867
|
+
return path.resolve(path.dirname(currentPhotonPath), dep.source);
|
|
868
|
+
}
|
|
869
|
+
return dep.source;
|
|
870
|
+
case 'marketplace':
|
|
871
|
+
return await this.resolveMarketplacePhoton(dep, currentPhotonPath);
|
|
872
|
+
case 'github':
|
|
873
|
+
return await this.fetchGithubPhoton(dep);
|
|
874
|
+
case 'npm':
|
|
875
|
+
return await this.resolveNpmPhoton(dep);
|
|
876
|
+
default:
|
|
877
|
+
throw new Error(`Unknown Photon source type: ${dep.sourceType}`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
async resolveMarketplacePhoton(dep, currentPhotonPath) {
|
|
881
|
+
const { slug, fileName, marketplaceHint } = this.normalizeMarketplaceSource(dep.source);
|
|
882
|
+
const photonDir = path.dirname(currentPhotonPath);
|
|
883
|
+
const candidates = [
|
|
884
|
+
path.resolve(photonDir, fileName),
|
|
885
|
+
path.resolve(photonDir, 'photons', fileName),
|
|
886
|
+
path.resolve(photonDir, 'templates', fileName),
|
|
887
|
+
path.join(process.cwd(), fileName),
|
|
888
|
+
path.join(process.cwd(), 'photons', fileName),
|
|
889
|
+
path.join(process.cwd(), 'templates', fileName),
|
|
890
|
+
path.join(os.homedir(), '.photon', fileName),
|
|
891
|
+
path.join(os.homedir(), '.photon', 'photons', fileName),
|
|
892
|
+
path.join(os.homedir(), '.photon', 'marketplace', fileName),
|
|
893
|
+
];
|
|
894
|
+
for (const candidate of candidates) {
|
|
895
|
+
if (await this.pathExists(candidate)) {
|
|
896
|
+
return candidate;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const downloaded = await this.fetchPhotonFromMarketplace(slug, marketplaceHint);
|
|
900
|
+
if (downloaded) {
|
|
901
|
+
return downloaded;
|
|
902
|
+
}
|
|
903
|
+
throw new Error(`Photon "${dep.source}" not found in local paths or configured marketplaces. ` +
|
|
904
|
+
`Checked: ${candidates.join(', ')}`);
|
|
905
|
+
}
|
|
906
|
+
normalizeMarketplaceSource(source) {
|
|
907
|
+
let slugSource = source;
|
|
908
|
+
let marketplaceHint;
|
|
909
|
+
if (source.includes('/')) {
|
|
910
|
+
const [hint, rest] = source.split('/', 2);
|
|
911
|
+
if (hint && rest) {
|
|
912
|
+
marketplaceHint = hint;
|
|
913
|
+
slugSource = rest;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const slug = slugSource
|
|
917
|
+
.replace(/\.photon\.ts$/, '')
|
|
918
|
+
.replace(/\.photon$/, '')
|
|
919
|
+
.replace(/\.ts$/, '')
|
|
920
|
+
.trim();
|
|
921
|
+
const normalizedSlug = slug || slugSource.replace(/[\\/]/g, '-');
|
|
922
|
+
const fileName = `${normalizedSlug}.photon.ts`;
|
|
923
|
+
return { slug: normalizedSlug, fileName, marketplaceHint };
|
|
924
|
+
}
|
|
925
|
+
async fetchPhotonFromMarketplace(slug, marketplaceHint) {
|
|
926
|
+
try {
|
|
927
|
+
const manager = await this.getMarketplaceManager();
|
|
928
|
+
try {
|
|
929
|
+
await manager.autoUpdateStaleCaches();
|
|
930
|
+
}
|
|
931
|
+
catch {
|
|
932
|
+
// Best effort; stale caches are acceptable
|
|
933
|
+
}
|
|
934
|
+
if (marketplaceHint) {
|
|
935
|
+
const hinted = manager.get(marketplaceHint);
|
|
936
|
+
if (hinted) {
|
|
937
|
+
const hintedPath = await this.fetchPhotonFromSpecificMarketplace(hinted, slug);
|
|
938
|
+
if (hintedPath) {
|
|
939
|
+
return hintedPath;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const result = await manager.fetchMCP(slug);
|
|
944
|
+
if (result?.content) {
|
|
945
|
+
return await this.writePhotonCacheFile(`${result.marketplace.name}-${slug}`, result.content, result.metadata?.hash);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
catch (error) {
|
|
949
|
+
this.log(` ⚠️ Marketplace lookup failed for ${slug}: ${getErrorMessage(error)}`);
|
|
950
|
+
}
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
async fetchPhotonFromSpecificMarketplace(marketplace, slug) {
|
|
954
|
+
try {
|
|
955
|
+
if (marketplace.sourceType === 'local') {
|
|
956
|
+
const localPath = marketplace.url.replace('file://', '');
|
|
957
|
+
const photonPath = path.join(localPath, `${slug}.photon.ts`);
|
|
958
|
+
if (await this.pathExists(photonPath)) {
|
|
959
|
+
return photonPath;
|
|
960
|
+
}
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
const baseUrl = marketplace.url.replace(/\/$/, '');
|
|
964
|
+
const response = await fetch(`${baseUrl}/${slug}.photon.ts`, {
|
|
965
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
966
|
+
});
|
|
967
|
+
if (response.ok) {
|
|
968
|
+
const content = await response.text();
|
|
969
|
+
return await this.writePhotonCacheFile(`${marketplace.name}-${slug}`, content);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
catch (error) {
|
|
973
|
+
this.log(` ⚠️ Failed to fetch ${slug} from marketplace ${marketplace.name}: ${getErrorMessage(error)}`);
|
|
974
|
+
}
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
async fetchGithubPhoton(dep) {
|
|
978
|
+
const info = this.parseGithubSource(dep);
|
|
979
|
+
const url = `https://raw.githubusercontent.com/${info.owner}/${info.repo}/${info.ref}/${info.filePath}`;
|
|
980
|
+
const response = await fetch(url, {
|
|
981
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
982
|
+
});
|
|
983
|
+
if (!response.ok) {
|
|
984
|
+
throw new Error(`Failed to download Photon from GitHub (${dep.source}): HTTP ${response.status}`);
|
|
985
|
+
}
|
|
986
|
+
const content = await response.text();
|
|
987
|
+
const label = `${info.owner}-${info.repo}-${info.filePath.replace(/[\\/]/g, '_')}`;
|
|
988
|
+
return await this.writePhotonCacheFile(label, content);
|
|
989
|
+
}
|
|
990
|
+
parseGithubSource(dep) {
|
|
991
|
+
let source = dep.source
|
|
992
|
+
.replace(/^github:/, '')
|
|
993
|
+
.replace(/^https?:\/\/github\.com\//, '')
|
|
994
|
+
.replace(/^git@github\.com:/, '')
|
|
995
|
+
.replace(/\.git$/, '');
|
|
996
|
+
const parts = source.split('/');
|
|
997
|
+
if (parts.length < 2) {
|
|
998
|
+
throw new Error(`Invalid GitHub source: ${dep.source}`);
|
|
999
|
+
}
|
|
1000
|
+
const owner = parts.shift();
|
|
1001
|
+
let repoPart = parts.shift();
|
|
1002
|
+
let ref = 'main';
|
|
1003
|
+
const repoRefMatch = repoPart.match(/([^@]+)@(.+)/);
|
|
1004
|
+
if (repoRefMatch) {
|
|
1005
|
+
repoPart = repoRefMatch[1];
|
|
1006
|
+
ref = repoRefMatch[2];
|
|
1007
|
+
}
|
|
1008
|
+
if (parts[0] === 'blob' && parts.length >= 2) {
|
|
1009
|
+
parts.shift();
|
|
1010
|
+
ref = parts.shift();
|
|
1011
|
+
}
|
|
1012
|
+
let filePath;
|
|
1013
|
+
if (parts.length === 0) {
|
|
1014
|
+
const slug = dep.name
|
|
1015
|
+
.replace(/([A-Z])/g, '-$1')
|
|
1016
|
+
.toLowerCase()
|
|
1017
|
+
.replace(/^-/, '');
|
|
1018
|
+
filePath = `${slug || 'photon'}.photon.ts`;
|
|
1019
|
+
}
|
|
1020
|
+
else {
|
|
1021
|
+
filePath = parts.join('/');
|
|
1022
|
+
if (!filePath.endsWith('.ts')) {
|
|
1023
|
+
filePath = filePath.endsWith('.photon') ? `${filePath}.ts` : `${filePath}.photon.ts`;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return { owner, repo: repoPart, ref, filePath };
|
|
1027
|
+
}
|
|
1028
|
+
async resolveNpmPhoton(dep) {
|
|
1029
|
+
const { packageSpec, packageName, filePath } = this.parseNpmSource(dep.source);
|
|
1030
|
+
const cacheDir = path.join(os.homedir(), '.photon', '.cache', 'npm', this.sanitizeCacheLabel(packageSpec));
|
|
1031
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
1032
|
+
await this.ensureNpmPackageInstalled(cacheDir, packageSpec, packageName);
|
|
1033
|
+
const packageRoot = this.getPackageInstallPath(cacheDir, packageName);
|
|
1034
|
+
if (!(await this.pathExists(packageRoot))) {
|
|
1035
|
+
throw new Error(`npm package "${packageSpec}" did not install correctly.`);
|
|
1036
|
+
}
|
|
1037
|
+
if (filePath) {
|
|
1038
|
+
const normalized = filePath.replace(/^\//, '');
|
|
1039
|
+
const explicitCandidates = [path.join(packageRoot, normalized)];
|
|
1040
|
+
if (!normalized.endsWith('.ts')) {
|
|
1041
|
+
explicitCandidates.push(path.join(packageRoot, `${normalized}.photon.ts`));
|
|
1042
|
+
}
|
|
1043
|
+
for (const candidate of explicitCandidates) {
|
|
1044
|
+
if (await this.pathExists(candidate)) {
|
|
1045
|
+
return candidate;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
const discovered = await this.findPhotonFile(packageRoot);
|
|
1050
|
+
if (discovered) {
|
|
1051
|
+
return discovered;
|
|
1052
|
+
}
|
|
1053
|
+
throw new Error(`Unable to locate a .photon.ts file within npm package ${packageSpec}.`);
|
|
1054
|
+
}
|
|
1055
|
+
parseNpmSource(source) {
|
|
1056
|
+
let cleaned = source.replace(/^npm:/, '');
|
|
1057
|
+
const [specPart, filePath] = cleaned.split('#', 2);
|
|
1058
|
+
return {
|
|
1059
|
+
packageSpec: specPart,
|
|
1060
|
+
packageName: this.extractPackageName(specPart),
|
|
1061
|
+
filePath,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
extractPackageName(spec) {
|
|
1065
|
+
if (spec.startsWith('@')) {
|
|
1066
|
+
const idx = spec.indexOf('@', 1);
|
|
1067
|
+
return idx === -1 ? spec : spec.slice(0, idx);
|
|
1068
|
+
}
|
|
1069
|
+
const idx = spec.indexOf('@');
|
|
1070
|
+
return idx === -1 ? spec : spec.slice(0, idx);
|
|
1071
|
+
}
|
|
1072
|
+
getPackageInstallPath(cacheDir, packageName) {
|
|
1073
|
+
if (packageName.startsWith('@')) {
|
|
1074
|
+
const segments = packageName.split('/');
|
|
1075
|
+
return path.join(cacheDir, 'node_modules', segments[0], segments[1] || '');
|
|
1076
|
+
}
|
|
1077
|
+
return path.join(cacheDir, 'node_modules', packageName);
|
|
1078
|
+
}
|
|
1079
|
+
async ensureNpmPackageInstalled(cacheDir, packageSpec, packageName) {
|
|
1080
|
+
const packageJsonPath = path.join(cacheDir, 'package.json');
|
|
1081
|
+
if (!(await this.pathExists(packageJsonPath))) {
|
|
1082
|
+
const pkgName = `photon-npm-${this.sanitizeCacheLabel(packageSpec)}`;
|
|
1083
|
+
await fs.writeFile(packageJsonPath, JSON.stringify({ name: pkgName, version: PHOTON_VERSION }, null, 2), 'utf-8');
|
|
1084
|
+
}
|
|
1085
|
+
const packageRoot = this.getPackageInstallPath(cacheDir, packageName);
|
|
1086
|
+
if (await this.pathExists(packageRoot)) {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
1090
|
+
await this.runCommand(npmCmd, ['install', packageSpec, '--omit=dev', '--silent', '--no-save'], cacheDir);
|
|
1091
|
+
}
|
|
1092
|
+
async findPhotonFile(dir, depth = 0) {
|
|
1093
|
+
if (depth > 4) {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
let entries;
|
|
1097
|
+
try {
|
|
1098
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
this.logger.debug('Failed to read directory', { error });
|
|
1102
|
+
return null; // directory inaccessible
|
|
1103
|
+
}
|
|
1104
|
+
for (const entry of entries) {
|
|
1105
|
+
if (entry.isFile() && entry.name.endsWith('.photon.ts')) {
|
|
1106
|
+
return path.join(dir, entry.name);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
for (const entry of entries) {
|
|
1110
|
+
if (entry.isDirectory()) {
|
|
1111
|
+
if (entry.name === 'node_modules' || entry.name === '.git') {
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
const child = await this.findPhotonFile(path.join(dir, entry.name), depth + 1);
|
|
1115
|
+
if (child) {
|
|
1116
|
+
return child;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
476
1122
|
/**
|
|
477
1123
|
* Resolve constructor arguments from environment variables
|
|
1124
|
+
* @deprecated Use resolveAllInjections instead
|
|
478
1125
|
*/
|
|
479
1126
|
resolveConstructorArgs(params, mcpName) {
|
|
480
1127
|
const values = [];
|
|
481
1128
|
const missing = [];
|
|
482
1129
|
for (const param of params) {
|
|
483
|
-
const envVarName =
|
|
1130
|
+
const envVarName = toEnvVarName(mcpName, param.name);
|
|
484
1131
|
const envValue = process.env[envVarName];
|
|
485
1132
|
if (envValue !== undefined) {
|
|
486
1133
|
// Environment variable provided - parse and use it
|
|
@@ -501,67 +1148,23 @@ export class PhotonLoader {
|
|
|
501
1148
|
values.push(undefined);
|
|
502
1149
|
}
|
|
503
1150
|
}
|
|
504
|
-
const configError = missing.length > 0
|
|
505
|
-
? this.generateConfigErrorMessage(mcpName, missing)
|
|
506
|
-
: null;
|
|
1151
|
+
const configError = missing.length > 0 ? generateConfigErrorMessage(mcpName, missing) : null;
|
|
507
1152
|
return { values, configError };
|
|
508
1153
|
}
|
|
509
|
-
/**
|
|
510
|
-
* Convert MCP name and parameter name to environment variable name
|
|
511
|
-
* Example: filesystem, workdir → FILESYSTEM_WORKDIR
|
|
512
|
-
*/
|
|
513
|
-
toEnvVarName(mcpName, paramName) {
|
|
514
|
-
const mcpPrefix = mcpName.toUpperCase().replace(/-/g, '_');
|
|
515
|
-
const paramSuffix = paramName
|
|
516
|
-
.replace(/([A-Z])/g, '_$1')
|
|
517
|
-
.toUpperCase()
|
|
518
|
-
.replace(/^_/, '');
|
|
519
|
-
return `${mcpPrefix}_${paramSuffix}`;
|
|
520
|
-
}
|
|
521
1154
|
/**
|
|
522
1155
|
* Parse environment variable value based on TypeScript type
|
|
1156
|
+
* Delegates to shared parseEnvValue from photon-core
|
|
523
1157
|
*/
|
|
524
1158
|
parseEnvValue(value, type) {
|
|
525
|
-
|
|
526
|
-
case 'number':
|
|
527
|
-
return parseFloat(value);
|
|
528
|
-
case 'boolean':
|
|
529
|
-
return value.toLowerCase() === 'true';
|
|
530
|
-
case 'string':
|
|
531
|
-
default:
|
|
532
|
-
return value;
|
|
533
|
-
}
|
|
1159
|
+
return sharedParseEnvValue(value, type);
|
|
534
1160
|
}
|
|
535
1161
|
/**
|
|
536
1162
|
* Enhance constructor error with configuration guidance
|
|
537
1163
|
*/
|
|
538
1164
|
enhanceConstructorError(error, mcpName, constructorParams, configError) {
|
|
539
|
-
const originalMessage = error
|
|
1165
|
+
const originalMessage = getErrorMessage(error);
|
|
540
1166
|
// Build detailed env var documentation with examples
|
|
541
|
-
const envVarDocs = constructorParams
|
|
542
|
-
const envVarName = this.toEnvVarName(mcpName, param.name);
|
|
543
|
-
const required = !param.isOptional && !param.hasDefault;
|
|
544
|
-
const status = required ? '[REQUIRED]' : '[OPTIONAL]';
|
|
545
|
-
// Generate helpful example values based on type and name
|
|
546
|
-
const exampleValue = this.generateExampleValue(param.name, param.type);
|
|
547
|
-
const defaultInfo = param.hasDefault
|
|
548
|
-
? ` (default: ${JSON.stringify(param.defaultValue)})`
|
|
549
|
-
: '';
|
|
550
|
-
let line = ` • ${envVarName} ${status}`;
|
|
551
|
-
line += `\n Type: ${param.type}${defaultInfo}`;
|
|
552
|
-
if (exampleValue) {
|
|
553
|
-
line += `\n Example: ${envVarName}="${exampleValue}"`;
|
|
554
|
-
}
|
|
555
|
-
return line;
|
|
556
|
-
}).join('\n\n');
|
|
557
|
-
// Build example config with placeholder values
|
|
558
|
-
const envExample = {};
|
|
559
|
-
constructorParams.forEach(param => {
|
|
560
|
-
const envVarName = this.toEnvVarName(mcpName, param.name);
|
|
561
|
-
if (!param.isOptional && !param.hasDefault) {
|
|
562
|
-
envExample[envVarName] = this.generateExampleValue(param.name, param.type) || `your-${param.name}`;
|
|
563
|
-
}
|
|
564
|
-
});
|
|
1167
|
+
const { docs: envVarDocs, exampleEnv: envExample } = summarizeConstructorParams(constructorParams, mcpName);
|
|
565
1168
|
const enhancedMessage = `
|
|
566
1169
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
567
1170
|
❌ Configuration Error: ${mcpName} MCP failed to initialize
|
|
@@ -604,98 +1207,20 @@ Run: photon mcp ${mcpName} --config
|
|
|
604
1207
|
enhanced.stack = error.stack;
|
|
605
1208
|
return enhanced;
|
|
606
1209
|
}
|
|
607
|
-
/**
|
|
608
|
-
* Generate helpful example values based on parameter name and type
|
|
609
|
-
*/
|
|
610
|
-
generateExampleValue(paramName, paramType) {
|
|
611
|
-
const lowerName = paramName.toLowerCase();
|
|
612
|
-
// API keys and tokens
|
|
613
|
-
if (lowerName.includes('apikey') || lowerName.includes('api_key')) {
|
|
614
|
-
return 'sk_your_api_key_here';
|
|
615
|
-
}
|
|
616
|
-
if (lowerName.includes('token') || lowerName.includes('secret')) {
|
|
617
|
-
return 'your_secret_token';
|
|
618
|
-
}
|
|
619
|
-
// URLs and endpoints
|
|
620
|
-
if (lowerName.includes('url') || lowerName.includes('endpoint')) {
|
|
621
|
-
return 'https://api.example.com';
|
|
622
|
-
}
|
|
623
|
-
if (lowerName.includes('host') || lowerName.includes('server')) {
|
|
624
|
-
return 'localhost';
|
|
625
|
-
}
|
|
626
|
-
// Ports
|
|
627
|
-
if (lowerName.includes('port')) {
|
|
628
|
-
return '5432';
|
|
629
|
-
}
|
|
630
|
-
// Database
|
|
631
|
-
if (lowerName.includes('database') || lowerName.includes('db')) {
|
|
632
|
-
return 'my_database';
|
|
633
|
-
}
|
|
634
|
-
if (lowerName.includes('user') || lowerName.includes('username')) {
|
|
635
|
-
return 'admin';
|
|
636
|
-
}
|
|
637
|
-
if (lowerName.includes('password')) {
|
|
638
|
-
return 'your_secure_password';
|
|
639
|
-
}
|
|
640
|
-
// Paths
|
|
641
|
-
if (lowerName.includes('path') || lowerName.includes('dir')) {
|
|
642
|
-
return '/path/to/directory';
|
|
643
|
-
}
|
|
644
|
-
// Common names
|
|
645
|
-
if (lowerName.includes('name')) {
|
|
646
|
-
return 'my-service';
|
|
647
|
-
}
|
|
648
|
-
if (lowerName.includes('region')) {
|
|
649
|
-
return 'us-east-1';
|
|
650
|
-
}
|
|
651
|
-
// Type-based defaults
|
|
652
|
-
if (paramType === 'boolean') {
|
|
653
|
-
return 'true';
|
|
654
|
-
}
|
|
655
|
-
if (paramType === 'number') {
|
|
656
|
-
return '3000';
|
|
657
|
-
}
|
|
658
|
-
return null;
|
|
659
|
-
}
|
|
660
|
-
/**
|
|
661
|
-
* Generate user-friendly configuration error message
|
|
662
|
-
*/
|
|
663
|
-
generateConfigErrorMessage(mcpName, missing) {
|
|
664
|
-
const envVarList = missing.map(m => ` • ${m.envVarName} (${m.paramName}: ${m.type})`).join('\n');
|
|
665
|
-
const exampleEnv = Object.fromEntries(missing.map(m => [m.envVarName, `<your-${m.paramName}>`]));
|
|
666
|
-
return `
|
|
667
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
668
|
-
⚠️ Configuration Warning: ${mcpName} MCP
|
|
669
|
-
|
|
670
|
-
Missing required environment variables:
|
|
671
|
-
${envVarList}
|
|
672
|
-
|
|
673
|
-
Tools will fail until configuration is fixed.
|
|
674
|
-
|
|
675
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
676
|
-
|
|
677
|
-
To fix, add environment variables to your MCP client config:
|
|
678
|
-
|
|
679
|
-
{
|
|
680
|
-
"mcpServers": {
|
|
681
|
-
"${mcpName}": {
|
|
682
|
-
"command": "npx",
|
|
683
|
-
"args": ["@portel/photon", "${mcpName}"],
|
|
684
|
-
"env": ${JSON.stringify(exampleEnv, null, 8).replace(/\n/g, '\n ')}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
Or run: photon ${mcpName} --config
|
|
690
|
-
|
|
691
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
692
|
-
`.trim();
|
|
693
|
-
}
|
|
694
1210
|
/**
|
|
695
1211
|
* Execute a tool on the loaded MCP instance
|
|
696
1212
|
* Handles both regular async methods and async generators
|
|
1213
|
+
*
|
|
1214
|
+
* For generators with checkpoint yields, automatically uses stateful execution
|
|
1215
|
+
* with JSONL persistence. The run ID is returned in the result for stateful workflows.
|
|
1216
|
+
*
|
|
1217
|
+
* @param mcp - The loaded Photon MCP
|
|
1218
|
+
* @param toolName - Name of the tool to execute
|
|
1219
|
+
* @param parameters - Input parameters for the tool
|
|
1220
|
+
* @param options - Optional execution options
|
|
1221
|
+
* @returns Tool result, or wrapped result with runId for stateful workflows
|
|
697
1222
|
*/
|
|
698
|
-
async executeTool(mcp, toolName, parameters) {
|
|
1223
|
+
async executeTool(mcp, toolName, parameters, options) {
|
|
699
1224
|
try {
|
|
700
1225
|
// Check for configuration errors before executing tool
|
|
701
1226
|
if (mcp.instance._photonConfigError) {
|
|
@@ -703,79 +1228,421 @@ Or run: photon ${mcpName} --config
|
|
|
703
1228
|
}
|
|
704
1229
|
// Check if instance has PhotonMCP's executeTool method
|
|
705
1230
|
if (typeof mcp.instance.executeTool === 'function') {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (!method || typeof method !== 'function') {
|
|
712
|
-
throw new Error(`Tool not found: ${toolName}`);
|
|
713
|
-
}
|
|
714
|
-
const result = method.call(mcp.instance, parameters);
|
|
715
|
-
// Check if result is an async generator
|
|
1231
|
+
// PhotonMCP base class handles execution
|
|
1232
|
+
const outputHandler = options?.outputHandler || this.createOutputHandler();
|
|
1233
|
+
const inputProvider = options?.inputProvider || this.createInputProvider();
|
|
1234
|
+
const result = await mcp.instance.executeTool(toolName, parameters, { outputHandler });
|
|
1235
|
+
// Handle generator result (if tool returns a generator)
|
|
716
1236
|
if (isAsyncGenerator(result)) {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
inputProvider: this.createInputProvider(),
|
|
721
|
-
outputHandler: this.createOutputHandler(),
|
|
1237
|
+
const finalResult = await executeGenerator(result, {
|
|
1238
|
+
inputProvider,
|
|
1239
|
+
outputHandler,
|
|
722
1240
|
});
|
|
1241
|
+
// Clear any lingering progress
|
|
1242
|
+
this.progressRenderer.done();
|
|
1243
|
+
return finalResult;
|
|
723
1244
|
}
|
|
724
|
-
|
|
1245
|
+
// Clear any lingering progress
|
|
1246
|
+
this.progressRenderer.done();
|
|
1247
|
+
return result;
|
|
1248
|
+
}
|
|
1249
|
+
// Plain class - call method directly with implicit stateful support
|
|
1250
|
+
// Check instance first, then prototype, then static methods on class
|
|
1251
|
+
let method = mcp.instance[toolName];
|
|
1252
|
+
let isStatic = false;
|
|
1253
|
+
if (typeof method !== 'function') {
|
|
1254
|
+
method = Object.getPrototypeOf(mcp.instance)?.[toolName];
|
|
1255
|
+
}
|
|
1256
|
+
// Check for static method on class constructor
|
|
1257
|
+
if (typeof method !== 'function' && mcp.classConstructor) {
|
|
1258
|
+
method = mcp.classConstructor[toolName];
|
|
1259
|
+
isStatic = true;
|
|
1260
|
+
}
|
|
1261
|
+
if (!method || typeof method !== 'function') {
|
|
1262
|
+
throw new Error(`Tool not found: ${toolName}`);
|
|
725
1263
|
}
|
|
1264
|
+
// Create a generator factory for maybeStatefulExecute
|
|
1265
|
+
// This allows re-execution on resume
|
|
1266
|
+
// For static methods, call on the class itself; for instance methods, bind to instance
|
|
1267
|
+
const generatorFn = isStatic
|
|
1268
|
+
? () => method.call(null, parameters)
|
|
1269
|
+
: () => method.call(mcp.instance, parameters);
|
|
1270
|
+
// Use maybeStatefulExecute for all executions
|
|
1271
|
+
// It handles both regular async and generators, detecting checkpoint yields
|
|
1272
|
+
const execResult = await maybeStatefulExecute(generatorFn, {
|
|
1273
|
+
photon: mcp.name,
|
|
1274
|
+
tool: toolName,
|
|
1275
|
+
params: parameters,
|
|
1276
|
+
inputProvider: options?.inputProvider || this.createInputProvider(),
|
|
1277
|
+
outputHandler: options?.outputHandler || this.createOutputHandler(),
|
|
1278
|
+
resumeRunId: options?.resumeRunId,
|
|
1279
|
+
});
|
|
1280
|
+
// Clear any lingering progress
|
|
1281
|
+
this.progressRenderer.done();
|
|
1282
|
+
// If there was an error, throw it
|
|
1283
|
+
if (execResult.error) {
|
|
1284
|
+
const error = new Error(execResult.error);
|
|
1285
|
+
if (execResult.runId) {
|
|
1286
|
+
error.runId = execResult.runId;
|
|
1287
|
+
}
|
|
1288
|
+
throw error;
|
|
1289
|
+
}
|
|
1290
|
+
// For stateful workflows, wrap result with metadata
|
|
1291
|
+
if (execResult.isStateful && execResult.runId) {
|
|
1292
|
+
return {
|
|
1293
|
+
_stateful: true,
|
|
1294
|
+
runId: execResult.runId,
|
|
1295
|
+
resumed: execResult.resumed,
|
|
1296
|
+
resumedFromStep: execResult.resumedFromStep,
|
|
1297
|
+
checkpointsCompleted: execResult.checkpointsCompleted,
|
|
1298
|
+
status: execResult.status,
|
|
1299
|
+
result: execResult.result,
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
// For ephemeral execution, return result directly
|
|
1303
|
+
return execResult.result;
|
|
726
1304
|
}
|
|
727
1305
|
catch (error) {
|
|
728
|
-
|
|
1306
|
+
// Clear progress on error too
|
|
1307
|
+
this.progressRenderer.done();
|
|
1308
|
+
this.logger.error(`Tool execution failed: ${toolName} - ${getErrorMessage(error)}`);
|
|
729
1309
|
throw error;
|
|
730
1310
|
}
|
|
731
1311
|
}
|
|
732
1312
|
/**
|
|
733
|
-
* Create an input provider for generator yields
|
|
734
|
-
*
|
|
1313
|
+
* Create an input provider for generator ask yields
|
|
1314
|
+
* Supports the new ask/emit pattern from photon-core 1.2.0
|
|
735
1315
|
*/
|
|
736
1316
|
createInputProvider() {
|
|
737
|
-
return async (
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1317
|
+
return async (ask) => {
|
|
1318
|
+
switch (ask.ask) {
|
|
1319
|
+
case 'text':
|
|
1320
|
+
case 'password':
|
|
1321
|
+
return await elicitPrompt(ask.message, 'default' in ask ? ask.default : undefined);
|
|
1322
|
+
case 'confirm':
|
|
1323
|
+
return await elicitConfirm(ask.message);
|
|
1324
|
+
case 'select': {
|
|
1325
|
+
const options = (ask.options || []).map((o) => typeof o === 'string' ? o : o.label);
|
|
1326
|
+
const result = await elicitPrompt(`${ask.message}\nOptions: ${options.join(', ')}`);
|
|
1327
|
+
return result;
|
|
1328
|
+
}
|
|
1329
|
+
case 'number': {
|
|
1330
|
+
const result = await elicitPrompt(ask.message, ask.default?.toString());
|
|
1331
|
+
return result ? parseFloat(result) : (ask.default ?? 0);
|
|
1332
|
+
}
|
|
1333
|
+
case 'date': {
|
|
1334
|
+
const result = await elicitPrompt(ask.message, ask.default);
|
|
1335
|
+
return result || ask.default || new Date().toISOString();
|
|
1336
|
+
}
|
|
1337
|
+
case 'file':
|
|
1338
|
+
// File selection not supported in CLI readline
|
|
1339
|
+
this.logger.warn(`⚠️ File selection not supported in CLI: ${ask.message}`);
|
|
1340
|
+
return null;
|
|
1341
|
+
default: {
|
|
1342
|
+
const unknownAsk = ask;
|
|
1343
|
+
this.logger.warn(`⚠️ Unknown ask type: ${unknownAsk.ask || 'unknown'}`);
|
|
1344
|
+
return undefined;
|
|
1345
|
+
}
|
|
753
1346
|
}
|
|
754
|
-
return undefined;
|
|
755
1347
|
};
|
|
756
1348
|
}
|
|
757
1349
|
/**
|
|
758
|
-
* Create an output handler for generator yields
|
|
1350
|
+
* Create an output handler for generator emit yields
|
|
1351
|
+
* Supports the new ask/emit pattern from photon-core 1.2.0
|
|
1352
|
+
* Uses inline progress animation for CLI
|
|
759
1353
|
*/
|
|
760
1354
|
createOutputHandler() {
|
|
761
|
-
return (
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1355
|
+
return (emit) => {
|
|
1356
|
+
switch (emit.emit) {
|
|
1357
|
+
case 'progress':
|
|
1358
|
+
// Use inline progress bar with spinner animation
|
|
1359
|
+
this.progressRenderer.render(emit.value, emit.message);
|
|
1360
|
+
// Clear progress line when complete
|
|
1361
|
+
if (emit.value >= 1) {
|
|
1362
|
+
this.progressRenderer.done();
|
|
1363
|
+
}
|
|
1364
|
+
break;
|
|
1365
|
+
case 'status':
|
|
1366
|
+
// Status shows as ephemeral spinner with auto-animation
|
|
1367
|
+
// Updates the message if already spinning, or starts a new spinner
|
|
1368
|
+
if (this.progressRenderer.active) {
|
|
1369
|
+
this.progressRenderer.updateMessage(emit.message);
|
|
1370
|
+
}
|
|
1371
|
+
else {
|
|
1372
|
+
this.progressRenderer.startSpinner(emit.message);
|
|
1373
|
+
}
|
|
1374
|
+
break;
|
|
1375
|
+
case 'log': {
|
|
1376
|
+
// Logs clear progress first to avoid overlap
|
|
1377
|
+
this.progressRenderer.done();
|
|
1378
|
+
const level = (emit.level || 'info');
|
|
1379
|
+
const prefix = level === 'error' ? '❌' : level === 'warn' ? '⚠️' : 'ℹ';
|
|
1380
|
+
this.logger[level](`${prefix} ${emit.message}`);
|
|
1381
|
+
break;
|
|
772
1382
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1383
|
+
case 'toast':
|
|
1384
|
+
this.progressRenderer.done();
|
|
1385
|
+
const icon = emit.type === 'error'
|
|
1386
|
+
? '❌'
|
|
1387
|
+
: emit.type === 'warning'
|
|
1388
|
+
? '⚠️'
|
|
1389
|
+
: emit.type === 'success'
|
|
1390
|
+
? '✅'
|
|
1391
|
+
: 'ℹ';
|
|
1392
|
+
this.logger.info(`${icon} ${emit.message}`);
|
|
1393
|
+
break;
|
|
1394
|
+
case 'thinking':
|
|
1395
|
+
if (emit.active) {
|
|
1396
|
+
// Show thinking as indeterminate progress
|
|
1397
|
+
this.progressRenderer.render(0, 'Processing...');
|
|
1398
|
+
}
|
|
1399
|
+
else {
|
|
1400
|
+
this.progressRenderer.done();
|
|
1401
|
+
}
|
|
1402
|
+
break;
|
|
1403
|
+
case 'stream':
|
|
1404
|
+
// Stream clears progress first
|
|
1405
|
+
this.progressRenderer.done();
|
|
1406
|
+
if (typeof emit.data === 'string') {
|
|
1407
|
+
process.stdout.write(emit.data);
|
|
1408
|
+
}
|
|
1409
|
+
else {
|
|
1410
|
+
process.stdout.write(JSON.stringify(emit.data));
|
|
1411
|
+
}
|
|
1412
|
+
break;
|
|
1413
|
+
case 'artifact':
|
|
1414
|
+
this.progressRenderer.done();
|
|
1415
|
+
this.logger.info(`📦 ${emit.title || emit.type}: ${emit.mimeType}`);
|
|
1416
|
+
break;
|
|
777
1417
|
}
|
|
778
1418
|
};
|
|
779
1419
|
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Auto-wire ReactiveArray/Map/Set properties for zero-boilerplate reactivity
|
|
1422
|
+
*
|
|
1423
|
+
* When a photon developer imports { Array } from '@portel/photon-core',
|
|
1424
|
+
* it shadows the global Array. The runtime then auto-wires these properties:
|
|
1425
|
+
* - Sets _propertyName to the property key (e.g., 'items')
|
|
1426
|
+
* - Sets _emitter to instance.emit.bind(instance)
|
|
1427
|
+
*
|
|
1428
|
+
* This enables the pattern:
|
|
1429
|
+
* ```typescript
|
|
1430
|
+
* import { Array } from '@portel/photon-core';
|
|
1431
|
+
*
|
|
1432
|
+
* export default class TodoList {
|
|
1433
|
+
* items: Array<Task> = []; // Just use it normally
|
|
1434
|
+
*
|
|
1435
|
+
* add(text: string) {
|
|
1436
|
+
* this.items.push({ id: crypto.randomUUID(), text });
|
|
1437
|
+
* // Auto-emits 'items:added' - no manual wiring needed!
|
|
1438
|
+
* }
|
|
1439
|
+
* }
|
|
1440
|
+
* ```
|
|
1441
|
+
*/
|
|
1442
|
+
wireReactiveCollections(instance) {
|
|
1443
|
+
// Get the emit function if available
|
|
1444
|
+
const emit = typeof instance.emit === 'function'
|
|
1445
|
+
? instance.emit.bind(instance)
|
|
1446
|
+
: null;
|
|
1447
|
+
if (!emit) {
|
|
1448
|
+
return; // No emit function, skip wiring
|
|
1449
|
+
}
|
|
1450
|
+
// Iterate over instance properties
|
|
1451
|
+
for (const key of Object.keys(instance)) {
|
|
1452
|
+
const value = instance[key];
|
|
1453
|
+
if (!value || typeof value !== 'object')
|
|
1454
|
+
continue;
|
|
1455
|
+
// Check by constructor name since different module instances break instanceof
|
|
1456
|
+
// (photon uses npm-cached module, runtime uses linked module)
|
|
1457
|
+
const ctorName = value.constructor?.name;
|
|
1458
|
+
if (ctorName === 'ReactiveArray' || ctorName === 'ReactiveMap' || ctorName === 'ReactiveSet' || ctorName === 'Collection') {
|
|
1459
|
+
value._propertyName = key;
|
|
1460
|
+
value._emitter = emit;
|
|
1461
|
+
this.log(`Wired ${ctorName}: ${key}`);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Extract @mcp dependencies from source and inject them as instance properties
|
|
1467
|
+
*
|
|
1468
|
+
* This enables the pattern:
|
|
1469
|
+
* ```typescript
|
|
1470
|
+
* /**
|
|
1471
|
+
* * @mcp github anthropics/mcp-server-github
|
|
1472
|
+
* *\/
|
|
1473
|
+
* export default class MyPhoton extends PhotonMCP {
|
|
1474
|
+
* async doSomething() {
|
|
1475
|
+
* const issues = await this.github.list_issues({ repo: 'owner/repo' });
|
|
1476
|
+
* }
|
|
1477
|
+
* }
|
|
1478
|
+
* ```
|
|
1479
|
+
*/
|
|
1480
|
+
async injectMCPDependencies(instance, source, photonName) {
|
|
1481
|
+
const extractor = new SchemaExtractor();
|
|
1482
|
+
const mcpDeps = extractor.extractMCPDependencies(source);
|
|
1483
|
+
if (mcpDeps.length === 0) {
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
this.log(`🔌 Found ${mcpDeps.length} @mcp dependencies`);
|
|
1487
|
+
// Build MCP config from declarations
|
|
1488
|
+
const mcpServers = {};
|
|
1489
|
+
for (const dep of mcpDeps) {
|
|
1490
|
+
try {
|
|
1491
|
+
const config = resolveMCPSource(dep.name, dep.source, dep.sourceType);
|
|
1492
|
+
mcpServers[dep.name] = config;
|
|
1493
|
+
this.log(` - ${dep.name}: ${dep.source} (${dep.sourceType})`);
|
|
1494
|
+
}
|
|
1495
|
+
catch (error) {
|
|
1496
|
+
this.logger.warn(`⚠️ Failed to resolve MCP ${dep.name}: ${getErrorMessage(error)}`);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (Object.keys(mcpServers).length === 0) {
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
// Create factory for these MCPs
|
|
1503
|
+
const mcpConfig = { mcpServers };
|
|
1504
|
+
const factory = new SDKMCPClientFactory(mcpConfig, this.verbose);
|
|
1505
|
+
// Inject each MCP as an instance property with proxy
|
|
1506
|
+
for (const dep of mcpDeps) {
|
|
1507
|
+
if (mcpServers[dep.name]) {
|
|
1508
|
+
const client = factory.create(dep.name);
|
|
1509
|
+
const proxy = createMCPProxy(client);
|
|
1510
|
+
// Inject as instance property: this.github, this.fs, etc.
|
|
1511
|
+
Object.defineProperty(instance, dep.name, {
|
|
1512
|
+
value: proxy,
|
|
1513
|
+
writable: false,
|
|
1514
|
+
enumerable: true,
|
|
1515
|
+
configurable: false,
|
|
1516
|
+
});
|
|
1517
|
+
this.log(` ✅ Injected this.${dep.name}`);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
// Store factory reference for cleanup
|
|
1521
|
+
instance._mcpClientFactory = factory;
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Check CLI dependencies declared via @cli tags
|
|
1525
|
+
*
|
|
1526
|
+
* Validates that required command-line tools are available on the system.
|
|
1527
|
+
* Throws a helpful error with install URLs if any are missing.
|
|
1528
|
+
*
|
|
1529
|
+
* Format: @cli <name> - <install_url>
|
|
1530
|
+
*
|
|
1531
|
+
* Example:
|
|
1532
|
+
* ```typescript
|
|
1533
|
+
* /**
|
|
1534
|
+
* * @cli git - https://git-scm.com/downloads
|
|
1535
|
+
* * @cli ffmpeg - https://ffmpeg.org/download.html
|
|
1536
|
+
* *\/
|
|
1537
|
+
* ```
|
|
1538
|
+
*/
|
|
1539
|
+
async checkCLIDependencies(source, photonName) {
|
|
1540
|
+
const extractor = new SchemaExtractor();
|
|
1541
|
+
const cliDeps = extractor.extractCLIDependencies(source);
|
|
1542
|
+
if (cliDeps.length === 0) {
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
this.log(`🔧 Checking ${cliDeps.length} CLI dependencies`);
|
|
1546
|
+
const missing = [];
|
|
1547
|
+
for (const dep of cliDeps) {
|
|
1548
|
+
const exists = await this.checkCLIExists(dep.name);
|
|
1549
|
+
if (exists) {
|
|
1550
|
+
this.log(` ✅ ${dep.name}`);
|
|
1551
|
+
}
|
|
1552
|
+
else {
|
|
1553
|
+
this.log(` ❌ ${dep.name} not found`);
|
|
1554
|
+
missing.push(dep);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
if (missing.length > 0) {
|
|
1558
|
+
const lines = missing.map((dep) => {
|
|
1559
|
+
if (dep.installUrl) {
|
|
1560
|
+
return ` - ${dep.name}: Install from ${dep.installUrl}`;
|
|
1561
|
+
}
|
|
1562
|
+
return ` - ${dep.name}`;
|
|
1563
|
+
});
|
|
1564
|
+
const error = new Error(`${photonName} requires the following CLI tools to be installed:\n${lines.join('\n')}`);
|
|
1565
|
+
error.name = 'CLIDependencyError';
|
|
1566
|
+
throw error;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Check if a CLI command exists on the system
|
|
1571
|
+
*/
|
|
1572
|
+
async checkCLIExists(command) {
|
|
1573
|
+
return new Promise((resolve) => {
|
|
1574
|
+
const check = spawn(process.platform === 'win32' ? 'where' : 'which', [command], {
|
|
1575
|
+
stdio: 'ignore',
|
|
1576
|
+
});
|
|
1577
|
+
check.on('close', (code) => resolve(code === 0));
|
|
1578
|
+
check.on('error', () => resolve(false));
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Discover and extract assets from a Photon file
|
|
1583
|
+
* Uses shared discoverAssets from photon-core for core logic,
|
|
1584
|
+
* then applies photon-specific extensions (method UI links, URI generation).
|
|
1585
|
+
*/
|
|
1586
|
+
async discoverAssets(photonPath, source) {
|
|
1587
|
+
const basename = path.basename(photonPath, '.photon.ts');
|
|
1588
|
+
// Use shared discovery from photon-core
|
|
1589
|
+
const assets = await sharedDiscoverAssets(photonPath, source);
|
|
1590
|
+
if (!assets) {
|
|
1591
|
+
return undefined;
|
|
1592
|
+
}
|
|
1593
|
+
// Apply method-level @ui links AFTER auto-discovery
|
|
1594
|
+
this.applyMethodUILinks(source, assets);
|
|
1595
|
+
// Generate ui:// URIs for MCP Apps Extension support (SEP-1865)
|
|
1596
|
+
this.generateAssetURIs(basename, assets);
|
|
1597
|
+
return assets;
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Generate ui:// URIs for all UI assets (MCP Apps Extension support)
|
|
1601
|
+
* URI format: ui://<photon-name>/<asset-id>
|
|
1602
|
+
*/
|
|
1603
|
+
generateAssetURIs(photonName, assets) {
|
|
1604
|
+
for (const ui of assets.ui) {
|
|
1605
|
+
// Add uri field for MCP Apps compatibility
|
|
1606
|
+
ui.uri = `ui://${photonName}/${ui.id}`;
|
|
1607
|
+
this.log(` 🔗 URI: ${ui.uri}`);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Apply method-level @ui annotations to link UI assets to tools
|
|
1612
|
+
* Called after auto-discovery so all UI assets are available
|
|
1613
|
+
*/
|
|
1614
|
+
applyMethodUILinks(source, assets) {
|
|
1615
|
+
// Match method JSDoc with @ui annotation: /** ... @ui <id> ... */ async methodName
|
|
1616
|
+
const methodUiRegex = /\/\*\*[\s\S]*?@ui\s+(\w[\w-]*)[\s\S]*?\*\/\s*(?:async\s+)?\*?\s*(\w+)/g;
|
|
1617
|
+
let match;
|
|
1618
|
+
while ((match = methodUiRegex.exec(source)) !== null) {
|
|
1619
|
+
const [, uiId, methodName] = match;
|
|
1620
|
+
const asset = assets.ui.find((u) => u.id === uiId);
|
|
1621
|
+
if (asset && !asset.linkedTool) {
|
|
1622
|
+
asset.linkedTool = methodName;
|
|
1623
|
+
this.log(` 🔗 UI ${uiId} → ${methodName}`);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
// autoDiscoverAssets is now handled by sharedDiscoverAssets from photon-core
|
|
1628
|
+
/**
|
|
1629
|
+
* Check if a file exists
|
|
1630
|
+
*/
|
|
1631
|
+
async fileExists(filePath) {
|
|
1632
|
+
try {
|
|
1633
|
+
await fs.access(filePath);
|
|
1634
|
+
return true;
|
|
1635
|
+
}
|
|
1636
|
+
catch {
|
|
1637
|
+
return false; // file does not exist
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Get MIME type from file extension
|
|
1642
|
+
* Delegates to shared getMimeType from photon-core
|
|
1643
|
+
*/
|
|
1644
|
+
getMimeType(filename) {
|
|
1645
|
+
return sharedGetMimeType(filename);
|
|
1646
|
+
}
|
|
780
1647
|
}
|
|
781
1648
|
//# sourceMappingURL=loader.js.map
|