@vfrey/vfrey 0.0.1 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (610) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +78 -3
  3. package/accounts.example.json +61 -0
  4. package/accounts.first-run.json +41 -0
  5. package/bin/vfrey.mjs +36 -0
  6. package/integration-tools/README.md +43 -0
  7. package/integration-tools/image/pillow.md +32 -0
  8. package/integration-tools/image/playwright.md +33 -0
  9. package/integration-tools/image/resvg.md +36 -0
  10. package/integration-tools/image/satori.md +36 -0
  11. package/integration-tools/image/sharp.md +38 -0
  12. package/integration-tools/video/blender.md +34 -0
  13. package/integration-tools/video/ffmpeg.md +41 -0
  14. package/integration-tools/video/manim.md +37 -0
  15. package/integration-tools/video/motion-canvas.md +37 -0
  16. package/integration-tools/video/remotion.md +50 -0
  17. package/integration-tools/video/revideo.md +32 -0
  18. package/package.json +74 -6
  19. package/skills/vfrey-orchestrator/SKILL.md +15 -0
  20. package/skills/vfrey-orchestrator/channel-video/SKILL.md +41 -0
  21. package/skills/vfrey-product/SKILL.md +63 -0
  22. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-one.svg +14 -0
  23. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-two.svg +22 -0
  24. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/index.html +70 -0
  25. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-one.svg +14 -0
  26. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-two.svg +22 -0
  27. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/index.tsx +49 -0
  28. package/skills/vfrey-product/examples/concurrency-vs-parallelism/script.json +15 -0
  29. package/skills/vfrey-product/examples/octopus-facts/index.html +60 -0
  30. package/skills/vfrey-product/examples/octopus-facts/octopus.svg +8 -0
  31. package/skills/vfrey-product/examples/octopus-facts/script.json +14 -0
  32. package/skills/vfrey-product/push/SKILL.md +15 -0
  33. package/skills/vfrey-product/scenes/NOTICE +5 -0
  34. package/skills/vfrey-product/scenes/blocks.md +57 -0
  35. package/skills/vfrey-product/scenes/craft.md +118 -0
  36. package/skills/vfrey-product/scenes/motion.md +88 -0
  37. package/src/analytics/dashboard.ts +109 -0
  38. package/src/analytics/index.ts +260 -0
  39. package/src/api/client.ts +361 -0
  40. package/src/api/cloud-files.ts +197 -0
  41. package/src/api/cloud-runs.ts +75 -0
  42. package/src/api/cloud.ts +413 -0
  43. package/src/api/commands.ts +330 -0
  44. package/src/api/config.ts +34 -0
  45. package/src/api/creators.ts +463 -0
  46. package/src/api/credentials.ts +152 -0
  47. package/src/api/mail.ts +333 -0
  48. package/src/api/topics.ts +207 -0
  49. package/src/api/types.ts +273 -0
  50. package/src/cli.ts +468 -0
  51. package/src/cloud/bundle.ts +70 -0
  52. package/src/cloud/cloudrun-executor.ts +237 -0
  53. package/src/cloud/executor.ts +281 -0
  54. package/src/cloud/gcs-sign.ts +76 -0
  55. package/src/cloud/job.ts +106 -0
  56. package/src/cloud/remote-job.ts +67 -0
  57. package/src/cloud/render-job.ts +191 -0
  58. package/src/config.ts +309 -0
  59. package/src/dashboard-link.ts +180 -0
  60. package/src/dashboard-start.ts +103 -0
  61. package/src/desk/channel.ts +64 -0
  62. package/src/desk/cli.ts +216 -0
  63. package/src/desk/creators-route.ts +129 -0
  64. package/src/desk/fonts/JetBrainsMono-OFL.txt +93 -0
  65. package/src/desk/fonts/google-sans-flex-latin.woff2 +0 -0
  66. package/src/desk/fonts/jetbrains-mono-medium.woff2 +0 -0
  67. package/src/desk/http.ts +460 -0
  68. package/src/desk/mail-route.ts +103 -0
  69. package/src/desk/overlay.ts +112 -0
  70. package/src/desk/preview.ts +422 -0
  71. package/src/desk/screens.ts +149 -0
  72. package/src/desk/secrets.ts +143 -0
  73. package/src/desk/series.ts +127 -0
  74. package/src/desk/state.ts +77 -0
  75. package/src/desk/studio-route.ts +119 -0
  76. package/src/desk/tick-runner.ts +66 -0
  77. package/src/desk/tick.ts +56 -0
  78. package/src/desk/topics-route.ts +41 -0
  79. package/src/desk/ui/app.css +505 -0
  80. package/src/desk/ui/app.js +185 -0
  81. package/src/desk/ui/apple-icon.png +0 -0
  82. package/src/desk/ui/components.js +79 -0
  83. package/src/desk/ui/core.js +156 -0
  84. package/src/desk/ui/favicon.ico +0 -0
  85. package/src/desk/ui/icon.svg +1 -0
  86. package/src/desk/ui/icons.js +49 -0
  87. package/src/desk/ui/index.html +41 -0
  88. package/src/desk/ui/metrics-math.d.ts +22 -0
  89. package/src/desk/ui/metrics-math.js +135 -0
  90. package/src/desk/ui/theme.js +5 -0
  91. package/src/desk/ui/views-creators.js +209 -0
  92. package/src/desk/ui/views-keys.js +114 -0
  93. package/src/desk/ui/views-mail.js +188 -0
  94. package/src/desk/ui/views-misc.js +123 -0
  95. package/src/desk/ui/views-review.js +119 -0
  96. package/src/desk/ui/views-runs.js +350 -0
  97. package/src/desk/ui/views-screens.js +163 -0
  98. package/src/desk/ui/views-series.js +318 -0
  99. package/src/desk/ui/views-topics.js +87 -0
  100. package/src/desk/verbs.ts +361 -0
  101. package/src/doctor.ts +212 -0
  102. package/src/drain.ts +14 -0
  103. package/src/engage/index.ts +217 -0
  104. package/src/generate/index.ts +110 -0
  105. package/src/generate/script.ts +152 -0
  106. package/src/generate/title.ts +78 -0
  107. package/src/help.ts +252 -0
  108. package/src/home.ts +45 -0
  109. package/src/llm/client.ts +177 -0
  110. package/src/log.ts +7 -0
  111. package/src/oauth/tokens.ts +189 -0
  112. package/src/oauth/vault.ts +72 -0
  113. package/src/pipeline/review.ts +642 -0
  114. package/src/pipeline/run.ts +443 -0
  115. package/src/pipeline/series-guard.ts +26 -0
  116. package/src/pipeline/source-fetch.ts +69 -0
  117. package/src/pipeline/stages.ts +728 -0
  118. package/src/pipeline/topics-source.ts +90 -0
  119. package/src/product/commands.ts +296 -0
  120. package/src/product/config-file.ts +38 -0
  121. package/src/product/engine-choice.ts +89 -0
  122. package/src/product/engines.ts +103 -0
  123. package/src/product/insights.ts +50 -0
  124. package/src/product/keys.ts +67 -0
  125. package/src/product/paths.ts +18 -0
  126. package/src/product/probes.ts +88 -0
  127. package/src/product/push-flow.ts +90 -0
  128. package/src/product/render-video.ts +21 -0
  129. package/src/product/short.ts +62 -0
  130. package/src/product/update.ts +32 -0
  131. package/src/project/canonical.ts +14 -0
  132. package/src/project/commands.ts +177 -0
  133. package/src/project/dashboard-status.ts +28 -0
  134. package/src/project/discover.ts +19 -0
  135. package/src/project/index-file.ts +73 -0
  136. package/src/project/marker.ts +92 -0
  137. package/src/project/migrate.ts +201 -0
  138. package/src/project/names.ts +27 -0
  139. package/src/project/notice.ts +16 -0
  140. package/src/project/open.ts +88 -0
  141. package/src/project/scaffold.ts +112 -0
  142. package/src/project/templates.ts +42 -0
  143. package/src/project/videos.ts +64 -0
  144. package/src/publish/facebook.ts +91 -0
  145. package/src/publish/index.ts +12 -0
  146. package/src/publish/instagram.ts +97 -0
  147. package/src/publish/rate.ts +62 -0
  148. package/src/publish/session.ts +242 -0
  149. package/src/publish/tiktok.ts +202 -0
  150. package/src/publish/youtube.ts +177 -0
  151. package/src/queue/desk-ops.ts +1170 -0
  152. package/src/queue/seal.ts +3 -0
  153. package/src/queue/store.ts +1006 -0
  154. package/src/remotion/AiLabel.tsx +26 -0
  155. package/src/remotion/Root.tsx +73 -0
  156. package/src/remotion/defaults.ts +26 -0
  157. package/src/remotion/films/claude/ClaudeFilm.tsx +1014 -0
  158. package/src/remotion/films/engine/EngineMark.tsx +17 -0
  159. package/src/remotion/films/rea/ReaShort.tsx +552 -0
  160. package/src/remotion/films/rea/drawings.tsx +907 -0
  161. package/src/remotion/films/rea/timing.ts +144 -0
  162. package/src/remotion/index.ts +4 -0
  163. package/src/remotion/templates/EditorialSplit.tsx +39 -0
  164. package/src/remotion/templates/KineticType.tsx +39 -0
  165. package/src/remotion/templates/SignalBoard.tsx +50 -0
  166. package/src/remotion/timing.ts +22 -0
  167. package/src/render/chrome.ts +78 -0
  168. package/src/render/engine-mark.ts +29 -0
  169. package/src/render/framey-template.ts +130 -0
  170. package/src/render/html-engine.ts +149 -0
  171. package/src/render/node-crypto-stub.ts +11 -0
  172. package/src/render/probe.ts +29 -0
  173. package/src/render/props.ts +17 -0
  174. package/src/render/vertical-short.ts +35 -0
  175. package/src/render/webpack.ts +33 -0
  176. package/src/render/worker.ts +65 -0
  177. package/src/scene/assets/fonts/TitanOne-OFL.txt +94 -0
  178. package/src/scene/assets/fonts/titan-one.woff2 +0 -0
  179. package/src/scene/assets/gsap/LICENSE.txt +8 -0
  180. package/src/scene/assets/gsap/gsap.min.js +11 -0
  181. package/src/scene/audio.ts +54 -0
  182. package/src/scene/blocks/LICENSE +190 -0
  183. package/src/scene/blocks/NOTICE +5 -0
  184. package/src/scene/blocks/bar-chart-race/bar-chart-race.html +585 -0
  185. package/src/scene/blocks/beat-freeze-cut/beat-freeze-cut.html +874 -0
  186. package/src/scene/blocks/camcorder-hud/camcorder-hud.html +164 -0
  187. package/src/scene/blocks/camera-dolly-zoom/camera-dolly-zoom.html +355 -0
  188. package/src/scene/blocks/code-diff/code-diff.html +817 -0
  189. package/src/scene/blocks/code-snippet-apple-terminal-pro/code-snippet-apple-terminal-pro.html +248 -0
  190. package/src/scene/blocks/code-typing/code-typing.html +764 -0
  191. package/src/scene/blocks/data-chart/data-chart.html +452 -0
  192. package/src/scene/blocks/flowchart-vertical/flowchart-vertical.html +454 -0
  193. package/src/scene/blocks/hw-scribble-transition/hw-scribble-transition.html +153 -0
  194. package/src/scene/blocks/index.json +824 -0
  195. package/src/scene/blocks/lt-accent-underline/lt-accent-underline.html +107 -0
  196. package/src/scene/blocks/lt-bold-block/lt-bold-block.html +109 -0
  197. package/src/scene/blocks/lt-clean-bar/lt-clean-bar.html +112 -0
  198. package/src/scene/blocks/lt-dark-card/lt-dark-card.html +111 -0
  199. package/src/scene/blocks/lt-kicker-name/lt-kicker-name.html +114 -0
  200. package/src/scene/blocks/lt-mask-reveal/lt-mask-reveal.html +120 -0
  201. package/src/scene/blocks/lt-neon-border/lt-neon-border.html +391 -0
  202. package/src/scene/blocks/lt-stack-bars/lt-stack-bars.html +109 -0
  203. package/src/scene/blocks/macos-notification/macos-notification.html +194 -0
  204. package/src/scene/blocks/mk-background/mk-background.html +236 -0
  205. package/src/scene/blocks/mk-callout-highlight/mk-callout-highlight.html +183 -0
  206. package/src/scene/blocks/mk-clone-wall-transition/mk-clone-wall-transition.html +226 -0
  207. package/src/scene/blocks/mk-line-graph/mk-line-graph.html +293 -0
  208. package/src/scene/blocks/mk-progress-stat/mk-progress-stat.html +186 -0
  209. package/src/scene/blocks/mk-specs-list/mk-specs-list.html +223 -0
  210. package/src/scene/blocks/news-ticker/news-ticker.html +409 -0
  211. package/src/scene/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html +126 -0
  212. package/src/scene/blocks/oscilloscope-trace/oscilloscope-trace.html +519 -0
  213. package/src/scene/blocks/snow/snow.html +205 -0
  214. package/src/scene/blocks/split-flap-board/split-flap-board.html +347 -0
  215. package/src/scene/blocks/spotify-card/spotify-card.html +294 -0
  216. package/src/scene/blocks/starfield/starfield.html +188 -0
  217. package/src/scene/blocks/thread-message-stack/thread-message-stack.html +239 -0
  218. package/src/scene/blocks/week-in-merges/week-in-merges.html +470 -0
  219. package/src/scene/blocks/x-post/x-post.html +437 -0
  220. package/src/scene/blocks/yt-comment-card/yt-comment-card.html +319 -0
  221. package/src/scene/blocks/yt-lcd-background/yt-lcd-background.html +307 -0
  222. package/src/scene/blocks/yt-prism-title/yt-prism-title.html +205 -0
  223. package/src/scene/blocks.ts +229 -0
  224. package/src/scene/command.ts +272 -0
  225. package/src/scene/figure.ts +181 -0
  226. package/src/scene/glyphs.ts +107 -0
  227. package/src/scene/init.ts +86 -0
  228. package/src/scene/inspect/css-tree.d.ts +6 -0
  229. package/src/scene/inspect/page-scripts.ts +272 -0
  230. package/src/scene/inspect/report.ts +405 -0
  231. package/src/scene/inspect/run.ts +200 -0
  232. package/src/scene/inspect/source.ts +236 -0
  233. package/src/scene/label.ts +131 -0
  234. package/src/scene/net-guard.ts +74 -0
  235. package/src/scene/render.ts +292 -0
  236. package/src/scene/runtime/browser.ts +199 -0
  237. package/src/scene/runtime/core.ts +181 -0
  238. package/src/scene/runtime/react.tsx +115 -0
  239. package/src/scene/runtime/transitions.ts +133 -0
  240. package/src/scene/sanitize.ts +181 -0
  241. package/src/scene/scene.ts +152 -0
  242. package/src/scene/studio-command.ts +48 -0
  243. package/src/scene/studio-skin.ts +59 -0
  244. package/src/scene/studio.ts +337 -0
  245. package/src/scene/supply.ts +111 -0
  246. package/src/scene/timing.ts +288 -0
  247. package/src/scene/transitions/LICENSE +190 -0
  248. package/src/scene/transitions/NOTICE +5 -0
  249. package/src/scene/transitions/SOURCE.json +24 -0
  250. package/src/scene/transitions/transitions.global.js +38 -0
  251. package/src/scene/transitions.ts +26 -0
  252. package/src/service/all-projects.ts +153 -0
  253. package/src/service/commands.ts +256 -0
  254. package/src/service/due.ts +82 -0
  255. package/src/service/lock.ts +101 -0
  256. package/src/service/log-file.ts +56 -0
  257. package/src/service/state.ts +41 -0
  258. package/src/service/units.ts +90 -0
  259. package/src/service/worker.ts +51 -0
  260. package/src/skill-agents.ts +74 -0
  261. package/src/skill-install.ts +334 -0
  262. package/src/sync-write.ts +21 -0
  263. package/src/tick-report.ts +205 -0
  264. package/src/tts.ts +124 -0
  265. package/vendor/@vfrey/contracts/dist/account.d.ts +97 -0
  266. package/vendor/@vfrey/contracts/dist/account.js +27 -0
  267. package/vendor/@vfrey/contracts/dist/checks.d.ts +8 -0
  268. package/vendor/@vfrey/contracts/dist/checks.js +1 -0
  269. package/vendor/@vfrey/contracts/dist/credentials/crypto.d.ts +4 -0
  270. package/vendor/@vfrey/contracts/dist/credentials/crypto.js +38 -0
  271. package/vendor/@vfrey/contracts/dist/domain/advice-persona.d.ts +10 -0
  272. package/vendor/@vfrey/contracts/dist/domain/advice-persona.js +24 -0
  273. package/vendor/@vfrey/contracts/dist/domain/backoff.d.ts +13 -0
  274. package/vendor/@vfrey/contracts/dist/domain/backoff.js +27 -0
  275. package/vendor/@vfrey/contracts/dist/domain/cadence.d.ts +29 -0
  276. package/vendor/@vfrey/contracts/dist/domain/cadence.js +95 -0
  277. package/vendor/@vfrey/contracts/dist/domain/creators.d.ts +146 -0
  278. package/vendor/@vfrey/contracts/dist/domain/creators.js +2 -0
  279. package/vendor/@vfrey/contracts/dist/domain/disclosure.d.ts +127 -0
  280. package/vendor/@vfrey/contracts/dist/domain/disclosure.js +250 -0
  281. package/vendor/@vfrey/contracts/dist/domain/errors.d.ts +34 -0
  282. package/vendor/@vfrey/contracts/dist/domain/errors.js +68 -0
  283. package/vendor/@vfrey/contracts/dist/domain/redact.d.ts +1 -0
  284. package/vendor/@vfrey/contracts/dist/domain/redact.js +21 -0
  285. package/vendor/@vfrey/contracts/dist/domain/revenue.d.ts +13 -0
  286. package/vendor/@vfrey/contracts/dist/domain/revenue.js +16 -0
  287. package/vendor/@vfrey/contracts/dist/domain/safety.d.ts +20 -0
  288. package/vendor/@vfrey/contracts/dist/domain/safety.js +61 -0
  289. package/vendor/@vfrey/contracts/dist/domain/similarity.d.ts +13 -0
  290. package/vendor/@vfrey/contracts/dist/domain/similarity.js +100 -0
  291. package/vendor/@vfrey/contracts/dist/domain/templates.d.ts +18 -0
  292. package/vendor/@vfrey/contracts/dist/domain/templates.js +67 -0
  293. package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.d.ts +19 -0
  294. package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.js +52 -0
  295. package/vendor/@vfrey/contracts/dist/domain/topics.d.ts +78 -0
  296. package/vendor/@vfrey/contracts/dist/domain/topics.js +4 -0
  297. package/vendor/@vfrey/contracts/dist/domain/types.d.ts +216 -0
  298. package/vendor/@vfrey/contracts/dist/domain/types.js +1 -0
  299. package/vendor/@vfrey/contracts/dist/domain/variation.d.ts +57 -0
  300. package/vendor/@vfrey/contracts/dist/domain/variation.js +278 -0
  301. package/vendor/@vfrey/contracts/dist/events.d.ts +20 -0
  302. package/vendor/@vfrey/contracts/dist/events.js +1 -0
  303. package/vendor/@vfrey/contracts/dist/http.d.ts +11 -0
  304. package/vendor/@vfrey/contracts/dist/http.js +37 -0
  305. package/vendor/@vfrey/contracts/dist/index.d.ts +22 -0
  306. package/vendor/@vfrey/contracts/dist/index.js +22 -0
  307. package/vendor/@vfrey/contracts/dist/log.d.ts +2 -0
  308. package/vendor/@vfrey/contracts/dist/log.js +10 -0
  309. package/vendor/@vfrey/contracts/dist/mail.d.ts +55 -0
  310. package/vendor/@vfrey/contracts/dist/mail.js +1 -0
  311. package/vendor/@vfrey/contracts/package.json +18 -0
  312. package/vendor/@vfrey/ingest/dist/index.d.ts +41 -0
  313. package/vendor/@vfrey/ingest/dist/index.js +221 -0
  314. package/vendor/@vfrey/ingest/dist/parse.d.ts +5 -0
  315. package/vendor/@vfrey/ingest/dist/parse.js +165 -0
  316. package/vendor/@vfrey/ingest/package.json +21 -0
  317. package/vendor/framey/LICENSE +190 -0
  318. package/vendor/framey/NOTICE +15 -0
  319. package/vendor/framey/SOURCE.json +24 -0
  320. package/vendor/framey/STUDIO-SOURCE.json +9 -0
  321. package/vendor/framey/bin/framey-localize-fonts.mjs +12 -0
  322. package/vendor/framey/bin/framey.mjs +16 -0
  323. package/vendor/framey/dist/add-VMJD5KBS.js +75 -0
  324. package/vendor/framey/dist/animatedGifPrep-QPKGJ6PM.js +43 -0
  325. package/vendor/framey/dist/api.d.ts +211 -0
  326. package/vendor/framey/dist/api.js +76 -0
  327. package/vendor/framey/dist/appHistory-WW56FZTT.js +31 -0
  328. package/vendor/framey/dist/asset-WQKGOELE.js +26 -0
  329. package/vendor/framey/dist/audio-RSJWVWRA.js +1936 -0
  330. package/vendor/framey/dist/batchRender-3HPICDCT.js +366 -0
  331. package/vendor/framey/dist/beats-HL2BDUZI.js +273 -0
  332. package/vendor/framey/dist/benchmark-RWCWHI5Z.js +230 -0
  333. package/vendor/framey/dist/browser-ECYET3Q2.js +206 -0
  334. package/vendor/framey/dist/bundleWithLocalizedFonts-ALYBQEQT.js +52 -0
  335. package/vendor/framey/dist/captureCompositionFrame-4DA6SEH2.js +94 -0
  336. package/vendor/framey/dist/catalog-5JVDEUWK.js +979 -0
  337. package/vendor/framey/dist/catch-up-NEBCL5NO.js +106 -0
  338. package/vendor/framey/dist/check-F5N544ML.js +111 -0
  339. package/vendor/framey/dist/checkBrowser-LZ66CMJG.js +110 -0
  340. package/vendor/framey/dist/chunk-2K7Z2HVP.js +372 -0
  341. package/vendor/framey/dist/chunk-2UZQHDM7.js +323 -0
  342. package/vendor/framey/dist/chunk-2X4PQEYM.js +34 -0
  343. package/vendor/framey/dist/chunk-3422KMTD.js +168 -0
  344. package/vendor/framey/dist/chunk-3FYD4YEK.js +72 -0
  345. package/vendor/framey/dist/chunk-3GXQPCJC.js +682 -0
  346. package/vendor/framey/dist/chunk-3LM6YPBR.js +179 -0
  347. package/vendor/framey/dist/chunk-442JNXRM.js +20 -0
  348. package/vendor/framey/dist/chunk-4BNN5FGX.js +182 -0
  349. package/vendor/framey/dist/chunk-4P7NX5BM.js +11532 -0
  350. package/vendor/framey/dist/chunk-4UMPIPCQ.js +88 -0
  351. package/vendor/framey/dist/chunk-4YYJOFKS.js +28 -0
  352. package/vendor/framey/dist/chunk-54HJ63Q2.js +29 -0
  353. package/vendor/framey/dist/chunk-5ETJX6WM.js +89 -0
  354. package/vendor/framey/dist/chunk-5SG54JRS.js +68 -0
  355. package/vendor/framey/dist/chunk-634HSJ3J.js +58 -0
  356. package/vendor/framey/dist/chunk-66LREXGS.js +48 -0
  357. package/vendor/framey/dist/chunk-6BEFEWFB.js +25 -0
  358. package/vendor/framey/dist/chunk-6BNTGNGD.js +286 -0
  359. package/vendor/framey/dist/chunk-6DDPOHMK.js +140 -0
  360. package/vendor/framey/dist/chunk-6GV5XSOR.js +214 -0
  361. package/vendor/framey/dist/chunk-6JR2XUEE.js +127 -0
  362. package/vendor/framey/dist/chunk-73QVATIM.js +362 -0
  363. package/vendor/framey/dist/chunk-7FMUTGOH.js +279 -0
  364. package/vendor/framey/dist/chunk-7JQFJ6JJ.js +291 -0
  365. package/vendor/framey/dist/chunk-AHPGAESX.js +113 -0
  366. package/vendor/framey/dist/chunk-AHX34EFP.js +15 -0
  367. package/vendor/framey/dist/chunk-AJ5CSAEZ.js +487 -0
  368. package/vendor/framey/dist/chunk-B7RJV74O.js +5047 -0
  369. package/vendor/framey/dist/chunk-BL4U6U52.js +254 -0
  370. package/vendor/framey/dist/chunk-BSQ7XANM.js +210 -0
  371. package/vendor/framey/dist/chunk-BZ56MTDX.js +6 -0
  372. package/vendor/framey/dist/chunk-C5BO7PMT.js +282 -0
  373. package/vendor/framey/dist/chunk-CFUCHO4T.js +358 -0
  374. package/vendor/framey/dist/chunk-CZGHFATT.js +31 -0
  375. package/vendor/framey/dist/chunk-DLA44CMS.js +5649 -0
  376. package/vendor/framey/dist/chunk-DPVXBVLL.js +31292 -0
  377. package/vendor/framey/dist/chunk-DQBY7FRR.js +397 -0
  378. package/vendor/framey/dist/chunk-EAMLBAHA.js +154 -0
  379. package/vendor/framey/dist/chunk-EOVYQ3IG.js +143 -0
  380. package/vendor/framey/dist/chunk-EVMY7IPL.js +612 -0
  381. package/vendor/framey/dist/chunk-FG4BBFZI.js +122 -0
  382. package/vendor/framey/dist/chunk-FI4I7CME.js +56 -0
  383. package/vendor/framey/dist/chunk-FLREUEHW.js +2081 -0
  384. package/vendor/framey/dist/chunk-FUFP7A7S.js +1126 -0
  385. package/vendor/framey/dist/chunk-FV3PIKGH.js +56 -0
  386. package/vendor/framey/dist/chunk-FXMKHHPO.js +123 -0
  387. package/vendor/framey/dist/chunk-GGZAM5VH.js +1187 -0
  388. package/vendor/framey/dist/chunk-GKZEVJIB.js +39 -0
  389. package/vendor/framey/dist/chunk-GMECXFAN.js +57 -0
  390. package/vendor/framey/dist/chunk-GNEFDLFB.js +621 -0
  391. package/vendor/framey/dist/chunk-GPMLSOEG.js +19274 -0
  392. package/vendor/framey/dist/chunk-GPYVT5EE.js +59 -0
  393. package/vendor/framey/dist/chunk-H6IEMZEV.js +40 -0
  394. package/vendor/framey/dist/chunk-HEFQCIXR.js +170 -0
  395. package/vendor/framey/dist/chunk-HFQ4CWHA.js +118 -0
  396. package/vendor/framey/dist/chunk-HPDTCOKE.js +304 -0
  397. package/vendor/framey/dist/chunk-HTUHQKDB.js +548 -0
  398. package/vendor/framey/dist/chunk-HU2SPFNG.js +103 -0
  399. package/vendor/framey/dist/chunk-HVW7ABXD.js +19 -0
  400. package/vendor/framey/dist/chunk-IG7NDKXO.js +26 -0
  401. package/vendor/framey/dist/chunk-II3J3WXK.js +246 -0
  402. package/vendor/framey/dist/chunk-IR35BCLK.js +13 -0
  403. package/vendor/framey/dist/chunk-ISN5GRYI.js +20 -0
  404. package/vendor/framey/dist/chunk-IUONB55J.js +669 -0
  405. package/vendor/framey/dist/chunk-IVSUQR2B.js +161 -0
  406. package/vendor/framey/dist/chunk-IXU6PQSW.js +259 -0
  407. package/vendor/framey/dist/chunk-IZCW2NKT.js +29 -0
  408. package/vendor/framey/dist/chunk-JGXJU5PF.js +68 -0
  409. package/vendor/framey/dist/chunk-JHOCK3DG.js +27 -0
  410. package/vendor/framey/dist/chunk-JVBRJMOP.js +121 -0
  411. package/vendor/framey/dist/chunk-K4MJX7SR.js +17 -0
  412. package/vendor/framey/dist/chunk-K4WKGXRC.js +18 -0
  413. package/vendor/framey/dist/chunk-KGPLIH7B.js +127 -0
  414. package/vendor/framey/dist/chunk-KLIKCX6Q.js +63 -0
  415. package/vendor/framey/dist/chunk-KXTKKBOC.js +61 -0
  416. package/vendor/framey/dist/chunk-KZI44OPA.js +185 -0
  417. package/vendor/framey/dist/chunk-LEKZNRKI.js +45 -0
  418. package/vendor/framey/dist/chunk-LFGJWOVA.js +61 -0
  419. package/vendor/framey/dist/chunk-LLMSX6V6.js +17 -0
  420. package/vendor/framey/dist/chunk-LTGTN2TH.js +195 -0
  421. package/vendor/framey/dist/chunk-M2WBOQ2Q.js +54 -0
  422. package/vendor/framey/dist/chunk-MI5FTLYK.js +399 -0
  423. package/vendor/framey/dist/chunk-MLQP2QUF.js +1809 -0
  424. package/vendor/framey/dist/chunk-MMQ52NIZ.js +956 -0
  425. package/vendor/framey/dist/chunk-ND46XKK3.js +742 -0
  426. package/vendor/framey/dist/chunk-NYEXNSHM.js +504 -0
  427. package/vendor/framey/dist/chunk-O4YRB27L.js +57 -0
  428. package/vendor/framey/dist/chunk-O7BGSQ5F.js +92 -0
  429. package/vendor/framey/dist/chunk-OFQWTKPW.js +117 -0
  430. package/vendor/framey/dist/chunk-P5ZBLXJ6.js +91 -0
  431. package/vendor/framey/dist/chunk-P6LJ4Y32.js +4662 -0
  432. package/vendor/framey/dist/chunk-PKZ7RK5N.js +281 -0
  433. package/vendor/framey/dist/chunk-PURHAXFL.js +15 -0
  434. package/vendor/framey/dist/chunk-Q5X2B5WT.js +29 -0
  435. package/vendor/framey/dist/chunk-QAMKOG67.js +248 -0
  436. package/vendor/framey/dist/chunk-R6YBK4XX.js +514 -0
  437. package/vendor/framey/dist/chunk-RGLQURIX.js +2997 -0
  438. package/vendor/framey/dist/chunk-RNV3DM4S.js +3573 -0
  439. package/vendor/framey/dist/chunk-ROEW726S.js +4638 -0
  440. package/vendor/framey/dist/chunk-RQVDTPGD.js +6 -0
  441. package/vendor/framey/dist/chunk-RVZHGOGR.js +26 -0
  442. package/vendor/framey/dist/chunk-RXATE6FZ.js +385 -0
  443. package/vendor/framey/dist/chunk-S3S4TGEH.js +424 -0
  444. package/vendor/framey/dist/chunk-SDYDNHEE.js +11202 -0
  445. package/vendor/framey/dist/chunk-SSWBU67I.js +173 -0
  446. package/vendor/framey/dist/chunk-ST35C4K6.js +28 -0
  447. package/vendor/framey/dist/chunk-SZNTYTIV.js +990 -0
  448. package/vendor/framey/dist/chunk-T2EUWCJE.js +29 -0
  449. package/vendor/framey/dist/chunk-T6AGUTZJ.js +444 -0
  450. package/vendor/framey/dist/chunk-THJALHFJ.js +11131 -0
  451. package/vendor/framey/dist/chunk-TIQB63MX.js +18128 -0
  452. package/vendor/framey/dist/chunk-U242RTCT.js +134 -0
  453. package/vendor/framey/dist/chunk-UJZREAV6.js +3901 -0
  454. package/vendor/framey/dist/chunk-UMD3V7CU.js +264 -0
  455. package/vendor/framey/dist/chunk-UOFAE5RF.js +42 -0
  456. package/vendor/framey/dist/chunk-UTEIUGAZ.js +29 -0
  457. package/vendor/framey/dist/chunk-VAS6HQX3.js +138 -0
  458. package/vendor/framey/dist/chunk-VC76R6W2.js +27 -0
  459. package/vendor/framey/dist/chunk-VFZ7W5W7.js +226 -0
  460. package/vendor/framey/dist/chunk-VHNLZAE2.js +348 -0
  461. package/vendor/framey/dist/chunk-VMC4WSH5.js +278 -0
  462. package/vendor/framey/dist/chunk-VORQIH64.js +108 -0
  463. package/vendor/framey/dist/chunk-VTXFRIHO.js +186 -0
  464. package/vendor/framey/dist/chunk-W2LGJSEN.js +182 -0
  465. package/vendor/framey/dist/chunk-WKBM7NDG.js +32 -0
  466. package/vendor/framey/dist/chunk-WMQNIQDJ.js +2057 -0
  467. package/vendor/framey/dist/chunk-WN4XZC5F.js +504 -0
  468. package/vendor/framey/dist/chunk-WQ6ZXHFQ.js +133 -0
  469. package/vendor/framey/dist/chunk-X7HBSTCZ.js +53 -0
  470. package/vendor/framey/dist/chunk-X7MTZ4T2.js +6 -0
  471. package/vendor/framey/dist/chunk-XPTOJZJF.js +308 -0
  472. package/vendor/framey/dist/chunk-XT6MXC7A.js +6 -0
  473. package/vendor/framey/dist/chunk-XT7X6QCP.js +6 -0
  474. package/vendor/framey/dist/chunk-XTMDWNEC.js +2820 -0
  475. package/vendor/framey/dist/chunk-XUHOIDKG.js +6 -0
  476. package/vendor/framey/dist/chunk-XX7LTW5Y.js +138 -0
  477. package/vendor/framey/dist/chunk-Y3YGTZEG.js +382 -0
  478. package/vendor/framey/dist/chunk-Y5SBSGN7.js +346 -0
  479. package/vendor/framey/dist/chunk-YCMVI4MZ.js +625 -0
  480. package/vendor/framey/dist/chunk-YOR4CTCE.js +261 -0
  481. package/vendor/framey/dist/chunk-YPRBGITP.js +2748 -0
  482. package/vendor/framey/dist/chunk-ZKDMAOFU.js +32 -0
  483. package/vendor/framey/dist/chunk-ZW6DURPF.js +1318 -0
  484. package/vendor/framey/dist/clean-ZJOGDKDM.js +336 -0
  485. package/vendor/framey/dist/cli.js +439 -0
  486. package/vendor/framey/dist/cloudrun-HGJ3B6ZV.js +767 -0
  487. package/vendor/framey/dist/commands/contrast-audit.browser.js +643 -0
  488. package/vendor/framey/dist/commands/layout-audit.browser.js +2914 -0
  489. package/vendor/framey/dist/commands/motion-sample.browser.js +74 -0
  490. package/vendor/framey/dist/commands/motion-signature.browser.js +475 -0
  491. package/vendor/framey/dist/compare-W7UY3BAZ.js +463 -0
  492. package/vendor/framey/dist/compiler-TM3W6GYF.js +136 -0
  493. package/vendor/framey/dist/component-QF5VGYMU.js +176 -0
  494. package/vendor/framey/dist/compositions-XT3PMNDF.js +238 -0
  495. package/vendor/framey/dist/contactSheet-7W6GYHBQ.js +24 -0
  496. package/vendor/framey/dist/deploy-6FJPWKF2.js +299 -0
  497. package/vendor/framey/dist/destroy-HE2RIPKZ.js +57 -0
  498. package/vendor/framey/dist/deterministicFonts-RADD3BSX.js +50 -0
  499. package/vendor/framey/dist/dist-4S2FGGVY.js +422 -0
  500. package/vendor/framey/dist/dist-BE4B2EZE.js +32 -0
  501. package/vendor/framey/dist/dist-EWXJGN63.js +129 -0
  502. package/vendor/framey/dist/docs-VKIRFEEX.js +131 -0
  503. package/vendor/framey/dist/doctor-2UNJ2G7U.js +74 -0
  504. package/vendor/framey/dist/dom-37K6YOH4.js +14 -0
  505. package/vendor/framey/dist/dotEnv-7PZQTVIM.js +54 -0
  506. package/vendor/framey/dist/events-ACY3P2PA.js +76 -0
  507. package/vendor/framey/dist/events-IHB3B2MK.js +69 -0
  508. package/vendor/framey/dist/ffmpeg-DR5LGGSK.js +63 -0
  509. package/vendor/framey/dist/figma-BODVUPHB.js +63 -0
  510. package/vendor/framey/dist/fontData.generated-NXAOI4GH.js +13 -0
  511. package/vendor/framey/dist/fontLocalizeCli.js +142 -0
  512. package/vendor/framey/dist/gpuPolicy-XLEXORZY.js +49 -0
  513. package/vendor/framey/dist/grade-compare-GAXFQ74C.js +661 -0
  514. package/vendor/framey/dist/gsapParser-IEUSLYDJ.js +2885 -0
  515. package/vendor/framey/dist/gsapParserAcorn-LZJKCFQI.js +30 -0
  516. package/vendor/framey/dist/help-VCB5X2HS.js +184 -0
  517. package/vendor/framey/dist/history-ENGZEVFT.js +471 -0
  518. package/vendor/framey/dist/hyperframe.manifest.json +22 -0
  519. package/vendor/framey/dist/hyperframe.runtime.iife.js +1132 -0
  520. package/vendor/framey/dist/hyperframes-player.global.js +478 -0
  521. package/vendor/framey/dist/hyperframes-slideshow.global.js +148 -0
  522. package/vendor/framey/dist/info-WPCJHBMU.js +182 -0
  523. package/vendor/framey/dist/init-G4WGLYGD.js +1176 -0
  524. package/vendor/framey/dist/inspect-5IQ6CNEQ.js +106 -0
  525. package/vendor/framey/dist/installer-ML4HWUV7.js +40 -0
  526. package/vendor/framey/dist/keyframes-Q6XJO7VW.js +2755 -0
  527. package/vendor/framey/dist/lambda-A7JC74JF.js +468 -0
  528. package/vendor/framey/dist/layout-YET6XNXA.js +95 -0
  529. package/vendor/framey/dist/lint-I7FZN2VJ.js +162 -0
  530. package/vendor/framey/dist/lintProject-PDB3UN72.js +30 -0
  531. package/vendor/framey/dist/manager-CD6LJ7ZN.js +45 -0
  532. package/vendor/framey/dist/manager-WA4F2GST.js +67 -0
  533. package/vendor/framey/dist/media-treatment-BXYS4WVA.js +1030 -0
  534. package/vendor/framey/dist/media-use-LWH3MANK.js +113 -0
  535. package/vendor/framey/dist/models-K363FYM4.js +195 -0
  536. package/vendor/framey/dist/motionShot-GPAVWXLH.js +640 -0
  537. package/vendor/framey/dist/normalize-FASIPBUM.js +24 -0
  538. package/vendor/framey/dist/normalize-audio-VDWYRHN6.js +268 -0
  539. package/vendor/framey/dist/open-E3TB6P6J.js +128 -0
  540. package/vendor/framey/dist/parakeetRunner-UP34UOI2.js +58 -0
  541. package/vendor/framey/dist/pipeline-VXIUYEBU.js +64 -0
  542. package/vendor/framey/dist/play-BUTDQ4DV.js +353 -0
  543. package/vendor/framey/dist/policies-OLDYGFK5.js +309 -0
  544. package/vendor/framey/dist/preflight-2KZ2GWAZ.js +55 -0
  545. package/vendor/framey/dist/present-GXYSM7NX.js +353 -0
  546. package/vendor/framey/dist/preview-EKZ33JOJ.js +1910 -0
  547. package/vendor/framey/dist/producer-SKU5EV3J.js +13 -0
  548. package/vendor/framey/dist/progress-ZWB4Z4ET.js +76 -0
  549. package/vendor/framey/dist/projectConfig-3MZAYVTS.js +60 -0
  550. package/vendor/framey/dist/publication-DTKWNF74.js +33 -0
  551. package/vendor/framey/dist/remove-background-OY5L7XN2.js +265 -0
  552. package/vendor/framey/dist/render-CDV4GDFX.js +187 -0
  553. package/vendor/framey/dist/render-YE5YAMOD.js +2620 -0
  554. package/vendor/framey/dist/render-batch-ZAMFPCLL.js +274 -0
  555. package/vendor/framey/dist/renderSetupWorker.js +120 -0
  556. package/vendor/framey/dist/resolver-D5BIPSEW.js +42 -0
  557. package/vendor/framey/dist/runtimeVersion.js +18 -0
  558. package/vendor/framey/dist/sam-WFZXI4F7.js +23 -0
  559. package/vendor/framey/dist/shaderTransitionWorker.js +44 -0
  560. package/vendor/framey/dist/sherpa-RQWLV7I3.js +79 -0
  561. package/vendor/framey/dist/sherpaWorker.js +84 -0
  562. package/vendor/framey/dist/sites-AMOVABTK.js +54 -0
  563. package/vendor/framey/dist/skill-ZHVLH7KX.js +15 -0
  564. package/vendor/framey/dist/slideshow-SJGK4RQS.js +24 -0
  565. package/vendor/framey/dist/snapshot-TUXL6LBQ.js +1515 -0
  566. package/vendor/framey/dist/src-JHQGB2S6.js +215 -0
  567. package/vendor/framey/dist/src-Z5PGEMXB.js +532 -0
  568. package/vendor/framey/dist/state-B6H3WHCF.js +28 -0
  569. package/vendor/framey/dist/staticProjectServer-DFVZOUGI.js +63 -0
  570. package/vendor/framey/dist/studio/assets/hyperframes-player-DLaKjw0L.js +478 -0
  571. package/vendor/framey/dist/studio/assets/index-BX3KHhGX.js +71 -0
  572. package/vendor/framey/dist/studio/assets/index-CYbvOVp-.css +1 -0
  573. package/vendor/framey/dist/studio/assets/index-Dp3QEUGL.js +402 -0
  574. package/vendor/framey/dist/studio/assets/index-qrTAXbqc.js +531 -0
  575. package/vendor/framey/dist/studio/favicon.svg +14 -0
  576. package/vendor/framey/dist/studio/icons/timeline/audio.svg +7 -0
  577. package/vendor/framey/dist/studio/icons/timeline/captions.svg +5 -0
  578. package/vendor/framey/dist/studio/icons/timeline/composition.svg +12 -0
  579. package/vendor/framey/dist/studio/icons/timeline/image.svg +18 -0
  580. package/vendor/framey/dist/studio/icons/timeline/music.svg +10 -0
  581. package/vendor/framey/dist/studio/icons/timeline/text.svg +3 -0
  582. package/vendor/framey/dist/studio/index.html +23 -0
  583. package/vendor/framey/dist/studio-api-2NECGRMB.js +179 -0
  584. package/vendor/framey/dist/studioSelectionClient-IUUPKUHZ.js +25 -0
  585. package/vendor/framey/dist/studioServer-N2OPUMOQ.js +1569 -0
  586. package/vendor/framey/dist/synthesize-PKTB5ATG.js +183 -0
  587. package/vendor/framey/dist/system-7F3UEHQT.js +27 -0
  588. package/vendor/framey/dist/telemetry-DQGPEKM6.js +145 -0
  589. package/vendor/framey/dist/telemetry-UN55N6U6.js +77 -0
  590. package/vendor/framey/dist/timeline-LHUUMK63.js +1295 -0
  591. package/vendor/framey/dist/tokens-OLECWDFK.js +92 -0
  592. package/vendor/framey/dist/transcribe-O7HQKMIV.js +331 -0
  593. package/vendor/framey/dist/transcribe-UJESPBT5.js +76 -0
  594. package/vendor/framey/dist/tts-YCCQHZD4.js +234 -0
  595. package/vendor/framey/dist/urlDownloader-H2LXLG4U.js +48 -0
  596. package/vendor/framey/dist/usage-HG4WMM7A.js +2167 -0
  597. package/vendor/framey/dist/validate-F2XMN6EV.js +87 -0
  598. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/GSAP-LICENSE.txt +8 -0
  599. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/CustomEase.min.js +11 -0
  600. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js +11 -0
  601. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/gsap.min.js +11 -0
  602. package/vendor/framey/dist/warmCheck.js +220 -0
  603. package/vendor/framey/dist/warmCheckClient.js +139 -0
  604. package/vendor/framey/dist/workspaceBuildCheck-XYD2YTFW.js +133 -0
  605. package/vendor/framey-player/LICENSE +190 -0
  606. package/vendor/framey-player/NOTICE +16 -0
  607. package/vendor/framey-player/SOURCE.json +6 -0
  608. package/vendor/framey-player/hyperframes-player.global.js +478 -0
  609. package/vendor/framey.pin +1 -0
  610. package/bin/index.js +0 -3
@@ -0,0 +1,205 @@
1
+ import path from "node:path";
2
+ import { storedVideoPath } from "./project/videos.js";
3
+ import { findDashboard, type Desk } from "./dashboard-link.js";
4
+ import { ensureDashboard, type EnsureOptions, type EnsureResult } from "./dashboard-start.js";
5
+ import type { Store } from "./queue/store.js";
6
+ import type { CheckResult } from "./pipeline/review.js";
7
+
8
+ export interface TickJobReport {
9
+ id: string;
10
+ series: string;
11
+ status: string;
12
+ mp4: string | null;
13
+ transcript: string | null;
14
+ /** The engine that rendered the video. Null when nothing rendered (dry run, or it failed first). */
15
+ engine: { id: string; label: string; source?: string } | null;
16
+ /** The rendered file's length in seconds, from ffprobe. Null when it was not probed (dry run, or no render). */
17
+ durationSec: number | null;
18
+ /** How the picture was made: an agent-written scene (kept at `dir`) or the default template. Null when nothing rendered. */
19
+ scene: { source: "agent" | "default"; dir: string | null; kind: "html" | "tsx" | null } | null;
20
+ /** The rendered file's size in pixels, from ffprobe. */
21
+ width: number | null;
22
+ height: number | null;
23
+ error: string | null;
24
+ publish: Array<{ platform: string; status: string; code: string | null; dryRun: boolean }>;
25
+ /** How the reply stage ended: `succeeded`, or `skipped` with a code such as `no_keys`. Null when it never ran. */
26
+ engage: { status: string; code: string | null } | null;
27
+ /** The run page on a dashboard that is listening now. Null when none is. */
28
+ runUrl: string | null;
29
+ /** Every quality-gate check that ran, with pass, fail, or skipped, and the Fix for a failure. Empty when none ran. */
30
+ checks: CheckResult[];
31
+ /** Who wrote the lines: the agent (`--script`), a configured model, or vfrey's stand-in writer. */
32
+ scriptSource: ScriptSource;
33
+ /** Set when the job waits for review (status `awaiting_review`): what the person runs to let it render, or to stop it. */
34
+ review: { approve: string; reject: string } | null;
35
+ }
36
+
37
+ export type ScriptSource = "agent" | "stand-in" | "model";
38
+
39
+ export interface TickReport {
40
+ processed: number;
41
+ scriptSource: ScriptSource;
42
+ jobs: TickJobReport[];
43
+ /** Why a series made nothing, one entry per series. */
44
+ notes: Array<{ series: string; message: string }>;
45
+ /** The project this tick ran in, or null on the machine desk or under an env override. */
46
+ project?: { name: string; root: string } | null;
47
+ }
48
+
49
+ /** What a tick made, read back from the queue so the summary matches what the dashboard shows. */
50
+ export function buildTickReport(store: Store, jobIds: string[], notes: Array<{ series: string; message: string }> = [], scriptSource: ScriptSource = "model", dataDir?: string): TickReport {
51
+ const jobs: TickJobReport[] = [];
52
+ for (const id of jobIds) {
53
+ const job = store.job(id);
54
+ if (!job) continue;
55
+ const content = job.payload.content;
56
+ const ended = store.eventsFor(id).filter((event) => event.stage === "engage" && event.status !== "running").at(-1);
57
+ const rendered = (job.payload as { rendered?: { durationSec: number; width: number; height: number } | null }).rendered ?? null;
58
+ const mp4 = content?.outputPath ? (dataDir ? storedVideoPath(dataDir, content.outputPath) : path.resolve(content.outputPath)) : null;
59
+ jobs.push({
60
+ id: job.id,
61
+ series: job.accountId,
62
+ status: job.status,
63
+ mp4,
64
+ engine: (job.payload as { engine?: { id: string; label: string; source?: string } }).engine ?? null,
65
+ transcript: mp4 && content?.voiceoverScript?.trim() ? mp4.replace(/\.mp4$/, ".txt") : null,
66
+ durationSec: rendered?.durationSec ?? null,
67
+ scene: sceneOf(job.payload),
68
+ width: rendered?.width ?? null,
69
+ height: rendered?.height ?? null,
70
+ error: job.lastError,
71
+ publish: Object.entries(job.payload.platforms).map(([platform, session]) => ({
72
+ platform,
73
+ status: session?.phase ?? "idle",
74
+ code: session?.errorCode ?? null,
75
+ // A dry-run publication carries the external id "dry-run": the video was never sent anywhere.
76
+ dryRun: session?.externalId === "dry-run",
77
+ })),
78
+ engage: ended ? { status: ended.status, code: ended.code } : null,
79
+ runUrl: null,
80
+ checks: checksOf(job.payload),
81
+ scriptSource,
82
+ review:
83
+ job.status === "awaiting_review"
84
+ ? { approve: `npx vfrey desk job approve --id ${job.id}`, reject: `npx vfrey desk job reject --id ${job.id} --reason "<why>"` }
85
+ : null,
86
+ });
87
+ }
88
+ return { processed: jobIds.length, scriptSource, jobs, notes };
89
+ }
90
+
91
+ /**
92
+ * Fills runUrl on every job so "See it:" is a live link. The tick ensures this desk's dashboard (the same code path as
93
+ * `vfrey open`, without the browser) unless VFREY_NO_DASHBOARD=1; with it set, a dashboard that is already listening is
94
+ * still used. A dashboard that cannot start never fails the tick: the line falls back to the command, and the reason goes to `warn`.
95
+ */
96
+ export async function attachRunUrls(
97
+ report: TickReport,
98
+ desk: Desk,
99
+ env: Record<string, string | undefined> = process.env,
100
+ warn: (text: string) => void = () => {},
101
+ ensure: (options: EnsureOptions) => Promise<EnsureResult> = ensureDashboard,
102
+ ): Promise<void> {
103
+ if (report.jobs.length === 0) return;
104
+ let origin: string | null = null;
105
+ if (env.VFREY_NO_DASHBOARD === "1") {
106
+ origin = await findDashboard(desk, env);
107
+ } else {
108
+ const result = await ensure({ desk, env });
109
+ if (result.ok) origin = result.origin;
110
+ else warn(`✗ ${result.message}\nFix: ${result.fix}\n`);
111
+ }
112
+ if (origin) for (const job of report.jobs) job.runUrl = `${origin}/runs/${job.id}`;
113
+ }
114
+
115
+ function publishLine(item: TickJobReport["publish"][number], series: string): string {
116
+ if (item.status === "done" && item.dryRun) {
117
+ return `Dry run: nothing was uploaded to ${item.platform}. To post for real: npx vfrey desk series live --id ${series} --confirm <channel id>`;
118
+ }
119
+ if (item.status === "skipped" && item.code === "publish_off") return `Upload skipped on ${item.platform}: the series' publish switch is off`;
120
+ if (item.status === "skipped" && item.code === "missing_key") return `Upload skipped: no key for ${item.platform}`;
121
+ if (item.status === "skipped") return `Upload skipped on ${item.platform}${item.code ? `: ${item.code}` : ""}`;
122
+ if (item.status === "done") return `Published to ${item.platform}`;
123
+ if (item.status === "failed") return `Upload failed on ${item.platform}${item.code ? `: ${item.code}` : ""}`;
124
+ return `Upload to ${item.platform}: ${item.status}${item.code ? ` (${item.code})` : ""}`;
125
+ }
126
+
127
+ export interface TickReportView {
128
+ /** The run already asked for a video now (--now, --about, or --script). */
129
+ forced?: boolean;
130
+ /** The data folder this run used, for the VFREY_HOME hint. */
131
+ home?: string;
132
+ }
133
+
134
+ /** What to try next after an empty run. It never repeats the command that was just run. */
135
+ function nextSteps(report: TickReport, view: TickReportView): string[] {
136
+ const steps: string[] = [];
137
+ const paused = report.notes.filter((note) => note.message === "paused");
138
+ for (const note of paused) steps.push(`Resume it: npx vfrey desk series resume --id ${note.series}`);
139
+ const busy = report.notes.some((note) => /in progress|waits to retry/.test(note.message));
140
+ if (busy) steps.push('Cancel that job (command above), or make a one-off video that does not wait for it: npx vfrey tick --now --about "<subject>"');
141
+ if (!view.forced && report.notes.some((note) => note.message.startsWith("not due"))) {
142
+ steps.push("Run `npx vfrey tick --now` to make a video now.");
143
+ }
144
+ if (view.forced) {
145
+ steps.push(`This desk is ${view.home ?? "~/.vfrey"}. If you expected another one, set VFREY_HOME to its folder.`);
146
+ }
147
+ return steps;
148
+ }
149
+
150
+ function checksOf(payload: unknown): CheckResult[] {
151
+ const checks = (payload as { checks?: unknown }).checks;
152
+ return Array.isArray(checks) ? (checks as CheckResult[]) : [];
153
+ }
154
+
155
+ function sceneOf(payload: unknown): TickJobReport["scene"] {
156
+ const scene = (payload as { scene?: { source?: string; dir?: string | null; kind?: "html" | "tsx" | null } }).scene;
157
+ if (!scene || (scene.source !== "agent" && scene.source !== "default")) return null;
158
+ return { source: scene.source, dir: scene.dir ?? null, kind: scene.kind ?? null };
159
+ }
160
+
161
+ export function formatTickReport(report: TickReport, view: TickReportView = {}): string {
162
+ if (report.jobs.length === 0) {
163
+ const why = report.notes.map((note) => ` ${note.series}: ${note.message}`);
164
+ return ["Nothing was made.", ...why, ...nextSteps(report, view), ""].join("\n");
165
+ }
166
+ const lines: string[] = [];
167
+ for (const job of report.jobs) {
168
+ if (job.review) {
169
+ const ran = job.checks.filter((check) => check.status !== "skipped").length;
170
+ lines.push(`⏸ Waiting for your review: ${job.series} (job ${job.id})`);
171
+ lines.push(" Script and scene passed the checks" + (ran > 0 ? ` (${ran} ran)` : "") + ". Nothing is rendered or published until you decide.");
172
+ lines.push(` Approve: ${job.review.approve}`);
173
+ lines.push(` Reject: ${job.review.reject}`);
174
+ lines.push(job.runUrl ? ` Review it: ${job.runUrl.replace(/\/runs\/[^/]+$/, "/review")}` : " Review it: npx vfrey open");
175
+ continue;
176
+ }
177
+ if (job.mp4 && !["rejected", "blocked", "dead"].includes(job.status)) {
178
+ const facts = job.durationSec !== null ? ` (${job.durationSec} s${job.width && job.height ? `, ${job.width}×${job.height}` : ""})` : "";
179
+ lines.push(`✓ Video ready: ${job.mp4}${facts}`);
180
+ if (job.engine) lines.push(` Engine: ${job.engine.label}${job.engine.source === "project" ? " (from vfrey.json)" : ""}`);
181
+ if (job.scene) lines.push(` Scene: ${job.scene.source === "agent" ? "agent-written" : "default template (plain text). Design one next time: npx vfrey scene init scenes/<idea>"}`);
182
+ if (job.transcript) lines.push(` Transcript: ${job.transcript}`);
183
+ const ran = job.checks.filter((check) => check.status !== "skipped").length;
184
+ if (ran > 0) lines.push(` Checks: ${ran} passed`);
185
+ if (job.scriptSource === "stand-in") {
186
+ lines.push(" Script: placeholder from vfrey's stand-in writer. For a real script, pass --script (see npx vfrey tick --script-template).");
187
+ }
188
+ for (const item of job.publish.filter((entry) => !(entry.status === "skipped" && !entry.code))) lines.push(` ${publishLine(item, job.series)}`);
189
+ if (job.engage?.status === "skipped") {
190
+ lines.push(job.engage.code === "no_keys" ? " Replies skipped: no platform keys" : ` Replies skipped${job.engage.code ? `: ${job.engage.code}` : ""}`);
191
+ }
192
+ } else {
193
+ // An engine failure carries its own Fix line. Show it on a line of its own, like the other refusals.
194
+ const [what, fix] = (job.error ?? "").split(" Fix: ");
195
+ lines.push(`✗ ${job.series}: no video (${job.status}${what ? `: ${what}` : ""})`);
196
+ if (fix) lines.push(` Fix: ${fix}`);
197
+ for (const check of job.checks.filter((item) => item.status === "fail")) {
198
+ lines.push(` Check failed: ${check.id} (${check.severity}): ${check.message}`);
199
+ if (check.fix && check.fix !== fix) lines.push(` Fix: ${check.fix}`);
200
+ }
201
+ }
202
+ lines.push(job.runUrl ? ` See it: ${job.runUrl}` : ` See it: npx vfrey open ${job.id}`);
203
+ }
204
+ return lines.join("\n") + "\n";
205
+ }
package/src/tts.ts ADDED
@@ -0,0 +1,124 @@
1
+ import { execFile } from "node:child_process";
2
+ import { accessSync, constants } from "node:fs";
3
+ import { mkdir, unlink, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { ConfigError, RetryableError } from "@vfrey/contracts";
7
+
8
+ const execFileAsync = promisify(execFile);
9
+
10
+ /** Each OpenAI voice name maps to its own macOS voice, so a series that rotates voices sounds different. */
11
+ const LOCAL_VOICES: Record<string, string> = {
12
+ alloy: "Samantha",
13
+ ash: "Alex",
14
+ ballad: "Victoria",
15
+ coral: "Karen",
16
+ echo: "Daniel",
17
+ fable: "Moira",
18
+ nova: "Tessa",
19
+ onyx: "Fred",
20
+ sage: "Ava",
21
+ shimmer: "Allison",
22
+ verse: "Tom",
23
+ };
24
+
25
+ type Env = Record<string, string | undefined>;
26
+
27
+ function onPath(command: string, env: Env): boolean {
28
+ for (const dir of (env.PATH ?? "").split(path.delimiter)) {
29
+ if (!dir) continue;
30
+ try {
31
+ accessSync(path.join(dir, command), constants.X_OK);
32
+ return true;
33
+ } catch {
34
+ /* not here */
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+
40
+ /** The sentence to show when the local voice cannot run on this machine, or null when it can. */
41
+ export function localVoiceProblem(env: Env): { message: string; fix: string } | null {
42
+ if (!onPath("say", env)) {
43
+ return {
44
+ message: "The local voice needs the macOS `say` command, and it is not on this machine.",
45
+ fix: "Set TTS_PROVIDER=openai with TTS_API_KEY, or set TTS_PROVIDER=none for captions only.",
46
+ };
47
+ }
48
+ if (!onPath("ffmpeg", env)) {
49
+ return {
50
+ message: "The local voice needs `ffmpeg`, and it is not on this machine.",
51
+ fix: "Install ffmpeg, or set TTS_PROVIDER=none for captions only.",
52
+ };
53
+ }
54
+ return null;
55
+ }
56
+
57
+ export async function synthesize(input: {
58
+ provider: "openai" | "local" | "none";
59
+ baseUrl: string;
60
+ apiKey: string | null;
61
+ model: string;
62
+ voice: string;
63
+ text: string;
64
+ outputPath: string;
65
+ fetchImpl: typeof fetch;
66
+ /** Where the local voice looks for `say`. Defaults to process.env. */
67
+ env?: Env;
68
+ }): Promise<string | null> {
69
+ if (input.provider === "none") return null;
70
+ if (input.provider === "local") return synthesizeLocal(input.text, input.outputPath, input.voice, input.env ?? process.env);
71
+ if (!input.apiKey) throw new ConfigError("tts_auth", "TTS_API_KEY is not set. Fix: set TTS_API_KEY, or set TTS_PROVIDER=none.");
72
+ await mkdir(path.dirname(input.outputPath), { recursive: true });
73
+ const response = await input.fetchImpl(`${input.baseUrl}/audio/speech`, {
74
+ method: "POST",
75
+ headers: {
76
+ authorization: `Bearer ${input.apiKey}`,
77
+ "content-type": "application/json",
78
+ },
79
+ body: JSON.stringify({
80
+ model: input.model,
81
+ voice: input.voice,
82
+ input: input.text.slice(0, 4000),
83
+ }),
84
+ });
85
+ if (response.status === 429 || response.status >= 500) {
86
+ throw new RetryableError("tts_rate", `TTS HTTP ${response.status}`);
87
+ }
88
+ if (response.status === 401 || response.status === 403) {
89
+ throw new ConfigError("tts_auth", `TTS refused the key (HTTP ${response.status}). Fix: check TTS_API_KEY and its permissions.`);
90
+ }
91
+ if (response.status === 404) {
92
+ throw new ConfigError("tts_model", "TTS did not find the model or endpoint (HTTP 404). Fix: check TTS_MODEL and TTS_BASE_URL.");
93
+ }
94
+ if (!response.ok) throw new RetryableError("tts_error", `TTS HTTP ${response.status}`);
95
+ const bytes = Buffer.from(await response.arrayBuffer());
96
+ await writeFile(input.outputPath, bytes);
97
+ return input.outputPath;
98
+ }
99
+
100
+ function speakerVoice(voice: string): string {
101
+ const trimmed = voice.trim();
102
+ if (!trimmed) return "Samantha";
103
+ return LOCAL_VOICES[trimmed.toLowerCase()] ?? trimmed;
104
+ }
105
+
106
+ async function synthesizeLocal(text: string, outputPath: string, voice: string, env: Env): Promise<string> {
107
+ const problem = localVoiceProblem(env);
108
+ if (problem) throw new ConfigError("tts_local_missing", `${problem.message} Fix: ${problem.fix}`);
109
+ const spoken = text.replace(/\s+/g, " ").trim().slice(0, 4000);
110
+ if (!spoken) throw new RetryableError("tts_error", "There is no script to speak.");
111
+ await mkdir(path.dirname(outputPath), { recursive: true });
112
+ const wav = outputPath.replace(/\.[^.]+$/, "") + ".wav";
113
+ const aiff = wav.replace(/\.wav$/, ".aiff");
114
+ try {
115
+ await execFileAsync("say", ["-v", speakerVoice(voice), "-o", aiff, "--", spoken], { env: env as NodeJS.ProcessEnv });
116
+ await execFileAsync("ffmpeg", ["-y", "-i", aiff, "-ar", "44100", "-ac", "1", wav], { env: env as NodeJS.ProcessEnv });
117
+ } catch (error) {
118
+ const message = error instanceof Error ? error.message : "local voice failed";
119
+ throw new RetryableError("tts_error", message);
120
+ } finally {
121
+ await unlink(aiff).catch(() => undefined);
122
+ }
123
+ return wav;
124
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Accounts and billing: the shapes the vfrey server and the CLI agree on (spec 014).
3
+ * Types plus pure helpers. No I/O.
4
+ */
5
+ /** What a paid feature is called on the wire. */
6
+ export type Entitlement = "topics" | "creators" | "mail";
7
+ export declare const ENTITLEMENTS: readonly Entitlement[];
8
+ /** Plans live in server code. `mail` is an add-on, not a plan. */
9
+ export type PlanId = "free" | "pro" | "research";
10
+ export declare const PLAN_IDS: readonly PlanId[];
11
+ /** A monthly meter, for example `topics.requests` or `creators.profiles`. */
12
+ export type UsageMeter = string;
13
+ export type OrgRole = "owner" | "admin" | "member";
14
+ export interface UsageReading {
15
+ used: number;
16
+ /** `null` means the plan has no cap on this meter. */
17
+ limit: number | null;
18
+ /** ISO 8601 time the meter resets. */
19
+ resetsAt: string;
20
+ }
21
+ /** `GET /api/v1/me` */
22
+ export interface MeResponse {
23
+ user: {
24
+ id: string;
25
+ email: string;
26
+ };
27
+ org: {
28
+ id: string;
29
+ name: string;
30
+ role: OrgRole;
31
+ };
32
+ plan: PlanId;
33
+ entitlements: Record<Entitlement, boolean>;
34
+ usage: Record<UsageMeter, UsageReading>;
35
+ /** Prepaid credits. `expiringSoon` is the part that expires within the warning window. */
36
+ credits: {
37
+ balance: number;
38
+ expiringSoon: number;
39
+ };
40
+ upgradeUrl: string;
41
+ }
42
+ export type ApiErrorCode = "unauthorized" | "plan_required" | "limit_reached" | "credits_required" | "capacity_paused" | "rate_limited" | "bad_request" | "not_found" | "internal";
43
+ /** Every non-2xx API response has this body. */
44
+ export interface ApiError {
45
+ error: {
46
+ code: ApiErrorCode | (string & {});
47
+ message: string;
48
+ upgradeUrl?: string;
49
+ /** ISO 8601 time a `limit_reached` meter resets. */
50
+ resetsAt?: string;
51
+ /** `credits_required` (402): where to buy credits, what the call costs, and what the org has. */
52
+ buyCreditsUrl?: string;
53
+ required?: number;
54
+ balance?: number;
55
+ };
56
+ }
57
+ export declare function isApiError(value: unknown): value is ApiError;
58
+ /** The one client id the CLI uses for the device flow. */
59
+ export declare const CLI_CLIENT_ID = "vfrey-cli";
60
+ /** `POST /api/v1/device/code` */
61
+ export interface DeviceCodeRequest {
62
+ client_id: string;
63
+ }
64
+ export interface DeviceCodeResponse {
65
+ device_code: string;
66
+ /** Shown as `ABCD-EFGH`. */
67
+ user_code: string;
68
+ verification_uri: string;
69
+ verification_uri_complete: string;
70
+ /** Seconds. 600 today. */
71
+ expires_in: number;
72
+ /** Seconds between polls. 5 today. */
73
+ interval: number;
74
+ }
75
+ /** `POST /api/v1/device/token` */
76
+ export interface DeviceTokenRequest {
77
+ device_code: string;
78
+ client_id: string;
79
+ }
80
+ export type DeviceTokenErrorCode = "authorization_pending" | "slow_down" | "access_denied" | "expired_token";
81
+ export interface DeviceTokenSuccess {
82
+ access_token: string;
83
+ token_type: "bearer";
84
+ org: {
85
+ id: string;
86
+ name: string;
87
+ };
88
+ }
89
+ export interface DeviceTokenError {
90
+ error: DeviceTokenErrorCode;
91
+ }
92
+ export type DeviceTokenResponse = DeviceTokenSuccess | DeviceTokenError;
93
+ export declare function isDeviceTokenSuccess(value: DeviceTokenResponse): value is DeviceTokenSuccess;
94
+ /** Format a raw 8-character user code as `ABCD-EFGH`. Other lengths pass through upper-cased. */
95
+ export declare function formatUserCode(code: string): string;
96
+ /** An API token starts with this prefix. Used to redact, never to authenticate. */
97
+ export declare const API_TOKEN_PREFIX = "vfr_";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Accounts and billing: the shapes the vfrey server and the CLI agree on (spec 014).
3
+ * Types plus pure helpers. No I/O.
4
+ */
5
+ export const ENTITLEMENTS = ["topics", "creators", "mail"];
6
+ export const PLAN_IDS = ["free", "pro", "research"];
7
+ export function isApiError(value) {
8
+ if (typeof value !== "object" || value === null)
9
+ return false;
10
+ const error = value.error;
11
+ if (typeof error !== "object" || error === null)
12
+ return false;
13
+ const e = error;
14
+ return typeof e.code === "string" && typeof e.message === "string";
15
+ }
16
+ /** The one client id the CLI uses for the device flow. */
17
+ export const CLI_CLIENT_ID = "vfrey-cli";
18
+ export function isDeviceTokenSuccess(value) {
19
+ return "access_token" in value;
20
+ }
21
+ /** Format a raw 8-character user code as `ABCD-EFGH`. Other lengths pass through upper-cased. */
22
+ export function formatUserCode(code) {
23
+ const clean = code.replace(/[\s-]/g, "").toUpperCase();
24
+ return clean.length === 8 ? `${clean.slice(0, 4)}-${clean.slice(4)}` : clean;
25
+ }
26
+ /** An API token starts with this prefix. Used to redact, never to authenticate. */
27
+ export const API_TOKEN_PREFIX = "vfr_";
@@ -0,0 +1,8 @@
1
+ export declare const REVIEW_CHECK_IDS: readonly ["pace", "frames", "style", "svg", "logo", "copy", "disclosure"];
2
+ export type ReviewCheckId = (typeof REVIEW_CHECK_IDS)[number];
3
+ export type ReviewFailureStatus = "rejected" | "blocked";
4
+ export interface ReviewFailure {
5
+ id: ReviewCheckId;
6
+ status: ReviewFailureStatus;
7
+ reason: string;
8
+ }
@@ -0,0 +1 @@
1
+ export const REVIEW_CHECK_IDS = ["pace", "frames", "style", "svg", "logo", "copy", "disclosure"];
@@ -0,0 +1,4 @@
1
+ export declare function credentialsKey(raw: string): Buffer;
2
+ export declare function encryptSecret(plain: string, key: Buffer): string;
3
+ export declare function decryptSecret(stored: string, key: Buffer): string;
4
+ export declare function isEncrypted(value: string | null | undefined): boolean;
@@ -0,0 +1,38 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
+ const PREFIX = "enc:v1";
3
+ const IV_BYTES = 12;
4
+ const TAG_BYTES = 16;
5
+ export function credentialsKey(raw) {
6
+ const key = Buffer.from(raw, "base64");
7
+ if (key.length !== 32) {
8
+ throw new Error("CREDENTIALS_KEY must be 32 bytes, base64-encoded");
9
+ }
10
+ return key;
11
+ }
12
+ export function encryptSecret(plain, key) {
13
+ const iv = randomBytes(IV_BYTES);
14
+ const cipher = createCipheriv("aes-256-gcm", key, iv, { authTagLength: TAG_BYTES });
15
+ const data = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
16
+ const tag = cipher.getAuthTag();
17
+ return [PREFIX, iv.toString("base64url"), tag.toString("base64url"), data.toString("base64url")].join(":");
18
+ }
19
+ export function decryptSecret(stored, key) {
20
+ const parts = stored.split(":");
21
+ if (parts.length !== 5 || parts[0] !== "enc" || parts[1] !== "v1") {
22
+ throw new Error("Stored credential is not enc:v1");
23
+ }
24
+ const iv = Buffer.from(parts[2] ?? "", "base64url");
25
+ const tag = Buffer.from(parts[3] ?? "", "base64url");
26
+ if (iv.length !== IV_BYTES || tag.length !== TAG_BYTES) {
27
+ throw new Error("Stored credential is not enc:v1 (bad IV or tag length)");
28
+ }
29
+ const decipher = createDecipheriv("aes-256-gcm", key, iv, { authTagLength: TAG_BYTES });
30
+ decipher.setAuthTag(tag);
31
+ return Buffer.concat([
32
+ decipher.update(Buffer.from(parts[4] ?? "", "base64url")),
33
+ decipher.final(),
34
+ ]).toString("utf8");
35
+ }
36
+ export function isEncrypted(value) {
37
+ return typeof value === "string" && value.startsWith(`${PREFIX}:`);
38
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * One definition of "the script or reply claims to be a licensed professional
3
+ * giving advice". Shared by the disclosure gate and the reply safety gate.
4
+ *
5
+ * It matches first-person claims ("As a doctor, I recommend", "I'm your
6
+ * lawyer", "I am a licensed nurse", "Financial advisor here"). Third-person
7
+ * mentions ("my doctor said", "ask your lawyer") do not match.
8
+ */
9
+ export declare const ADVICE_PERSONA: RegExp;
10
+ export declare function claimsAdvicePersona(text: string): boolean;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * One definition of "the script or reply claims to be a licensed professional
3
+ * giving advice". Shared by the disclosure gate and the reply safety gate.
4
+ *
5
+ * It matches first-person claims ("As a doctor, I recommend", "I'm your
6
+ * lawyer", "I am a licensed nurse", "Financial advisor here"). Third-person
7
+ * mentions ("my doctor said", "ask your lawyer") do not match.
8
+ */
9
+ const PRO = "(?:doctor|physician|nurse|lawyer|attorney|therapist|psychiatrist|psychologist|cpa|(?:financial|investment|tax)\\s+(?:advis[eo]r|planner))";
10
+ const MOD = "(?:[a-z-]+\\s+){0,3}";
11
+ const APOS = "['’]";
12
+ export const ADVICE_PERSONA = new RegExp([
13
+ // "as your doctor"
14
+ `\\bas\\s+your\\s+${MOD}${PRO}\\b`,
15
+ // "as a doctor, I recommend ..." / "as a doctor myself"
16
+ `\\bas\\s+an?\\s+${MOD}${PRO}\\b(?:\\s+myself\\b|[^.!?]{0,30}\\b(?:i|i${APOS}(?:m|d|ve|ll)|my|me|we|let\\s+me)\\b)`,
17
+ // "I'm your doctor", "I am a licensed nurse", "I'm the attorney"
18
+ `\\bi(?:\\s+am|${APOS}m)\\s+(?:your|an?|the)\\s+${MOD}${PRO}\\b`,
19
+ // "Doctor here:", "Financial advisor speaking"
20
+ `(?:^|[.!?]\\s+)${PRO}\\s+(?:here|speaking)\\b`,
21
+ ].join("|"), "i");
22
+ export function claimsAdvicePersona(text) {
23
+ return ADVICE_PERSONA.test(text);
24
+ }
@@ -0,0 +1,13 @@
1
+ export interface BackoffInput {
2
+ attempt: number;
3
+ baseMs?: number;
4
+ capMs?: number;
5
+ /** 0..1 fraction of the delay added as deterministic jitter when random is omitted. */
6
+ jitterRatio?: number;
7
+ random?: () => number;
8
+ /** Server-provided wait. Never capped. */
9
+ retryAfterMs?: number;
10
+ }
11
+ /** Exponential backoff with a cap. Attempt 1 is the first retry. */
12
+ export declare function backoffMs(input: BackoffInput): number;
13
+ export declare function parseRetryAfter(header: string | null, nowMs: number): number | undefined;
@@ -0,0 +1,27 @@
1
+ /** Exponential backoff with a cap. Attempt 1 is the first retry. */
2
+ export function backoffMs(input) {
3
+ const base = input.baseMs ?? 2_000;
4
+ const cap = input.capMs ?? 15 * 60_000;
5
+ const jitterRatio = input.jitterRatio ?? 0.2;
6
+ const attempt = Math.max(1, input.attempt);
7
+ const exponential = Math.min(cap, base * 2 ** (attempt - 1));
8
+ const random = input.random ?? Math.random;
9
+ const jitter = Math.floor(exponential * jitterRatio * random());
10
+ const computed = Math.min(cap, exponential + jitter);
11
+ if (input.retryAfterMs !== undefined && Number.isFinite(input.retryAfterMs) && input.retryAfterMs > 0) {
12
+ // A server-provided Retry-After is honored in full. Only computed backoff is capped.
13
+ return Math.max(computed, input.retryAfterMs);
14
+ }
15
+ return computed;
16
+ }
17
+ export function parseRetryAfter(header, nowMs) {
18
+ if (!header)
19
+ return undefined;
20
+ const seconds = Number(header);
21
+ if (Number.isFinite(seconds))
22
+ return Math.max(0, seconds * 1000);
23
+ const when = Date.parse(header);
24
+ if (Number.isFinite(when))
25
+ return Math.max(0, when - nowMs);
26
+ return undefined;
27
+ }
@@ -0,0 +1,29 @@
1
+ export interface Cadence {
2
+ timezone: string;
3
+ /** Minimum hours between successful publishes for this account. */
4
+ minHoursBetweenPosts: number;
5
+ maxPostsPerDay: number;
6
+ /** Local windows, "HH:MM-HH:MM", start inclusive, end exclusive. Empty means all day. */
7
+ windows: string[];
8
+ }
9
+ export interface ZonedParts {
10
+ year: number;
11
+ month: number;
12
+ day: number;
13
+ hour: number;
14
+ minute: number;
15
+ }
16
+ export declare function zonedParts(date: Date, timeZone: string): ZonedParts;
17
+ export declare function localDayKey(date: Date, timeZone: string): string;
18
+ /** Throws ConfigError for a timezone Intl rejects, malformed windows, or non-sensible limits. */
19
+ export declare function validateCadence(cadence: Cadence): void;
20
+ export interface CadenceDecision {
21
+ due: boolean;
22
+ reason: string;
23
+ }
24
+ export declare function cadenceDecision(input: {
25
+ cadence: Cadence;
26
+ now: Date;
27
+ lastPublishedAt: Date | null;
28
+ postsOnLocalDay: number;
29
+ }): CadenceDecision;