@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.
Files changed (395) hide show
  1. package/README.md +326 -1177
  2. package/dist/auto-ui/beam.d.ts +14 -0
  3. package/dist/auto-ui/beam.d.ts.map +1 -0
  4. package/dist/auto-ui/beam.js +3057 -0
  5. package/dist/auto-ui/beam.js.map +1 -0
  6. package/dist/auto-ui/bridge/index.d.ts +37 -0
  7. package/dist/auto-ui/bridge/index.d.ts.map +1 -0
  8. package/dist/auto-ui/bridge/index.js +555 -0
  9. package/dist/auto-ui/bridge/index.js.map +1 -0
  10. package/dist/auto-ui/bridge/openai-shim.d.ts +20 -0
  11. package/dist/auto-ui/bridge/openai-shim.d.ts.map +1 -0
  12. package/dist/auto-ui/bridge/openai-shim.js +231 -0
  13. package/dist/auto-ui/bridge/openai-shim.js.map +1 -0
  14. package/dist/auto-ui/bridge/photon-app.d.ts +162 -0
  15. package/dist/auto-ui/bridge/photon-app.d.ts.map +1 -0
  16. package/dist/auto-ui/bridge/photon-app.js +460 -0
  17. package/dist/auto-ui/bridge/photon-app.js.map +1 -0
  18. package/dist/auto-ui/bridge/types.d.ts +128 -0
  19. package/dist/auto-ui/bridge/types.d.ts.map +1 -0
  20. package/dist/auto-ui/bridge/types.js +7 -0
  21. package/dist/auto-ui/bridge/types.js.map +1 -0
  22. package/dist/auto-ui/components/card.d.ts +13 -0
  23. package/dist/auto-ui/components/card.d.ts.map +1 -0
  24. package/dist/auto-ui/components/card.js +64 -0
  25. package/dist/auto-ui/components/card.js.map +1 -0
  26. package/dist/auto-ui/components/form.d.ts +15 -0
  27. package/dist/auto-ui/components/form.d.ts.map +1 -0
  28. package/dist/auto-ui/components/form.js +72 -0
  29. package/dist/auto-ui/components/form.js.map +1 -0
  30. package/dist/auto-ui/components/list.d.ts +13 -0
  31. package/dist/auto-ui/components/list.d.ts.map +1 -0
  32. package/dist/auto-ui/components/list.js +58 -0
  33. package/dist/auto-ui/components/list.js.map +1 -0
  34. package/dist/auto-ui/components/progress.d.ts +18 -0
  35. package/dist/auto-ui/components/progress.d.ts.map +1 -0
  36. package/dist/auto-ui/components/progress.js +125 -0
  37. package/dist/auto-ui/components/progress.js.map +1 -0
  38. package/dist/auto-ui/components/table.d.ts +13 -0
  39. package/dist/auto-ui/components/table.d.ts.map +1 -0
  40. package/dist/auto-ui/components/table.js +82 -0
  41. package/dist/auto-ui/components/table.js.map +1 -0
  42. package/dist/auto-ui/components/tree.d.ts +13 -0
  43. package/dist/auto-ui/components/tree.d.ts.map +1 -0
  44. package/dist/auto-ui/components/tree.js +61 -0
  45. package/dist/auto-ui/components/tree.js.map +1 -0
  46. package/dist/auto-ui/daemon-tools.d.ts +45 -0
  47. package/dist/auto-ui/daemon-tools.d.ts.map +1 -0
  48. package/dist/auto-ui/daemon-tools.js +580 -0
  49. package/dist/auto-ui/daemon-tools.js.map +1 -0
  50. package/dist/auto-ui/design-system/index.d.ts +21 -0
  51. package/dist/auto-ui/design-system/index.d.ts.map +1 -0
  52. package/dist/auto-ui/design-system/index.js +27 -0
  53. package/dist/auto-ui/design-system/index.js.map +1 -0
  54. package/dist/auto-ui/design-system/tokens.d.ts +9 -0
  55. package/dist/auto-ui/design-system/tokens.d.ts.map +1 -0
  56. package/dist/auto-ui/design-system/tokens.js +27 -0
  57. package/dist/auto-ui/design-system/tokens.js.map +1 -0
  58. package/dist/auto-ui/design-system/transaction-ui.d.ts +70 -0
  59. package/dist/auto-ui/design-system/transaction-ui.d.ts.map +1 -0
  60. package/dist/auto-ui/design-system/transaction-ui.js +982 -0
  61. package/dist/auto-ui/design-system/transaction-ui.js.map +1 -0
  62. package/dist/auto-ui/frontend/index.html +84 -0
  63. package/dist/auto-ui/index.d.ts +23 -0
  64. package/dist/auto-ui/index.d.ts.map +1 -0
  65. package/dist/auto-ui/index.js +28 -0
  66. package/dist/auto-ui/index.js.map +1 -0
  67. package/dist/auto-ui/openapi-generator.d.ts +71 -0
  68. package/dist/auto-ui/openapi-generator.d.ts.map +1 -0
  69. package/dist/auto-ui/openapi-generator.js +223 -0
  70. package/dist/auto-ui/openapi-generator.js.map +1 -0
  71. package/dist/auto-ui/photon-bridge.d.ts +159 -0
  72. package/dist/auto-ui/photon-bridge.d.ts.map +1 -0
  73. package/dist/auto-ui/photon-bridge.js +262 -0
  74. package/dist/auto-ui/photon-bridge.js.map +1 -0
  75. package/dist/auto-ui/photon-host.d.ts +113 -0
  76. package/dist/auto-ui/photon-host.d.ts.map +1 -0
  77. package/dist/auto-ui/photon-host.js +284 -0
  78. package/dist/auto-ui/photon-host.js.map +1 -0
  79. package/dist/auto-ui/platform-compat.d.ts +71 -0
  80. package/dist/auto-ui/platform-compat.d.ts.map +1 -0
  81. package/dist/auto-ui/platform-compat.js +628 -0
  82. package/dist/auto-ui/platform-compat.js.map +1 -0
  83. package/dist/auto-ui/playground-html.d.ts +15 -0
  84. package/dist/auto-ui/playground-html.d.ts.map +1 -0
  85. package/dist/auto-ui/playground-html.js +1113 -0
  86. package/dist/auto-ui/playground-html.js.map +1 -0
  87. package/dist/auto-ui/playground-server.d.ts +7 -0
  88. package/dist/auto-ui/playground-server.d.ts.map +1 -0
  89. package/dist/auto-ui/playground-server.js +840 -0
  90. package/dist/auto-ui/playground-server.js.map +1 -0
  91. package/dist/auto-ui/registry.d.ts +13 -0
  92. package/dist/auto-ui/registry.d.ts.map +1 -0
  93. package/dist/auto-ui/registry.js +62 -0
  94. package/dist/auto-ui/registry.js.map +1 -0
  95. package/dist/auto-ui/renderer.d.ts +14 -0
  96. package/dist/auto-ui/renderer.d.ts.map +1 -0
  97. package/dist/auto-ui/renderer.js +88 -0
  98. package/dist/auto-ui/renderer.js.map +1 -0
  99. package/dist/auto-ui/rendering/components.d.ts +29 -0
  100. package/dist/auto-ui/rendering/components.d.ts.map +1 -0
  101. package/dist/auto-ui/rendering/components.js +773 -0
  102. package/dist/auto-ui/rendering/components.js.map +1 -0
  103. package/dist/auto-ui/rendering/field-analyzer.d.ts +48 -0
  104. package/dist/auto-ui/rendering/field-analyzer.d.ts.map +1 -0
  105. package/dist/auto-ui/rendering/field-analyzer.js +270 -0
  106. package/dist/auto-ui/rendering/field-analyzer.js.map +1 -0
  107. package/dist/auto-ui/rendering/field-renderers.d.ts +64 -0
  108. package/dist/auto-ui/rendering/field-renderers.d.ts.map +1 -0
  109. package/dist/auto-ui/rendering/field-renderers.js +317 -0
  110. package/dist/auto-ui/rendering/field-renderers.js.map +1 -0
  111. package/dist/auto-ui/rendering/index.d.ts +28 -0
  112. package/dist/auto-ui/rendering/index.d.ts.map +1 -0
  113. package/dist/auto-ui/rendering/index.js +60 -0
  114. package/dist/auto-ui/rendering/index.js.map +1 -0
  115. package/dist/auto-ui/rendering/layout-selector.d.ts +48 -0
  116. package/dist/auto-ui/rendering/layout-selector.d.ts.map +1 -0
  117. package/dist/auto-ui/rendering/layout-selector.js +352 -0
  118. package/dist/auto-ui/rendering/layout-selector.js.map +1 -0
  119. package/dist/auto-ui/rendering/template-engine.d.ts +41 -0
  120. package/dist/auto-ui/rendering/template-engine.d.ts.map +1 -0
  121. package/dist/auto-ui/rendering/template-engine.js +238 -0
  122. package/dist/auto-ui/rendering/template-engine.js.map +1 -0
  123. package/dist/auto-ui/streamable-http-transport.d.ts +103 -0
  124. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -0
  125. package/dist/auto-ui/streamable-http-transport.js +1875 -0
  126. package/dist/auto-ui/streamable-http-transport.js.map +1 -0
  127. package/dist/auto-ui/types.d.ts +384 -0
  128. package/dist/auto-ui/types.d.ts.map +1 -0
  129. package/dist/auto-ui/types.js +92 -0
  130. package/dist/auto-ui/types.js.map +1 -0
  131. package/dist/beam.bundle.js +63137 -0
  132. package/dist/beam.bundle.js.map +7 -0
  133. package/dist/claude-code-plugin.d.ts.map +1 -1
  134. package/dist/claude-code-plugin.js +30 -30
  135. package/dist/claude-code-plugin.js.map +1 -1
  136. package/dist/cli/commands/info.d.ts +11 -0
  137. package/dist/cli/commands/info.d.ts.map +1 -0
  138. package/dist/cli/commands/info.js +313 -0
  139. package/dist/cli/commands/info.js.map +1 -0
  140. package/dist/cli/commands/marketplace.d.ts +11 -0
  141. package/dist/cli/commands/marketplace.d.ts.map +1 -0
  142. package/dist/cli/commands/marketplace.js +198 -0
  143. package/dist/cli/commands/marketplace.js.map +1 -0
  144. package/dist/cli/commands/package-app.d.ts +9 -0
  145. package/dist/cli/commands/package-app.d.ts.map +1 -0
  146. package/dist/cli/commands/package-app.js +191 -0
  147. package/dist/cli/commands/package-app.js.map +1 -0
  148. package/dist/cli/commands/package.d.ts +11 -0
  149. package/dist/cli/commands/package.d.ts.map +1 -0
  150. package/dist/cli/commands/package.js +573 -0
  151. package/dist/cli/commands/package.js.map +1 -0
  152. package/dist/cli-alias.d.ts.map +1 -1
  153. package/dist/cli-alias.js +30 -28
  154. package/dist/cli-alias.js.map +1 -1
  155. package/dist/cli-formatter.d.ts +8 -24
  156. package/dist/cli-formatter.d.ts.map +1 -1
  157. package/dist/cli-formatter.js +8 -325
  158. package/dist/cli-formatter.js.map +1 -1
  159. package/dist/cli.d.ts +15 -1
  160. package/dist/cli.d.ts.map +1 -1
  161. package/dist/cli.js +1166 -1131
  162. package/dist/cli.js.map +1 -1
  163. package/dist/daemon/client.d.ts +84 -3
  164. package/dist/daemon/client.d.ts.map +1 -1
  165. package/dist/daemon/client.js +561 -11
  166. package/dist/daemon/client.js.map +1 -1
  167. package/dist/daemon/manager.d.ts +51 -12
  168. package/dist/daemon/manager.d.ts.map +1 -1
  169. package/dist/daemon/manager.js +122 -61
  170. package/dist/daemon/manager.js.map +1 -1
  171. package/dist/daemon/protocol.d.ts +62 -6
  172. package/dist/daemon/protocol.d.ts.map +1 -1
  173. package/dist/daemon/protocol.js +76 -1
  174. package/dist/daemon/protocol.js.map +1 -1
  175. package/dist/daemon/server.d.ts +6 -6
  176. package/dist/daemon/server.js +743 -133
  177. package/dist/daemon/server.js.map +1 -1
  178. package/dist/daemon/session-manager.d.ts +8 -1
  179. package/dist/daemon/session-manager.d.ts.map +1 -1
  180. package/dist/daemon/session-manager.js +32 -9
  181. package/dist/daemon/session-manager.js.map +1 -1
  182. package/dist/deploy/cloudflare.d.ts +12 -0
  183. package/dist/deploy/cloudflare.d.ts.map +1 -0
  184. package/dist/deploy/cloudflare.js +216 -0
  185. package/dist/deploy/cloudflare.js.map +1 -0
  186. package/dist/index.d.ts +1 -0
  187. package/dist/index.d.ts.map +1 -1
  188. package/dist/index.js +3 -0
  189. package/dist/index.js.map +1 -1
  190. package/dist/loader.d.ts +191 -21
  191. package/dist/loader.d.ts.map +1 -1
  192. package/dist/loader.js +1186 -319
  193. package/dist/loader.js.map +1 -1
  194. package/dist/markdown-utils.d.ts +8 -0
  195. package/dist/markdown-utils.d.ts.map +1 -0
  196. package/dist/markdown-utils.js +63 -0
  197. package/dist/markdown-utils.js.map +1 -0
  198. package/dist/marketplace-manager.d.ts +10 -0
  199. package/dist/marketplace-manager.d.ts.map +1 -1
  200. package/dist/marketplace-manager.js +112 -28
  201. package/dist/marketplace-manager.js.map +1 -1
  202. package/dist/mcp-client.d.ts +9 -0
  203. package/dist/mcp-client.d.ts.map +1 -0
  204. package/dist/mcp-client.js +11 -0
  205. package/dist/mcp-client.js.map +1 -0
  206. package/dist/mcp-elicitation.d.ts +32 -0
  207. package/dist/mcp-elicitation.d.ts.map +1 -0
  208. package/dist/mcp-elicitation.js +26 -0
  209. package/dist/mcp-elicitation.js.map +1 -0
  210. package/dist/path-resolver.d.ts +9 -12
  211. package/dist/path-resolver.d.ts.map +1 -1
  212. package/dist/path-resolver.js +13 -43
  213. package/dist/path-resolver.js.map +1 -1
  214. package/dist/photon-cli-runner.d.ts.map +1 -1
  215. package/dist/photon-cli-runner.js +204 -77
  216. package/dist/photon-cli-runner.js.map +1 -1
  217. package/dist/photon-doc-extractor.d.ts +89 -0
  218. package/dist/photon-doc-extractor.d.ts.map +1 -1
  219. package/dist/photon-doc-extractor.js +560 -32
  220. package/dist/photon-doc-extractor.js.map +1 -1
  221. package/dist/photons/maker.photon.d.ts +182 -0
  222. package/dist/photons/maker.photon.d.ts.map +1 -0
  223. package/dist/photons/maker.photon.js +504 -0
  224. package/dist/photons/maker.photon.js.map +1 -0
  225. package/dist/photons/maker.photon.ts +626 -0
  226. package/dist/photons/marketplace.photon.d.ts +110 -0
  227. package/dist/photons/marketplace.photon.d.ts.map +1 -0
  228. package/dist/photons/marketplace.photon.js +260 -0
  229. package/dist/photons/marketplace.photon.js.map +1 -0
  230. package/dist/photons/marketplace.photon.ts +378 -0
  231. package/dist/photons/tunnel.photon.d.ts +80 -0
  232. package/dist/photons/tunnel.photon.d.ts.map +1 -0
  233. package/dist/photons/tunnel.photon.js +269 -0
  234. package/dist/photons/tunnel.photon.js.map +1 -0
  235. package/dist/photons/tunnel.photon.ts +345 -0
  236. package/dist/security-scanner.d.ts.map +1 -1
  237. package/dist/security-scanner.js +18 -15
  238. package/dist/security-scanner.js.map +1 -1
  239. package/dist/serv/auth/jwt.d.ts +89 -0
  240. package/dist/serv/auth/jwt.d.ts.map +1 -0
  241. package/dist/serv/auth/jwt.js +239 -0
  242. package/dist/serv/auth/jwt.js.map +1 -0
  243. package/dist/serv/auth/oauth.d.ts +117 -0
  244. package/dist/serv/auth/oauth.d.ts.map +1 -0
  245. package/dist/serv/auth/oauth.js +395 -0
  246. package/dist/serv/auth/oauth.js.map +1 -0
  247. package/dist/serv/auth/well-known.d.ts +60 -0
  248. package/dist/serv/auth/well-known.d.ts.map +1 -0
  249. package/dist/serv/auth/well-known.js +154 -0
  250. package/dist/serv/auth/well-known.js.map +1 -0
  251. package/dist/serv/db/d1-client.d.ts +65 -0
  252. package/dist/serv/db/d1-client.d.ts.map +1 -0
  253. package/dist/serv/db/d1-client.js +137 -0
  254. package/dist/serv/db/d1-client.js.map +1 -0
  255. package/dist/serv/db/d1-stores.d.ts +62 -0
  256. package/dist/serv/db/d1-stores.d.ts.map +1 -0
  257. package/dist/serv/db/d1-stores.js +307 -0
  258. package/dist/serv/db/d1-stores.js.map +1 -0
  259. package/dist/serv/index.d.ts +114 -0
  260. package/dist/serv/index.d.ts.map +1 -0
  261. package/dist/serv/index.js +172 -0
  262. package/dist/serv/index.js.map +1 -0
  263. package/dist/serv/local.d.ts +118 -0
  264. package/dist/serv/local.d.ts.map +1 -0
  265. package/dist/serv/local.js +392 -0
  266. package/dist/serv/local.js.map +1 -0
  267. package/dist/serv/middleware/auth.d.ts +66 -0
  268. package/dist/serv/middleware/auth.d.ts.map +1 -0
  269. package/dist/serv/middleware/auth.js +178 -0
  270. package/dist/serv/middleware/auth.js.map +1 -0
  271. package/dist/serv/middleware/tenant.d.ts +94 -0
  272. package/dist/serv/middleware/tenant.d.ts.map +1 -0
  273. package/dist/serv/middleware/tenant.js +152 -0
  274. package/dist/serv/middleware/tenant.js.map +1 -0
  275. package/dist/serv/runtime/executor.d.ts +76 -0
  276. package/dist/serv/runtime/executor.d.ts.map +1 -0
  277. package/dist/serv/runtime/executor.js +105 -0
  278. package/dist/serv/runtime/executor.js.map +1 -0
  279. package/dist/serv/runtime/index.d.ts +8 -0
  280. package/dist/serv/runtime/index.d.ts.map +1 -0
  281. package/dist/serv/runtime/index.js +10 -0
  282. package/dist/serv/runtime/index.js.map +1 -0
  283. package/dist/serv/runtime/oauth-context.d.ts +121 -0
  284. package/dist/serv/runtime/oauth-context.d.ts.map +1 -0
  285. package/dist/serv/runtime/oauth-context.js +153 -0
  286. package/dist/serv/runtime/oauth-context.js.map +1 -0
  287. package/dist/serv/session/kv-store.d.ts +54 -0
  288. package/dist/serv/session/kv-store.d.ts.map +1 -0
  289. package/dist/serv/session/kv-store.js +149 -0
  290. package/dist/serv/session/kv-store.js.map +1 -0
  291. package/dist/serv/session/store.d.ts +113 -0
  292. package/dist/serv/session/store.d.ts.map +1 -0
  293. package/dist/serv/session/store.js +284 -0
  294. package/dist/serv/session/store.js.map +1 -0
  295. package/dist/serv/types/index.d.ts +147 -0
  296. package/dist/serv/types/index.d.ts.map +1 -0
  297. package/dist/serv/types/index.js +8 -0
  298. package/dist/serv/types/index.js.map +1 -0
  299. package/dist/serv/vault/token-vault.d.ts +102 -0
  300. package/dist/serv/vault/token-vault.d.ts.map +1 -0
  301. package/dist/serv/vault/token-vault.js +177 -0
  302. package/dist/serv/vault/token-vault.js.map +1 -0
  303. package/dist/server.d.ts +184 -0
  304. package/dist/server.d.ts.map +1 -1
  305. package/dist/server.js +1995 -86
  306. package/dist/server.js.map +1 -1
  307. package/dist/shared/cli-sections.d.ts +6 -0
  308. package/dist/shared/cli-sections.d.ts.map +1 -0
  309. package/dist/shared/cli-sections.js +16 -0
  310. package/dist/shared/cli-sections.js.map +1 -0
  311. package/dist/shared/cli-utils.d.ts +81 -0
  312. package/dist/shared/cli-utils.d.ts.map +1 -0
  313. package/dist/shared/cli-utils.js +174 -0
  314. package/dist/shared/cli-utils.js.map +1 -0
  315. package/dist/shared/config-docs.d.ts +6 -0
  316. package/dist/shared/config-docs.d.ts.map +1 -0
  317. package/dist/shared/config-docs.js +6 -0
  318. package/dist/shared/config-docs.js.map +1 -0
  319. package/dist/shared/error-handler.d.ts +128 -0
  320. package/dist/shared/error-handler.d.ts.map +1 -0
  321. package/dist/shared/error-handler.js +342 -0
  322. package/dist/shared/error-handler.js.map +1 -0
  323. package/dist/shared/logger.d.ts +42 -0
  324. package/dist/shared/logger.d.ts.map +1 -0
  325. package/dist/shared/logger.js +123 -0
  326. package/dist/shared/logger.js.map +1 -0
  327. package/dist/shared/performance.d.ts +65 -0
  328. package/dist/shared/performance.d.ts.map +1 -0
  329. package/dist/shared/performance.js +136 -0
  330. package/dist/shared/performance.js.map +1 -0
  331. package/dist/shared/task-runner.d.ts +2 -0
  332. package/dist/shared/task-runner.d.ts.map +1 -0
  333. package/dist/shared/task-runner.js +16 -0
  334. package/dist/shared/task-runner.js.map +1 -0
  335. package/dist/shared/validation.d.ts +6 -0
  336. package/dist/shared/validation.d.ts.map +1 -0
  337. package/dist/shared/validation.js +6 -0
  338. package/dist/shared/validation.js.map +1 -0
  339. package/dist/shared-utils.d.ts +63 -0
  340. package/dist/shared-utils.d.ts.map +1 -0
  341. package/dist/shared-utils.js +123 -0
  342. package/dist/shared-utils.js.map +1 -0
  343. package/dist/template-manager.d.ts +23 -2
  344. package/dist/template-manager.d.ts.map +1 -1
  345. package/dist/template-manager.js +176 -87
  346. package/dist/template-manager.js.map +1 -1
  347. package/dist/test-client.d.ts.map +1 -1
  348. package/dist/test-client.js +10 -8
  349. package/dist/test-client.js.map +1 -1
  350. package/dist/test-runner.d.ts +52 -0
  351. package/dist/test-runner.d.ts.map +1 -0
  352. package/dist/test-runner.js +785 -0
  353. package/dist/test-runner.js.map +1 -0
  354. package/dist/testing.d.ts +103 -0
  355. package/dist/testing.d.ts.map +1 -0
  356. package/dist/testing.js +163 -0
  357. package/dist/testing.js.map +1 -0
  358. package/dist/version-checker.d.ts.map +1 -1
  359. package/dist/version-checker.js +2 -2
  360. package/dist/version-checker.js.map +1 -1
  361. package/dist/version.d.ts +10 -0
  362. package/dist/version.d.ts.map +1 -0
  363. package/dist/version.js +21 -0
  364. package/dist/version.js.map +1 -0
  365. package/dist/watcher.d.ts +6 -3
  366. package/dist/watcher.d.ts.map +1 -1
  367. package/dist/watcher.js +49 -10
  368. package/dist/watcher.js.map +1 -1
  369. package/package.json +57 -7
  370. package/templates/cloudflare/worker.ts.template +381 -0
  371. package/templates/cloudflare/wrangler.toml.template +9 -0
  372. package/dist/base.d.ts +0 -58
  373. package/dist/base.d.ts.map +0 -1
  374. package/dist/base.js +0 -92
  375. package/dist/base.js.map +0 -1
  376. package/dist/dependency-manager.d.ts +0 -49
  377. package/dist/dependency-manager.d.ts.map +0 -1
  378. package/dist/dependency-manager.js +0 -165
  379. package/dist/dependency-manager.js.map +0 -1
  380. package/dist/registry-manager.d.ts +0 -76
  381. package/dist/registry-manager.d.ts.map +0 -1
  382. package/dist/registry-manager.js +0 -220
  383. package/dist/registry-manager.js.map +0 -1
  384. package/dist/schema-extractor.d.ts +0 -110
  385. package/dist/schema-extractor.d.ts.map +0 -1
  386. package/dist/schema-extractor.js +0 -727
  387. package/dist/schema-extractor.js.map +0 -1
  388. package/dist/test-marketplace-sources.d.ts +0 -5
  389. package/dist/test-marketplace-sources.d.ts.map +0 -1
  390. package/dist/test-marketplace-sources.js +0 -53
  391. package/dist/test-marketplace-sources.js.map +0 -1
  392. package/dist/types.d.ts +0 -109
  393. package/dist/types.d.ts.map +0 -1
  394. package/dist/types.js +0 -12
  395. 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, isInputYield,
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, } from '@portel/photon-core';
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
- 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) {
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
- console.error(message);
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(mcpName) {
35
- 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);
36
160
  }
37
161
  /**
38
162
  * Path to metadata file describing installed dependencies
39
163
  */
40
- getDependencyMetadataPath(mcpName) {
41
- return path.join(this.getDependencyCacheDir(mcpName), 'metadata.json');
164
+ getDependencyMetadataPath(cacheKey) {
165
+ return path.join(this.getDependencyCacheDir(cacheKey), 'metadata.json');
42
166
  }
43
- async readDependencyMetadata(mcpName) {
167
+ async readDependencyMetadata(cacheKey) {
44
168
  try {
45
- const data = await fs.readFile(this.getDependencyMetadataPath(mcpName), 'utf-8');
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 null;
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.map(d => `${d.name}@${d.version}`).sort().join('|');
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(mcpName, hash, dependencies) {
63
- const metadataPath = this.getDependencyMetadataPath(mcpName);
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({ hash, dependencies }, null, 2), 'utf-8');
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(mcpName);
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 ? this.dependenciesEqual(metadata.dependencies, dependencies) : false;
71
- const needsClear = Boolean(metadata && (!hashMatches || !depsMatch));
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.log(`🔄 Dependencies changed for ${mcpName}, clearing cache`);
74
- await this.dependencyManager.clearCache(mcpName);
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(mcpName, dependencies);
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(mcpName, sourceHash, dependencies);
239
+ await this.writeDependencyMetadata(cacheKey, sourceHash, dependencies, photonPath, resolvedCoreVersion);
81
240
  return nodeModules;
82
241
  }
83
242
  shouldRetryInstall(error) {
84
- const message = error?.message?.toString() || '';
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].split(',').map(entry => entry.trim()).filter(Boolean);
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, mcpName, tsContent);
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.dependencyManager.clearCache(mcpName);
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
- // Extract constructor parameters from source
177
- const constructorParams = await this.extractConstructorParams(absolutePath);
178
- // Resolve values from environment variables
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
- // Enhance the error message with config information
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
- console.error(`⚠️ ${name} loaded with configuration warnings:`);
196
- console.error(configError);
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
- if (instance.onInitialize) {
402
+ const onInitialize = instance.onInitialize;
403
+ if (typeof onInitialize === 'function') {
200
404
  try {
201
- await instance.onInitialize();
405
+ await onInitialize.call(instance);
202
406
  }
203
407
  catch (error) {
204
- const initError = new Error(`Initialization failed for ${name}: ${error.message}\n` +
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
- initError.stack = error.stack;
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
- ].filter(Boolean).join(', ');
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
- return {
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
- console.error(`❌ Failed to load ${filePath}: ${error.message}`);
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 cacheDir = path.join(os.homedir(), '.cache', 'photon-mcp', 'dependencies', mcpName);
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(cacheDir, `${fileName}.${hash}.mjs`);
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, mcpName, tsContent) {
261
- const source = tsContent ?? await fs.readFile(tsFilePath, 'utf-8');
262
- const hash = crypto.createHash('sha256').update(source).digest('hex').slice(0, 16);
263
- // Store compiled files in the same directory as dependencies for module resolution
264
- const cacheDir = path.join(os.homedir(), '.cache', 'photon-mcp', 'dependencies', mcpName);
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
- * Looks for default export or any class with async methods
488
+ * Delegates to shared findPhotonClass from photon-core
295
489
  */
296
490
  findMCPClass(module) {
297
- // Try default export first
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 typeof fn === 'function' && /^\s*class\s+/.test(fn.toString());
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
- const prototype = ClassConstructor.prototype;
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
- if (typeof mcpClass.getMCPName === 'function') {
342
- return mcpClass.getMCPName();
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 mcpClass.name
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
- if (typeof mcpClass.getToolMethods === 'function') {
358
- return mcpClass.getToolMethods();
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 = mcpClass.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
- // Skip constructor, private methods (starting with _), and lifecycle hooks
365
- if (name !== 'constructor' &&
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
- if (jsonError.code === 'ENOENT') {
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
- console.error(`⚠️ Failed to extract schemas: ${error.message}. Using basic tools.`);
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
- console.error(`Failed to extract constructor params: ${error.message}`);
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 = this.toEnvVarName(mcpName, param.name);
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
- switch (type) {
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.message;
1165
+ const originalMessage = getErrorMessage(error);
540
1166
  // Build detailed env var documentation with examples
541
- const envVarDocs = constructorParams.map(param => {
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
- return await mcp.instance.executeTool(toolName, parameters);
707
- }
708
- else {
709
- // Plain class - call method directly
710
- const method = mcp.instance[toolName];
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
- // Execute the generator with input/output handling
718
- // Cast to expected type (generator methods should yield PhotonYield)
719
- return await executeGenerator(result, {
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
- return await result;
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
- console.error(`Tool execution failed: ${toolName} - ${error.message}`);
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
- * Uses the global prompt handler or falls back to native dialogs
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 (yielded) => {
738
- if (!isInputYield(yielded))
739
- return undefined;
740
- if ('prompt' in yielded) {
741
- // Use global prompt handler (set by CLI/MCP) or fallback to elicit
742
- return await elicitPrompt(yielded.prompt, yielded.default);
743
- }
744
- if ('confirm' in yielded) {
745
- return await elicitConfirm(yielded.confirm);
746
- }
747
- if ('select' in yielded) {
748
- // For select, show options and get selection
749
- // TODO: Implement select handling
750
- const options = yielded.options.map((o) => typeof o === 'string' ? o : o.label);
751
- const result = await elicitPrompt(`${yielded.select}\nOptions: ${options.join(', ')}`);
752
- return result;
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 (progress, stream, log)
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 (yielded) => {
762
- if ('progress' in yielded) {
763
- console.error(`[progress] ${yielded.progress}% ${yielded.status || ''}`);
764
- }
765
- else if ('stream' in yielded) {
766
- // Stream data to stdout
767
- if (typeof yielded.stream === 'string') {
768
- process.stdout.write(yielded.stream);
769
- }
770
- else {
771
- console.log(JSON.stringify(yielded.stream));
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
- else if ('log' in yielded) {
775
- const level = yielded.level || 'info';
776
- console.error(`[${level}] ${yielded.log}`);
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