wizards 0.1.9

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 (308) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +170 -0
  3. package/apps/runtime/dist/agents/architect.js +453 -0
  4. package/apps/runtime/dist/agents/project-assistant.js +354 -0
  5. package/apps/runtime/dist/agents/subscription.js +206 -0
  6. package/apps/runtime/dist/agents/thought.js +21 -0
  7. package/apps/runtime/dist/agents/website.js +197 -0
  8. package/apps/runtime/dist/app.js +153 -0
  9. package/apps/runtime/dist/auth/account.js +121 -0
  10. package/apps/runtime/dist/auth/index.js +329 -0
  11. package/apps/runtime/dist/auth/keys.js +55 -0
  12. package/apps/runtime/dist/auth/local-ticket.js +23 -0
  13. package/apps/runtime/dist/authoring/example.js +73 -0
  14. package/apps/runtime/dist/authoring/guide.js +259 -0
  15. package/apps/runtime/dist/authoring/ops.js +117 -0
  16. package/apps/runtime/dist/cli/autostart.js +134 -0
  17. package/apps/runtime/dist/cli/clients.js +44 -0
  18. package/apps/runtime/dist/cli/environment.js +81 -0
  19. package/apps/runtime/dist/cli/exec.js +45 -0
  20. package/apps/runtime/dist/cli/home.js +74 -0
  21. package/apps/runtime/dist/cli/machine.js +145 -0
  22. package/apps/runtime/dist/cli/main.js +125 -0
  23. package/apps/runtime/dist/cli/setup.js +273 -0
  24. package/apps/runtime/dist/cli/start.js +190 -0
  25. package/apps/runtime/dist/cli/status.js +120 -0
  26. package/apps/runtime/dist/cli/ui.js +12 -0
  27. package/apps/runtime/dist/cli/update.js +58 -0
  28. package/apps/runtime/dist/connectors/builtin.js +70 -0
  29. package/apps/runtime/dist/connectors/external.js +400 -0
  30. package/apps/runtime/dist/connectors/gmail.js +107 -0
  31. package/apps/runtime/dist/connectors/imap.js +217 -0
  32. package/apps/runtime/dist/connectors/index.js +266 -0
  33. package/apps/runtime/dist/connectors/mail.js +72 -0
  34. package/apps/runtime/dist/connectors/outlook.js +90 -0
  35. package/apps/runtime/dist/credits/credits.js +103 -0
  36. package/apps/runtime/dist/credits/estimate.js +132 -0
  37. package/apps/runtime/dist/db/adopt.js +149 -0
  38. package/apps/runtime/dist/db/client.js +146 -0
  39. package/apps/runtime/dist/db/control-schema.js +70 -0
  40. package/apps/runtime/dist/db/migrations/control/0000_last_absorbing_man.sql +47 -0
  41. package/apps/runtime/dist/db/migrations/control/0001_gorgeous_nicolaos.sql +41 -0
  42. package/apps/runtime/dist/db/migrations/control/0002_secret_penance.sql +1 -0
  43. package/apps/runtime/dist/db/migrations/control/0003_nasty_mordo.sql +3 -0
  44. package/apps/runtime/dist/db/migrations/control/0004_breezy_eddie_brock.sql +6 -0
  45. package/apps/runtime/dist/db/migrations/control/meta/0000_snapshot.json +311 -0
  46. package/apps/runtime/dist/db/migrations/control/meta/0001_snapshot.json +599 -0
  47. package/apps/runtime/dist/db/migrations/control/meta/0002_snapshot.json +607 -0
  48. package/apps/runtime/dist/db/migrations/control/meta/0003_snapshot.json +311 -0
  49. package/apps/runtime/dist/db/migrations/control/meta/0004_snapshot.json +350 -0
  50. package/apps/runtime/dist/db/migrations/control/meta/_journal.json +41 -0
  51. package/apps/runtime/dist/db/migrations/tenant/0000_eminent_freak.sql +188 -0
  52. package/apps/runtime/dist/db/migrations/tenant/0001_salty_master_chief.sql +1 -0
  53. package/apps/runtime/dist/db/migrations/tenant/0002_cloudy_firebird.sql +1 -0
  54. package/apps/runtime/dist/db/migrations/tenant/0003_brown_warbound.sql +52 -0
  55. package/apps/runtime/dist/db/migrations/tenant/0004_romantic_nocturne.sql +6 -0
  56. package/apps/runtime/dist/db/migrations/tenant/meta/0000_snapshot.json +1323 -0
  57. package/apps/runtime/dist/db/migrations/tenant/meta/0001_snapshot.json +1330 -0
  58. package/apps/runtime/dist/db/migrations/tenant/meta/0002_snapshot.json +1337 -0
  59. package/apps/runtime/dist/db/migrations/tenant/meta/0003_snapshot.json +1605 -0
  60. package/apps/runtime/dist/db/migrations/tenant/meta/0004_snapshot.json +1645 -0
  61. package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +41 -0
  62. package/apps/runtime/dist/db/schema.js +278 -0
  63. package/apps/runtime/dist/documents/parse.js +230 -0
  64. package/apps/runtime/dist/engenty/connections-external/build-connector.js +193 -0
  65. package/apps/runtime/dist/engenty/connections-external/errors.js +11 -0
  66. package/apps/runtime/dist/engenty/connections-external/import-service.js +270 -0
  67. package/apps/runtime/dist/engenty/connections-external/importer/classify.js +34 -0
  68. package/apps/runtime/dist/engenty/connections-external/importer/map-auth.js +185 -0
  69. package/apps/runtime/dist/engenty/connections-external/importer/normalize-mcp.js +63 -0
  70. package/apps/runtime/dist/engenty/connections-external/importer/normalize-openapi.js +130 -0
  71. package/apps/runtime/dist/engenty/connections-external/invoke/http-invoker.js +143 -0
  72. package/apps/runtime/dist/engenty/connections-external/invoke/mcp-client.js +143 -0
  73. package/apps/runtime/dist/engenty/connections-external/net/guarded-fetch.js +119 -0
  74. package/apps/runtime/dist/engenty/connections-external/oauth-dcr.js +88 -0
  75. package/apps/runtime/dist/engenty/connections-external/registry-client.js +336 -0
  76. package/apps/runtime/dist/engenty/connections-external/registry-source.js +82 -0
  77. package/apps/runtime/dist/engenty/connections-external/types.js +7 -0
  78. package/apps/runtime/dist/engenty/connections-github/connector.js +209 -0
  79. package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +359 -0
  80. package/apps/runtime/dist/engenty/connections-google/connectors/contacts.js +94 -0
  81. package/apps/runtime/dist/engenty/connections-google/connectors/drive.js +290 -0
  82. package/apps/runtime/dist/engenty/connections-google/connectors/gmail.js +567 -0
  83. package/apps/runtime/dist/engenty/connections-google/definitions.js +11 -0
  84. package/apps/runtime/dist/engenty/connections-google/shared.js +73 -0
  85. package/apps/runtime/dist/engenty/connections-hubspot/connector.js +140 -0
  86. package/apps/runtime/dist/engenty/connections-microsoft/action.js +17 -0
  87. package/apps/runtime/dist/engenty/connections-microsoft/graph.js +114 -0
  88. package/apps/runtime/dist/engenty/connections-microsoft/onedrive.js +286 -0
  89. package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +464 -0
  90. package/apps/runtime/dist/engenty/connections-s3/s3.js +328 -0
  91. package/apps/runtime/dist/engenty/connections-sdk/files-capability.js +86 -0
  92. package/apps/runtime/dist/engenty/connections-sdk/oauth2.js +150 -0
  93. package/apps/runtime/dist/engenty/connections-sdk/registry.js +89 -0
  94. package/apps/runtime/dist/engenty/connections-sdk/storage-capability.js +84 -0
  95. package/apps/runtime/dist/engenty/connections-sdk/types.js +30 -0
  96. package/apps/runtime/dist/engenty/connections-slack/connector.js +360 -0
  97. package/apps/runtime/dist/engenty/csv-import/csv-matrix.js +178 -0
  98. package/apps/runtime/dist/engenty/csv-import/import-sources.js +87 -0
  99. package/apps/runtime/dist/engenty/csv-import/parse-csv.js +129 -0
  100. package/apps/runtime/dist/engenty/csv-import/serialize-csv.js +20 -0
  101. package/apps/runtime/dist/engenty/csv-import/types.js +1 -0
  102. package/apps/runtime/dist/engenty/csv-import/xlsx-workbook.js +159 -0
  103. package/apps/runtime/dist/engenty/doc-converter/interface.js +9 -0
  104. package/apps/runtime/dist/engenty/doc-converter/page-break.js +92 -0
  105. package/apps/runtime/dist/engenty/doc-converter/providers/local/index.js +72 -0
  106. package/apps/runtime/dist/engenty/document-scanner/schemas/classifier.js +30 -0
  107. package/apps/runtime/dist/engenty/document-scanner/schemas/index.js +4 -0
  108. package/apps/runtime/dist/engenty/document-scanner/schemas/invoice.js +73 -0
  109. package/apps/runtime/dist/engenty/document-scanner/schemas/receipt.js +63 -0
  110. package/apps/runtime/dist/engenty/document-scanner/schemas/shared.js +40 -0
  111. package/apps/runtime/dist/engenty/search-index/chunks.js +164 -0
  112. package/apps/runtime/dist/engenty/search-index/contracts.js +9 -0
  113. package/apps/runtime/dist/engenty/shims/connections-sdk.js +39 -0
  114. package/apps/runtime/dist/engenty/shims/plugin-sdk.js +1 -0
  115. package/apps/runtime/dist/engenty/shims/web-ingest.js +3 -0
  116. package/apps/runtime/dist/engenty/web-ingest/ssrf.js +277 -0
  117. package/apps/runtime/dist/engine/asks.js +60 -0
  118. package/apps/runtime/dist/engine/events.js +46 -0
  119. package/apps/runtime/dist/engine/input.js +170 -0
  120. package/apps/runtime/dist/engine/prepare.js +51 -0
  121. package/apps/runtime/dist/engine/requirements.js +124 -0
  122. package/apps/runtime/dist/engine/resources.js +89 -0
  123. package/apps/runtime/dist/engine/runner.js +560 -0
  124. package/apps/runtime/dist/engine/steps.js +598 -0
  125. package/apps/runtime/dist/engine/template.js +183 -0
  126. package/apps/runtime/dist/engine/types.js +3 -0
  127. package/apps/runtime/dist/env.js +224 -0
  128. package/apps/runtime/dist/files/blobs.js +22 -0
  129. package/apps/runtime/dist/files/objects.js +86 -0
  130. package/apps/runtime/dist/files/storage.js +95 -0
  131. package/apps/runtime/dist/geocode.js +58 -0
  132. package/apps/runtime/dist/harness/claude.js +138 -0
  133. package/apps/runtime/dist/harness/codex.js +228 -0
  134. package/apps/runtime/dist/harness/cursor.js +93 -0
  135. package/apps/runtime/dist/harness/env.js +124 -0
  136. package/apps/runtime/dist/harness/gemini.js +126 -0
  137. package/apps/runtime/dist/harness/index.js +99 -0
  138. package/apps/runtime/dist/harness/model.js +267 -0
  139. package/apps/runtime/dist/harness/prompt.js +121 -0
  140. package/apps/runtime/dist/harness/terminal.js +193 -0
  141. package/apps/runtime/dist/harness/types.js +7 -0
  142. package/apps/runtime/dist/index.js +76 -0
  143. package/apps/runtime/dist/limits.js +57 -0
  144. package/apps/runtime/dist/link-preview.js +59 -0
  145. package/apps/runtime/dist/manage.js +151 -0
  146. package/apps/runtime/dist/mcp/auth.js +102 -0
  147. package/apps/runtime/dist/mcp/bridge.js +59 -0
  148. package/apps/runtime/dist/mcp/handler.js +42 -0
  149. package/apps/runtime/dist/mcp/scopes.js +5 -0
  150. package/apps/runtime/dist/mcp/tools.js +401 -0
  151. package/apps/runtime/dist/media/ffmpeg.js +121 -0
  152. package/apps/runtime/dist/media/generate.js +120 -0
  153. package/apps/runtime/dist/media/marking.js +121 -0
  154. package/apps/runtime/dist/media/transcribe.js +30 -0
  155. package/apps/runtime/dist/model-errors.js +3 -0
  156. package/apps/runtime/dist/models.js +489 -0
  157. package/apps/runtime/dist/plugin.js +61 -0
  158. package/apps/runtime/dist/render/chromium.js +85 -0
  159. package/apps/runtime/dist/render/convert.js +161 -0
  160. package/apps/runtime/dist/render/downloads.js +277 -0
  161. package/apps/runtime/dist/render/guard.js +25 -0
  162. package/apps/runtime/dist/routes/api.js +74 -0
  163. package/apps/runtime/dist/routes/delivery.js +52 -0
  164. package/apps/runtime/dist/routes/internal.js +49 -0
  165. package/apps/runtime/dist/routes/marketplace.js +36 -0
  166. package/apps/runtime/dist/routes/runs.js +805 -0
  167. package/apps/runtime/dist/routes/studio.js +702 -0
  168. package/apps/runtime/dist/routes/wizard-stream.js +30 -0
  169. package/apps/runtime/dist/running.js +39 -0
  170. package/apps/runtime/dist/sandbox/agentos.js +191 -0
  171. package/apps/runtime/dist/sandbox/docker.js +77 -0
  172. package/apps/runtime/dist/sandbox/index.js +30 -0
  173. package/apps/runtime/dist/sandbox/types.js +1 -0
  174. package/apps/runtime/dist/secrets/crypto.js +28 -0
  175. package/apps/runtime/dist/secrets/signing.js +34 -0
  176. package/apps/runtime/dist/secrets/vault.js +59 -0
  177. package/apps/runtime/dist/services/architect.js +92 -0
  178. package/apps/runtime/dist/services/brand.js +20 -0
  179. package/apps/runtime/dist/services/cloud.js +52 -0
  180. package/apps/runtime/dist/services/draft-events.js +27 -0
  181. package/apps/runtime/dist/services/errors.js +24 -0
  182. package/apps/runtime/dist/services/files.js +129 -0
  183. package/apps/runtime/dist/services/links.js +16 -0
  184. package/apps/runtime/dist/services/marketplace.js +292 -0
  185. package/apps/runtime/dist/services/package.js +202 -0
  186. package/apps/runtime/dist/services/profile.js +23 -0
  187. package/apps/runtime/dist/services/project-files.js +266 -0
  188. package/apps/runtime/dist/services/project-index.js +109 -0
  189. package/apps/runtime/dist/services/projects.js +181 -0
  190. package/apps/runtime/dist/services/runs.js +224 -0
  191. package/apps/runtime/dist/services/shares.js +129 -0
  192. package/apps/runtime/dist/services/widgets.js +34 -0
  193. package/apps/runtime/dist/services/wizards.js +342 -0
  194. package/apps/runtime/dist/settings.js +16 -0
  195. package/apps/runtime/dist/store/index.js +279 -0
  196. package/apps/runtime/dist/tenants/control.js +106 -0
  197. package/apps/runtime/dist/tenants/tenant.js +26 -0
  198. package/apps/runtime/dist/tools/browser.js +380 -0
  199. package/apps/runtime/dist/tools/connector.js +204 -0
  200. package/apps/runtime/dist/tools/index.js +234 -0
  201. package/apps/runtime/dist/tools/mail.js +104 -0
  202. package/apps/runtime/dist/tools/net-guard.js +59 -0
  203. package/apps/runtime/dist/tools/project.js +53 -0
  204. package/apps/runtime/dist/tools/shared.js +42 -0
  205. package/apps/runtime/dist/tools/store.js +238 -0
  206. package/apps/runtime/dist/update.js +118 -0
  207. package/apps/runtime/dist/widgets/bundle.js +99 -0
  208. package/apps/runtime/dist/widgets/render.js +312 -0
  209. package/apps/runtime/dist/widgets/runtime.js +84 -0
  210. package/apps/runtime/dist/widgets/script.js +30 -0
  211. package/apps/runtime/dist/widgets/step.js +192 -0
  212. package/apps/web/dist/ai-labels/ai-black.svg +20 -0
  213. package/apps/web/dist/ai-labels/ai-generated-black.svg +31 -0
  214. package/apps/web/dist/ai-labels/ai-generated-white.svg +32 -0
  215. package/apps/web/dist/ai-labels/ai-modified-black.svg +30 -0
  216. package/apps/web/dist/ai-labels/ai-modified-white.svg +31 -0
  217. package/apps/web/dist/ai-labels/ai-white.svg +21 -0
  218. package/apps/web/dist/assets/ChangelogDialog-O00vEnbm.js +1 -0
  219. package/apps/web/dist/assets/ChatPanel-CH9ry0Hp.js +1 -0
  220. package/apps/web/dist/assets/CreditsDialog-DRzecwUw.js +1 -0
  221. package/apps/web/dist/assets/EditorPage-BRnBf02Q.js +2 -0
  222. package/apps/web/dist/assets/FlowPreview-DDOj8IAH.js +1 -0
  223. package/apps/web/dist/assets/HomePage-CAGTTG3N.js +1 -0
  224. package/apps/web/dist/assets/ImportWizard-0RRrAnX3.js +1 -0
  225. package/apps/web/dist/assets/LocalRuntime-BrP-ENHg.css +1 -0
  226. package/apps/web/dist/assets/LocalRuntime-C-TBYrzs.js +16 -0
  227. package/apps/web/dist/assets/NewWizardPage-kI90h3_9.js +2 -0
  228. package/apps/web/dist/assets/PublicRunner-CKRDeIVd.js +1 -0
  229. package/apps/web/dist/assets/RunnerView-Rc4H8wYH.js +2 -0
  230. package/apps/web/dist/assets/SettingsPage-CESMuZxq.js +2 -0
  231. package/apps/web/dist/assets/SetupPage-D1rrYU14.js +1 -0
  232. package/apps/web/dist/assets/SharePage-COTAmOgh.js +1 -0
  233. package/apps/web/dist/assets/api-BY28cjGY.js +10 -0
  234. package/apps/web/dist/assets/arrow-left-UpjOBfQp.js +1 -0
  235. package/apps/web/dist/assets/arrow-right-CucQycEy.js +1 -0
  236. package/apps/web/dist/assets/changelog-p3wsaDDS.js +1 -0
  237. package/apps/web/dist/assets/check-Bjx41xbB.js +1 -0
  238. package/apps/web/dist/assets/createLucideIcon-DBu2Ovxj.js +1 -0
  239. package/apps/web/dist/assets/definition-cy7uvY5L.js +55 -0
  240. package/apps/web/dist/assets/engenty-CWIFpgKq.js +1 -0
  241. package/apps/web/dist/assets/esm-D1OEhjTK.js +7 -0
  242. package/apps/web/dist/assets/external-link-CRg6DVdO.js +1 -0
  243. package/apps/web/dist/assets/fluffy-engenty-CJ3wCQgT.js +456 -0
  244. package/apps/web/dist/assets/index-CQ_q9iMO.css +1 -0
  245. package/apps/web/dist/assets/index-D0gxh90t.js +10 -0
  246. package/apps/web/dist/assets/jsx-runtime-Dk72oS4N.js +1 -0
  247. package/apps/web/dist/assets/meta-BbSdtTUa.js +1 -0
  248. package/apps/web/dist/assets/mic-CeqQ89Dj.js +1 -0
  249. package/apps/web/dist/assets/oss-credits-cPuZUFHD.js +1 -0
  250. package/apps/web/dist/assets/outputs-D6pxH6ve.js +69 -0
  251. package/apps/web/dist/assets/plus-HA210wXA.js +1 -0
  252. package/apps/web/dist/assets/preload-helper-CNEI7N0N.js +1 -0
  253. package/apps/web/dist/assets/qr-scanner-worker.min-CJaXo3yu.js +98 -0
  254. package/apps/web/dist/assets/qr-scanner.min-C_UI0gH-.js +1 -0
  255. package/apps/web/dist/assets/type-BzYgVWKL.js +1 -0
  256. package/apps/web/dist/assets/ui-Ddwl35A6.js +1 -0
  257. package/apps/web/dist/assets/upload-DsgLfQ3U.js +1 -0
  258. package/apps/web/dist/assets/video-Dk1fuTDl.js +1 -0
  259. package/apps/web/dist/assets/wand-sparkles-Bf97fs8T.js +1 -0
  260. package/apps/web/dist/embed.js +102 -0
  261. package/apps/web/dist/favicon.svg +1 -0
  262. package/apps/web/dist/icons/apple-touch-icon.png +0 -0
  263. package/apps/web/dist/icons/icon-192.png +0 -0
  264. package/apps/web/dist/icons/icon-512.png +0 -0
  265. package/apps/web/dist/icons/icon-maskable-512.png +0 -0
  266. package/apps/web/dist/index.html +46 -0
  267. package/apps/web/dist/install.sh +532 -0
  268. package/apps/web/dist/manifest.webmanifest +20 -0
  269. package/apps/web/dist/sw.js +15 -0
  270. package/bin/wizards.mjs +13 -0
  271. package/node_modules/@engenty-wizards/shared/dist/connectors.d.ts +77 -0
  272. package/node_modules/@engenty-wizards/shared/dist/connectors.js +2 -0
  273. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +1415 -0
  274. package/node_modules/@engenty-wizards/shared/dist/definition.js +762 -0
  275. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.d.ts +5 -0
  276. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.js +132 -0
  277. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.d.ts +11 -0
  278. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.js +209 -0
  279. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.d.ts +532 -0
  280. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.js +216 -0
  281. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.d.ts +3 -0
  282. package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.js +3 -0
  283. package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.d.ts +14 -0
  284. package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.js +166 -0
  285. package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.d.ts +33 -0
  286. package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.js +99 -0
  287. package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.d.ts +35 -0
  288. package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.js +86 -0
  289. package/node_modules/@engenty-wizards/shared/dist/marketplace-search.d.ts +20 -0
  290. package/node_modules/@engenty-wizards/shared/dist/marketplace-search.js +133 -0
  291. package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +298 -0
  292. package/node_modules/@engenty-wizards/shared/dist/marketplace.js +211 -0
  293. package/node_modules/@engenty-wizards/shared/dist/merge.d.ts +7 -0
  294. package/node_modules/@engenty-wizards/shared/dist/merge.js +40 -0
  295. package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +74 -0
  296. package/node_modules/@engenty-wizards/shared/dist/projects.js +96 -0
  297. package/node_modules/@engenty-wizards/shared/dist/run.d.ts +219 -0
  298. package/node_modules/@engenty-wizards/shared/dist/run.js +1 -0
  299. package/node_modules/@engenty-wizards/shared/dist/store.d.ts +100 -0
  300. package/node_modules/@engenty-wizards/shared/dist/store.js +44 -0
  301. package/node_modules/@engenty-wizards/shared/dist/workspace.d.ts +16 -0
  302. package/node_modules/@engenty-wizards/shared/dist/workspace.js +51 -0
  303. package/node_modules/@engenty-wizards/shared/package.json +10 -0
  304. package/package.json +62 -0
  305. package/plugin/.claude-plugin/plugin.json +9 -0
  306. package/plugin/.mcp.json +8 -0
  307. package/plugin/commands/wizard.md +8 -0
  308. package/plugin/skills/build-wizard/SKILL.md +27 -0
@@ -0,0 +1,59 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { randomBytes } from "node:crypto";
3
+ import { createMcpHandler, fromJsonSchema, McpServer, } from "@modelcontextprotocol/server";
4
+ import { env } from "../env.js";
5
+ const bridges = new Map();
6
+ const textOf = (value) => typeof value === "string" ? value : JSON.stringify(value ?? null);
7
+ /** A tool's result as MCP content: text, or the blocks its `toModelOutput` shapes (text and images). */
8
+ function contentOf(tool, output) {
9
+ const shaped = tool.toModelOutput?.(output);
10
+ if (shaped?.type === "content") {
11
+ return {
12
+ content: shaped.value.map((part) => part.type === "image-data"
13
+ ? { type: "image", data: part.data ?? "", mimeType: part.mediaType ?? "image/png" }
14
+ : { type: "text", text: part.text ?? textOf(part) }),
15
+ };
16
+ }
17
+ return { content: [{ type: "text", text: textOf(shaped ? shaped.value : output) }] };
18
+ }
19
+ export function openBridge(tools) {
20
+ const token = randomBytes(24).toString("base64url");
21
+ // The client's requests arrive on their own; the tools run as the call that opened the bridge —
22
+ // in its tenant, as its person.
23
+ const inCall = AsyncLocalStorage.snapshot();
24
+ const handler = createMcpHandler(() => {
25
+ const server = new McpServer({ name: "step", version: "0.1.0" }, { capabilities: { tools: {} } });
26
+ for (const tool of tools) {
27
+ server.registerTool(tool.name, {
28
+ description: tool.description,
29
+ inputSchema: fromJsonSchema(tool.inputSchema),
30
+ }, (async (args) => {
31
+ try {
32
+ return contentOf(tool, await inCall(() => tool.execute(args)));
33
+ }
34
+ catch (err) {
35
+ return {
36
+ isError: true,
37
+ content: [{ type: "text", text: err?.message ?? String(err) }],
38
+ };
39
+ }
40
+ }));
41
+ }
42
+ return server;
43
+ }, { legacy: "stateless" });
44
+ bridges.set(token, handler);
45
+ return {
46
+ url: `http://127.0.0.1:${env.port}/api/mcp/bridge/${token}`,
47
+ close: () => {
48
+ bridges.delete(token);
49
+ },
50
+ };
51
+ }
52
+ /** Answers an MCP request of an open bridge; a closed or unknown one is not found. */
53
+ export function bridgeRequest(token, request) {
54
+ const handler = bridges.get(token);
55
+ if (!handler) {
56
+ return Promise.resolve(new Response("not found", { status: 404 }));
57
+ }
58
+ return handler.fetch(request);
59
+ }
@@ -0,0 +1,42 @@
1
+ import { createMcpHandler, McpServer, originValidationResponse, } from "@modelcontextprotocol/server";
2
+ import { withTenant } from "../db/client.js";
3
+ import { env } from "../env.js";
4
+ import { tenantStatus } from "../tenants/control.js";
5
+ import { authenticate, principalOf } from "./auth.js";
6
+ import { registerPrompts, registerTools } from "./tools.js";
7
+ const INSTRUCTIONS = `engenty wizards: build page-by-page AI wizards that people open on a shared link.
8
+ Start with get_authoring_guide (schema + design rules), look at a close starter with get_starter, then create_wizard and refine with edit_wizard until "issues" is empty.
9
+ Check it with start_test_run + get_test_run (waitSeconds) — test runs spend the admin's credits.
10
+ Publish only when the admin asks. Hand the admin the studioUrl: the studio shows the wizard as a diagram.`;
11
+ /** A wizard definition is a few KB; this leaves room and refuses anything far beyond it. */
12
+ const MAX_BODY_BYTES = 300_000;
13
+ /**
14
+ * Streamable HTTP MCP endpoint, stateless: every request authenticates — with an access token of
15
+ * the Manage-App or an API key — and gets a fresh server whose tools act as that admin, in the
16
+ * tenant the token or key names.
17
+ */
18
+ export function mcpHandler() {
19
+ const allowedHosts = [new URL(env.appUrl).hostname, "localhost", "127.0.0.1"];
20
+ const handler = createMcpHandler((ctx) => {
21
+ const server = new McpServer({ name: "engenty-wizards", version: "0.1.0" }, { instructions: INSTRUCTIONS, capabilities: { tools: {}, prompts: {} } });
22
+ const who = principalOf(ctx.authInfo);
23
+ registerTools(server, who);
24
+ registerPrompts(server);
25
+ return server;
26
+ }, { legacy: "stateless", maxRequestBodySize: MAX_BODY_BYTES });
27
+ return async (request) => {
28
+ const rejected = originValidationResponse(request, allowedHosts);
29
+ if (rejected) {
30
+ return rejected;
31
+ }
32
+ const authInfo = await authenticate(request);
33
+ if (authInfo instanceof Response) {
34
+ return authInfo;
35
+ }
36
+ const who = principalOf(authInfo);
37
+ if ((await tenantStatus(who.tenantId)) === "suspended") {
38
+ return new Response(JSON.stringify({ error: "tenant_suspended" }), { status: 403 });
39
+ }
40
+ return withTenant(who.tenantId, () => handler.fetch(request, { authInfo }));
41
+ };
42
+ }
@@ -0,0 +1,5 @@
1
+ import { env } from "../env.js";
2
+ /** What an MCP client may do; the Manage-App's consent page lists them, tools register only when granted. */
3
+ export const SCOPES = ["wizards:read", "wizards:write", "wizards:publish", "runs:test"];
4
+ /** The protected resource OAuth tokens are bound to (RFC 8707 / RFC 9728). */
5
+ export const MCP_RESOURCE = `${env.appUrl}/api/mcp`;
@@ -0,0 +1,401 @@
1
+ import { z } from "zod";
2
+ import { authoringGuide } from "../authoring/guide.js";
3
+ import { wizardOpSchema } from "../authoring/ops.js";
4
+ import { importFromRegistry, listConnectors, searchRegistry } from "../connectors/external.js";
5
+ import { RunConflict } from "../engine/runner.js";
6
+ import { ServiceError } from "../services/errors.js";
7
+ import { deleteFile, listFiles, readFileText, writeFile } from "../services/files.js";
8
+ import { marketplaceWizard, searchMarketplace } from "../services/marketplace.js";
9
+ import { defaultProject, listProjects, ownedProject } from "../services/projects.js";
10
+ import { startTestRun, testRunReport } from "../services/runs.js";
11
+ import { checkDraftWidget } from "../services/widgets.js";
12
+ import { createWizard, editWizard, listWizards, ownedWizard, parseDraft, publishWizard, shareUrl, studioUrl, updateWizardSettings, wizardState, writeDraft, } from "../services/wizards.js";
13
+ const definition = z
14
+ .record(z.string(), z.unknown())
15
+ .describe("The complete wizard JSON as described by get_authoring_guide.");
16
+ const note = z
17
+ .string()
18
+ .max(500)
19
+ .optional()
20
+ .describe("One sentence for the admin about what changed, in their language. Shown in the studio.");
21
+ const baseRevision = z
22
+ .number()
23
+ .int()
24
+ .min(0)
25
+ .describe("The revision you last read. A newer one on the server answers revision_conflict.");
26
+ function ok(value) {
27
+ return {
28
+ content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value) }],
29
+ };
30
+ }
31
+ function failure(err) {
32
+ let body;
33
+ if (err instanceof ServiceError) {
34
+ body = err.toJSON();
35
+ }
36
+ else if (err instanceof RunConflict) {
37
+ body = { error: err.message, code: "conflict" };
38
+ }
39
+ else if (err instanceof z.ZodError) {
40
+ body = { error: "Invalid input", code: "invalid", issues: err.issues };
41
+ }
42
+ else {
43
+ console.error("[mcp]", err);
44
+ body = { error: "Internal error", code: "internal" };
45
+ }
46
+ return { isError: true, content: [{ type: "text", text: JSON.stringify(body) }] };
47
+ }
48
+ export function registerTools(server, who) {
49
+ const writer = { source: "mcp", client: who.client };
50
+ const tool = (name, scope, config, run) => {
51
+ if (!who.scopes.includes(scope)) {
52
+ return;
53
+ }
54
+ server.registerTool(name, {
55
+ title: config.title,
56
+ description: config.description,
57
+ inputSchema: config.input,
58
+ annotations: { readOnlyHint: config.readOnly ?? false, openWorldHint: false },
59
+ },
60
+ // The SDK has parsed `args` with `input` before this runs.
61
+ (async (args) => {
62
+ try {
63
+ return ok(await run(args));
64
+ }
65
+ catch (err) {
66
+ return failure(err);
67
+ }
68
+ }));
69
+ };
70
+ // --- read ------------------------------------------------------------------
71
+ tool("get_authoring_guide", "wizards:read", {
72
+ title: "Authoring guide",
73
+ description: "Read this before writing a wizard: the JSON schema, field kinds, step types and tools, templates, design rules, and the MCP servers of the project.",
74
+ input: z.object({
75
+ projectId: z.string().optional().describe("Defaults to the admin's first project."),
76
+ }),
77
+ readOnly: true,
78
+ }, async ({ projectId }) => {
79
+ const project = projectId
80
+ ? await ownedProject(who.userId, projectId)
81
+ : await defaultProject(who.userId);
82
+ return authoringGuide(project.mcpServers.map((s) => ({ id: s.id, name: s.name })), await listConnectors(project.id));
83
+ });
84
+ // --- connectors ------------------------------------------------------------
85
+ tool("find_connectors", "wizards:read", {
86
+ title: "Find connectors",
87
+ description: "Search the integrations registry for a service that can be imported as a connector, from its OpenAPI spec or its MCP server.",
88
+ input: z.object({ query: z.string().min(1).max(200) }),
89
+ readOnly: true,
90
+ }, async ({ query }) => ({ services: await searchRegistry(query) }));
91
+ tool("list_connectors", "wizards:read", {
92
+ title: "List connectors",
93
+ description: "The connectors a project has imported, each with its actions.",
94
+ input: z.object({ projectId: z.string().optional() }),
95
+ readOnly: true,
96
+ }, async ({ projectId }) => {
97
+ const project = projectId
98
+ ? await ownedProject(who.userId, projectId)
99
+ : await defaultProject(who.userId);
100
+ return { projectId: project.id, connectors: await listConnectors(project.id) };
101
+ });
102
+ tool("import_connector", "wizards:write", {
103
+ title: "Import connector",
104
+ description: "Import a registry service into a project as a connector. Returns its id, tool prefix, how people connect and its actions. A wizard uses it through a connection { id, connector }.",
105
+ input: z.object({
106
+ projectId: z.string().optional(),
107
+ domain: z.string().describe("A domain from find_connectors, e.g. notion.com"),
108
+ kind: z.enum(["mcp", "openapi"]).optional(),
109
+ }),
110
+ }, async ({ projectId, domain, kind }) => {
111
+ const project = projectId
112
+ ? await ownedProject(who.userId, projectId)
113
+ : await defaultProject(who.userId);
114
+ return importFromRegistry(who.userId, project.id, { domain, kind });
115
+ });
116
+ tool("list_starters", "wizards:read", {
117
+ title: "List starters",
118
+ description: "The marketplace's ready-made wizards (tweet with image, video ad, research briefing, dashboard, invoice, offer …), with what each makes and needs. Give a query to search them; without one you get the starters.",
119
+ input: z.object({ query: z.string().max(200).optional() }),
120
+ readOnly: true,
121
+ }, async ({ query }) => (await searchMarketplace({ q: query ?? "", lang: "de", limit: 30 })).entries
122
+ .filter((s) => s.usable && (query?.trim() || s.starter))
123
+ .map((s) => ({
124
+ id: s.id,
125
+ title: s.title,
126
+ pitch: s.pitch,
127
+ formats: s.formats,
128
+ useCases: s.useCases,
129
+ capabilities: s.capabilities,
130
+ })));
131
+ tool("get_starter", "wizards:read", {
132
+ title: "Get starter",
133
+ description: "A starter's complete wizard JSON — the best example to copy from.",
134
+ input: z.object({ starterId: z.string() }),
135
+ readOnly: true,
136
+ }, async ({ starterId }) => {
137
+ const starter = await marketplaceWizard(starterId, "de");
138
+ if (!starter) {
139
+ throw new ServiceError("not_found", `There is no starter "${starterId}".`);
140
+ }
141
+ return starter.definition;
142
+ });
143
+ tool("list_projects", "wizards:read", {
144
+ title: "List projects",
145
+ description: "The admin's projects. Each holds wizards, a brand and MCP servers.",
146
+ input: z.object({}),
147
+ readOnly: true,
148
+ }, async () => (await listProjects(who.userId)).map((p) => ({
149
+ id: p.id,
150
+ name: p.name,
151
+ wizardCount: p.wizardCount,
152
+ })));
153
+ tool("list_wizards", "wizards:read", {
154
+ title: "List wizards",
155
+ description: "The admin's wizards, newest first.",
156
+ input: z.object({ projectId: z.string().optional() }),
157
+ readOnly: true,
158
+ }, async ({ projectId }) => (await listWizards(who.userId, projectId)).map((w) => ({
159
+ id: w.id,
160
+ projectId: w.projectId,
161
+ title: w.title,
162
+ revision: w.revision,
163
+ published: w.published,
164
+ updatedAt: w.updatedAt,
165
+ studioUrl: studioUrl(w.id),
166
+ })));
167
+ tool("get_wizard", "wizards:read", {
168
+ title: "Get wizard",
169
+ description: "A wizard's draft with its revision and validator issues, whether it differs from the published version, and its links.",
170
+ input: z.object({ wizardId: z.string() }),
171
+ readOnly: true,
172
+ }, async ({ wizardId }) => {
173
+ const s = await wizardState(await ownedWizard(who.userId, wizardId));
174
+ return {
175
+ wizardId: s.id,
176
+ projectId: s.projectId,
177
+ revision: s.revision,
178
+ draft: s.draft,
179
+ issues: s.issues,
180
+ publishedVersion: s.publishedVersion,
181
+ dirty: s.dirty,
182
+ shareEnabled: s.shareEnabled,
183
+ dailyRunLimit: s.dailyRunLimit,
184
+ shareUrl: s.shareUrl,
185
+ studioUrl: s.studioUrl,
186
+ };
187
+ });
188
+ tool("validate_wizard", "wizards:read", {
189
+ title: "Validate wizard",
190
+ description: "Check a wizard JSON without saving it. Returns ok and the issues.",
191
+ input: z.object({ definition }),
192
+ readOnly: true,
193
+ }, async (args) => {
194
+ try {
195
+ const { issues } = parseDraft(args.definition);
196
+ return { ok: issues.length === 0, issues };
197
+ }
198
+ catch (err) {
199
+ if (err instanceof ServiceError && err.code === "invalid") {
200
+ return { ok: false, issues: err.data.issues };
201
+ }
202
+ throw err;
203
+ }
204
+ });
205
+ // --- write -----------------------------------------------------------------
206
+ tool("create_wizard", "wizards:write", {
207
+ title: "Create wizard",
208
+ description: "Create a wizard from your JSON, or copy a starter. The draft may still have issues; fix them with edit_wizard.",
209
+ input: z.object({
210
+ projectId: z.string().optional().describe("Defaults to the admin's first project."),
211
+ definition: definition.optional(),
212
+ starterId: z.string().optional().describe("Copy this starter instead of sending JSON."),
213
+ note,
214
+ }),
215
+ }, async (args) => {
216
+ const created = await createWizard(who.userId, args, writer);
217
+ return {
218
+ wizardId: created.id,
219
+ revision: created.revision,
220
+ issues: created.issues,
221
+ studioUrl: created.studioUrl,
222
+ };
223
+ });
224
+ tool("edit_wizard", "wizards:write", {
225
+ title: "Edit wizard",
226
+ description: `Apply ops to the draft atomically, in order:
227
+ - set_meta {title?, description?, avatar?, intro? (null removes)}
228
+ - set_lists {lists} / set_connections {connections}: replace the wizard's lists or connections as a whole
229
+ - upsert_step {step (complete), before?|after?}: replaces the step with that id, else inserts it (default: before the final result step)
230
+ - remove_step {stepId}
231
+ - move_step {stepId, before|after}
232
+ Returns the new revision and the issues.`,
233
+ input: z.object({
234
+ wizardId: z.string(),
235
+ baseRevision,
236
+ ops: z.array(wizardOpSchema).min(1).max(50),
237
+ note,
238
+ }),
239
+ }, async ({ wizardId, ...input }) => {
240
+ const { revision, issues } = await editWizard(who.userId, wizardId, input, writer);
241
+ return { revision, issues };
242
+ });
243
+ tool("replace_wizard", "wizards:write", {
244
+ title: "Replace wizard",
245
+ description: "Replace the whole draft. Use edit_wizard for smaller changes.",
246
+ input: z.object({ wizardId: z.string(), baseRevision, definition, note }),
247
+ }, async ({ wizardId, ...input }) => {
248
+ const { revision, issues } = await writeDraft(who.userId, wizardId, input, writer);
249
+ return { revision, issues };
250
+ });
251
+ // --- publish ---------------------------------------------------------------
252
+ tool("publish_wizard", "wizards:publish", {
253
+ title: "Publish wizard",
254
+ description: "Publish the draft as a new version behind the share link. Refused while the draft has issues. Only when the admin asked for it.",
255
+ input: z.object({ wizardId: z.string() }),
256
+ }, async ({ wizardId }) => publishWizard(who.userId, wizardId));
257
+ tool("set_sharing", "wizards:publish", {
258
+ title: "Set sharing",
259
+ description: "Turn the share link on or off, or change how many runs it allows per day.",
260
+ input: z.object({
261
+ wizardId: z.string(),
262
+ enabled: z.boolean().optional(),
263
+ dailyRunLimit: z.number().int().min(1).max(10_000).optional(),
264
+ }),
265
+ }, async ({ wizardId, enabled, dailyRunLimit }) => {
266
+ const w = await updateWizardSettings(who.userId, wizardId, {
267
+ shareEnabled: enabled,
268
+ dailyRunLimit,
269
+ });
270
+ return {
271
+ shareEnabled: w.shareEnabled,
272
+ dailyRunLimit: w.dailyRunLimit,
273
+ shareUrl: shareUrl(w.shareToken),
274
+ };
275
+ });
276
+ // --- test runs ---------------------------------------------------------------
277
+ tool("start_test_run", "runs:test", {
278
+ title: "Start test run",
279
+ description: "Run the draft once with your answers. SPENDS THE ADMIN'S CREDITS (short copy + image ~15, research ~150, video ~250). Pages are filled from `answers`, reviews accepted when acceptReviews is true. Follow it with get_test_run.",
280
+ input: z.object({
281
+ wizardId: z.string(),
282
+ answers: z
283
+ .record(z.string(), z.unknown())
284
+ .default({})
285
+ .describe("Field id → value, for every page (field ids are unique in a wizard). Items fields take an array of row objects."),
286
+ acceptReviews: z.boolean().default(true),
287
+ }),
288
+ }, async ({ wizardId, answers, acceptReviews }) => startTestRun(who.userId, wizardId, { answers, acceptReviews }));
289
+ tool("get_test_run", "runs:test", {
290
+ title: "Get test run",
291
+ description: "Status, recent events, outputs (text clipped) with signed download links valid one hour, credits spent, and what the run waits for.",
292
+ input: z.object({
293
+ runId: z.string(),
294
+ waitSeconds: z
295
+ .number()
296
+ .int()
297
+ .min(0)
298
+ .max(45)
299
+ .default(0)
300
+ .describe("Wait up to this long while the run is still working."),
301
+ }),
302
+ readOnly: true,
303
+ }, async ({ runId, waitSeconds }) => testRunReport(who.userId, runId, waitSeconds));
304
+ // --- workspace (widget code, libraries, reference data) --------------------
305
+ tool("list_files", "wizards:read", {
306
+ title: "List workspace files",
307
+ description: "The wizard's workspace: widget HTML/JS/CSS, vendored libraries, reference data. Runs use a snapshot of it.",
308
+ input: z.object({ wizardId: z.string() }),
309
+ readOnly: true,
310
+ }, async ({ wizardId }) => (await listFiles(who.userId, wizardId)).map(({ path, mime, size }) => ({ path, mime, size })));
311
+ tool("read_file", "wizards:read", {
312
+ title: "Read workspace file",
313
+ description: "A workspace file as text (binary files are described, not returned).",
314
+ input: z.object({ wizardId: z.string(), path: z.string() }),
315
+ readOnly: true,
316
+ }, async ({ wizardId, path }) => readFileText(who.userId, wizardId, path));
317
+ tool("write_file", "wizards:write", {
318
+ title: "Write workspace file",
319
+ description: "Create or overwrite a workspace file: `content` for text (HTML, JS, CSS, JSON, CSV, SVG) or `base64` for binary (images, fonts). Max 5 MB per file, 25 MB per workspace. A widget may only use files from here — no network when it runs.",
320
+ input: z.object({
321
+ wizardId: z.string(),
322
+ path: z.string().describe('Relative path, e.g. "weather/index.html" or "lib/d3.min.js".'),
323
+ content: z.string().optional(),
324
+ base64: z.string().optional(),
325
+ mime: z.string().optional(),
326
+ }),
327
+ }, async ({ wizardId, path, content, base64, mime }) => {
328
+ if ((content === undefined) === (base64 === undefined)) {
329
+ throw new ServiceError("invalid", "Pass either content or base64.");
330
+ }
331
+ return writeFile(who.userId, wizardId, path, content ?? Buffer.from(base64 ?? "", "base64"), mime);
332
+ });
333
+ tool("delete_file", "wizards:write", {
334
+ title: "Delete workspace file",
335
+ description: "Remove a file from the workspace.",
336
+ input: z.object({ wizardId: z.string(), path: z.string() }),
337
+ }, async ({ wizardId, path }) => {
338
+ await deleteFile(who.userId, wizardId, path);
339
+ return { ok: true };
340
+ });
341
+ if (who.scopes.includes("wizards:read")) {
342
+ const input = z.object({
343
+ wizardId: z.string(),
344
+ stepId: z.string().describe("A widget step of the draft."),
345
+ data: z
346
+ .record(z.string(), z.unknown())
347
+ .optional()
348
+ .describe("Data to show instead of the step's sample file."),
349
+ });
350
+ server.registerTool("check_widget", {
351
+ title: "Check widget",
352
+ description: "Load a widget step of the draft in a real browser with its sample data: script errors, the timeline it registered (seconds, for the MP4 export) and a screenshot.",
353
+ inputSchema: input,
354
+ annotations: { readOnlyHint: true, openWorldHint: false },
355
+ }, (async ({ wizardId, stepId, data }) => {
356
+ try {
357
+ const r = await checkDraftWidget(who.userId, wizardId, stepId, data);
358
+ return {
359
+ content: [
360
+ {
361
+ type: "text",
362
+ text: JSON.stringify({
363
+ errors: r.errors,
364
+ timelineSeconds: r.duration,
365
+ size: r.size,
366
+ bundleBytes: r.bytes,
367
+ }),
368
+ },
369
+ { type: "image", data: Buffer.from(r.png).toString("base64"), mimeType: "image/png" },
370
+ ],
371
+ };
372
+ }
373
+ catch (err) {
374
+ return failure(err);
375
+ }
376
+ }));
377
+ }
378
+ }
379
+ export function registerPrompts(server) {
380
+ server.registerPrompt("design_wizard", {
381
+ title: "Design a wizard",
382
+ description: "Build an engenty wizard from a description.",
383
+ argsSchema: z.object({ description: z.string().describe("What the wizard should do.") }),
384
+ }, ({ description }) => ({
385
+ messages: [
386
+ {
387
+ role: "user",
388
+ content: {
389
+ type: "text",
390
+ text: `Build an engenty wizard: ${description}
391
+
392
+ 1. Call get_authoring_guide and follow its rules.
393
+ 2. Pick the closest starter (list_starters, get_starter) as an example.
394
+ 3. create_wizard with a short note; fix every issue with edit_wizard.
395
+ 4. start_test_run with realistic answers, then get_test_run with waitSeconds until it is done; fix what looks wrong.
396
+ 5. Give me the studioUrl and a short summary. Publish only if I ask.`,
397
+ },
398
+ },
399
+ ],
400
+ }));
401
+ }
@@ -0,0 +1,121 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { env } from "../env.js";
6
+ let ffmpegCheck = null;
7
+ export function hasFfmpeg() {
8
+ ffmpegCheck ??= new Promise((resolve) => {
9
+ const p = spawn(env.ffmpegPath, ["-version"], { stdio: "ignore" });
10
+ p.on("error", () => resolve(false));
11
+ p.on("exit", (code) => resolve(code === 0));
12
+ });
13
+ return ffmpegCheck;
14
+ }
15
+ /** Runs ffmpeg to its end; what it wrote to stderr comes back either way. */
16
+ export function ffmpeg(args) {
17
+ return new Promise((resolve, reject) => {
18
+ const p = spawn(env.ffmpegPath, ["-hide_banner", ...args], {
19
+ stdio: ["ignore", "ignore", "pipe"],
20
+ });
21
+ let stderr = "";
22
+ p.stderr.on("data", (d) => {
23
+ stderr = (stderr + String(d)).slice(-20_000);
24
+ });
25
+ p.on("error", reject);
26
+ p.on("exit", (code) => resolve({ code: code ?? 1, stderr }));
27
+ });
28
+ }
29
+ /** A folder for one job's files, removed when the job is over. */
30
+ export async function withTempDir(use) {
31
+ const dir = await mkdtemp(join(tmpdir(), "wizard-media-"));
32
+ try {
33
+ return await use(dir);
34
+ }
35
+ finally {
36
+ await rm(dir, { recursive: true, force: true });
37
+ }
38
+ }
39
+ /** What ffmpeg says about a media file: how long it is, whether it carries sound, its picture size. */
40
+ export async function probeMedia(path) {
41
+ // Without an output ffmpeg prints the streams and exits with an error; the print is the answer.
42
+ const { stderr } = await ffmpeg(["-i", path]);
43
+ const m = stderr.match(/Duration:\s*(\d+):(\d+):(\d+(?:\.\d+)?)/);
44
+ const seconds = m ? Number(m[1]) * 3600 + Number(m[2]) * 60 + Number(m[3]) : 0;
45
+ const size = stderr.match(/Stream #\d+:\d+.*Video:.*?, (\d{2,5})x(\d{2,5})/);
46
+ return {
47
+ seconds,
48
+ audio: /Stream #\d+:\d+.*Audio:/.test(stderr),
49
+ width: size ? Number(size[1]) : 0,
50
+ height: size ? Number(size[2]) : 0,
51
+ };
52
+ }
53
+ /**
54
+ * A video no larger than `short` pixels on its short side (720 for 720p), sound kept. One that
55
+ * fits already comes back as it is; without ffmpeg too.
56
+ */
57
+ export async function capVideo(bytes, short) {
58
+ if (!(await hasFfmpeg())) {
59
+ return bytes;
60
+ }
61
+ return withTempDir(async (dir) => {
62
+ const input = join(dir, "in.mp4");
63
+ const out = join(dir, "out.mp4");
64
+ await writeFile(input, bytes);
65
+ const { width, height } = await probeMedia(input);
66
+ if (!(width && height) || Math.min(width, height) <= short) {
67
+ return bytes;
68
+ }
69
+ const scale = width >= height ? `scale=-2:${short}` : `scale=${short}:-2`;
70
+ const { code } = await ffmpeg([
71
+ "-y",
72
+ "-loglevel",
73
+ "error",
74
+ "-i",
75
+ input,
76
+ "-vf",
77
+ scale,
78
+ "-c:v",
79
+ "libx264",
80
+ "-preset",
81
+ "veryfast",
82
+ "-crf",
83
+ "21",
84
+ "-pix_fmt",
85
+ "yuv420p",
86
+ "-c:a",
87
+ "copy",
88
+ "-movflags",
89
+ "+faststart",
90
+ out,
91
+ ]);
92
+ return code === 0 ? new Uint8Array(await readFile(out)) : bytes;
93
+ });
94
+ }
95
+ /**
96
+ * Any audio as MP3 — what every phone and browser plays — with `tags` written into the file
97
+ * (ID3). An MP3 is not encoded again, only tagged. `null` without ffmpeg.
98
+ */
99
+ export async function toMp3(bytes, ext, tags = {}) {
100
+ if (!(await hasFfmpeg())) {
101
+ return null;
102
+ }
103
+ return withTempDir(async (dir) => {
104
+ const input = join(dir, `in.${ext}`);
105
+ const out = join(dir, "out.mp3");
106
+ await writeFile(input, bytes);
107
+ const { code } = await ffmpeg([
108
+ "-y",
109
+ "-loglevel",
110
+ "error",
111
+ "-i",
112
+ input,
113
+ ...(ext === "mp3" ? ["-codec:a", "copy"] : ["-codec:a", "libmp3lame", "-q:a", "3"]),
114
+ "-id3v2_version",
115
+ "3",
116
+ ...Object.entries(tags).flatMap(([key, value]) => ["-metadata", `${key}=${value}`]),
117
+ out,
118
+ ]);
119
+ return code === 0 ? new Uint8Array(await readFile(out)) : null;
120
+ });
121
+ }