@portel/photon 1.4.0 → 1.5.1

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