@openhands/agent-canvas 1.19.0 → 1.20.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 (326) hide show
  1. package/README.md +1 -1
  2. package/README.windows.md +2 -2
  3. package/build/assets/{acp-providers-BQ4o4vqy.js → acp-providers-DOKcGeaV.js} +1 -1
  4. package/build/assets/active-backend-context-BETEdaJl.js +2 -0
  5. package/build/assets/{agent-context-settings-D9i6ezoJ.js → agent-context-settings-Dir0loAn.js} +1 -1
  6. package/build/assets/{agent-profiles-service.api-U-9YzDFU.js → agent-profiles-service.api-CiMVQAIT.js} +1 -1
  7. package/build/assets/{agent-profiles-settings-D-bUV2PX.js → agent-profiles-settings-CT5owzRT.js} +1 -1
  8. package/build/assets/{agent-server-conversation-service.api-BaVQ6NpE.js → agent-server-conversation-service.api-BtOlitCD.js} +3 -3
  9. package/build/assets/agent-settings-BnzyTgCb.js +2 -0
  10. package/build/assets/agent-settings-CeXj3xK-.js +1 -0
  11. package/build/assets/{alert-banner-BkVt_8Or.js → alert-banner-Ccx1Du6n.js} +1 -1
  12. package/build/assets/{api-key-entry-screen-fAIld-n7.js → api-key-entry-screen-DXIuzBjY.js} +1 -1
  13. package/build/assets/{app-settings-DITbrM2u.js → app-settings-DkIJVTKL.js} +1 -1
  14. package/build/assets/{automation-detail-DawHmaOl.js → automation-detail-CF2SweVs.js} +1 -1
  15. package/build/assets/{automation-disabled-reason-DtgC8Ppt.js → automation-disabled-reason-QFtMrdOr.js} +1 -1
  16. package/build/assets/{automation-export-Kv6e0bHx.js → automation-export-CkvehhPF.js} +1 -1
  17. package/build/assets/{automation-git-sync-Dd98G_Oz.js → automation-git-sync-di4tBAyZ.js} +1 -1
  18. package/build/assets/automation-setup-route-FERj7WEe.js +3 -0
  19. package/build/assets/{automation-templates-Xhr-FDAB.js → automation-templates-DlaCRvD-.js} +1 -1
  20. package/build/assets/{automations-list-BeP1LuzQ.js → automations-list-yK7ETmYP.js} +1 -1
  21. package/build/assets/{back-nav-button-DWNiN277.js → back-nav-button-oDvIxpQ5.js} +1 -1
  22. package/build/assets/{backend-form-modal-DeaUJIik.js → backend-form-modal-DzYf_arO.js} +1 -1
  23. package/build/assets/{backend-synced-settings-badge-Br85iWIY.js → backend-synced-settings-badge-DXDDHWX4.js} +1 -1
  24. package/build/assets/{badge-Cdnei7Ly.js → badge-CDHZEVeN.js} +1 -1
  25. package/build/assets/{base-modal-Ds7grR67.js → base-modal-CZq4kB_H.js} +1 -1
  26. package/build/assets/{brand-button-mQq2Ld1I.js → brand-button-CGUwj6zK.js} +1 -1
  27. package/build/assets/{browser-Bfum8fkE.js → browser-kY6jN61T.js} +1 -1
  28. package/build/assets/{browser-tab-CmYT4j_r.js → browser-tab-DFyZXsnB.js} +1 -1
  29. package/build/assets/{canvas-extension-page-CokKSVRG.js → canvas-extension-page-DsGq1DtT.js} +1 -1
  30. package/build/assets/{canvas-extensions-BoQEYG-2.js → canvas-extensions-Cb2Z0Hdc.js} +1 -1
  31. package/build/assets/{canvas-extensions-runtime-Du2M5iEX.js → canvas-extensions-runtime-DS6eT4nK.js} +1 -1
  32. package/build/assets/{chat-send-button-DM3JgMc2.js → chat-send-button-B6aVaD9V.js} +1 -1
  33. package/build/assets/{circle-plus-check-toggle-BbYBiUwv.js → circle-plus-check-toggle-ZOXpJ3I2.js} +1 -1
  34. package/build/assets/{cloud-funnel-analytics-D02kgq1s.js → cloud-funnel-analytics-BH_zxjiO.js} +1 -1
  35. package/build/assets/{cog-Dy1eWbSZ.js → cog-DVZT1f0s.js} +1 -1
  36. package/build/assets/{command-menu-mehU3Evb.js → command-menu-ImeHQ6c2.js} +1 -1
  37. package/build/assets/{commits-tab-DhQW8CXi.js → commits-tab-B5SUF66c.js} +1 -1
  38. package/build/assets/{condenser-settings-eRk6Fkx6.js → condenser-settings-BlGOXEXM.js} +1 -1
  39. package/build/assets/{confirmation-modal-BmVt_c0I.js → confirmation-modal-oN1EId79.js} +1 -1
  40. package/build/assets/{context-menu-list-item-lx2zk9G7.js → context-menu-list-item-DIm5xQg6.js} +1 -1
  41. package/build/assets/{conversation-BOj0ENI6.js → conversation-9a53tdbO.js} +3 -3
  42. package/build/assets/conversation-XRsI28ZM.js +1 -0
  43. package/build/assets/conversation-panel-XRsI28ZM.js +1 -0
  44. package/build/assets/{conversation-state-store-C6YncQNo.js → conversation-state-store-Br-eE-gg.js} +1 -1
  45. package/build/assets/{conversation-tab-empty-state-DIhwCC2J.js → conversation-tab-empty-state-BT1GrnKP.js} +1 -1
  46. package/build/assets/{copy-to-clipboard-button-DVNXLylP.js → copy-to-clipboard-button-CvTHqY13.js} +1 -1
  47. package/build/assets/{device-verify-LP-x7nra.js → device-verify-DX0xdNjo.js} +1 -1
  48. package/build/assets/{dropdown-classes-goDldkiu.js → dropdown-classes-DyyMaaXl.js} +1 -1
  49. package/build/assets/edit-automation-modal-bfD-5NIc.js +1 -0
  50. package/build/assets/{empty-state-Cd-TFe7i.js → empty-state-B5advKVW.js} +1 -1
  51. package/build/assets/{entry.client-CJdjIvMZ.js → entry.client-D_2gnSyB.js} +2 -2
  52. package/build/assets/{enum-filter-dropdown-pocrz9AL.js → enum-filter-dropdown-VAJp5mqh.js} +1 -1
  53. package/build/assets/{environment-switch-overlay-Dwe78wRl.js → environment-switch-overlay-C7x_yhEs.js} +1 -1
  54. package/build/assets/{error-state-SuWwztxV.js → error-state-BgYl7YeB.js} +1 -1
  55. package/build/assets/{extensions-hub-O0esK3Er.js → extensions-hub-DjhOed1I.js} +1 -1
  56. package/build/assets/{extensions-navigation-CgCVSXje.js → extensions-navigation-Di-J2KUR.js} +1 -1
  57. package/build/assets/{files-tab-B_6t8ljw.js → files-tab-BNDp3e_x.js} +1 -1
  58. package/build/assets/{form-control-classes-lQfLxzH4.js → form-control-classes-BVI_szaj.js} +1 -1
  59. package/build/assets/{get-git-path-Bp5rsUkl.js → get-git-path-DOe6PM_a.js} +1 -1
  60. package/build/assets/git-repo-dropdown-1fZGYtYm.js +1 -0
  61. package/build/assets/{git-repo-dropdown-DPQVumZg.js → git-repo-dropdown-DNlinQ7r.js} +1 -1
  62. package/build/assets/{highlighted-source-view-Bp0sgK-f.js → highlighted-source-view-DPerL6tF.js} +1 -1
  63. package/build/assets/home-4lyLzYjG.js +1 -0
  64. package/build/assets/{home-BghzJd-D.js → home-BGaB0EeK.js} +1 -1
  65. package/build/assets/{home-automation-run-tooltip-BdpCckKO.js → home-automation-run-tooltip-v5n7zXR0.js} +1 -1
  66. package/build/assets/{home-store-DNQpfIEO.js → home-store-roz1x6D5.js} +1 -1
  67. package/build/assets/{hooks-service-DkxnQa4N.js → hooks-service-tzRqtAlm.js} +1 -1
  68. package/build/assets/index-home-6Y_Y7kOo.js +1 -0
  69. package/build/assets/{install-server-modal-DvxxYKia.js → install-server-modal-DkMCfj0J.js} +1 -1
  70. package/build/assets/{key-CwTE6XJj.js → key-DpfBWQbk.js} +1 -1
  71. package/build/assets/{launch-jsRGPRBx.js → launch-TrnYlJe6.js} +1 -1
  72. package/build/assets/llm-not-configured-banner-BdjItKQR.js +27 -0
  73. package/build/assets/{llm-settings-dkQ5Oq7W.js → llm-settings-CUgt39uu.js} +1 -1
  74. package/build/assets/llm-settings-DEfw-fUJ.js +1 -0
  75. package/build/assets/{llm-subscription-service-BiQaZbvv.js → llm-subscription-service-LGYVzyAc.js} +1 -1
  76. package/build/assets/{loading-spinner-DK1sAwVg.js → loading-spinner-BJc2J0Dw.js} +1 -1
  77. package/build/assets/{manage-backends-modal-DqCJohsX.js → manage-backends-modal-CIjk0B2H.js} +1 -1
  78. package/build/assets/manifest-23d79467.js +1 -0
  79. package/build/assets/{manifest-subpage-layout-sxuPqfUr.js → manifest-subpage-layout-DGYRoP_8.js} +1 -1
  80. package/build/assets/{markdown-renderer-BH5pqDuJ.js → markdown-renderer-DRpwRlNm.js} +1 -1
  81. package/build/assets/{mcp-BtaJDXnP.js → mcp-CoUOIGBF.js} +1 -1
  82. package/build/assets/{mcp-installed-servers-Br4ZLsjr.js → mcp-installed-servers-BW51n6PC.js} +1 -1
  83. package/build/assets/{mcp-page-DwbnQpVs.js → mcp-page-hZ4aJjj_.js} +1 -1
  84. package/build/assets/{messages-DmFJlW6c.js → messages-WUb9Yh-p.js} +1 -1
  85. package/build/assets/{modal-body-DBweM2EN.js → modal-body-B_95ci1c.js} +1 -1
  86. package/build/assets/{modal-classes-jvG85nkU.js → modal-classes-BPcBCWVj.js} +1 -1
  87. package/build/assets/{modal-close-button-BrM5fNnn.js → modal-close-button-Cq3dma5u.js} +1 -1
  88. package/build/assets/{onboarding-CQ8T6T0U.js → onboarding-Cc4Av7bi.js} +1 -1
  89. package/build/assets/{onboarding-modal-CnliiaSN.js → onboarding-modal-DCFD4Dsa.js} +1 -1
  90. package/build/assets/{organization-service.api-DmFUEmu5.js → organization-service.api-zhHpQU0t.js} +1 -1
  91. package/build/assets/{planner-tab-CXph_C9F.js → planner-tab-yISBVUKu.js} +1 -1
  92. package/build/assets/{plugins-management-service-CcEpfNKz.js → plugins-management-service-2oP6_tWr.js} +1 -1
  93. package/build/assets/{profiles-service.api-DdF_4Zhe.js → profiles-service.api-B5thiTI3.js} +1 -1
  94. package/build/assets/{providers-BWRnl31s.js → providers-Co9PsD0s.js} +1 -1
  95. package/build/assets/{proxy-DbEf3Nqr.js → proxy-BCCQUna8.js} +1 -1
  96. package/build/assets/{recommended-automations-launcher-r0oI91HN.js → recommended-automations-launcher-D1Do753p.js} +1 -1
  97. package/build/assets/{root-j2fFRlWS.js → root-D1y82Lhg.js} +2 -2
  98. package/build/assets/{root-layout-CfGgm2lo.js → root-layout-cQTYEG2w.js} +2 -2
  99. package/build/assets/{schema-field-c3Ncgm4x.js → schema-field-DSi0d7a8.js} +1 -1
  100. package/build/assets/{sdk-section-page-DFEiuiFQ.js → sdk-section-page-DCoI2dtd.js} +1 -1
  101. package/build/assets/{secret-form-WXXNFqTM.js → secret-form-BDvzojzN.js} +1 -1
  102. package/build/assets/{secrets-service-DU6wh3wX.js → secrets-service-C3CNNEpY.js} +1 -1
  103. package/build/assets/{secrets-settings-BEqbPvjy.js → secrets-settings-BKqfYQt5.js} +1 -1
  104. package/build/assets/{settings-DjaSQwFh.js → settings-C8ZLONyx.js} +1 -1
  105. package/build/assets/{settings-dropdown-input-BRR2OZwi.js → settings-dropdown-input-Df_HXRk4.js} +1 -1
  106. package/build/assets/{settings-index-DHv4XFcM.js → settings-index-BWvlGHg3.js} +1 -1
  107. package/build/assets/{settings-input-Cm4VxUon.js → settings-input-B3i5U9GL.js} +1 -1
  108. package/build/assets/{settings-list-classes-CaUfKZ33.js → settings-list-classes-DbqcSiVW.js} +1 -1
  109. package/build/assets/{settings-modal-djXuAnBO.js → settings-modal-DHKJSBPL.js} +1 -1
  110. package/build/assets/{settings-nav-DFb3hBMr.js → settings-nav-BThjwLWD.js} +1 -1
  111. package/build/assets/{settings-service.api-Q9lKW1TX.js → settings-service.api-CYBTLGOd.js} +1 -1
  112. package/build/assets/{settings-switch-seaSJc7t.js → settings-switch-CI93Iozg.js} +1 -1
  113. package/build/assets/{shared-conversation-x3rxVCon.js → shared-conversation-C7OILS-U.js} +1 -1
  114. package/build/assets/{sidebar-layout-B6mcvvdV.js → sidebar-layout-Csb6Geq0.js} +1 -1
  115. package/build/assets/{sidebar-mobile-menu-toggle-CEqS8KnA.js → sidebar-mobile-menu-toggle-B2xwXm4s.js} +1 -1
  116. package/build/assets/{sidebar-nav-link-BTHX11LG.js → sidebar-nav-link-Xv7QEMVS.js} +1 -1
  117. package/build/assets/{sidebar-onboarding-checklist-storage-BP1kzn6p.js → sidebar-onboarding-checklist-storage-_zxMqHS0.js} +1 -1
  118. package/build/assets/{skill-card-pill-row-TD39nfP3.js → skill-card-pill-row-CxOFySng.js} +1 -1
  119. package/build/assets/{skill-enablement-DXeaImBz.js → skill-enablement-BBKya60C.js} +1 -1
  120. package/build/assets/{skill-filter-CdBbbwjm.js → skill-filter-BvtbD--b.js} +1 -1
  121. package/build/assets/{skill-icon-badge-vUEp7gsC.js → skill-icon-badge-CVxE42yg.js} +1 -1
  122. package/build/assets/{skill-scope-BTRBQt0-.js → skill-scope-C0NxJnpT.js} +1 -1
  123. package/build/assets/{skills-plugins-k1tUNAw4.js → skills-plugins-BGjX4jve.js} +1 -1
  124. package/build/assets/{skills-settings-CENviPT7.js → skills-settings-d3ZwIjSP.js} +1 -1
  125. package/build/assets/{styled-tooltip-DVAlWYnL.js → styled-tooltip-CknPQH4Y.js} +1 -1
  126. package/build/assets/{task-list-tab-BLKF_x9f.js → task-list-tab-CQisRcPh.js} +1 -1
  127. package/build/assets/{terminal-Ckbnqb23.js → terminal-Bs50gJPC.js} +1 -1
  128. package/build/assets/{toggle-switch-DeiG9Mf6.js → toggle-switch-D3TBxlsc.js} +1 -1
  129. package/build/assets/{trash-CXN2R54L.js → trash-D1QMHInX.js} +1 -1
  130. package/build/assets/{typography-NarNyelM.js → typography-BdAApXGl.js} +1 -1
  131. package/build/assets/{usage-tab-Drd3O006.js → usage-tab-ClVHAFJy.js} +1 -1
  132. package/build/assets/{use-acp-credential-form-CJ5wPsQm.js → use-acp-credential-form-GfqcTJl5.js} +1 -1
  133. package/build/assets/{use-activate-agent-profile-iQZ0U3qV.js → use-activate-agent-profile-DO6SNPkK.js} +1 -1
  134. package/build/assets/{use-agent-profiles-LCvb5OXT.js → use-agent-profiles-YsBlt3tT.js} +1 -1
  135. package/build/assets/{use-automation-health-DdgSa8sA.js → use-automation-health-CbQ9n-k7.js} +1 -1
  136. package/build/assets/{use-automation-permissions-D3kEeieu.js → use-automation-permissions-gFy-2VD0.js} +1 -1
  137. package/build/assets/{use-automations-CEubcYTD.js → use-automations-7_XBXHQK.js} +1 -1
  138. package/build/assets/{use-can-manage-org-profiles-CB8ybxFK.js → use-can-manage-org-profiles-Dt4YkPO7.js} +1 -1
  139. package/build/assets/{use-canvas-extensions-BMacwi2v.js → use-canvas-extensions-BhYQrtMK.js} +1 -1
  140. package/build/assets/{use-cloud-current-user-id-BEKRd6XZ.js → use-cloud-current-user-id-Dqw6qQUg.js} +1 -1
  141. package/build/assets/{use-config-3WYcJsgC.js → use-config-BJ_FJCjV.js} +1 -1
  142. package/build/assets/use-create-conversation-D15Px574.js +1 -0
  143. package/build/assets/use-create-secret-D_qPU88t.js +1 -0
  144. package/build/assets/{use-free-models-Ci3FkKEm.js → use-free-models-FAk2nngq.js} +1 -1
  145. package/build/assets/{use-get-secrets-fqS0kxt6.js → use-get-secrets-Pbo9jh2A.js} +1 -1
  146. package/build/assets/{use-llm-configured-B71ZEVg2.js → use-llm-configured-DF9BYmIe.js} +1 -1
  147. package/build/assets/{use-llm-profiles-xtCBMbef.js → use-llm-profiles-U4u1gzuk.js} +1 -1
  148. package/build/assets/use-manifest-capabilities-D81_YdAF.js +1 -0
  149. package/build/assets/{use-onboarding-completion-CEgjL02y.js → use-onboarding-completion-C4W-m2Jk.js} +1 -1
  150. package/build/assets/{use-pinned-home-route-pK4IlJ-p.js → use-pinned-home-route-DByWTojw.js} +1 -1
  151. package/build/assets/{use-plugins-marketplace-B8-ZEB34.js → use-plugins-marketplace-CTRoukH0.js} +1 -1
  152. package/build/assets/{use-save-agent-profile-D7hJuW_u.js → use-save-agent-profile-xpHBxMZM.js} +1 -1
  153. package/build/assets/{use-save-settings-CikTu5xV.js → use-save-settings-CmX41wlf.js} +1 -1
  154. package/build/assets/{use-settings-BWIH5EuN.js → use-settings-D69cw06D.js} +1 -1
  155. package/build/assets/{use-settings-nav-items-DSKwLDnd.js → use-settings-nav-items-CVz0SnDr.js} +1 -1
  156. package/build/assets/use-tracking-BAFpgpgq.js +3 -0
  157. package/build/assets/{use-user-conversation-C_l65DqW.js → use-user-conversation-BBDXUbOc.js} +1 -1
  158. package/build/assets/utils-CbjwNcR-.js +1 -0
  159. package/build/assets/{vendor~browser-hbU4u2Ag.js → vendor~browser-BZ-zIfb6.js} +1 -1
  160. package/build/assets/vendor~entry.client~root~root-layout~index-home~home~conversation-panel~conversation~launch~b0x1wrp2-DvZtOcoT.js +2 -0
  161. package/build/assets/{vendor~root-layout~index-home~home~conversation-panel~conversation~mcp~automations-list~aut~ibfbmr9f-CxhwKuAd.js → vendor~root-layout~index-home~home~conversation-panel~conversation~mcp~automations-list~aut~ibfbmr9f-BIjRfQ4u.js} +1 -1
  162. package/build/assets/{vendor~root~root-layout~index-home~home~conversation-panel~conversation~launch~extensions-h~inibp4qe-BFRW8wt-.js → vendor~root~root-layout~index-home~home~conversation-panel~conversation~launch~extensions-h~inibp4qe-BBfdJIV9.js} +1 -1
  163. package/build/assets/{vendor~root~root-layout~index-home~home~conversation-panel~conversation~launch~skills-setti~eo2nf80n-3eJ4ExtV.js → vendor~root~root-layout~index-home~home~conversation-panel~conversation~launch~skills-setti~eo2nf80n-fgzdgvrL.js} +3579 -165
  164. package/build/assets/{vendor~root~root-layout~index-home~home~conversation-panel~conversation~launch~skills-setti~jz4f6vo2-C7HiY4Ae.js → vendor~root~root-layout~index-home~home~conversation-panel~conversation~launch~skills-setti~jz4f6vo2-BqdU447s.js} +794 -227
  165. package/build/assets/{verification-settings-PKtoIUUY.js → verification-settings-BXspn27q.js} +1 -1
  166. package/build/index.html +4 -4
  167. package/build/locales/ar/openhands.json +8 -1
  168. package/build/locales/ca/openhands.json +8 -1
  169. package/build/locales/de/openhands.json +8 -1
  170. package/build/locales/en/openhands.json +8 -1
  171. package/build/locales/es/openhands.json +8 -1
  172. package/build/locales/fr/openhands.json +8 -1
  173. package/build/locales/it/openhands.json +8 -1
  174. package/build/locales/ja/openhands.json +8 -1
  175. package/build/locales/ko-KR/openhands.json +8 -1
  176. package/build/locales/no/openhands.json +8 -1
  177. package/build/locales/pt/openhands.json +8 -1
  178. package/build/locales/tr/openhands.json +8 -1
  179. package/build/locales/uk/openhands.json +8 -1
  180. package/build/locales/zh-CN/openhands.json +8 -1
  181. package/build/locales/zh-TW/openhands.json +8 -1
  182. package/config/defaults.json +3 -3
  183. package/dist/api/agent-profiles-service/profile-field-support.d.ts +2 -0
  184. package/dist/components/features/automations/agent-profile-selector.d.ts +7 -0
  185. package/dist/config/defaults.cjs +1 -1
  186. package/dist/config/defaults.cjs.map +1 -1
  187. package/dist/config/defaults.js +3 -3
  188. package/dist/config/defaults.js.map +1 -1
  189. package/dist/constants/profile-scope.d.ts +2 -2
  190. package/dist/hooks/mutation/use-create-conversation.cjs +1 -1
  191. package/dist/hooks/mutation/use-create-conversation.cjs.map +1 -1
  192. package/dist/hooks/mutation/use-create-conversation.js +30 -19
  193. package/dist/hooks/mutation/use-create-conversation.js.map +1 -1
  194. package/dist/hooks/mutation/use-switch-acp-model.cjs +1 -1
  195. package/dist/hooks/mutation/use-switch-acp-model.cjs.map +1 -1
  196. package/dist/hooks/mutation/use-switch-acp-model.js +11 -12
  197. package/dist/hooks/mutation/use-switch-acp-model.js.map +1 -1
  198. package/dist/hooks/query/query-keys.cjs +1 -1
  199. package/dist/hooks/query/query-keys.cjs.map +1 -1
  200. package/dist/hooks/query/query-keys.d.ts +1 -0
  201. package/dist/hooks/query/query-keys.js +10 -1
  202. package/dist/hooks/query/query-keys.js.map +1 -1
  203. package/dist/hooks/query/use-active-acp-profile-detail.cjs +1 -1
  204. package/dist/hooks/query/use-active-acp-profile-detail.cjs.map +1 -1
  205. package/dist/hooks/query/use-active-acp-profile-detail.d.ts +0 -9
  206. package/dist/hooks/query/use-active-acp-profile-detail.js +4 -13
  207. package/dist/hooks/query/use-active-acp-profile-detail.js.map +1 -1
  208. package/dist/i18n/declaration.cjs +1 -1
  209. package/dist/i18n/declaration.cjs.map +1 -1
  210. package/dist/i18n/declaration.d.ts +8 -1
  211. package/dist/i18n/declaration.js +1 -1
  212. package/dist/i18n/declaration.js.map +1 -1
  213. package/dist/i18n/translation.cjs +2 -2
  214. package/dist/i18n/translation.cjs.map +1 -1
  215. package/dist/i18n/translation.js +119 -0
  216. package/dist/i18n/translation.js.map +1 -1
  217. package/dist/locales/ar/openhands.json +8 -1
  218. package/dist/locales/ca/openhands.json +8 -1
  219. package/dist/locales/de/openhands.json +8 -1
  220. package/dist/locales/en/openhands.json +8 -1
  221. package/dist/locales/es/openhands.json +8 -1
  222. package/dist/locales/fr/openhands.json +8 -1
  223. package/dist/locales/it/openhands.json +8 -1
  224. package/dist/locales/ja/openhands.json +8 -1
  225. package/dist/locales/ko-KR/openhands.json +8 -1
  226. package/dist/locales/no/openhands.json +8 -1
  227. package/dist/locales/pt/openhands.json +8 -1
  228. package/dist/locales/tr/openhands.json +8 -1
  229. package/dist/locales/uk/openhands.json +8 -1
  230. package/dist/locales/zh-CN/openhands.json +8 -1
  231. package/dist/locales/zh-TW/openhands.json +8 -1
  232. package/dist/manifests/automation-setup.cjs.map +1 -1
  233. package/dist/manifests/automation-setup.js.map +1 -1
  234. package/dist/manifests/manifest-local-validation.cjs.map +1 -1
  235. package/dist/manifests/manifest-local-validation.js.map +1 -1
  236. package/dist/node_modules/@openhands/extensions/automations/bundle-index.cjs +3670 -247
  237. package/dist/node_modules/@openhands/extensions/automations/bundle-index.cjs.map +1 -1
  238. package/dist/node_modules/@openhands/extensions/automations/bundle-index.js +26 -3
  239. package/dist/node_modules/@openhands/extensions/automations/bundle-index.js.map +1 -1
  240. package/dist/node_modules/@openhands/extensions/automations/catalog/github-agents-md-maintainer/manifest.cjs +1 -1
  241. package/dist/node_modules/@openhands/extensions/automations/catalog/github-agents-md-maintainer/manifest.cjs.map +1 -1
  242. package/dist/node_modules/@openhands/extensions/automations/catalog/github-agents-md-maintainer/manifest.js +4 -1
  243. package/dist/node_modules/@openhands/extensions/automations/catalog/github-agents-md-maintainer/manifest.js.map +1 -1
  244. package/dist/node_modules/@openhands/extensions/automations/catalog/github-delivery-watchdog/manifest.cjs +2 -0
  245. package/dist/node_modules/@openhands/extensions/automations/catalog/github-delivery-watchdog/manifest.cjs.map +1 -0
  246. package/dist/node_modules/@openhands/extensions/automations/catalog/github-delivery-watchdog/manifest.js +79 -0
  247. package/dist/node_modules/@openhands/extensions/automations/catalog/github-delivery-watchdog/manifest.js.map +1 -0
  248. package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-to-pr/manifest.cjs +1 -11
  249. package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-to-pr/manifest.cjs.map +1 -1
  250. package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-to-pr/manifest.js +25 -11
  251. package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-to-pr/manifest.js.map +1 -1
  252. package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-triage/manifest.cjs +2 -0
  253. package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-triage/manifest.cjs.map +1 -0
  254. package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-triage/manifest.js +72 -0
  255. package/dist/node_modules/@openhands/extensions/automations/catalog/github-issue-triage/manifest.js.map +1 -0
  256. package/dist/node_modules/@openhands/extensions/automations/catalog/github-pr-reviewer/manifest.cjs +1 -10
  257. package/dist/node_modules/@openhands/extensions/automations/catalog/github-pr-reviewer/manifest.cjs.map +1 -1
  258. package/dist/node_modules/@openhands/extensions/automations/catalog/github-pr-reviewer/manifest.js +24 -11
  259. package/dist/node_modules/@openhands/extensions/automations/catalog/github-pr-reviewer/manifest.js.map +1 -1
  260. package/dist/node_modules/@openhands/extensions/automations/catalog/gitlab-issue-to-mr/manifest.cjs +12 -0
  261. package/dist/node_modules/@openhands/extensions/automations/catalog/gitlab-issue-to-mr/manifest.cjs.map +1 -0
  262. package/dist/node_modules/@openhands/extensions/automations/catalog/gitlab-issue-to-mr/manifest.js +115 -0
  263. package/dist/node_modules/@openhands/extensions/automations/catalog/gitlab-issue-to-mr/manifest.js.map +1 -0
  264. package/dist/node_modules/@openhands/extensions/automations/catalog-index.cjs +1 -1
  265. package/dist/node_modules/@openhands/extensions/automations/catalog-index.cjs.map +1 -1
  266. package/dist/node_modules/@openhands/extensions/automations/catalog-index.js +20 -14
  267. package/dist/node_modules/@openhands/extensions/automations/catalog-index.js.map +1 -1
  268. package/dist/node_modules/@openhands/extensions/integrations/catalog/reportportal.cjs +2 -0
  269. package/dist/node_modules/@openhands/extensions/integrations/catalog/reportportal.cjs.map +1 -0
  270. package/dist/node_modules/@openhands/extensions/integrations/catalog/reportportal.js +73 -0
  271. package/dist/node_modules/@openhands/extensions/integrations/catalog/reportportal.js.map +1 -0
  272. package/dist/node_modules/@openhands/extensions/integrations/catalog-index.cjs +1 -1
  273. package/dist/node_modules/@openhands/extensions/integrations/catalog-index.cjs.map +1 -1
  274. package/dist/node_modules/@openhands/extensions/integrations/catalog-index.js +9 -7
  275. package/dist/node_modules/@openhands/extensions/integrations/catalog-index.js.map +1 -1
  276. package/dist/node_modules/@openhands/extensions/skills/index.cjs +794 -227
  277. package/dist/node_modules/@openhands/extensions/skills/index.cjs.map +1 -1
  278. package/dist/node_modules/@openhands/extensions/skills/index.js +46 -6
  279. package/dist/node_modules/@openhands/extensions/skills/index.js.map +1 -1
  280. package/dist/node_modules/@openhands/typescript-client/dist/client/openhands-client.cjs +1 -1
  281. package/dist/node_modules/@openhands/typescript-client/dist/client/openhands-client.cjs.map +1 -1
  282. package/dist/node_modules/@openhands/typescript-client/dist/client/openhands-client.js +9 -10
  283. package/dist/node_modules/@openhands/typescript-client/dist/client/openhands-client.js.map +1 -1
  284. package/dist/node_modules/@openhands/typescript-client/dist/client/settings-client.cjs +1 -1
  285. package/dist/node_modules/@openhands/typescript-client/dist/client/settings-client.cjs.map +1 -1
  286. package/dist/node_modules/@openhands/typescript-client/dist/client/settings-client.js +2 -2
  287. package/dist/node_modules/@openhands/typescript-client/dist/client/settings-client.js.map +1 -1
  288. package/dist/node_modules/@openhands/typescript-client/dist/clients.cjs +1 -1
  289. package/dist/node_modules/@openhands/typescript-client/dist/clients.js +0 -1
  290. package/dist/node_modules/@openhands/typescript-client/dist/conversation/conversation-manager.cjs +1 -1
  291. package/dist/node_modules/@openhands/typescript-client/dist/conversation/conversation-manager.js +0 -1
  292. package/dist/package.cjs +1 -1
  293. package/dist/package.cjs.map +1 -1
  294. package/dist/package.js +3 -3
  295. package/dist/package.js.map +1 -1
  296. package/dist/routes/agent-settings.d.ts +6 -0
  297. package/dist/types/automation.cjs.map +1 -1
  298. package/dist/types/automation.d.ts +3 -1
  299. package/dist/types/automation.js.map +1 -1
  300. package/package.json +3 -3
  301. package/scripts/check-sdk-version-sync.mjs +6 -6
  302. package/scripts/dev-safe.mjs +14 -1
  303. package/scripts/docker-build.mjs +3 -0
  304. package/build/assets/active-backend-context-BsLfwrXB.js +0 -2
  305. package/build/assets/agent-settings-BZJHtaIK.js +0 -2
  306. package/build/assets/agent-settings-D4snDd6u.js +0 -1
  307. package/build/assets/automation-setup-route-Cdc109ZD.js +0 -3
  308. package/build/assets/conversation-SouWlGvs.js +0 -1
  309. package/build/assets/conversation-panel-SouWlGvs.js +0 -1
  310. package/build/assets/edit-automation-modal-BJJR9wSW.js +0 -1
  311. package/build/assets/git-repo-dropdown-CbD0_LPN.js +0 -1
  312. package/build/assets/home-CXZ8zawS.js +0 -1
  313. package/build/assets/index-home-Ca6Q5CtT.js +0 -1
  314. package/build/assets/llm-not-configured-banner-DMdL1vaX.js +0 -27
  315. package/build/assets/llm-settings-lNDerSlu.js +0 -1
  316. package/build/assets/manifest-ff93dc8c.js +0 -1
  317. package/build/assets/use-create-conversation-Be-eVpFp.js +0 -1
  318. package/build/assets/use-create-secret-Bk9ZxTjl.js +0 -1
  319. package/build/assets/use-manifest-capabilities-Bso6A_-A.js +0 -1
  320. package/build/assets/use-tracking-ChnSFChv.js +0 -3
  321. package/build/assets/utils-D-gOfhmN.js +0 -1
  322. package/build/assets/vendor~entry.client~root~root-layout~index-home~home~conversation-panel~conversation~launch~b0x1wrp2-D6owle9R.js +0 -2
  323. package/dist/node_modules/@openhands/typescript-client/dist/client/desktop-client.cjs +0 -2
  324. package/dist/node_modules/@openhands/typescript-client/dist/client/desktop-client.cjs.map +0 -1
  325. package/dist/node_modules/@openhands/typescript-client/dist/client/desktop-client.js +0 -18
  326. package/dist/node_modules/@openhands/typescript-client/dist/client/desktop-client.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "ملف تعريف غير متاح ({{id}})",
2
3
  "COMMON$PATTERN": "النمط:",
3
4
  "COMMON$INCLUDE": "تضمين:",
4
5
  "COMMON$RESULTS": "النتائج ({{count}})",
@@ -2465,5 +2466,11 @@
2465
2466
  "SETTINGS$PROVIDER_CONNECTION_UPDATED": "تم تحديث الاتصال \"{{name}}\"",
2466
2467
  "BUTTON$VIEW_FULL_SIZE_IMAGE": "عرض الصورة بالحجم الكامل",
2467
2468
  "BUTTON$REMOVE_IMAGE": "إزالة الصورة",
2468
- "IMAGE$FULL_SIZE_PREVIEW": "معاينة الصورة بالحجم الكامل"
2469
+ "IMAGE$FULL_SIZE_PREVIEW": "معاينة الصورة بالحجم الكامل",
2470
+ "SETTINGS$AGENT_PROFILE_SECRETS": "الأسرار",
2471
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL": "بلا تقييد من الملف الشخصي",
2472
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "اختيار الأسرار",
2473
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "يسمح بالأسرار المقدمة عند بدء الوكيل. للأتمتة، اختر الأسرار صراحةً في هذا الملف الشخصي.",
2474
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "لا يمكن لهذا الوكيل استخدام سوى الأسرار التي تحددها، فتأكد من أنه يملك كل ما يحتاجه. ويحتاج الوكيل الخارجي إلى بيانات اعتماد مزوّده الخاصة لتسجيل الدخول.",
2475
+ "SETTINGS$AGENT_PROFILE_SECRETS_NONE": "لم تحفظ أي أسرار بعد."
2469
2476
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "Perfil no disponible ({{id}})",
2
3
  "COMMON$PATTERN": "Patró:",
3
4
  "COMMON$INCLUDE": "Inclou:",
4
5
  "COMMON$RESULTS": "Resultats ({{count}})",
@@ -2465,5 +2466,11 @@
2465
2466
  "SETTINGS$PROVIDER_CONNECTION_UPDATED": "S'ha actualitzat la connexió \"{{name}}\"",
2466
2467
  "BUTTON$VIEW_FULL_SIZE_IMAGE": "Mostra la imatge a mida completa",
2467
2468
  "BUTTON$REMOVE_IMAGE": "Elimina la imatge",
2468
- "IMAGE$FULL_SIZE_PREVIEW": "Previsualització de la imatge a mida completa"
2469
+ "IMAGE$FULL_SIZE_PREVIEW": "Previsualització de la imatge a mida completa",
2470
+ "SETTINGS$AGENT_PROFILE_SECRETS": "Secrets",
2471
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL": "Sense restricció del perfil",
2472
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "Tria els secrets",
2473
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "Permet els secrets proporcionats en iniciar l’agent. Per a les automatitzacions, tria explícitament els secrets en aquest perfil.",
2474
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "Aquest agent només pot fer servir els secrets que seleccionis; assegura't que té tot el que necessita. Un agent extern necessita les seves pròpies credencials del proveïdor per iniciar la sessió.",
2475
+ "SETTINGS$AGENT_PROFILE_SECRETS_NONE": "Encara no has desat cap secret."
2469
2476
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "Nicht verfügbares Profil ({{id}})",
2
3
  "COMMON$PATTERN": "Muster:",
3
4
  "COMMON$INCLUDE": "Einschließen:",
4
5
  "COMMON$RESULTS": "Ergebnisse ({{count}})",
@@ -2465,5 +2466,11 @@
2465
2466
  "SETTINGS$PROVIDER_CONNECTION_UPDATED": "Verbindung \"{{name}}\" aktualisiert",
2466
2467
  "BUTTON$VIEW_FULL_SIZE_IMAGE": "Bild in voller Größe anzeigen",
2467
2468
  "BUTTON$REMOVE_IMAGE": "Bild entfernen",
2468
- "IMAGE$FULL_SIZE_PREVIEW": "Vorschau des Bildes in voller Größe"
2469
+ "IMAGE$FULL_SIZE_PREVIEW": "Vorschau des Bildes in voller Größe",
2470
+ "SETTINGS$AGENT_PROFILE_SECRETS": "Geheimnisse",
2471
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL": "Keine Profileinschränkung",
2472
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "Geheimnisse auswählen",
2473
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "Erlaubt beim Start des Agenten übergebene Geheimnisse. Wählen Sie für Automatisierungen die Geheimnisse in diesem Profil ausdrücklich aus.",
2474
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "Dieser Agent kann nur die von Ihnen ausgewählten Geheimnisse verwenden – stellen Sie sicher, dass er alles Nötige hat. Ein externer Agent benötigt zum Anmelden seine eigenen Anbieter-Anmeldedaten.",
2475
+ "SETTINGS$AGENT_PROFILE_SECRETS_NONE": "Sie haben noch keine Geheimnisse gespeichert."
2469
2476
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "Unavailable profile ({{id}})",
2
3
  "COMMON$PATTERN": "Pattern:",
3
4
  "COMMON$INCLUDE": "Include:",
4
5
  "COMMON$RESULTS": "Results ({{count}})",
@@ -2465,5 +2466,11 @@
2465
2466
  "SETTINGS$PROVIDER_CONNECTION_UPDATED": "Connection \"{{name}}\" updated",
2466
2467
  "BUTTON$VIEW_FULL_SIZE_IMAGE": "View full-size image",
2467
2468
  "BUTTON$REMOVE_IMAGE": "Remove image",
2468
- "IMAGE$FULL_SIZE_PREVIEW": "Full-size image preview"
2469
+ "IMAGE$FULL_SIZE_PREVIEW": "Full-size image preview",
2470
+ "SETTINGS$AGENT_PROFILE_SECRETS": "Secrets",
2471
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL": "No profile restriction",
2472
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "Choose secrets",
2473
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "Allows secrets supplied when the agent starts. For automations, choose secrets explicitly in this profile.",
2474
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "This agent can use only the secrets you select — make sure it has everything it needs. An external agent needs its own provider credentials to sign in.",
2475
+ "SETTINGS$AGENT_PROFILE_SECRETS_NONE": "You have not saved any secrets yet."
2469
2476
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "Perfil no disponible ({{id}})",
2
3
  "COMMON$PATTERN": "Patrón:",
3
4
  "COMMON$INCLUDE": "Incluir:",
4
5
  "COMMON$RESULTS": "Resultados ({{count}})",
@@ -2465,5 +2466,11 @@
2465
2466
  "SETTINGS$PROVIDER_CONNECTION_UPDATED": "Conexión \"{{name}}\" actualizada",
2466
2467
  "BUTTON$VIEW_FULL_SIZE_IMAGE": "Ver imagen a tamaño completo",
2467
2468
  "BUTTON$REMOVE_IMAGE": "Eliminar imagen",
2468
- "IMAGE$FULL_SIZE_PREVIEW": "Vista previa de la imagen a tamaño completo"
2469
+ "IMAGE$FULL_SIZE_PREVIEW": "Vista previa de la imagen a tamaño completo",
2470
+ "SETTINGS$AGENT_PROFILE_SECRETS": "Secretos",
2471
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL": "Sin restricción del perfil",
2472
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "Elegir secretos",
2473
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "Permite los secretos proporcionados al iniciar el agente. Para las automatizaciones, elige los secretos explícitamente en este perfil.",
2474
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "Este agente solo puede usar los secretos que selecciones; asegúrate de que tenga todo lo que necesita. Un agente externo necesita sus propias credenciales del proveedor para iniciar sesión.",
2475
+ "SETTINGS$AGENT_PROFILE_SECRETS_NONE": "Aún no has guardado ningún secreto."
2469
2476
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "Profil indisponible ({{id}})",
2
3
  "COMMON$PATTERN": "Motif :",
3
4
  "COMMON$INCLUDE": "Inclure:",
4
5
  "COMMON$RESULTS": "Résultats ({{count}})",
@@ -2465,5 +2466,11 @@
2465
2466
  "SETTINGS$PROVIDER_CONNECTION_UPDATED": "Connexion « {{name}} » mise à jour",
2466
2467
  "BUTTON$VIEW_FULL_SIZE_IMAGE": "Afficher l'image en taille réelle",
2467
2468
  "BUTTON$REMOVE_IMAGE": "Supprimer l'image",
2468
- "IMAGE$FULL_SIZE_PREVIEW": "Aperçu de l'image en taille réelle"
2469
+ "IMAGE$FULL_SIZE_PREVIEW": "Aperçu de l'image en taille réelle",
2470
+ "SETTINGS$AGENT_PROFILE_SECRETS": "Secrets",
2471
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL": "Aucune restriction du profil",
2472
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "Choisir les secrets",
2473
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "Autorise les secrets fournis au démarrage de l’agent. Pour les automatisations, choisissez explicitement les secrets dans ce profil.",
2474
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "Cet agent ne peut utiliser que les secrets que vous sélectionnez : vérifiez qu'il dispose de tout ce dont il a besoin. Un agent externe a besoin de ses propres identifiants de fournisseur pour se connecter.",
2475
+ "SETTINGS$AGENT_PROFILE_SECRETS_NONE": "Vous n'avez encore enregistré aucun secret."
2469
2476
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "Profilo non disponibile ({{id}})",
2
3
  "COMMON$PATTERN": "Modello:",
3
4
  "COMMON$INCLUDE": "Includi:",
4
5
  "COMMON$RESULTS": "Risultati ({{count}})",
@@ -2465,5 +2466,11 @@
2465
2466
  "SETTINGS$PROVIDER_CONNECTION_UPDATED": "Connessione \"{{name}}\" aggiornata",
2466
2467
  "BUTTON$VIEW_FULL_SIZE_IMAGE": "Visualizza immagine a dimensione originale",
2467
2468
  "BUTTON$REMOVE_IMAGE": "Rimuovi immagine",
2468
- "IMAGE$FULL_SIZE_PREVIEW": "Anteprima dell'immagine a dimensione originale"
2469
+ "IMAGE$FULL_SIZE_PREVIEW": "Anteprima dell'immagine a dimensione originale",
2470
+ "SETTINGS$AGENT_PROFILE_SECRETS": "Segreti",
2471
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL": "Nessuna restrizione del profilo",
2472
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "Scegli i segreti",
2473
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "Consente i segreti forniti all’avvio dell’agente. Per le automazioni, scegli esplicitamente i segreti in questo profilo.",
2474
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "Questo agente può usare solo i segreti che selezioni: assicurati che abbia tutto ciò che gli serve. Un agente esterno ha bisogno delle proprie credenziali del provider per accedere.",
2475
+ "SETTINGS$AGENT_PROFILE_SECRETS_NONE": "Non hai ancora salvato alcun segreto."
2469
2476
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "利用できないプロファイル ({{id}})",
2
3
  "COMMON$PATTERN": "パターン:",
3
4
  "COMMON$INCLUDE": "対象:",
4
5
  "COMMON$RESULTS": "結果 ({{count}})",
@@ -2465,5 +2466,11 @@
2465
2466
  "SETTINGS$PROVIDER_CONNECTION_UPDATED": "接続「{{name}}」を更新しました",
2466
2467
  "BUTTON$VIEW_FULL_SIZE_IMAGE": "フルサイズの画像を表示",
2467
2468
  "BUTTON$REMOVE_IMAGE": "画像を削除",
2468
- "IMAGE$FULL_SIZE_PREVIEW": "フルサイズの画像プレビュー"
2469
+ "IMAGE$FULL_SIZE_PREVIEW": "フルサイズの画像プレビュー",
2470
+ "SETTINGS$AGENT_PROFILE_SECRETS": "シークレット",
2471
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL": "プロファイルによる制限なし",
2472
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "シークレットを選択",
2473
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "エージェントの起動時に渡されたシークレットを許可します。自動化では、このプロファイルでシークレットを明示的に選択してください。",
2474
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "このエージェントは選択したシークレットのみを使用できます。必要なものがすべて含まれているか確認してください。外部エージェントはサインインに自身のプロバイダー認証情報が必要です。",
2475
+ "SETTINGS$AGENT_PROFILE_SECRETS_NONE": "シークレットはまだ保存されていません。"
2469
2476
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "사용할 수 없는 프로필 ({{id}})",
2
3
  "COMMON$PATTERN": "패턴:",
3
4
  "COMMON$INCLUDE": "포함:",
4
5
  "COMMON$RESULTS": "결과 ({{count}})",
@@ -2465,5 +2466,11 @@
2465
2466
  "SETTINGS$PROVIDER_CONNECTION_UPDATED": "\"{{name}}\" 연결이 업데이트되었습니다",
2466
2467
  "BUTTON$VIEW_FULL_SIZE_IMAGE": "원본 크기 이미지 보기",
2467
2468
  "BUTTON$REMOVE_IMAGE": "이미지 제거",
2468
- "IMAGE$FULL_SIZE_PREVIEW": "원본 크기 이미지 미리보기"
2469
+ "IMAGE$FULL_SIZE_PREVIEW": "원본 크기 이미지 미리보기",
2470
+ "SETTINGS$AGENT_PROFILE_SECRETS": "시크릿",
2471
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL": "프로필 제한 없음",
2472
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "시크릿 선택",
2473
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "에이전트 시작 시 제공된 시크릿을 허용합니다. 자동화에서는 이 프로필에서 시크릿을 명시적으로 선택하세요.",
2474
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "이 에이전트는 선택한 시크릿만 사용할 수 있으므로 필요한 항목이 모두 포함되었는지 확인하세요. 외부 에이전트는 로그인하려면 자체 공급자 자격 증명이 필요합니다.",
2475
+ "SETTINGS$AGENT_PROFILE_SECRETS_NONE": "아직 저장된 시크릿이 없습니다."
2469
2476
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "Utilgjengelig profil ({{id}})",
2
3
  "COMMON$PATTERN": "Mønster:",
3
4
  "COMMON$INCLUDE": "Inkluder:",
4
5
  "COMMON$RESULTS": "Resultater ({{count}})",
@@ -2465,5 +2466,11 @@
2465
2466
  "SETTINGS$PROVIDER_CONNECTION_UPDATED": "Tilkoblingen «{{name}}» ble oppdatert",
2466
2467
  "BUTTON$VIEW_FULL_SIZE_IMAGE": "Vis bildet i full størrelse",
2467
2468
  "BUTTON$REMOVE_IMAGE": "Fjern bilde",
2468
- "IMAGE$FULL_SIZE_PREVIEW": "Forhåndsvisning av bilde i full størrelse"
2469
+ "IMAGE$FULL_SIZE_PREVIEW": "Forhåndsvisning av bilde i full størrelse",
2470
+ "SETTINGS$AGENT_PROFILE_SECRETS": "Hemmeligheter",
2471
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL": "Ingen profilbegrensning",
2472
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "Velg hemmeligheter",
2473
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "Tillater hemmeligheter som oppgis når agenten starter. For automatiseringer må du velge hemmeligheter eksplisitt i denne profilen.",
2474
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "Denne agenten kan bare bruke hemmelighetene du velger – sørg for at den har alt den trenger. En ekstern agent trenger sine egne leverandørlegitimasjoner for å logge inn.",
2475
+ "SETTINGS$AGENT_PROFILE_SECRETS_NONE": "Du har ikke lagret noen hemmeligheter ennå."
2469
2476
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "Perfil indisponível ({{id}})",
2
3
  "COMMON$PATTERN": "Padrão:",
3
4
  "COMMON$INCLUDE": "Incluir:",
4
5
  "COMMON$RESULTS": "Resultados ({{count}})",
@@ -2465,5 +2466,11 @@
2465
2466
  "SETTINGS$PROVIDER_CONNECTION_UPDATED": "Conexão \"{{name}}\" atualizada",
2466
2467
  "BUTTON$VIEW_FULL_SIZE_IMAGE": "Ver imagem em tamanho original",
2467
2468
  "BUTTON$REMOVE_IMAGE": "Remover imagem",
2468
- "IMAGE$FULL_SIZE_PREVIEW": "Pré-visualização da imagem em tamanho original"
2469
+ "IMAGE$FULL_SIZE_PREVIEW": "Pré-visualização da imagem em tamanho original",
2470
+ "SETTINGS$AGENT_PROFILE_SECRETS": "Segredos",
2471
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL": "Sem restrição do perfil",
2472
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "Escolher segredos",
2473
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "Permite os segredos fornecidos quando o agente inicia. Para automações, escolha explicitamente os segredos neste perfil.",
2474
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "Este agente pode usar apenas os segredos que você selecionar — confirme que ele tem tudo o que precisa. Um agente externo precisa das próprias credenciais do provedor para entrar.",
2475
+ "SETTINGS$AGENT_PROFILE_SECRETS_NONE": "Você ainda não salvou nenhum segredo."
2469
2476
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "Kullanılamayan profil ({{id}})",
2
3
  "COMMON$PATTERN": "Desen:",
3
4
  "COMMON$INCLUDE": "Dahil:",
4
5
  "COMMON$RESULTS": "Sonuçlar ({{count}})",
@@ -2465,5 +2466,11 @@
2465
2466
  "SETTINGS$PROVIDER_CONNECTION_UPDATED": "\"{{name}}\" bağlantısı güncellendi",
2466
2467
  "BUTTON$VIEW_FULL_SIZE_IMAGE": "Görseli tam boyutta görüntüle",
2467
2468
  "BUTTON$REMOVE_IMAGE": "Görseli kaldır",
2468
- "IMAGE$FULL_SIZE_PREVIEW": "Tam boyutlu görsel önizlemesi"
2469
+ "IMAGE$FULL_SIZE_PREVIEW": "Tam boyutlu görsel önizlemesi",
2470
+ "SETTINGS$AGENT_PROFILE_SECRETS": "Gizli bilgiler",
2471
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL": "Profil kısıtlaması yok",
2472
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "Gizli bilgileri seçin",
2473
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "Aracı başlatılırken sağlanan gizli bilgilere izin verir. Otomasyonlar için bu profilde gizli bilgileri açıkça seçin.",
2474
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "Bu aracı yalnızca seçtiğiniz gizli bilgileri kullanabilir; ihtiyacı olan her şeyin bulunduğundan emin olun. Harici bir aracının oturum açmak için kendi sağlayıcı kimlik bilgilerine ihtiyacı vardır.",
2475
+ "SETTINGS$AGENT_PROFILE_SECRETS_NONE": "Henüz gizli bilgi kaydetmediniz."
2469
2476
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "Недоступний профіль ({{id}})",
2
3
  "COMMON$PATTERN": "Шаблон:",
3
4
  "COMMON$INCLUDE": "Включити:",
4
5
  "COMMON$RESULTS": "Результати ({{count}})",
@@ -2465,5 +2466,11 @@
2465
2466
  "SETTINGS$PROVIDER_CONNECTION_UPDATED": "Підключення \"{{name}}\" оновлено",
2466
2467
  "BUTTON$VIEW_FULL_SIZE_IMAGE": "Переглянути зображення в повному розмірі",
2467
2468
  "BUTTON$REMOVE_IMAGE": "Видалити зображення",
2468
- "IMAGE$FULL_SIZE_PREVIEW": "Попередній перегляд зображення в повному розмірі"
2469
+ "IMAGE$FULL_SIZE_PREVIEW": "Попередній перегляд зображення в повному розмірі",
2470
+ "SETTINGS$AGENT_PROFILE_SECRETS": "Секрети",
2471
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL": "Без обмежень профілю",
2472
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "Вибрати секрети",
2473
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "Дозволяє секрети, передані під час запуску агента. Для автоматизацій явно виберіть секрети в цьому профілі.",
2474
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "Цей агент може використовувати лише вибрані вами секрети — переконайтеся, що він має все потрібне. Зовнішньому агенту для входу потрібні власні облікові дані провайдера.",
2475
+ "SETTINGS$AGENT_PROFILE_SECRETS_NONE": "Ви ще не зберегли жодного секрету."
2469
2476
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "不可用的配置文件 ({{id}})",
2
3
  "COMMON$PATTERN": "模式:",
3
4
  "COMMON$INCLUDE": "包含:",
4
5
  "COMMON$RESULTS": "结果 ({{count}})",
@@ -2465,5 +2466,11 @@
2465
2466
  "SETTINGS$PROVIDER_CONNECTION_UPDATED": "已更新连接“{{name}}”",
2466
2467
  "BUTTON$VIEW_FULL_SIZE_IMAGE": "查看原始尺寸图片",
2467
2468
  "BUTTON$REMOVE_IMAGE": "移除图片",
2468
- "IMAGE$FULL_SIZE_PREVIEW": "原始尺寸图片预览"
2469
+ "IMAGE$FULL_SIZE_PREVIEW": "原始尺寸图片预览",
2470
+ "SETTINGS$AGENT_PROFILE_SECRETS": "机密",
2471
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL": "不由配置文件限制",
2472
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "自选机密",
2473
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "允许使用代理启动时提供的机密。用于自动化时,请在此配置文件中明确选择机密。",
2474
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "该代理只能使用你选择的机密,请确认它需要的都已包含。外部代理需要自身的提供方凭据才能登录。",
2475
+ "SETTINGS$AGENT_PROFILE_SECRETS_NONE": "你还没有保存任何机密。"
2469
2476
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AUTOMATIONS$UNAVAILABLE_AGENT_PROFILE": "無法使用的設定檔 ({{id}})",
2
3
  "COMMON$PATTERN": "模式:",
3
4
  "COMMON$INCLUDE": "包含:",
4
5
  "COMMON$RESULTS": "結果 ({{count}})",
@@ -2465,5 +2466,11 @@
2465
2466
  "SETTINGS$PROVIDER_CONNECTION_UPDATED": "已更新連線「{{name}}」",
2466
2467
  "BUTTON$VIEW_FULL_SIZE_IMAGE": "檢視原始尺寸圖片",
2467
2468
  "BUTTON$REMOVE_IMAGE": "移除圖片",
2468
- "IMAGE$FULL_SIZE_PREVIEW": "原始尺寸圖片預覽"
2469
+ "IMAGE$FULL_SIZE_PREVIEW": "原始尺寸圖片預覽",
2470
+ "SETTINGS$AGENT_PROFILE_SECRETS": "機密",
2471
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL": "不由設定檔限制",
2472
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE": "自選機密",
2473
+ "SETTINGS$AGENT_PROFILE_SECRETS_ALL_HINT": "允許使用代理啟動時提供的機密。用於自動化時,請在此設定檔中明確選擇機密。",
2474
+ "SETTINGS$AGENT_PROFILE_SECRETS_CHOOSE_HINT": "該代理只能使用你選擇的機密,請確認它需要的都已包含。外部代理需要自身的供應商憑證才能登入。",
2475
+ "SETTINGS$AGENT_PROFILE_SECRETS_NONE": "你還沒有儲存任何機密。"
2469
2476
  }
@@ -1 +1 @@
1
- {"version":3,"file":"automation-setup.cjs","names":[],"sources":["../../src/manifests/automation-setup.ts"],"sourcesContent":["/**\n * The only module that knows a setup entry configures an *automation*.\n *\n * The published contract states just what varies between entries; everything\n * derivable is the host's to generate. That derivation is here, and nowhere\n * else, so the rest of `src/manifests/` stays about forms rather than about\n * automations.\n *\n * The algorithm mirrors `tests/test_automation_setup.py::_render_payload` in\n * `OpenHands/extensions`, which is the authoritative reference: it produces the\n * request bodies published in that repository's contract fixtures, and those\n * were verified against the live service. The create model is `extra=\"forbid\"`,\n * so any divergence is a hard 422 rather than a dropped field.\n */\n\nimport i18n from \"#/i18n\";\nimport { I18nKey } from \"#/i18n/declaration\";\nimport { findAutomationCommand } from \"#/utils/automation-catalog\";\nimport { getAutomationEndpoint } from \"./automation-interface\";\nimport {\n actionKinds,\n collectFields,\n fieldText,\n fieldValues,\n} from \"./manifest-local-validation\";\nimport { interpolateText, interpolateValue } from \"./manifest-template\";\nimport type {\n SetupActionKind,\n SetupBlock,\n SetupBundleConfigValue,\n SetupEntry,\n SetupFormValues,\n SetupRequestBody,\n SetupTriggerKind,\n} from \"./types\";\n\n/**\n * The creation endpoint a derived draft would be posted to. Resolved on call\n * rather than at import, because the endpoint is the interface manifest's and\n * this module loads whether or not one was admitted.\n *\n * A bundle entry is created through the raw endpoint, because what it sends is\n * a tarball it uploaded rather than arguments to a preset. Called without an\n * entry - as the import path does - it answers for a prompt.\n */\nexport function automationCreateEndpoint(\n entry?: SetupEntry,\n selectedAction?: string | null,\n): string {\n const kind = selectedActionKind(entry, selectedAction);\n if (kind === \"plugin\") return getAutomationEndpoint(\"createPlugin\");\n if (kind === \"upload\" || kind === \"bundle\") {\n return requireBundleEndpoint(\"createBundle\");\n }\n return getAutomationEndpoint(\"createPrompt\");\n}\n\n/** Where a bundle's tarball is uploaded, before the create call. */\nexport function automationUploadEndpoint(): string {\n return requireBundleEndpoint(\"uploads\");\n}\n\n/** The endpoints a bundle entry cannot be created without. */\nconst BUNDLE_ENDPOINTS = [\"createBundle\", \"uploads\"] as const;\n\n/**\n * An endpoint only a bundle needs. The interface manifest may predate bundles,\n * and the host holds no path of its own to fall back to, so this is where that\n * runs out rather than somewhere deep in a request.\n */\nfunction requireBundleEndpoint(\n name: (typeof BUNDLE_ENDPOINTS)[number],\n): string {\n const path = getAutomationEndpoint(name);\n if (!path) {\n throw new Error(\n `The published automation interface declares no '${name}' endpoint, ` +\n \"so this deployment cannot create an automation from a script bundle.\",\n );\n }\n return path;\n}\n\n/**\n * The endpoints this entry needs that the published interface does not declare.\n *\n * Asked before the form renders rather than discovered at the moment of\n * creating: a pinned package that predates bundles can never answer, and the\n * dialog can say so while nothing has been filled in yet. Empty for every\n * entry that is not a bundle, which needs nothing beyond what the block has\n * always declared.\n */\nfunction missingEndpointsForAction(kind: SetupActionKind | \"bundle\"): string[] {\n return kind === \"upload\" || kind === \"bundle\"\n ? BUNDLE_ENDPOINTS.filter((name) => !getAutomationEndpoint(name))\n : [];\n}\n\nexport function missingCreateEndpoints(\n entry: SetupEntry,\n selectedAction?: string | null,\n): string[] {\n if (isBundleEntry(entry)) return missingEndpointsForAction(\"bundle\");\n if (!entry.setup.actions) return [];\n\n if (selectedAction && selectedAction in entry.setup.actions) {\n return missingEndpointsForAction(selectedAction as SetupActionKind);\n }\n\n const missingByAction = actionKinds(entry.setup).map(\n missingEndpointsForAction,\n );\n if (missingByAction.some((missing) => missing.length === 0)) return [];\n return Array.from(new Set(missingByAction.flat()));\n}\n\n/** Whether this entry ships a script tarball instead of a prompt. */\nexport function isBundleEntry(entry: SetupEntry): boolean {\n return entry.setup.mode === \"direct\" && entry.setup.bundle !== undefined;\n}\n\nexport function isUploadAction(\n entry: SetupEntry,\n selectedAction?: string | null,\n): boolean {\n return selectedActionKind(entry, selectedAction) === \"upload\";\n}\n\nexport function selectedActionKind(\n entry: SetupEntry | undefined,\n selectedAction?: string | null,\n): SetupActionKind | \"bundle\" | \"prompt\" {\n if (!entry) return \"prompt\";\n if (isBundleEntry(entry)) return \"bundle\";\n const actions = entry.setup.actions ?? {};\n if (selectedAction && selectedAction in actions) {\n return selectedAction as SetupActionKind;\n }\n const keys = actionKinds(entry.setup);\n if (keys.length === 1) return keys[0];\n return \"prompt\";\n}\n\n/**\n * The `tarball_path` a preflight draft carries.\n *\n * Preflight runs on every field blur and the upload happens once, at submit,\n * so there is no real path to send yet. The service checks this field's scheme\n * at preflight and its ownership only at creation, so a well-formed stand-in\n * validates exactly what preflight is for - the rest of the body - without\n * uploading an archive per keystroke.\n */\nexport const PREFLIGHT_TARBALL_PATH =\n \"oh-internal://uploads/00000000-0000-0000-0000-000000000000\";\n\n/**\n * Trigger properties a form field may fill, per trigger kind. A field under a\n * trigger kind whose name is listed here fills the trigger property of the same\n * name; anything else there, such as a phrase to match, is an input to `filter`.\n */\nconst TRIGGER_PROPERTIES: Record<SetupTriggerKind, readonly string[]> = {\n cron: [\"schedule\", \"timezone\"],\n event: [\"source\", \"on\"],\n};\n\nconst OPTIONAL_CREATE_PROPERTIES = [\"model\", \"timeout\"] as const;\n\n/**\n * Repository properties a form field may fill, read the same way\n * {@link TRIGGER_PROPERTIES} is: a field whose name is listed here fills the\n * `repos[]` property of the same name. `url` and `provider` are absent because\n * neither is answered by a field of its own — they come from the repo-picker's\n * value and from its declared provider.\n *\n * These two lists are the only field names the derivation knows; everything\n * else it reads it finds by field type. An entry that wants a base branch in\n * its request therefore has to name that field `ref`.\n */\nconst REPO_PROPERTIES: readonly string[] = [\"ref\"];\n\nconst FORM_PLACEHOLDER_PATTERN = /\\{\\{form\\.([A-Za-z0-9_.]+)\\}\\}/g;\n\n/** The repository input, which supplies `repos` and an event trigger's source. */\nfunction findRepoPickerField(\n setup: SetupBlock,\n selectedAction?: string | null,\n) {\n const match = Object.entries(collectFields(setup, null, selectedAction)).find(\n ([, field]) => field.type === \"repo-picker\",\n );\n return match ? { name: match[0], field: match[1] } : null;\n}\n\n/** Every repository the form collected, whether the picker takes one or many. */\nfunction repositories(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string[] {\n const picker = findRepoPickerField(setup, selectedAction);\n return picker ? fieldValues(values[picker.name]) : [];\n}\n\n/**\n * The created automation's name.\n *\n * One repository is worth naming; several are not, so the count stands in\n * rather than a list of names that would not fit.\n */\nfunction deriveName(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string {\n if (\n \"name\" in collectFields(entry.setup, null, selectedAction) &&\n \"name\" in values\n ) {\n return fieldText(values.name);\n }\n\n const repos = repositories(entry.setup, values, selectedAction);\n if (repos.length === 0) return entry.name;\n if (repos.length === 1) return `${entry.name} - ${repos[0]}`;\n // The count is the one word here the host writes rather than reads off the\n // entry, so it is translated. There is no translator to pass in: the\n // derivation runs from a memo, an upload and a test alike.\n const count = i18n.t(I18nKey.SETUP$REPOSITORY_COUNT, {\n total: repos.length,\n });\n return `${entry.name} - ${count}`;\n}\n\n/** The active trigger kind for a direct entry, with its fields. */\nfunction getTrigger(setup: SetupBlock, selectedTrigger?: string | null) {\n const triggers = setup.form.triggers ?? {};\n const entries = Object.entries(triggers);\n if (entries.length === 0) return null;\n const match =\n selectedTrigger && selectedTrigger in triggers\n ? [selectedTrigger, triggers[selectedTrigger as SetupTriggerKind]]\n : entries.length === 1\n ? entries[0]\n : null;\n if (!match) return null;\n const [kind, fields] = match;\n return { kind: kind as SetupTriggerKind, fields: fields ?? {} };\n}\n\nfunction hasPayloadValue(\n value: SetupRequestBody[string] | undefined,\n): value is SetupRequestBody[string] {\n return value !== undefined && value !== null && value !== \"\";\n}\n\nfunction fieldPayloadValue(\n fieldType: string | undefined,\n value: SetupFormValues[string],\n) {\n if (fieldType === \"number\") {\n const text = fieldText(value);\n return text === \"\" ? undefined : Number(text);\n }\n return fieldText(value);\n}\n\nfunction optionalCreateProperties(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n return Object.fromEntries(\n OPTIONAL_CREATE_PROPERTIES.flatMap((name) => {\n const field = collectFields(setup, null, selectedAction)[name];\n const value = fieldPayloadValue(field?.type, values[name]);\n return hasPayloadValue(value) ? [[name, value]] : [];\n }),\n ) as SetupRequestBody;\n}\n\nfunction repositoryProperty(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n const repoPicker = findRepoPickerField(setup, selectedAction);\n const repos = repositories(setup, values, selectedAction);\n if (repos.length === 0 || !repoPicker?.field.provider) return {};\n\n const declared = REPO_PROPERTIES.filter((name) =>\n hasPayloadValue(fieldText(values[name])),\n );\n return {\n repos: repos.map((url) => ({\n url,\n ...Object.fromEntries(\n declared.map((name) => [name, fieldText(values[name])]),\n ),\n provider: repoPicker.field.provider as string,\n })),\n };\n}\n\nfunction parsePluginSources(\n value: SetupRequestBody[string],\n): SetupRequestBody[string] {\n if (!hasPayloadValue(value) || typeof value !== \"string\") return value;\n const trimmed = value.trim();\n if (!trimmed) return [];\n if (trimmed.startsWith(\"[\")) {\n try {\n return JSON.parse(trimmed) as SetupRequestBody[string];\n } catch {\n return [{ source: trimmed }];\n }\n }\n return trimmed\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => {\n const [source, ...parts] = line.split(/\\s+/);\n const plugin: Record<string, string> = { source };\n parts.forEach((part) => {\n const [key, ...raw] = part.split(\"=\");\n const valuePart = raw.join(\"=\");\n if ((key === \"ref\" || key === \"repo_path\") && valuePart) {\n plugin[key] = valuePart;\n }\n });\n return plugin;\n });\n}\n\n/**\n * The create request body these form values produce.\n *\n * No entry declares it: `name` comes from the entry, `repos` from the\n * repo-picker field and its declared provider, and `trigger` from the key and\n * fields under `form.triggers`. Only `prompt` and an event `filter` are\n * declared, because only they cannot be read off the form.\n *\n * Returns null for an assisted entry, which hands setup to a conversation\n * instead of sending a body.\n */\nexport function buildCreatePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n /** Bundle/upload entries only: what the upload returned. */\n tarballPath: string = PREFLIGHT_TARBALL_PATH,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const { setup } = entry;\n if (setup.mode !== \"direct\") return null;\n if (setup.bundle) {\n return buildBundlePayload(entry, values, tarballPath, selectedTrigger);\n }\n if (setup.actions) {\n return buildActionPayload(\n entry,\n values,\n tarballPath,\n selectedTrigger,\n selectedActionKey,\n );\n }\n if (!setup.prompt) return null;\n\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n prompt: interpolateText(setup.prompt, scope),\n ...optionalCreateProperties(setup, values),\n ...repositoryProperty(setup, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\nfunction buildActionPayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const kind = selectedActionKind(entry, selectedActionKey);\n if (kind === \"bundle\") return null;\n\n const setup = entry.setup;\n const scope = { form: values, automation: entry };\n const payload: SetupRequestBody = {\n name: deriveName(entry, values, selectedActionKey),\n };\n\n const trigger = buildTrigger(\n entry,\n values,\n selectedTrigger,\n selectedActionKey,\n );\n if (trigger) payload.trigger = trigger;\n\n Object.assign(\n payload,\n optionalCreateProperties(setup, values, selectedActionKey),\n );\n\n if (kind === \"upload\") {\n const action = setup.actions?.upload;\n if (!action) return null;\n payload.tarball_path =\n tarballPath || interpolateText(action.tarballPath, scope);\n payload.entrypoint = interpolateText(action.entrypoint, scope);\n if (action.setupScript) {\n const setupScript = interpolateText(action.setupScript, scope);\n if (hasPayloadValue(setupScript)) payload.setup_script_path = setupScript;\n }\n return payload;\n }\n\n const action =\n kind === \"plugin\" ? setup.actions?.plugin : setup.actions?.prompt;\n if (!action) return null;\n payload.prompt = interpolateText(action.prompt, scope);\n Object.assign(payload, repositoryProperty(setup, values, selectedActionKey));\n\n if (kind === \"plugin\") {\n const pluginAction = setup.actions?.plugin;\n if (!pluginAction) return null;\n payload.plugins = parsePluginSources(\n interpolateValue(pluginAction.plugins, scope),\n );\n }\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\n/**\n * The `template` provenance a prompt entry sends, or undefined for one that\n * publishes no version.\n *\n * It is what lets the service recognise a second setup of the same entry as\n * the automation it already holds. The bundle path derives the same block from\n * the config the bundle declares; a prompt entry declares none, so the answer\n * to \"what was this created from\" is the form as submitted.\n */\nfunction buildTemplate(\n entry: SetupEntry,\n values: SetupFormValues,\n): SetupRequestBody | undefined {\n if (!entry.version) return undefined;\n return {\n id: entry.id,\n version: entry.version,\n config: { ...values } as SetupRequestBody,\n };\n}\n\n/**\n * The `trigger` object, read off the key and fields under `form.triggers`.\n *\n * Identical for both kinds of direct entry: only the create endpoint and what\n * the automation is told to do differ between a prompt and a bundle.\n */\nfunction buildTrigger(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | undefined {\n const trigger = getTrigger(entry.setup, selectedTrigger);\n if (!trigger) return undefined;\n\n const properties = TRIGGER_PROPERTIES[trigger.kind];\n const declared = Object.keys(trigger.fields).filter((name) =>\n properties.includes(name),\n );\n const repoPicker = findRepoPickerField(entry.setup, selectedAction);\n const derived = Object.fromEntries(\n declared\n .map((name) => [name, fieldText(values[name])])\n .filter(([, value]) => hasPayloadValue(value)),\n );\n\n const filter = entry.setup.filter\n ? interpolateText(entry.setup.filter, {\n form: values,\n automation: entry,\n })\n : undefined;\n\n return {\n type: trigger.kind,\n ...derived,\n ...(trigger.kind === \"event\" && {\n ...(!(\"source\" in derived) && {\n source: repoPicker?.field.provider ?? \"\",\n }),\n ...(hasPayloadValue(filter) && { filter }),\n }),\n };\n}\n\n/**\n * The raw create body a bundle entry produces.\n *\n * `tarball_path` is the one value neither declared nor derived: the host packs\n * and uploads the bundle first, and creates from what came back. `template` is\n * the provenance that makes enabling the same entry twice return the\n * automation that already exists rather than a second one.\n *\n * There is no `repos`: the raw endpoint has no such field, and a bundle's\n * script fetches what it needs itself.\n */\nfunction buildBundlePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n): SetupRequestBody {\n const bundle = entry.setup.bundle!;\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n payload.tarball_path = tarballPath;\n payload.entrypoint = bundle.entrypoint;\n if (bundle.setupScript) payload.setup_script_path = bundle.setupScript;\n if (bundle.timeout !== undefined) payload.timeout = bundle.timeout;\n payload.template = {\n id: entry.id,\n version: bundle.version,\n config: interpolateConfig(bundle.config, scope) as SetupRequestBody,\n };\n\n return payload;\n}\n\n/**\n * Placeholder substitution over the config tree. Only string leaves are\n * templated; a number, a boolean or a null is written through as itself, so an\n * entry can state a value the script reads as the type it expects.\n */\nfunction interpolateConfig(\n node: SetupBundleConfigValue,\n scope: Parameters<typeof interpolateText>[1],\n): SetupBundleConfigValue {\n if (typeof node === \"string\") {\n return interpolateValue(node, scope);\n }\n if (Array.isArray(node)) {\n return node.map((item) => interpolateConfig(item, scope));\n }\n if (typeof node === \"object\" && node !== null) {\n return Object.fromEntries(\n Object.entries(node).map(([key, value]) => [\n key,\n interpolateConfig(value, scope),\n ]),\n );\n }\n return node;\n}\n\n/**\n * The preflight body the host sends. The same shape for every entry, so no\n * entry declares it.\n */\nexport function buildPreflightBody(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | null {\n const draft = buildCreatePayload(\n entry,\n values,\n PREFLIGHT_TARBALL_PATH,\n selectedTrigger,\n selectedAction,\n );\n if (!draft) return null;\n\n return {\n automationId: entry.id,\n endpoint: automationCreateEndpoint(entry, selectedAction),\n draft,\n };\n}\n\n/**\n * What an assisted entry sends into the conversation that finishes setup.\n *\n * The command is not declared either: it lives once, in the owning skill's own\n * `triggers` frontmatter, and the skill defaults to the entry's id. An entry\n * whose skill is invoked by description rather than by command contributes\n * nothing here, and the answers alone open the conversation.\n */\nexport function buildAssistedMessage(\n entry: SetupEntry,\n values: SetupFormValues,\n): string {\n const command = findAutomationCommand(entry);\n const message = entry.setup.message\n ? interpolateText(entry.setup.message, { form: values, automation: entry })\n : \"\";\n\n return [command, message].filter(Boolean).join(\"\\n\\n\");\n}\n\nfunction collectPlaceholderFields(value: string): string[] {\n const names = Array.from(\n value.matchAll(FORM_PLACEHOLDER_PATTERN),\n (match) => match[1],\n );\n return Array.from(new Set(names));\n}\n\n/**\n * Which form fields built each payload path.\n *\n * Preflight and the create endpoint reject a draft by payload path, and the\n * host has to turn that back into a highlighted input. Building the body with\n * each field standing in for its own value recovers the mapping exactly, so an\n * entry does not declare it.\n */\nexport function deriveErrorMap(\n entry: SetupEntry,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): Record<string, string[]> {\n const mapping: Record<string, string[]> = {};\n\n const triggerEntries = Object.keys(entry.setup.form.triggers ?? {});\n const triggerVariants = selectedTrigger\n ? [selectedTrigger]\n : triggerEntries.length > 1\n ? triggerEntries\n : [null];\n\n const actionEntries = actionKinds(entry.setup);\n const actionVariants = selectedAction\n ? [selectedAction]\n : actionEntries.length > 1\n ? actionEntries\n : [null];\n\n const walk = (node: unknown, path: string): void => {\n if (Array.isArray(node)) {\n node.forEach((item, index) => walk(item, `${path}[${index}]`));\n return;\n }\n if (typeof node === \"object\" && node !== null) {\n Object.entries(node).forEach(([key, item]) =>\n walk(item, path ? `${path}.${key}` : key),\n );\n return;\n }\n if (typeof node === \"string\") {\n const names = collectPlaceholderFields(node);\n if (names.length > 0) {\n mapping[path] = Array.from(\n new Set([...(mapping[path] ?? []), ...names]),\n );\n }\n }\n };\n\n actionVariants.forEach((actionVariant) => {\n triggerVariants.forEach((triggerVariant) => {\n const names = Object.keys(\n collectFields(entry.setup, triggerVariant, actionVariant),\n );\n const variantStandIns = Object.fromEntries(\n names.map((name) => [name, `{{form.${name}}}`]),\n );\n const template = buildCreatePayload(\n entry,\n variantStandIns,\n actionVariant === \"upload\" ? \"\" : PREFLIGHT_TARBALL_PATH,\n triggerVariant,\n actionVariant,\n );\n if (template) walk(template, \"\");\n });\n });\n return mapping;\n}\n"],"mappings":"gNA6CA,SAAgB,EACd,EACA,EACQ,CACR,IAAM,EAAO,EAAmB,EAAO,CAAc,EAKrD,OAJI,IAAS,SAAiB,EAAA,sBAAsB,cAAc,EAC9D,IAAS,UAAY,IAAS,SACzB,EAAsB,cAAc,EAEtC,EAAA,sBAAsB,cAAc,CAC7C,CAGA,SAAgB,GAAmC,CACjD,OAAO,EAAsB,SAAS,CACxC,CAUA,SAAS,EACP,EACQ,CACR,IAAM,EAAO,EAAA,sBAAsB,CAAI,EACvC,GAAI,CAAC,EACH,MAAU,MACR,mDAAmD,EAAK,iFAE1D,EAEF,OAAO,CACT,CAoCA,SAAgB,EAAc,EAA4B,CACxD,OAAO,EAAM,MAAM,OAAS,UAAY,EAAM,MAAM,SAAW,IAAA,EACjE,CASA,SAAgB,EACd,EACA,EACuC,CACvC,GAAI,CAAC,EAAO,MAAO,SACnB,GAAI,EAAc,CAAK,EAAG,MAAO,SACjC,IAAM,EAAU,EAAM,MAAM,SAAW,CAAC,EACxC,GAAI,GAAkB,KAAkB,EACtC,OAAO,EAET,IAAM,EAAO,EAAA,YAAY,EAAM,KAAK,EAEpC,OADI,EAAK,SAAW,EAAU,EAAK,GAC5B,QACT"}
1
+ {"version":3,"file":"automation-setup.cjs","names":[],"sources":["../../src/manifests/automation-setup.ts"],"sourcesContent":["/**\n * The only module that knows a setup entry configures an *automation*.\n *\n * The published contract states just what varies between entries; everything\n * derivable is the host's to generate. That derivation is here, and nowhere\n * else, so the rest of `src/manifests/` stays about forms rather than about\n * automations.\n *\n * The algorithm mirrors `tests/test_automation_setup.py::_render_payload` in\n * `OpenHands/extensions`, which is the authoritative reference: it produces the\n * request bodies published in that repository's contract fixtures, and those\n * were verified against the live service. The create model is `extra=\"forbid\"`,\n * so any divergence is a hard 422 rather than a dropped field.\n */\n\nimport i18n from \"#/i18n\";\nimport { I18nKey } from \"#/i18n/declaration\";\nimport { findAutomationCommand } from \"#/utils/automation-catalog\";\nimport { getAutomationEndpoint } from \"./automation-interface\";\nimport {\n actionKinds,\n collectFields,\n fieldText,\n fieldValues,\n} from \"./manifest-local-validation\";\nimport { interpolateText, interpolateValue } from \"./manifest-template\";\nimport type {\n SetupActionKind,\n SetupBlock,\n SetupBundleConfigValue,\n SetupEntry,\n SetupFormValues,\n SetupRequestBody,\n SetupTriggerKind,\n} from \"./types\";\n\n/**\n * The creation endpoint a derived draft would be posted to. Resolved on call\n * rather than at import, because the endpoint is the interface manifest's and\n * this module loads whether or not one was admitted.\n *\n * A bundle entry is created through the raw endpoint, because what it sends is\n * a tarball it uploaded rather than arguments to a preset. Called without an\n * entry - as the import path does - it answers for a prompt.\n */\nexport function automationCreateEndpoint(\n entry?: SetupEntry,\n selectedAction?: string | null,\n): string {\n const kind = selectedActionKind(entry, selectedAction);\n if (kind === \"plugin\") return getAutomationEndpoint(\"createPlugin\");\n if (kind === \"upload\" || kind === \"bundle\") {\n return requireBundleEndpoint(\"createBundle\");\n }\n return getAutomationEndpoint(\"createPrompt\");\n}\n\n/** Where a bundle's tarball is uploaded, before the create call. */\nexport function automationUploadEndpoint(): string {\n return requireBundleEndpoint(\"uploads\");\n}\n\n/** The endpoints a bundle entry cannot be created without. */\nconst BUNDLE_ENDPOINTS = [\"createBundle\", \"uploads\"] as const;\n\n/**\n * An endpoint only a bundle needs. The interface manifest may predate bundles,\n * and the host holds no path of its own to fall back to, so this is where that\n * runs out rather than somewhere deep in a request.\n */\nfunction requireBundleEndpoint(\n name: (typeof BUNDLE_ENDPOINTS)[number],\n): string {\n const path = getAutomationEndpoint(name);\n if (!path) {\n throw new Error(\n `The published automation interface declares no '${name}' endpoint, ` +\n \"so this deployment cannot create an automation from a script bundle.\",\n );\n }\n return path;\n}\n\n/**\n * The endpoints this entry needs that the published interface does not declare.\n *\n * Asked before the form renders rather than discovered at the moment of\n * creating: a pinned package that predates bundles can never answer, and the\n * dialog can say so while nothing has been filled in yet. Empty for every\n * entry that is not a bundle, which needs nothing beyond what the block has\n * always declared.\n */\nfunction missingEndpointsForAction(kind: SetupActionKind | \"bundle\"): string[] {\n return kind === \"upload\" || kind === \"bundle\"\n ? BUNDLE_ENDPOINTS.filter((name) => !getAutomationEndpoint(name))\n : [];\n}\n\nexport function missingCreateEndpoints(\n entry: SetupEntry,\n selectedAction?: string | null,\n): string[] {\n if (isBundleEntry(entry)) return missingEndpointsForAction(\"bundle\");\n if (!entry.setup.actions) return [];\n\n if (selectedAction && selectedAction in entry.setup.actions) {\n return missingEndpointsForAction(selectedAction as SetupActionKind);\n }\n\n const missingByAction = actionKinds(entry.setup).map(\n missingEndpointsForAction,\n );\n if (missingByAction.some((missing) => missing.length === 0)) return [];\n return Array.from(new Set(missingByAction.flat()));\n}\n\n/** Whether this entry ships a script tarball instead of a prompt. */\nexport function isBundleEntry(entry: SetupEntry): boolean {\n return entry.setup.mode === \"direct\" && entry.setup.bundle !== undefined;\n}\n\nexport function isUploadAction(\n entry: SetupEntry,\n selectedAction?: string | null,\n): boolean {\n return selectedActionKind(entry, selectedAction) === \"upload\";\n}\n\nexport function selectedActionKind(\n entry: SetupEntry | undefined,\n selectedAction?: string | null,\n): SetupActionKind | \"bundle\" | \"prompt\" {\n if (!entry) return \"prompt\";\n if (isBundleEntry(entry)) return \"bundle\";\n const actions = entry.setup.actions ?? {};\n if (selectedAction && selectedAction in actions) {\n return selectedAction as SetupActionKind;\n }\n const keys = actionKinds(entry.setup);\n if (keys.length === 1) return keys[0];\n return \"prompt\";\n}\n\n/**\n * The `tarball_path` a preflight draft carries.\n *\n * Preflight runs on every field blur and the upload happens once, at submit,\n * so there is no real path to send yet. The service checks this field's scheme\n * at preflight and its ownership only at creation, so a well-formed stand-in\n * validates exactly what preflight is for - the rest of the body - without\n * uploading an archive per keystroke.\n */\nexport const PREFLIGHT_TARBALL_PATH =\n \"oh-internal://uploads/00000000-0000-0000-0000-000000000000\";\n\n/**\n * Trigger properties a form field may fill, per trigger kind. A field under a\n * trigger kind whose name is listed here fills the trigger property of the same\n * name; anything else there, such as a phrase to match, is an input to `filter`.\n */\nconst TRIGGER_PROPERTIES: Record<SetupTriggerKind, readonly string[]> = {\n cron: [\"schedule\", \"timezone\"],\n event: [\"source\", \"on\"],\n};\n\nconst OPTIONAL_CREATE_PROPERTIES = [\n \"model\",\n \"timeout\",\n \"agent_profile_id\",\n] as const;\n\n/**\n * Repository properties a form field may fill, read the same way\n * {@link TRIGGER_PROPERTIES} is: a field whose name is listed here fills the\n * `repos[]` property of the same name. `url` and `provider` are absent because\n * neither is answered by a field of its own — they come from the repo-picker's\n * value and from its declared provider.\n *\n * These two lists are the only field names the derivation knows; everything\n * else it reads it finds by field type. An entry that wants a base branch in\n * its request therefore has to name that field `ref`.\n */\nconst REPO_PROPERTIES: readonly string[] = [\"ref\"];\n\nconst FORM_PLACEHOLDER_PATTERN = /\\{\\{form\\.([A-Za-z0-9_.]+)\\}\\}/g;\n\n/** The repository input, which supplies `repos` and an event trigger's source. */\nfunction findRepoPickerField(\n setup: SetupBlock,\n selectedAction?: string | null,\n) {\n const match = Object.entries(collectFields(setup, null, selectedAction)).find(\n ([, field]) => field.type === \"repo-picker\",\n );\n return match ? { name: match[0], field: match[1] } : null;\n}\n\n/** Every repository the form collected, whether the picker takes one or many. */\nfunction repositories(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string[] {\n const picker = findRepoPickerField(setup, selectedAction);\n return picker ? fieldValues(values[picker.name]) : [];\n}\n\n/**\n * The created automation's name.\n *\n * One repository is worth naming; several are not, so the count stands in\n * rather than a list of names that would not fit.\n */\nfunction deriveName(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string {\n if (\n \"name\" in collectFields(entry.setup, null, selectedAction) &&\n \"name\" in values\n ) {\n return fieldText(values.name);\n }\n\n const repos = repositories(entry.setup, values, selectedAction);\n if (repos.length === 0) return entry.name;\n if (repos.length === 1) return `${entry.name} - ${repos[0]}`;\n // The count is the one word here the host writes rather than reads off the\n // entry, so it is translated. There is no translator to pass in: the\n // derivation runs from a memo, an upload and a test alike.\n const count = i18n.t(I18nKey.SETUP$REPOSITORY_COUNT, {\n total: repos.length,\n });\n return `${entry.name} - ${count}`;\n}\n\n/** The active trigger kind for a direct entry, with its fields. */\nfunction getTrigger(setup: SetupBlock, selectedTrigger?: string | null) {\n const triggers = setup.form.triggers ?? {};\n const entries = Object.entries(triggers);\n if (entries.length === 0) return null;\n const match =\n selectedTrigger && selectedTrigger in triggers\n ? [selectedTrigger, triggers[selectedTrigger as SetupTriggerKind]]\n : entries.length === 1\n ? entries[0]\n : null;\n if (!match) return null;\n const [kind, fields] = match;\n return { kind: kind as SetupTriggerKind, fields: fields ?? {} };\n}\n\nfunction hasPayloadValue(\n value: SetupRequestBody[string] | undefined,\n): value is SetupRequestBody[string] {\n return value !== undefined && value !== null && value !== \"\";\n}\n\nfunction fieldPayloadValue(\n fieldType: string | undefined,\n value: SetupFormValues[string],\n) {\n if (fieldType === \"number\") {\n const text = fieldText(value);\n return text === \"\" ? undefined : Number(text);\n }\n return fieldText(value);\n}\n\nfunction optionalCreateProperties(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n return Object.fromEntries(\n OPTIONAL_CREATE_PROPERTIES.flatMap((name) => {\n if (name === \"model\" && values.agent_profile_id) return [];\n const field = collectFields(setup, null, selectedAction)[name];\n const value = fieldPayloadValue(field?.type, values[name]);\n return hasPayloadValue(value) ? [[name, value]] : [];\n }),\n ) as SetupRequestBody;\n}\n\nfunction repositoryProperty(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n const repoPicker = findRepoPickerField(setup, selectedAction);\n const repos = repositories(setup, values, selectedAction);\n if (repos.length === 0 || !repoPicker?.field.provider) return {};\n\n const declared = REPO_PROPERTIES.filter((name) =>\n hasPayloadValue(fieldText(values[name])),\n );\n return {\n repos: repos.map((url) => ({\n url,\n ...Object.fromEntries(\n declared.map((name) => [name, fieldText(values[name])]),\n ),\n provider: repoPicker.field.provider as string,\n })),\n };\n}\n\nfunction parsePluginSources(\n value: SetupRequestBody[string],\n): SetupRequestBody[string] {\n if (!hasPayloadValue(value) || typeof value !== \"string\") return value;\n const trimmed = value.trim();\n if (!trimmed) return [];\n if (trimmed.startsWith(\"[\")) {\n try {\n return JSON.parse(trimmed) as SetupRequestBody[string];\n } catch {\n return [{ source: trimmed }];\n }\n }\n return trimmed\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => {\n const [source, ...parts] = line.split(/\\s+/);\n const plugin: Record<string, string> = { source };\n parts.forEach((part) => {\n const [key, ...raw] = part.split(\"=\");\n const valuePart = raw.join(\"=\");\n if ((key === \"ref\" || key === \"repo_path\") && valuePart) {\n plugin[key] = valuePart;\n }\n });\n return plugin;\n });\n}\n\n/**\n * The create request body these form values produce.\n *\n * No entry declares it: `name` comes from the entry, `repos` from the\n * repo-picker field and its declared provider, and `trigger` from the key and\n * fields under `form.triggers`. Only `prompt` and an event `filter` are\n * declared, because only they cannot be read off the form.\n *\n * Returns null for an assisted entry, which hands setup to a conversation\n * instead of sending a body.\n */\nexport function buildCreatePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n /** Bundle/upload entries only: what the upload returned. */\n tarballPath: string = PREFLIGHT_TARBALL_PATH,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const { setup } = entry;\n if (setup.mode !== \"direct\") return null;\n if (setup.bundle) {\n return buildBundlePayload(entry, values, tarballPath, selectedTrigger);\n }\n if (setup.actions) {\n return buildActionPayload(\n entry,\n values,\n tarballPath,\n selectedTrigger,\n selectedActionKey,\n );\n }\n if (!setup.prompt) return null;\n\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n prompt: interpolateText(setup.prompt, scope),\n ...optionalCreateProperties(setup, values),\n ...repositoryProperty(setup, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\nfunction buildActionPayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const kind = selectedActionKind(entry, selectedActionKey);\n if (kind === \"bundle\") return null;\n\n const setup = entry.setup;\n const scope = { form: values, automation: entry };\n const payload: SetupRequestBody = {\n name: deriveName(entry, values, selectedActionKey),\n };\n\n const trigger = buildTrigger(\n entry,\n values,\n selectedTrigger,\n selectedActionKey,\n );\n if (trigger) payload.trigger = trigger;\n\n Object.assign(\n payload,\n optionalCreateProperties(setup, values, selectedActionKey),\n );\n\n if (kind === \"upload\") {\n const action = setup.actions?.upload;\n if (!action) return null;\n payload.tarball_path =\n tarballPath || interpolateText(action.tarballPath, scope);\n payload.entrypoint = interpolateText(action.entrypoint, scope);\n if (action.setupScript) {\n const setupScript = interpolateText(action.setupScript, scope);\n if (hasPayloadValue(setupScript)) payload.setup_script_path = setupScript;\n }\n return payload;\n }\n\n const action =\n kind === \"plugin\" ? setup.actions?.plugin : setup.actions?.prompt;\n if (!action) return null;\n payload.prompt = interpolateText(action.prompt, scope);\n Object.assign(payload, repositoryProperty(setup, values, selectedActionKey));\n\n if (kind === \"plugin\") {\n const pluginAction = setup.actions?.plugin;\n if (!pluginAction) return null;\n payload.plugins = parsePluginSources(\n interpolateValue(pluginAction.plugins, scope),\n );\n }\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\n/**\n * The `template` provenance a prompt entry sends, or undefined for one that\n * publishes no version.\n *\n * It is what lets the service recognise a second setup of the same entry as\n * the automation it already holds. The bundle path derives the same block from\n * the config the bundle declares; a prompt entry declares none, so the answer\n * to \"what was this created from\" is the form as submitted.\n */\nfunction buildTemplate(\n entry: SetupEntry,\n values: SetupFormValues,\n): SetupRequestBody | undefined {\n if (!entry.version) return undefined;\n return {\n id: entry.id,\n version: entry.version,\n config: { ...values } as SetupRequestBody,\n };\n}\n\n/**\n * The `trigger` object, read off the key and fields under `form.triggers`.\n *\n * Identical for both kinds of direct entry: only the create endpoint and what\n * the automation is told to do differ between a prompt and a bundle.\n */\nfunction buildTrigger(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | undefined {\n const trigger = getTrigger(entry.setup, selectedTrigger);\n if (!trigger) return undefined;\n\n const properties = TRIGGER_PROPERTIES[trigger.kind];\n const declared = Object.keys(trigger.fields).filter((name) =>\n properties.includes(name),\n );\n const repoPicker = findRepoPickerField(entry.setup, selectedAction);\n const derived = Object.fromEntries(\n declared\n .map((name) => [name, fieldText(values[name])])\n .filter(([, value]) => hasPayloadValue(value)),\n );\n\n const filter = entry.setup.filter\n ? interpolateText(entry.setup.filter, {\n form: values,\n automation: entry,\n })\n : undefined;\n\n return {\n type: trigger.kind,\n ...derived,\n ...(trigger.kind === \"event\" && {\n ...(!(\"source\" in derived) && {\n source: repoPicker?.field.provider ?? \"\",\n }),\n ...(hasPayloadValue(filter) && { filter }),\n }),\n };\n}\n\n/**\n * The raw create body a bundle entry produces.\n *\n * `tarball_path` is the one value neither declared nor derived: the host packs\n * and uploads the bundle first, and creates from what came back. `template` is\n * the provenance that makes enabling the same entry twice return the\n * automation that already exists rather than a second one.\n *\n * There is no `repos`: the raw endpoint has no such field, and a bundle's\n * script fetches what it needs itself.\n */\nfunction buildBundlePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n): SetupRequestBody {\n const bundle = entry.setup.bundle!;\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n ...optionalCreateProperties(entry.setup, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n payload.tarball_path = tarballPath;\n payload.entrypoint = bundle.entrypoint;\n if (bundle.setupScript) payload.setup_script_path = bundle.setupScript;\n if (bundle.timeout !== undefined) payload.timeout = bundle.timeout;\n payload.template = {\n id: entry.id,\n version: bundle.version,\n config: interpolateConfig(bundle.config, scope) as SetupRequestBody,\n };\n\n return payload;\n}\n\n/**\n * Placeholder substitution over the config tree. Only string leaves are\n * templated; a number, a boolean or a null is written through as itself, so an\n * entry can state a value the script reads as the type it expects.\n */\nfunction interpolateConfig(\n node: SetupBundleConfigValue,\n scope: Parameters<typeof interpolateText>[1],\n): SetupBundleConfigValue {\n if (typeof node === \"string\") {\n return interpolateValue(node, scope);\n }\n if (Array.isArray(node)) {\n return node.map((item) => interpolateConfig(item, scope));\n }\n if (typeof node === \"object\" && node !== null) {\n return Object.fromEntries(\n Object.entries(node).map(([key, value]) => [\n key,\n interpolateConfig(value, scope),\n ]),\n );\n }\n return node;\n}\n\n/**\n * The preflight body the host sends. The same shape for every entry, so no\n * entry declares it.\n */\nexport function buildPreflightBody(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | null {\n const draft = buildCreatePayload(\n entry,\n values,\n PREFLIGHT_TARBALL_PATH,\n selectedTrigger,\n selectedAction,\n );\n if (!draft) return null;\n\n return {\n automationId: entry.id,\n endpoint: automationCreateEndpoint(entry, selectedAction),\n draft,\n };\n}\n\n/**\n * What an assisted entry sends into the conversation that finishes setup.\n *\n * The command is not declared either: it lives once, in the owning skill's own\n * `triggers` frontmatter, and the skill defaults to the entry's id. An entry\n * whose skill is invoked by description rather than by command contributes\n * nothing here, and the answers alone open the conversation.\n */\nexport function buildAssistedMessage(\n entry: SetupEntry,\n values: SetupFormValues,\n): string {\n const command = findAutomationCommand(entry);\n const message = entry.setup.message\n ? interpolateText(entry.setup.message, { form: values, automation: entry })\n : \"\";\n\n return [command, message].filter(Boolean).join(\"\\n\\n\");\n}\n\nfunction collectPlaceholderFields(value: string): string[] {\n const names = Array.from(\n value.matchAll(FORM_PLACEHOLDER_PATTERN),\n (match) => match[1],\n );\n return Array.from(new Set(names));\n}\n\n/**\n * Which form fields built each payload path.\n *\n * Preflight and the create endpoint reject a draft by payload path, and the\n * host has to turn that back into a highlighted input. Building the body with\n * each field standing in for its own value recovers the mapping exactly, so an\n * entry does not declare it.\n */\nexport function deriveErrorMap(\n entry: SetupEntry,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): Record<string, string[]> {\n const mapping: Record<string, string[]> = {};\n\n const triggerEntries = Object.keys(entry.setup.form.triggers ?? {});\n const triggerVariants = selectedTrigger\n ? [selectedTrigger]\n : triggerEntries.length > 1\n ? triggerEntries\n : [null];\n\n const actionEntries = actionKinds(entry.setup);\n const actionVariants = selectedAction\n ? [selectedAction]\n : actionEntries.length > 1\n ? actionEntries\n : [null];\n\n const walk = (node: unknown, path: string): void => {\n if (Array.isArray(node)) {\n node.forEach((item, index) => walk(item, `${path}[${index}]`));\n return;\n }\n if (typeof node === \"object\" && node !== null) {\n Object.entries(node).forEach(([key, item]) =>\n walk(item, path ? `${path}.${key}` : key),\n );\n return;\n }\n if (typeof node === \"string\") {\n const names = collectPlaceholderFields(node);\n if (names.length > 0) {\n mapping[path] = Array.from(\n new Set([...(mapping[path] ?? []), ...names]),\n );\n }\n }\n };\n\n actionVariants.forEach((actionVariant) => {\n triggerVariants.forEach((triggerVariant) => {\n const names = Object.keys(\n collectFields(entry.setup, triggerVariant, actionVariant),\n );\n const variantStandIns = Object.fromEntries(\n names.map((name) => [name, `{{form.${name}}}`]),\n );\n const template = buildCreatePayload(\n entry,\n variantStandIns,\n actionVariant === \"upload\" ? \"\" : PREFLIGHT_TARBALL_PATH,\n triggerVariant,\n actionVariant,\n );\n if (template) walk(template, \"\");\n });\n });\n return mapping;\n}\n"],"mappings":"gNA6CA,SAAgB,EACd,EACA,EACQ,CACR,IAAM,EAAO,EAAmB,EAAO,CAAc,EAKrD,OAJI,IAAS,SAAiB,EAAA,sBAAsB,cAAc,EAC9D,IAAS,UAAY,IAAS,SACzB,EAAsB,cAAc,EAEtC,EAAA,sBAAsB,cAAc,CAC7C,CAGA,SAAgB,GAAmC,CACjD,OAAO,EAAsB,SAAS,CACxC,CAUA,SAAS,EACP,EACQ,CACR,IAAM,EAAO,EAAA,sBAAsB,CAAI,EACvC,GAAI,CAAC,EACH,MAAU,MACR,mDAAmD,EAAK,iFAE1D,EAEF,OAAO,CACT,CAoCA,SAAgB,EAAc,EAA4B,CACxD,OAAO,EAAM,MAAM,OAAS,UAAY,EAAM,MAAM,SAAW,IAAA,EACjE,CASA,SAAgB,EACd,EACA,EACuC,CACvC,GAAI,CAAC,EAAO,MAAO,SACnB,GAAI,EAAc,CAAK,EAAG,MAAO,SACjC,IAAM,EAAU,EAAM,MAAM,SAAW,CAAC,EACxC,GAAI,GAAkB,KAAkB,EACtC,OAAO,EAET,IAAM,EAAO,EAAA,YAAY,EAAM,KAAK,EAEpC,OADI,EAAK,SAAW,EAAU,EAAK,GAC5B,QACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"automation-setup.js","names":[],"sources":["../../src/manifests/automation-setup.ts"],"sourcesContent":["/**\n * The only module that knows a setup entry configures an *automation*.\n *\n * The published contract states just what varies between entries; everything\n * derivable is the host's to generate. That derivation is here, and nowhere\n * else, so the rest of `src/manifests/` stays about forms rather than about\n * automations.\n *\n * The algorithm mirrors `tests/test_automation_setup.py::_render_payload` in\n * `OpenHands/extensions`, which is the authoritative reference: it produces the\n * request bodies published in that repository's contract fixtures, and those\n * were verified against the live service. The create model is `extra=\"forbid\"`,\n * so any divergence is a hard 422 rather than a dropped field.\n */\n\nimport i18n from \"#/i18n\";\nimport { I18nKey } from \"#/i18n/declaration\";\nimport { findAutomationCommand } from \"#/utils/automation-catalog\";\nimport { getAutomationEndpoint } from \"./automation-interface\";\nimport {\n actionKinds,\n collectFields,\n fieldText,\n fieldValues,\n} from \"./manifest-local-validation\";\nimport { interpolateText, interpolateValue } from \"./manifest-template\";\nimport type {\n SetupActionKind,\n SetupBlock,\n SetupBundleConfigValue,\n SetupEntry,\n SetupFormValues,\n SetupRequestBody,\n SetupTriggerKind,\n} from \"./types\";\n\n/**\n * The creation endpoint a derived draft would be posted to. Resolved on call\n * rather than at import, because the endpoint is the interface manifest's and\n * this module loads whether or not one was admitted.\n *\n * A bundle entry is created through the raw endpoint, because what it sends is\n * a tarball it uploaded rather than arguments to a preset. Called without an\n * entry - as the import path does - it answers for a prompt.\n */\nexport function automationCreateEndpoint(\n entry?: SetupEntry,\n selectedAction?: string | null,\n): string {\n const kind = selectedActionKind(entry, selectedAction);\n if (kind === \"plugin\") return getAutomationEndpoint(\"createPlugin\");\n if (kind === \"upload\" || kind === \"bundle\") {\n return requireBundleEndpoint(\"createBundle\");\n }\n return getAutomationEndpoint(\"createPrompt\");\n}\n\n/** Where a bundle's tarball is uploaded, before the create call. */\nexport function automationUploadEndpoint(): string {\n return requireBundleEndpoint(\"uploads\");\n}\n\n/** The endpoints a bundle entry cannot be created without. */\nconst BUNDLE_ENDPOINTS = [\"createBundle\", \"uploads\"] as const;\n\n/**\n * An endpoint only a bundle needs. The interface manifest may predate bundles,\n * and the host holds no path of its own to fall back to, so this is where that\n * runs out rather than somewhere deep in a request.\n */\nfunction requireBundleEndpoint(\n name: (typeof BUNDLE_ENDPOINTS)[number],\n): string {\n const path = getAutomationEndpoint(name);\n if (!path) {\n throw new Error(\n `The published automation interface declares no '${name}' endpoint, ` +\n \"so this deployment cannot create an automation from a script bundle.\",\n );\n }\n return path;\n}\n\n/**\n * The endpoints this entry needs that the published interface does not declare.\n *\n * Asked before the form renders rather than discovered at the moment of\n * creating: a pinned package that predates bundles can never answer, and the\n * dialog can say so while nothing has been filled in yet. Empty for every\n * entry that is not a bundle, which needs nothing beyond what the block has\n * always declared.\n */\nfunction missingEndpointsForAction(kind: SetupActionKind | \"bundle\"): string[] {\n return kind === \"upload\" || kind === \"bundle\"\n ? BUNDLE_ENDPOINTS.filter((name) => !getAutomationEndpoint(name))\n : [];\n}\n\nexport function missingCreateEndpoints(\n entry: SetupEntry,\n selectedAction?: string | null,\n): string[] {\n if (isBundleEntry(entry)) return missingEndpointsForAction(\"bundle\");\n if (!entry.setup.actions) return [];\n\n if (selectedAction && selectedAction in entry.setup.actions) {\n return missingEndpointsForAction(selectedAction as SetupActionKind);\n }\n\n const missingByAction = actionKinds(entry.setup).map(\n missingEndpointsForAction,\n );\n if (missingByAction.some((missing) => missing.length === 0)) return [];\n return Array.from(new Set(missingByAction.flat()));\n}\n\n/** Whether this entry ships a script tarball instead of a prompt. */\nexport function isBundleEntry(entry: SetupEntry): boolean {\n return entry.setup.mode === \"direct\" && entry.setup.bundle !== undefined;\n}\n\nexport function isUploadAction(\n entry: SetupEntry,\n selectedAction?: string | null,\n): boolean {\n return selectedActionKind(entry, selectedAction) === \"upload\";\n}\n\nexport function selectedActionKind(\n entry: SetupEntry | undefined,\n selectedAction?: string | null,\n): SetupActionKind | \"bundle\" | \"prompt\" {\n if (!entry) return \"prompt\";\n if (isBundleEntry(entry)) return \"bundle\";\n const actions = entry.setup.actions ?? {};\n if (selectedAction && selectedAction in actions) {\n return selectedAction as SetupActionKind;\n }\n const keys = actionKinds(entry.setup);\n if (keys.length === 1) return keys[0];\n return \"prompt\";\n}\n\n/**\n * The `tarball_path` a preflight draft carries.\n *\n * Preflight runs on every field blur and the upload happens once, at submit,\n * so there is no real path to send yet. The service checks this field's scheme\n * at preflight and its ownership only at creation, so a well-formed stand-in\n * validates exactly what preflight is for - the rest of the body - without\n * uploading an archive per keystroke.\n */\nexport const PREFLIGHT_TARBALL_PATH =\n \"oh-internal://uploads/00000000-0000-0000-0000-000000000000\";\n\n/**\n * Trigger properties a form field may fill, per trigger kind. A field under a\n * trigger kind whose name is listed here fills the trigger property of the same\n * name; anything else there, such as a phrase to match, is an input to `filter`.\n */\nconst TRIGGER_PROPERTIES: Record<SetupTriggerKind, readonly string[]> = {\n cron: [\"schedule\", \"timezone\"],\n event: [\"source\", \"on\"],\n};\n\nconst OPTIONAL_CREATE_PROPERTIES = [\"model\", \"timeout\"] as const;\n\n/**\n * Repository properties a form field may fill, read the same way\n * {@link TRIGGER_PROPERTIES} is: a field whose name is listed here fills the\n * `repos[]` property of the same name. `url` and `provider` are absent because\n * neither is answered by a field of its own — they come from the repo-picker's\n * value and from its declared provider.\n *\n * These two lists are the only field names the derivation knows; everything\n * else it reads it finds by field type. An entry that wants a base branch in\n * its request therefore has to name that field `ref`.\n */\nconst REPO_PROPERTIES: readonly string[] = [\"ref\"];\n\nconst FORM_PLACEHOLDER_PATTERN = /\\{\\{form\\.([A-Za-z0-9_.]+)\\}\\}/g;\n\n/** The repository input, which supplies `repos` and an event trigger's source. */\nfunction findRepoPickerField(\n setup: SetupBlock,\n selectedAction?: string | null,\n) {\n const match = Object.entries(collectFields(setup, null, selectedAction)).find(\n ([, field]) => field.type === \"repo-picker\",\n );\n return match ? { name: match[0], field: match[1] } : null;\n}\n\n/** Every repository the form collected, whether the picker takes one or many. */\nfunction repositories(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string[] {\n const picker = findRepoPickerField(setup, selectedAction);\n return picker ? fieldValues(values[picker.name]) : [];\n}\n\n/**\n * The created automation's name.\n *\n * One repository is worth naming; several are not, so the count stands in\n * rather than a list of names that would not fit.\n */\nfunction deriveName(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string {\n if (\n \"name\" in collectFields(entry.setup, null, selectedAction) &&\n \"name\" in values\n ) {\n return fieldText(values.name);\n }\n\n const repos = repositories(entry.setup, values, selectedAction);\n if (repos.length === 0) return entry.name;\n if (repos.length === 1) return `${entry.name} - ${repos[0]}`;\n // The count is the one word here the host writes rather than reads off the\n // entry, so it is translated. There is no translator to pass in: the\n // derivation runs from a memo, an upload and a test alike.\n const count = i18n.t(I18nKey.SETUP$REPOSITORY_COUNT, {\n total: repos.length,\n });\n return `${entry.name} - ${count}`;\n}\n\n/** The active trigger kind for a direct entry, with its fields. */\nfunction getTrigger(setup: SetupBlock, selectedTrigger?: string | null) {\n const triggers = setup.form.triggers ?? {};\n const entries = Object.entries(triggers);\n if (entries.length === 0) return null;\n const match =\n selectedTrigger && selectedTrigger in triggers\n ? [selectedTrigger, triggers[selectedTrigger as SetupTriggerKind]]\n : entries.length === 1\n ? entries[0]\n : null;\n if (!match) return null;\n const [kind, fields] = match;\n return { kind: kind as SetupTriggerKind, fields: fields ?? {} };\n}\n\nfunction hasPayloadValue(\n value: SetupRequestBody[string] | undefined,\n): value is SetupRequestBody[string] {\n return value !== undefined && value !== null && value !== \"\";\n}\n\nfunction fieldPayloadValue(\n fieldType: string | undefined,\n value: SetupFormValues[string],\n) {\n if (fieldType === \"number\") {\n const text = fieldText(value);\n return text === \"\" ? undefined : Number(text);\n }\n return fieldText(value);\n}\n\nfunction optionalCreateProperties(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n return Object.fromEntries(\n OPTIONAL_CREATE_PROPERTIES.flatMap((name) => {\n const field = collectFields(setup, null, selectedAction)[name];\n const value = fieldPayloadValue(field?.type, values[name]);\n return hasPayloadValue(value) ? [[name, value]] : [];\n }),\n ) as SetupRequestBody;\n}\n\nfunction repositoryProperty(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n const repoPicker = findRepoPickerField(setup, selectedAction);\n const repos = repositories(setup, values, selectedAction);\n if (repos.length === 0 || !repoPicker?.field.provider) return {};\n\n const declared = REPO_PROPERTIES.filter((name) =>\n hasPayloadValue(fieldText(values[name])),\n );\n return {\n repos: repos.map((url) => ({\n url,\n ...Object.fromEntries(\n declared.map((name) => [name, fieldText(values[name])]),\n ),\n provider: repoPicker.field.provider as string,\n })),\n };\n}\n\nfunction parsePluginSources(\n value: SetupRequestBody[string],\n): SetupRequestBody[string] {\n if (!hasPayloadValue(value) || typeof value !== \"string\") return value;\n const trimmed = value.trim();\n if (!trimmed) return [];\n if (trimmed.startsWith(\"[\")) {\n try {\n return JSON.parse(trimmed) as SetupRequestBody[string];\n } catch {\n return [{ source: trimmed }];\n }\n }\n return trimmed\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => {\n const [source, ...parts] = line.split(/\\s+/);\n const plugin: Record<string, string> = { source };\n parts.forEach((part) => {\n const [key, ...raw] = part.split(\"=\");\n const valuePart = raw.join(\"=\");\n if ((key === \"ref\" || key === \"repo_path\") && valuePart) {\n plugin[key] = valuePart;\n }\n });\n return plugin;\n });\n}\n\n/**\n * The create request body these form values produce.\n *\n * No entry declares it: `name` comes from the entry, `repos` from the\n * repo-picker field and its declared provider, and `trigger` from the key and\n * fields under `form.triggers`. Only `prompt` and an event `filter` are\n * declared, because only they cannot be read off the form.\n *\n * Returns null for an assisted entry, which hands setup to a conversation\n * instead of sending a body.\n */\nexport function buildCreatePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n /** Bundle/upload entries only: what the upload returned. */\n tarballPath: string = PREFLIGHT_TARBALL_PATH,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const { setup } = entry;\n if (setup.mode !== \"direct\") return null;\n if (setup.bundle) {\n return buildBundlePayload(entry, values, tarballPath, selectedTrigger);\n }\n if (setup.actions) {\n return buildActionPayload(\n entry,\n values,\n tarballPath,\n selectedTrigger,\n selectedActionKey,\n );\n }\n if (!setup.prompt) return null;\n\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n prompt: interpolateText(setup.prompt, scope),\n ...optionalCreateProperties(setup, values),\n ...repositoryProperty(setup, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\nfunction buildActionPayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const kind = selectedActionKind(entry, selectedActionKey);\n if (kind === \"bundle\") return null;\n\n const setup = entry.setup;\n const scope = { form: values, automation: entry };\n const payload: SetupRequestBody = {\n name: deriveName(entry, values, selectedActionKey),\n };\n\n const trigger = buildTrigger(\n entry,\n values,\n selectedTrigger,\n selectedActionKey,\n );\n if (trigger) payload.trigger = trigger;\n\n Object.assign(\n payload,\n optionalCreateProperties(setup, values, selectedActionKey),\n );\n\n if (kind === \"upload\") {\n const action = setup.actions?.upload;\n if (!action) return null;\n payload.tarball_path =\n tarballPath || interpolateText(action.tarballPath, scope);\n payload.entrypoint = interpolateText(action.entrypoint, scope);\n if (action.setupScript) {\n const setupScript = interpolateText(action.setupScript, scope);\n if (hasPayloadValue(setupScript)) payload.setup_script_path = setupScript;\n }\n return payload;\n }\n\n const action =\n kind === \"plugin\" ? setup.actions?.plugin : setup.actions?.prompt;\n if (!action) return null;\n payload.prompt = interpolateText(action.prompt, scope);\n Object.assign(payload, repositoryProperty(setup, values, selectedActionKey));\n\n if (kind === \"plugin\") {\n const pluginAction = setup.actions?.plugin;\n if (!pluginAction) return null;\n payload.plugins = parsePluginSources(\n interpolateValue(pluginAction.plugins, scope),\n );\n }\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\n/**\n * The `template` provenance a prompt entry sends, or undefined for one that\n * publishes no version.\n *\n * It is what lets the service recognise a second setup of the same entry as\n * the automation it already holds. The bundle path derives the same block from\n * the config the bundle declares; a prompt entry declares none, so the answer\n * to \"what was this created from\" is the form as submitted.\n */\nfunction buildTemplate(\n entry: SetupEntry,\n values: SetupFormValues,\n): SetupRequestBody | undefined {\n if (!entry.version) return undefined;\n return {\n id: entry.id,\n version: entry.version,\n config: { ...values } as SetupRequestBody,\n };\n}\n\n/**\n * The `trigger` object, read off the key and fields under `form.triggers`.\n *\n * Identical for both kinds of direct entry: only the create endpoint and what\n * the automation is told to do differ between a prompt and a bundle.\n */\nfunction buildTrigger(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | undefined {\n const trigger = getTrigger(entry.setup, selectedTrigger);\n if (!trigger) return undefined;\n\n const properties = TRIGGER_PROPERTIES[trigger.kind];\n const declared = Object.keys(trigger.fields).filter((name) =>\n properties.includes(name),\n );\n const repoPicker = findRepoPickerField(entry.setup, selectedAction);\n const derived = Object.fromEntries(\n declared\n .map((name) => [name, fieldText(values[name])])\n .filter(([, value]) => hasPayloadValue(value)),\n );\n\n const filter = entry.setup.filter\n ? interpolateText(entry.setup.filter, {\n form: values,\n automation: entry,\n })\n : undefined;\n\n return {\n type: trigger.kind,\n ...derived,\n ...(trigger.kind === \"event\" && {\n ...(!(\"source\" in derived) && {\n source: repoPicker?.field.provider ?? \"\",\n }),\n ...(hasPayloadValue(filter) && { filter }),\n }),\n };\n}\n\n/**\n * The raw create body a bundle entry produces.\n *\n * `tarball_path` is the one value neither declared nor derived: the host packs\n * and uploads the bundle first, and creates from what came back. `template` is\n * the provenance that makes enabling the same entry twice return the\n * automation that already exists rather than a second one.\n *\n * There is no `repos`: the raw endpoint has no such field, and a bundle's\n * script fetches what it needs itself.\n */\nfunction buildBundlePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n): SetupRequestBody {\n const bundle = entry.setup.bundle!;\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n payload.tarball_path = tarballPath;\n payload.entrypoint = bundle.entrypoint;\n if (bundle.setupScript) payload.setup_script_path = bundle.setupScript;\n if (bundle.timeout !== undefined) payload.timeout = bundle.timeout;\n payload.template = {\n id: entry.id,\n version: bundle.version,\n config: interpolateConfig(bundle.config, scope) as SetupRequestBody,\n };\n\n return payload;\n}\n\n/**\n * Placeholder substitution over the config tree. Only string leaves are\n * templated; a number, a boolean or a null is written through as itself, so an\n * entry can state a value the script reads as the type it expects.\n */\nfunction interpolateConfig(\n node: SetupBundleConfigValue,\n scope: Parameters<typeof interpolateText>[1],\n): SetupBundleConfigValue {\n if (typeof node === \"string\") {\n return interpolateValue(node, scope);\n }\n if (Array.isArray(node)) {\n return node.map((item) => interpolateConfig(item, scope));\n }\n if (typeof node === \"object\" && node !== null) {\n return Object.fromEntries(\n Object.entries(node).map(([key, value]) => [\n key,\n interpolateConfig(value, scope),\n ]),\n );\n }\n return node;\n}\n\n/**\n * The preflight body the host sends. The same shape for every entry, so no\n * entry declares it.\n */\nexport function buildPreflightBody(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | null {\n const draft = buildCreatePayload(\n entry,\n values,\n PREFLIGHT_TARBALL_PATH,\n selectedTrigger,\n selectedAction,\n );\n if (!draft) return null;\n\n return {\n automationId: entry.id,\n endpoint: automationCreateEndpoint(entry, selectedAction),\n draft,\n };\n}\n\n/**\n * What an assisted entry sends into the conversation that finishes setup.\n *\n * The command is not declared either: it lives once, in the owning skill's own\n * `triggers` frontmatter, and the skill defaults to the entry's id. An entry\n * whose skill is invoked by description rather than by command contributes\n * nothing here, and the answers alone open the conversation.\n */\nexport function buildAssistedMessage(\n entry: SetupEntry,\n values: SetupFormValues,\n): string {\n const command = findAutomationCommand(entry);\n const message = entry.setup.message\n ? interpolateText(entry.setup.message, { form: values, automation: entry })\n : \"\";\n\n return [command, message].filter(Boolean).join(\"\\n\\n\");\n}\n\nfunction collectPlaceholderFields(value: string): string[] {\n const names = Array.from(\n value.matchAll(FORM_PLACEHOLDER_PATTERN),\n (match) => match[1],\n );\n return Array.from(new Set(names));\n}\n\n/**\n * Which form fields built each payload path.\n *\n * Preflight and the create endpoint reject a draft by payload path, and the\n * host has to turn that back into a highlighted input. Building the body with\n * each field standing in for its own value recovers the mapping exactly, so an\n * entry does not declare it.\n */\nexport function deriveErrorMap(\n entry: SetupEntry,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): Record<string, string[]> {\n const mapping: Record<string, string[]> = {};\n\n const triggerEntries = Object.keys(entry.setup.form.triggers ?? {});\n const triggerVariants = selectedTrigger\n ? [selectedTrigger]\n : triggerEntries.length > 1\n ? triggerEntries\n : [null];\n\n const actionEntries = actionKinds(entry.setup);\n const actionVariants = selectedAction\n ? [selectedAction]\n : actionEntries.length > 1\n ? actionEntries\n : [null];\n\n const walk = (node: unknown, path: string): void => {\n if (Array.isArray(node)) {\n node.forEach((item, index) => walk(item, `${path}[${index}]`));\n return;\n }\n if (typeof node === \"object\" && node !== null) {\n Object.entries(node).forEach(([key, item]) =>\n walk(item, path ? `${path}.${key}` : key),\n );\n return;\n }\n if (typeof node === \"string\") {\n const names = collectPlaceholderFields(node);\n if (names.length > 0) {\n mapping[path] = Array.from(\n new Set([...(mapping[path] ?? []), ...names]),\n );\n }\n }\n };\n\n actionVariants.forEach((actionVariant) => {\n triggerVariants.forEach((triggerVariant) => {\n const names = Object.keys(\n collectFields(entry.setup, triggerVariant, actionVariant),\n );\n const variantStandIns = Object.fromEntries(\n names.map((name) => [name, `{{form.${name}}}`]),\n );\n const template = buildCreatePayload(\n entry,\n variantStandIns,\n actionVariant === \"upload\" ? \"\" : PREFLIGHT_TARBALL_PATH,\n triggerVariant,\n actionVariant,\n );\n if (template) walk(template, \"\");\n });\n });\n return mapping;\n}\n"],"mappings":";;;;;AA6CA,SAAgB,EACd,GACA,GACQ;CACR,IAAM,IAAO,EAAmB,GAAO,CAAc;CAKrD,OAJI,MAAS,WAAiB,EAAsB,cAAc,IAC9D,MAAS,YAAY,MAAS,WACzB,EAAsB,cAAc,IAEtC,EAAsB,cAAc;AAC7C;AAGA,SAAgB,IAAmC;CACjD,OAAO,EAAsB,SAAS;AACxC;AAUA,SAAS,EACP,GACQ;CACR,IAAM,IAAO,EAAsB,CAAI;CACvC,IAAI,CAAC,GACH,MAAU,MACR,mDAAmD,EAAK,iFAE1D;CAEF,OAAO;AACT;AAoCA,SAAgB,EAAc,GAA4B;CACxD,OAAO,EAAM,MAAM,SAAS,YAAY,EAAM,MAAM,WAAW,KAAA;AACjE;AASA,SAAgB,EACd,GACA,GACuC;CACvC,IAAI,CAAC,GAAO,OAAO;CACnB,IAAI,EAAc,CAAK,GAAG,OAAO;CACjC,IAAM,IAAU,EAAM,MAAM,WAAW,CAAC;CACxC,IAAI,KAAkB,KAAkB,GACtC,OAAO;CAET,IAAM,IAAO,EAAY,EAAM,KAAK;CAEpC,OADI,EAAK,WAAW,IAAU,EAAK,KAC5B;AACT"}
1
+ {"version":3,"file":"automation-setup.js","names":[],"sources":["../../src/manifests/automation-setup.ts"],"sourcesContent":["/**\n * The only module that knows a setup entry configures an *automation*.\n *\n * The published contract states just what varies between entries; everything\n * derivable is the host's to generate. That derivation is here, and nowhere\n * else, so the rest of `src/manifests/` stays about forms rather than about\n * automations.\n *\n * The algorithm mirrors `tests/test_automation_setup.py::_render_payload` in\n * `OpenHands/extensions`, which is the authoritative reference: it produces the\n * request bodies published in that repository's contract fixtures, and those\n * were verified against the live service. The create model is `extra=\"forbid\"`,\n * so any divergence is a hard 422 rather than a dropped field.\n */\n\nimport i18n from \"#/i18n\";\nimport { I18nKey } from \"#/i18n/declaration\";\nimport { findAutomationCommand } from \"#/utils/automation-catalog\";\nimport { getAutomationEndpoint } from \"./automation-interface\";\nimport {\n actionKinds,\n collectFields,\n fieldText,\n fieldValues,\n} from \"./manifest-local-validation\";\nimport { interpolateText, interpolateValue } from \"./manifest-template\";\nimport type {\n SetupActionKind,\n SetupBlock,\n SetupBundleConfigValue,\n SetupEntry,\n SetupFormValues,\n SetupRequestBody,\n SetupTriggerKind,\n} from \"./types\";\n\n/**\n * The creation endpoint a derived draft would be posted to. Resolved on call\n * rather than at import, because the endpoint is the interface manifest's and\n * this module loads whether or not one was admitted.\n *\n * A bundle entry is created through the raw endpoint, because what it sends is\n * a tarball it uploaded rather than arguments to a preset. Called without an\n * entry - as the import path does - it answers for a prompt.\n */\nexport function automationCreateEndpoint(\n entry?: SetupEntry,\n selectedAction?: string | null,\n): string {\n const kind = selectedActionKind(entry, selectedAction);\n if (kind === \"plugin\") return getAutomationEndpoint(\"createPlugin\");\n if (kind === \"upload\" || kind === \"bundle\") {\n return requireBundleEndpoint(\"createBundle\");\n }\n return getAutomationEndpoint(\"createPrompt\");\n}\n\n/** Where a bundle's tarball is uploaded, before the create call. */\nexport function automationUploadEndpoint(): string {\n return requireBundleEndpoint(\"uploads\");\n}\n\n/** The endpoints a bundle entry cannot be created without. */\nconst BUNDLE_ENDPOINTS = [\"createBundle\", \"uploads\"] as const;\n\n/**\n * An endpoint only a bundle needs. The interface manifest may predate bundles,\n * and the host holds no path of its own to fall back to, so this is where that\n * runs out rather than somewhere deep in a request.\n */\nfunction requireBundleEndpoint(\n name: (typeof BUNDLE_ENDPOINTS)[number],\n): string {\n const path = getAutomationEndpoint(name);\n if (!path) {\n throw new Error(\n `The published automation interface declares no '${name}' endpoint, ` +\n \"so this deployment cannot create an automation from a script bundle.\",\n );\n }\n return path;\n}\n\n/**\n * The endpoints this entry needs that the published interface does not declare.\n *\n * Asked before the form renders rather than discovered at the moment of\n * creating: a pinned package that predates bundles can never answer, and the\n * dialog can say so while nothing has been filled in yet. Empty for every\n * entry that is not a bundle, which needs nothing beyond what the block has\n * always declared.\n */\nfunction missingEndpointsForAction(kind: SetupActionKind | \"bundle\"): string[] {\n return kind === \"upload\" || kind === \"bundle\"\n ? BUNDLE_ENDPOINTS.filter((name) => !getAutomationEndpoint(name))\n : [];\n}\n\nexport function missingCreateEndpoints(\n entry: SetupEntry,\n selectedAction?: string | null,\n): string[] {\n if (isBundleEntry(entry)) return missingEndpointsForAction(\"bundle\");\n if (!entry.setup.actions) return [];\n\n if (selectedAction && selectedAction in entry.setup.actions) {\n return missingEndpointsForAction(selectedAction as SetupActionKind);\n }\n\n const missingByAction = actionKinds(entry.setup).map(\n missingEndpointsForAction,\n );\n if (missingByAction.some((missing) => missing.length === 0)) return [];\n return Array.from(new Set(missingByAction.flat()));\n}\n\n/** Whether this entry ships a script tarball instead of a prompt. */\nexport function isBundleEntry(entry: SetupEntry): boolean {\n return entry.setup.mode === \"direct\" && entry.setup.bundle !== undefined;\n}\n\nexport function isUploadAction(\n entry: SetupEntry,\n selectedAction?: string | null,\n): boolean {\n return selectedActionKind(entry, selectedAction) === \"upload\";\n}\n\nexport function selectedActionKind(\n entry: SetupEntry | undefined,\n selectedAction?: string | null,\n): SetupActionKind | \"bundle\" | \"prompt\" {\n if (!entry) return \"prompt\";\n if (isBundleEntry(entry)) return \"bundle\";\n const actions = entry.setup.actions ?? {};\n if (selectedAction && selectedAction in actions) {\n return selectedAction as SetupActionKind;\n }\n const keys = actionKinds(entry.setup);\n if (keys.length === 1) return keys[0];\n return \"prompt\";\n}\n\n/**\n * The `tarball_path` a preflight draft carries.\n *\n * Preflight runs on every field blur and the upload happens once, at submit,\n * so there is no real path to send yet. The service checks this field's scheme\n * at preflight and its ownership only at creation, so a well-formed stand-in\n * validates exactly what preflight is for - the rest of the body - without\n * uploading an archive per keystroke.\n */\nexport const PREFLIGHT_TARBALL_PATH =\n \"oh-internal://uploads/00000000-0000-0000-0000-000000000000\";\n\n/**\n * Trigger properties a form field may fill, per trigger kind. A field under a\n * trigger kind whose name is listed here fills the trigger property of the same\n * name; anything else there, such as a phrase to match, is an input to `filter`.\n */\nconst TRIGGER_PROPERTIES: Record<SetupTriggerKind, readonly string[]> = {\n cron: [\"schedule\", \"timezone\"],\n event: [\"source\", \"on\"],\n};\n\nconst OPTIONAL_CREATE_PROPERTIES = [\n \"model\",\n \"timeout\",\n \"agent_profile_id\",\n] as const;\n\n/**\n * Repository properties a form field may fill, read the same way\n * {@link TRIGGER_PROPERTIES} is: a field whose name is listed here fills the\n * `repos[]` property of the same name. `url` and `provider` are absent because\n * neither is answered by a field of its own — they come from the repo-picker's\n * value and from its declared provider.\n *\n * These two lists are the only field names the derivation knows; everything\n * else it reads it finds by field type. An entry that wants a base branch in\n * its request therefore has to name that field `ref`.\n */\nconst REPO_PROPERTIES: readonly string[] = [\"ref\"];\n\nconst FORM_PLACEHOLDER_PATTERN = /\\{\\{form\\.([A-Za-z0-9_.]+)\\}\\}/g;\n\n/** The repository input, which supplies `repos` and an event trigger's source. */\nfunction findRepoPickerField(\n setup: SetupBlock,\n selectedAction?: string | null,\n) {\n const match = Object.entries(collectFields(setup, null, selectedAction)).find(\n ([, field]) => field.type === \"repo-picker\",\n );\n return match ? { name: match[0], field: match[1] } : null;\n}\n\n/** Every repository the form collected, whether the picker takes one or many. */\nfunction repositories(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string[] {\n const picker = findRepoPickerField(setup, selectedAction);\n return picker ? fieldValues(values[picker.name]) : [];\n}\n\n/**\n * The created automation's name.\n *\n * One repository is worth naming; several are not, so the count stands in\n * rather than a list of names that would not fit.\n */\nfunction deriveName(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedAction?: string | null,\n): string {\n if (\n \"name\" in collectFields(entry.setup, null, selectedAction) &&\n \"name\" in values\n ) {\n return fieldText(values.name);\n }\n\n const repos = repositories(entry.setup, values, selectedAction);\n if (repos.length === 0) return entry.name;\n if (repos.length === 1) return `${entry.name} - ${repos[0]}`;\n // The count is the one word here the host writes rather than reads off the\n // entry, so it is translated. There is no translator to pass in: the\n // derivation runs from a memo, an upload and a test alike.\n const count = i18n.t(I18nKey.SETUP$REPOSITORY_COUNT, {\n total: repos.length,\n });\n return `${entry.name} - ${count}`;\n}\n\n/** The active trigger kind for a direct entry, with its fields. */\nfunction getTrigger(setup: SetupBlock, selectedTrigger?: string | null) {\n const triggers = setup.form.triggers ?? {};\n const entries = Object.entries(triggers);\n if (entries.length === 0) return null;\n const match =\n selectedTrigger && selectedTrigger in triggers\n ? [selectedTrigger, triggers[selectedTrigger as SetupTriggerKind]]\n : entries.length === 1\n ? entries[0]\n : null;\n if (!match) return null;\n const [kind, fields] = match;\n return { kind: kind as SetupTriggerKind, fields: fields ?? {} };\n}\n\nfunction hasPayloadValue(\n value: SetupRequestBody[string] | undefined,\n): value is SetupRequestBody[string] {\n return value !== undefined && value !== null && value !== \"\";\n}\n\nfunction fieldPayloadValue(\n fieldType: string | undefined,\n value: SetupFormValues[string],\n) {\n if (fieldType === \"number\") {\n const text = fieldText(value);\n return text === \"\" ? undefined : Number(text);\n }\n return fieldText(value);\n}\n\nfunction optionalCreateProperties(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n return Object.fromEntries(\n OPTIONAL_CREATE_PROPERTIES.flatMap((name) => {\n if (name === \"model\" && values.agent_profile_id) return [];\n const field = collectFields(setup, null, selectedAction)[name];\n const value = fieldPayloadValue(field?.type, values[name]);\n return hasPayloadValue(value) ? [[name, value]] : [];\n }),\n ) as SetupRequestBody;\n}\n\nfunction repositoryProperty(\n setup: SetupBlock,\n values: SetupFormValues,\n selectedAction?: string | null,\n): SetupRequestBody {\n const repoPicker = findRepoPickerField(setup, selectedAction);\n const repos = repositories(setup, values, selectedAction);\n if (repos.length === 0 || !repoPicker?.field.provider) return {};\n\n const declared = REPO_PROPERTIES.filter((name) =>\n hasPayloadValue(fieldText(values[name])),\n );\n return {\n repos: repos.map((url) => ({\n url,\n ...Object.fromEntries(\n declared.map((name) => [name, fieldText(values[name])]),\n ),\n provider: repoPicker.field.provider as string,\n })),\n };\n}\n\nfunction parsePluginSources(\n value: SetupRequestBody[string],\n): SetupRequestBody[string] {\n if (!hasPayloadValue(value) || typeof value !== \"string\") return value;\n const trimmed = value.trim();\n if (!trimmed) return [];\n if (trimmed.startsWith(\"[\")) {\n try {\n return JSON.parse(trimmed) as SetupRequestBody[string];\n } catch {\n return [{ source: trimmed }];\n }\n }\n return trimmed\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => {\n const [source, ...parts] = line.split(/\\s+/);\n const plugin: Record<string, string> = { source };\n parts.forEach((part) => {\n const [key, ...raw] = part.split(\"=\");\n const valuePart = raw.join(\"=\");\n if ((key === \"ref\" || key === \"repo_path\") && valuePart) {\n plugin[key] = valuePart;\n }\n });\n return plugin;\n });\n}\n\n/**\n * The create request body these form values produce.\n *\n * No entry declares it: `name` comes from the entry, `repos` from the\n * repo-picker field and its declared provider, and `trigger` from the key and\n * fields under `form.triggers`. Only `prompt` and an event `filter` are\n * declared, because only they cannot be read off the form.\n *\n * Returns null for an assisted entry, which hands setup to a conversation\n * instead of sending a body.\n */\nexport function buildCreatePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n /** Bundle/upload entries only: what the upload returned. */\n tarballPath: string = PREFLIGHT_TARBALL_PATH,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const { setup } = entry;\n if (setup.mode !== \"direct\") return null;\n if (setup.bundle) {\n return buildBundlePayload(entry, values, tarballPath, selectedTrigger);\n }\n if (setup.actions) {\n return buildActionPayload(\n entry,\n values,\n tarballPath,\n selectedTrigger,\n selectedActionKey,\n );\n }\n if (!setup.prompt) return null;\n\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n prompt: interpolateText(setup.prompt, scope),\n ...optionalCreateProperties(setup, values),\n ...repositoryProperty(setup, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\nfunction buildActionPayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n selectedActionKey?: string | null,\n): SetupRequestBody | null {\n const kind = selectedActionKind(entry, selectedActionKey);\n if (kind === \"bundle\") return null;\n\n const setup = entry.setup;\n const scope = { form: values, automation: entry };\n const payload: SetupRequestBody = {\n name: deriveName(entry, values, selectedActionKey),\n };\n\n const trigger = buildTrigger(\n entry,\n values,\n selectedTrigger,\n selectedActionKey,\n );\n if (trigger) payload.trigger = trigger;\n\n Object.assign(\n payload,\n optionalCreateProperties(setup, values, selectedActionKey),\n );\n\n if (kind === \"upload\") {\n const action = setup.actions?.upload;\n if (!action) return null;\n payload.tarball_path =\n tarballPath || interpolateText(action.tarballPath, scope);\n payload.entrypoint = interpolateText(action.entrypoint, scope);\n if (action.setupScript) {\n const setupScript = interpolateText(action.setupScript, scope);\n if (hasPayloadValue(setupScript)) payload.setup_script_path = setupScript;\n }\n return payload;\n }\n\n const action =\n kind === \"plugin\" ? setup.actions?.plugin : setup.actions?.prompt;\n if (!action) return null;\n payload.prompt = interpolateText(action.prompt, scope);\n Object.assign(payload, repositoryProperty(setup, values, selectedActionKey));\n\n if (kind === \"plugin\") {\n const pluginAction = setup.actions?.plugin;\n if (!pluginAction) return null;\n payload.plugins = parsePluginSources(\n interpolateValue(pluginAction.plugins, scope),\n );\n }\n\n const template = buildTemplate(entry, values);\n if (template) payload.template = template;\n\n return payload;\n}\n\n/**\n * The `template` provenance a prompt entry sends, or undefined for one that\n * publishes no version.\n *\n * It is what lets the service recognise a second setup of the same entry as\n * the automation it already holds. The bundle path derives the same block from\n * the config the bundle declares; a prompt entry declares none, so the answer\n * to \"what was this created from\" is the form as submitted.\n */\nfunction buildTemplate(\n entry: SetupEntry,\n values: SetupFormValues,\n): SetupRequestBody | undefined {\n if (!entry.version) return undefined;\n return {\n id: entry.id,\n version: entry.version,\n config: { ...values } as SetupRequestBody,\n };\n}\n\n/**\n * The `trigger` object, read off the key and fields under `form.triggers`.\n *\n * Identical for both kinds of direct entry: only the create endpoint and what\n * the automation is told to do differ between a prompt and a bundle.\n */\nfunction buildTrigger(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | undefined {\n const trigger = getTrigger(entry.setup, selectedTrigger);\n if (!trigger) return undefined;\n\n const properties = TRIGGER_PROPERTIES[trigger.kind];\n const declared = Object.keys(trigger.fields).filter((name) =>\n properties.includes(name),\n );\n const repoPicker = findRepoPickerField(entry.setup, selectedAction);\n const derived = Object.fromEntries(\n declared\n .map((name) => [name, fieldText(values[name])])\n .filter(([, value]) => hasPayloadValue(value)),\n );\n\n const filter = entry.setup.filter\n ? interpolateText(entry.setup.filter, {\n form: values,\n automation: entry,\n })\n : undefined;\n\n return {\n type: trigger.kind,\n ...derived,\n ...(trigger.kind === \"event\" && {\n ...(!(\"source\" in derived) && {\n source: repoPicker?.field.provider ?? \"\",\n }),\n ...(hasPayloadValue(filter) && { filter }),\n }),\n };\n}\n\n/**\n * The raw create body a bundle entry produces.\n *\n * `tarball_path` is the one value neither declared nor derived: the host packs\n * and uploads the bundle first, and creates from what came back. `template` is\n * the provenance that makes enabling the same entry twice return the\n * automation that already exists rather than a second one.\n *\n * There is no `repos`: the raw endpoint has no such field, and a bundle's\n * script fetches what it needs itself.\n */\nfunction buildBundlePayload(\n entry: SetupEntry,\n values: SetupFormValues,\n tarballPath: string,\n selectedTrigger?: string | null,\n): SetupRequestBody {\n const bundle = entry.setup.bundle!;\n const scope = { form: values, automation: entry };\n\n const payload: SetupRequestBody = {\n name: deriveName(entry, values),\n ...optionalCreateProperties(entry.setup, values),\n };\n\n const trigger = buildTrigger(entry, values, selectedTrigger);\n if (trigger) payload.trigger = trigger;\n\n payload.tarball_path = tarballPath;\n payload.entrypoint = bundle.entrypoint;\n if (bundle.setupScript) payload.setup_script_path = bundle.setupScript;\n if (bundle.timeout !== undefined) payload.timeout = bundle.timeout;\n payload.template = {\n id: entry.id,\n version: bundle.version,\n config: interpolateConfig(bundle.config, scope) as SetupRequestBody,\n };\n\n return payload;\n}\n\n/**\n * Placeholder substitution over the config tree. Only string leaves are\n * templated; a number, a boolean or a null is written through as itself, so an\n * entry can state a value the script reads as the type it expects.\n */\nfunction interpolateConfig(\n node: SetupBundleConfigValue,\n scope: Parameters<typeof interpolateText>[1],\n): SetupBundleConfigValue {\n if (typeof node === \"string\") {\n return interpolateValue(node, scope);\n }\n if (Array.isArray(node)) {\n return node.map((item) => interpolateConfig(item, scope));\n }\n if (typeof node === \"object\" && node !== null) {\n return Object.fromEntries(\n Object.entries(node).map(([key, value]) => [\n key,\n interpolateConfig(value, scope),\n ]),\n );\n }\n return node;\n}\n\n/**\n * The preflight body the host sends. The same shape for every entry, so no\n * entry declares it.\n */\nexport function buildPreflightBody(\n entry: SetupEntry,\n values: SetupFormValues,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupRequestBody | null {\n const draft = buildCreatePayload(\n entry,\n values,\n PREFLIGHT_TARBALL_PATH,\n selectedTrigger,\n selectedAction,\n );\n if (!draft) return null;\n\n return {\n automationId: entry.id,\n endpoint: automationCreateEndpoint(entry, selectedAction),\n draft,\n };\n}\n\n/**\n * What an assisted entry sends into the conversation that finishes setup.\n *\n * The command is not declared either: it lives once, in the owning skill's own\n * `triggers` frontmatter, and the skill defaults to the entry's id. An entry\n * whose skill is invoked by description rather than by command contributes\n * nothing here, and the answers alone open the conversation.\n */\nexport function buildAssistedMessage(\n entry: SetupEntry,\n values: SetupFormValues,\n): string {\n const command = findAutomationCommand(entry);\n const message = entry.setup.message\n ? interpolateText(entry.setup.message, { form: values, automation: entry })\n : \"\";\n\n return [command, message].filter(Boolean).join(\"\\n\\n\");\n}\n\nfunction collectPlaceholderFields(value: string): string[] {\n const names = Array.from(\n value.matchAll(FORM_PLACEHOLDER_PATTERN),\n (match) => match[1],\n );\n return Array.from(new Set(names));\n}\n\n/**\n * Which form fields built each payload path.\n *\n * Preflight and the create endpoint reject a draft by payload path, and the\n * host has to turn that back into a highlighted input. Building the body with\n * each field standing in for its own value recovers the mapping exactly, so an\n * entry does not declare it.\n */\nexport function deriveErrorMap(\n entry: SetupEntry,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): Record<string, string[]> {\n const mapping: Record<string, string[]> = {};\n\n const triggerEntries = Object.keys(entry.setup.form.triggers ?? {});\n const triggerVariants = selectedTrigger\n ? [selectedTrigger]\n : triggerEntries.length > 1\n ? triggerEntries\n : [null];\n\n const actionEntries = actionKinds(entry.setup);\n const actionVariants = selectedAction\n ? [selectedAction]\n : actionEntries.length > 1\n ? actionEntries\n : [null];\n\n const walk = (node: unknown, path: string): void => {\n if (Array.isArray(node)) {\n node.forEach((item, index) => walk(item, `${path}[${index}]`));\n return;\n }\n if (typeof node === \"object\" && node !== null) {\n Object.entries(node).forEach(([key, item]) =>\n walk(item, path ? `${path}.${key}` : key),\n );\n return;\n }\n if (typeof node === \"string\") {\n const names = collectPlaceholderFields(node);\n if (names.length > 0) {\n mapping[path] = Array.from(\n new Set([...(mapping[path] ?? []), ...names]),\n );\n }\n }\n };\n\n actionVariants.forEach((actionVariant) => {\n triggerVariants.forEach((triggerVariant) => {\n const names = Object.keys(\n collectFields(entry.setup, triggerVariant, actionVariant),\n );\n const variantStandIns = Object.fromEntries(\n names.map((name) => [name, `{{form.${name}}}`]),\n );\n const template = buildCreatePayload(\n entry,\n variantStandIns,\n actionVariant === \"upload\" ? \"\" : PREFLIGHT_TARBALL_PATH,\n triggerVariant,\n actionVariant,\n );\n if (template) walk(template, \"\");\n });\n });\n return mapping;\n}\n"],"mappings":";;;;;AA6CA,SAAgB,EACd,GACA,GACQ;CACR,IAAM,IAAO,EAAmB,GAAO,CAAc;CAKrD,OAJI,MAAS,WAAiB,EAAsB,cAAc,IAC9D,MAAS,YAAY,MAAS,WACzB,EAAsB,cAAc,IAEtC,EAAsB,cAAc;AAC7C;AAGA,SAAgB,IAAmC;CACjD,OAAO,EAAsB,SAAS;AACxC;AAUA,SAAS,EACP,GACQ;CACR,IAAM,IAAO,EAAsB,CAAI;CACvC,IAAI,CAAC,GACH,MAAU,MACR,mDAAmD,EAAK,iFAE1D;CAEF,OAAO;AACT;AAoCA,SAAgB,EAAc,GAA4B;CACxD,OAAO,EAAM,MAAM,SAAS,YAAY,EAAM,MAAM,WAAW,KAAA;AACjE;AASA,SAAgB,EACd,GACA,GACuC;CACvC,IAAI,CAAC,GAAO,OAAO;CACnB,IAAI,EAAc,CAAK,GAAG,OAAO;CACjC,IAAM,IAAU,EAAM,MAAM,WAAW,CAAC;CACxC,IAAI,KAAkB,KAAkB,GACtC,OAAO;CAET,IAAM,IAAO,EAAY,EAAM,KAAK;CAEpC,OADI,EAAK,WAAW,IAAU,EAAK,KAC5B;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-local-validation.cjs","names":[],"sources":["../../src/manifests/manifest-local-validation.ts"],"sourcesContent":["/**\n * The form model, and the host's own check of what the user typed into it.\n *\n * Local validation is a convenience, not the authority: it is instant and needs\n * no round trip, so it catches empty required fields and obvious typos.\n * Deployment-specific questions (\"is a one-minute schedule allowed here?\")\n * belong to preflight, which is authoritative.\n *\n * Errors are returned as codes rather than sentences so the host can render\n * them through its own translations; only manifest-authored copy is literal.\n */\n\nimport type {\n DeploymentCapabilities,\n SetupActionKind,\n SetupBlock,\n SetupFieldOption,\n SetupFormField,\n SetupFormFields,\n SetupFormValue,\n SetupFormValues,\n} from \"./types\";\n\n/** Characters that would break out of an expression string literal. */\nconst UNSAFE_EXPRESSION_LITERAL_PATTERN = /[\"'\\\\]/;\n\nexport type SetupFieldError =\n | { code: \"required\" }\n | { code: \"minLength\"; length: number }\n | { code: \"maxLength\"; length: number }\n | { code: \"min\"; value: number }\n | { code: \"max\"; value: number }\n | { code: \"invalidOption\" }\n | { code: \"unsafeExpressionLiteral\" };\n\nexport type SetupFieldErrors = Record<string, SetupFieldError>;\n\n/** Field constraints supplied by the deployment rather than by the manifest. */\nexport interface SetupFieldOverride {\n options?: SetupFieldOption[];\n}\n\nexport function triggerKinds(setup: SetupBlock): string[] {\n return Object.keys(setup.form.triggers ?? {});\n}\n\nexport function initialTriggerKind(setup: SetupBlock): string | null {\n return triggerKinds(setup)[0] ?? null;\n}\n\nexport function actionKinds(setup: SetupBlock): SetupActionKind[] {\n return Object.keys(setup.actions ?? {}) as SetupActionKind[];\n}\n\nexport function initialActionKind(setup: SetupBlock): SetupActionKind | null {\n return actionKinds(setup)[0] ?? null;\n}\n\nexport type SetupFieldOverrides = Record<string, SetupFieldOverride>;\n\n/**\n * Every input the form declares, keyed by name, whichever half it is in.\n *\n * Trigger inputs come first so the user is asked when it runs before what it\n * runs on, and so every derived view of the form keeps that order. Admission\n * rejects a name declared in both halves, so the merge cannot lose a field.\n */\nexport function collectFields(\n setup: SetupBlock,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFormFields {\n const triggers = setup.form.triggers ?? {};\n const triggerEntries = Object.entries(triggers);\n const activeTrigger =\n selectedTrigger && selectedTrigger in triggers\n ? triggers[selectedTrigger as keyof typeof triggers]\n : triggerEntries.length === 1\n ? triggerEntries[0][1]\n : {};\n const actions = setup.actions ?? {};\n const actionEntries = Object.entries(actions);\n const activeAction =\n selectedAction && selectedAction in actions\n ? actions[selectedAction as SetupActionKind]?.args\n : actionEntries.length === 1\n ? actionEntries[0][1]?.args\n : {};\n return Object.assign(\n {},\n activeTrigger,\n setup.form.args,\n activeAction,\n ) as SetupFormFields;\n}\n\n/**\n * Feed deployment values into form field constraints, so the form offers only\n * what the deployment accepts.\n *\n * A `timezone` field is the case that needs it: a manifest declares no options\n * for one, because the accepted zones belong to the deployment. The cron\n * interval floor is deliberately left to preflight — the deployment owns that\n * limit and states it in its own words, and a local approximation would\n * pre-empt the authoritative message with a worse one.\n */\nexport function resolveFieldOverrides(\n setup: SetupBlock,\n capabilities: DeploymentCapabilities | null,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFieldOverrides {\n const overrides: SetupFieldOverrides = {};\n const fields = collectFields(setup, selectedTrigger, selectedAction);\n const timezones = capabilities?.triggers?.cron?.timezones;\n if (timezones?.length) {\n const options = timezones.map((zone) => ({ value: zone, label: zone }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"timezone\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n if (capabilities?.eventSources?.length) {\n const options = capabilities.eventSources.map((source) => ({\n value: source,\n label: source,\n }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"event-source\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n if (capabilities?.eventTypes?.length) {\n const options = capabilities.eventTypes.map((type) => ({\n value: type,\n label: type,\n }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"event-type\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n return overrides;\n}\n\n/** The options a field offers, after deployment constraints are applied. */\nexport function getFieldOptions(\n name: string,\n field: SetupFormField,\n overrides: SetupFieldOverrides = {},\n): SetupFieldOption[] {\n return overrides[name]?.options ?? field.options ?? [];\n}\n\n/**\n * Every value a field holds, whether it collects one or many.\n *\n * Reading both shapes as a list is what keeps validation, interpolation and\n * the payload mapping from branching on `multiple` at every use.\n */\nfunction isFileValue(value: SetupFormValue | undefined): value is File {\n return typeof File !== \"undefined\" && value instanceof File;\n}\n\nexport function fieldValues(value: SetupFormValue | undefined): string[] {\n if (Array.isArray(value)) return value.filter((item) => item.trim() !== \"\");\n if (isFileValue(value)) return value.name ? [value.name] : [];\n if (typeof value === \"string\" && value.trim() === \"\") return [];\n if (value === undefined || value === null) return [];\n return [String(value)];\n}\n\n/** The single value a field holds, or \"\" for a field collecting several. */\nexport function fieldText(value: SetupFormValue | undefined): string {\n if (Array.isArray(value) || value === undefined || value === null) return \"\";\n if (isFileValue(value)) return value.name;\n return String(value);\n}\n\n/** Initial form state: every declared field, seeded with its declared default. */\nexport function getInitialFormValues(\n setup: SetupBlock,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFormValues {\n return Object.fromEntries(\n Object.entries(collectFields(setup, selectedTrigger, selectedAction)).map(\n ([name, field]) => [\n name,\n // A field collecting several values starts empty rather than holding one\n // blank entry, so \"required\" means \"add one\" rather than \"fill this in\".\n field.multiple ? [] : (field.default ?? \"\"),\n ],\n ),\n );\n}\n\nfunction validateField(\n name: string,\n field: SetupFormField,\n rawValue: SetupFormValue | undefined,\n overrides: SetupFieldOverrides,\n): SetupFieldError | null {\n const entered = fieldValues(rawValue);\n\n if (entered.length === 0) {\n return field.required ? { code: \"required\" } : null;\n }\n\n // Every entry of a multi-value field answers the same field, so each is held\n // to the same rules and the first failure is the one reported.\n const failures = entered\n .map((item) => validateValue(name, field, item.trim(), overrides))\n .filter((error): error is SetupFieldError => error !== null);\n return failures[0] ?? null;\n}\n\nfunction validateValue(\n name: string,\n field: SetupFormField,\n value: string,\n overrides: SetupFieldOverrides,\n): SetupFieldError | null {\n const { minLength, maxLength, min, max, format } = field.constraints ?? {};\n if (minLength !== undefined && value.length < minLength) {\n return { code: \"minLength\", length: minLength };\n }\n if (maxLength !== undefined && value.length > maxLength) {\n return { code: \"maxLength\", length: maxLength };\n }\n if (field.type === \"number\") {\n const numeric = Number(value);\n if (Number.isNaN(numeric)) return { code: \"invalidOption\" };\n if (min !== undefined && numeric < min) return { code: \"min\", value: min };\n if (max !== undefined && numeric > max) return { code: \"max\", value: max };\n }\n if (\n format === \"safeExpressionLiteral\" &&\n UNSAFE_EXPRESSION_LITERAL_PATTERN.test(value)\n ) {\n return { code: \"unsafeExpressionLiteral\" };\n }\n\n // Any field offering a closed set of values, whether the manifest declared it\n // or the deployment supplied it, must be answered from that set.\n const options = getFieldOptions(name, field, overrides);\n if (options.length > 0 && !options.some((option) => option.value === value)) {\n return { code: \"invalidOption\" };\n }\n\n return null;\n}\n\n/** Check every declared field. Returns only the fields that failed. */\nexport function validateFormValues(\n setup: SetupBlock,\n values: SetupFormValues,\n overrides: SetupFieldOverrides = {},\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFieldErrors {\n return Object.entries(\n collectFields(setup, selectedTrigger, selectedAction),\n ).reduce<SetupFieldErrors>((errors, [name, field]) => {\n const error = validateField(name, field, values[name], overrides);\n return error ? { ...errors, [name]: error } : errors;\n }, {});\n}\n"],"mappings":"6CAkDA,SAAgB,EAAY,EAAsC,CAChE,OAAO,OAAO,KAAK,EAAM,SAAW,CAAC,CAAC,CACxC"}
1
+ {"version":3,"file":"manifest-local-validation.cjs","names":[],"sources":["../../src/manifests/manifest-local-validation.ts"],"sourcesContent":["/**\n * The form model, and the host's own check of what the user typed into it.\n *\n * Local validation is a convenience, not the authority: it is instant and needs\n * no round trip, so it catches empty required fields and obvious typos.\n * Deployment-specific questions (\"is a one-minute schedule allowed here?\")\n * belong to preflight, which is authoritative.\n *\n * Errors are returned as codes rather than sentences so the host can render\n * them through its own translations; only manifest-authored copy is literal.\n */\n\nimport type {\n DeploymentCapabilities,\n SetupActionKind,\n SetupBlock,\n SetupFieldOption,\n SetupFormField,\n SetupFormFields,\n SetupFormValue,\n SetupFormValues,\n} from \"./types\";\n\n/** Characters that would break out of an expression string literal. */\nconst UNSAFE_EXPRESSION_LITERAL_PATTERN = /[\"'\\\\]/;\n\nexport type SetupFieldError =\n | { code: \"required\" }\n | { code: \"minLength\"; length: number }\n | { code: \"maxLength\"; length: number }\n | { code: \"min\"; value: number }\n | { code: \"max\"; value: number }\n | { code: \"invalidOption\" }\n | { code: \"unsafeExpressionLiteral\" };\n\nexport type SetupFieldErrors = Record<string, SetupFieldError>;\n\n/** Field constraints supplied by the deployment rather than by the manifest. */\nexport interface SetupFieldOverride {\n options?: SetupFieldOption[];\n}\n\nexport function triggerKinds(setup: SetupBlock): string[] {\n return Object.keys(setup.form.triggers ?? {});\n}\n\nexport function initialTriggerKind(setup: SetupBlock): string | null {\n return triggerKinds(setup)[0] ?? null;\n}\n\nexport function actionKinds(setup: SetupBlock): SetupActionKind[] {\n return Object.keys(setup.actions ?? {}) as SetupActionKind[];\n}\n\nexport function initialActionKind(setup: SetupBlock): SetupActionKind | null {\n return actionKinds(setup)[0] ?? null;\n}\n\nexport type SetupFieldOverrides = Record<string, SetupFieldOverride>;\n\n/**\n * Every input the form declares, keyed by name, whichever half it is in.\n *\n * Trigger inputs come first so the user is asked when it runs before what it\n * runs on, and so every derived view of the form keeps that order. Admission\n * rejects a name declared in both halves, so the merge cannot lose a field.\n */\nexport function collectFields(\n setup: SetupBlock,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFormFields {\n const triggers = setup.form.triggers ?? {};\n const triggerEntries = Object.entries(triggers);\n const activeTrigger =\n selectedTrigger && selectedTrigger in triggers\n ? triggers[selectedTrigger as keyof typeof triggers]\n : triggerEntries.length === 1\n ? triggerEntries[0][1]\n : {};\n const actions = setup.actions ?? {};\n const actionEntries = Object.entries(actions);\n const activeAction =\n selectedAction && selectedAction in actions\n ? actions[selectedAction as SetupActionKind]?.args\n : actionEntries.length === 1\n ? actionEntries[0][1]?.args\n : {};\n return Object.assign(\n {},\n activeTrigger,\n setup.form.args,\n activeAction,\n ) as SetupFormFields;\n}\n\n/**\n * Feed deployment values into form field constraints, so the form offers only\n * what the deployment accepts.\n *\n * A `timezone` field is the case that needs it: a manifest declares no options\n * for one, because the accepted zones belong to the deployment. The cron\n * interval floor is deliberately left to preflight — the deployment owns that\n * limit and states it in its own words, and a local approximation would\n * pre-empt the authoritative message with a worse one.\n */\nexport function resolveFieldOverrides(\n setup: SetupBlock,\n capabilities: DeploymentCapabilities | null,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFieldOverrides {\n const overrides: SetupFieldOverrides = {};\n const fields = collectFields(setup, selectedTrigger, selectedAction);\n const timezones = capabilities?.triggers?.cron?.timezones;\n if (timezones?.length) {\n const options = timezones.map((zone) => ({ value: zone, label: zone }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"timezone\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n if (capabilities?.eventSources?.length) {\n const options = capabilities.eventSources.map((source) => ({\n value: source,\n label: source,\n }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"event-source\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n if (capabilities?.eventTypes?.length) {\n const options = capabilities.eventTypes.map((type) => ({\n value: type,\n label: type,\n }));\n Object.entries(fields)\n .filter(([, field]) => field.type === \"event-type\")\n .forEach(([name]) => {\n overrides[name] = { options };\n });\n }\n\n return overrides;\n}\n\n/** The options a field offers, after deployment constraints are applied. */\nexport function getFieldOptions(\n name: string,\n field: SetupFormField,\n overrides: SetupFieldOverrides = {},\n): SetupFieldOption[] {\n return overrides[name]?.options ?? field.options ?? [];\n}\n\n/**\n * Every value a field holds, whether it collects one or many.\n *\n * Reading both shapes as a list is what keeps validation, interpolation and\n * the payload mapping from branching on `multiple` at every use.\n */\nfunction isFileValue(value: SetupFormValue | undefined): value is File {\n return typeof File !== \"undefined\" && value instanceof File;\n}\n\nexport function fieldValues(value: SetupFormValue | undefined): string[] {\n if (Array.isArray(value)) return value.filter((item) => item.trim() !== \"\");\n if (isFileValue(value)) return value.name ? [value.name] : [];\n if (typeof value === \"string\" && value.trim() === \"\") return [];\n if (value === undefined || value === null) return [];\n return [String(value)];\n}\n\n/** The single value a field holds, or \"\" for a field collecting several. */\nexport function fieldText(value: SetupFormValue | undefined): string {\n if (Array.isArray(value) || value === undefined || value === null) return \"\";\n if (isFileValue(value)) return value.name;\n return String(value);\n}\n\n/** Initial form state: every declared field, seeded with its declared default. */\nexport function getInitialFormValues(\n setup: SetupBlock,\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFormValues {\n return Object.fromEntries(\n Object.entries(collectFields(setup, selectedTrigger, selectedAction)).map(\n ([name, field]) => [\n name,\n // A field collecting several values starts empty rather than holding one\n // blank entry, so \"required\" means \"add one\" rather than \"fill this in\".\n field.multiple ? [] : (field.default ?? \"\"),\n ],\n ),\n );\n}\n\nfunction validateField(\n name: string,\n field: SetupFormField,\n rawValue: SetupFormValue | undefined,\n overrides: SetupFieldOverrides,\n): SetupFieldError | null {\n const entered = fieldValues(rawValue);\n\n if (entered.length === 0) {\n return field.required ? { code: \"required\" } : null;\n }\n\n // Every entry of a multi-value field answers the same field, so each is held\n // to the same rules and the first failure is the one reported.\n const failures = entered\n .map((item) => validateValue(name, field, item.trim(), overrides))\n .filter((error): error is SetupFieldError => error !== null);\n return failures[0] ?? null;\n}\n\nfunction validateValue(\n name: string,\n field: SetupFormField,\n value: string,\n overrides: SetupFieldOverrides,\n): SetupFieldError | null {\n const { minLength, maxLength, min, max, format } = field.constraints ?? {};\n if (minLength !== undefined && value.length < minLength) {\n return { code: \"minLength\", length: minLength };\n }\n if (maxLength !== undefined && value.length > maxLength) {\n return { code: \"maxLength\", length: maxLength };\n }\n if (field.type === \"number\") {\n const numeric = Number(value);\n if (Number.isNaN(numeric)) return { code: \"invalidOption\" };\n if (min !== undefined && numeric < min) return { code: \"min\", value: min };\n if (max !== undefined && numeric > max) return { code: \"max\", value: max };\n }\n if (\n format === \"safeExpressionLiteral\" &&\n UNSAFE_EXPRESSION_LITERAL_PATTERN.test(value)\n ) {\n return { code: \"unsafeExpressionLiteral\" };\n }\n\n // Any field offering a closed set of values, whether the manifest declared it\n // or the deployment supplied it, must be answered from that set.\n const options = getFieldOptions(name, field, overrides);\n if (options.length > 0 && !options.some((option) => option.value === value)) {\n return { code: \"invalidOption\" };\n }\n\n return null;\n}\n\n/** Check every declared field. Returns only the fields that failed. */\nexport function validateFormValues(\n setup: SetupBlock,\n values: SetupFormValues,\n overrides: SetupFieldOverrides = {},\n selectedTrigger?: string | null,\n selectedAction?: string | null,\n): SetupFieldErrors {\n return Object.entries(\n collectFields(setup, selectedTrigger, selectedAction),\n ).reduce<SetupFieldErrors>((errors, [name, field]) => {\n if (values.agent_profile_id && field.type === \"llm-profile\") return errors;\n const error = validateField(name, field, values[name], overrides);\n return error ? { ...errors, [name]: error } : errors;\n }, {});\n}\n"],"mappings":"6CAkDA,SAAgB,EAAY,EAAsC,CAChE,OAAO,OAAO,KAAK,EAAM,SAAW,CAAC,CAAC,CACxC"}