@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,463 @@
1
+ // vfrey Creators (spec 017): `creators search | show | watch | export | alerts`, and the helpers the desk route shares.
2
+ // Every call goes through ApiClient; the token is read from the token store in openSession and never leaves this process.
3
+ import { ApiFailure, clean } from "./client.js";
4
+ import { ApiConfigError } from "./config.js";
5
+ import type { ApiIo } from "./commands.js";
6
+ import { formatAge, formatViews, openSession, withInputFix, type TopicsDeps } from "./topics.js";
7
+ import type { BreakoutView, CreatorAlertsResponse, CreatorProfileView, CreatorSummaryView, WatchlistItemView, WatchlistView } from "./types.js";
8
+
9
+ export type CreatorsDeps = TopicsDeps;
10
+
11
+ export const CREATOR_PLATFORM_IDS = ["tiktok", "instagram", "youtube"] as const;
12
+ export const CREATOR_SORTS = ["engagement", "growth", "views", "followers"] as const;
13
+ const PLATFORM_NAMES: Record<string, string> = { tiktok: "TikTok", instagram: "Instagram", youtube: "YouTube" };
14
+ const platformName = (p: string): string => PLATFORM_NAMES[p] ?? clean(p);
15
+ export const DEFAULT_WATCHLIST = "Watching";
16
+
17
+ // ---------- input: "10k-200k", "4%", "@Handle" ----------
18
+
19
+ /** "10k", "1.5m", "200000" to a number, or null. */
20
+ export function parseCount(text: string): number | null {
21
+ const m = /^(\d+(?:\.\d+)?)([km]?)$/i.exec(text.trim());
22
+ if (!m) return null;
23
+ const unit = m[2]!.toLowerCase();
24
+ const n = Number(m[1]) * (unit === "k" ? 1_000 : unit === "m" ? 1_000_000 : 1);
25
+ return Number.isFinite(n) ? Math.round(n) : null;
26
+ }
27
+
28
+ /** "10k-200k", "10k-" (at least), "-200k" (at most) or "10k" (at least). */
29
+ export function parseFollowers(text: string): { min?: number; max?: number } | null {
30
+ const t = text.trim();
31
+ if (!t) return null;
32
+ if (!t.includes("-")) {
33
+ const n = parseCount(t);
34
+ return n === null ? null : { min: n };
35
+ }
36
+ const [lo = "", hi = "", ...extra] = t.split("-");
37
+ if (extra.length > 0 || (!lo && !hi)) return null;
38
+ const min = lo ? parseCount(lo) : undefined;
39
+ const max = hi ? parseCount(hi) : undefined;
40
+ if (min === null || max === null) return null;
41
+ if (min !== undefined && max !== undefined && min > max) return null;
42
+ return { ...(min !== undefined ? { min } : {}), ...(max !== undefined ? { max } : {}) };
43
+ }
44
+
45
+ /** "4%" or "0.04": returns the text the server takes, or null. */
46
+ export function parseEngagement(text: string): string | null {
47
+ const t = text.trim();
48
+ return /^(\d+(?:\.\d+)?)(%?)$/.test(t) ? t : null;
49
+ }
50
+
51
+ export function normalizeHandle(text: string): string {
52
+ return text.trim().replace(/^@+/, "").toLowerCase();
53
+ }
54
+
55
+ export interface SearchInput {
56
+ niche?: string | undefined;
57
+ platform?: string | undefined;
58
+ followers?: string | undefined;
59
+ postsPerWeek?: string | undefined;
60
+ engagement?: string | undefined;
61
+ sort?: string | undefined;
62
+ limit?: string | undefined;
63
+ }
64
+
65
+ /** Validates the search inputs and returns the server's query fields, or the mistake in the person's words. */
66
+ export function buildSearchQuery(input: SearchInput): { query: Record<string, string> } | { error: string } {
67
+ const query: Record<string, string> = {};
68
+ if (input.niche?.trim()) query.niche = input.niche.trim();
69
+ if (input.platform) {
70
+ const list = input.platform.split(",").map((p) => p.trim().toLowerCase()).filter(Boolean);
71
+ if (list.length === 0 || !list.every((p) => (CREATOR_PLATFORM_IDS as readonly string[]).includes(p))) return { error: "--platform must be a comma list of tiktok, instagram, youtube." };
72
+ query.platforms = list.join(",");
73
+ }
74
+ if (input.followers) {
75
+ const range = parseFollowers(input.followers);
76
+ if (!range) return { error: "--followers must look like 10k-200k, 10k- (at least) or -200k (at most)." };
77
+ if (range.min !== undefined) query.followersMin = String(range.min);
78
+ if (range.max !== undefined) query.followersMax = String(range.max);
79
+ }
80
+ if (input.postsPerWeek) {
81
+ const n = Number(input.postsPerWeek);
82
+ if (!Number.isFinite(n) || n < 0) return { error: "--posts-per-week must be a number of 0 or more." };
83
+ query.postsPerWeekMin = String(n);
84
+ }
85
+ if (input.engagement) {
86
+ const e = parseEngagement(input.engagement);
87
+ if (e === null) return { error: "--engagement must look like 4% or 0.04." };
88
+ query.engagementMin = e;
89
+ }
90
+ if (input.sort) {
91
+ if (!(CREATOR_SORTS as readonly string[]).includes(input.sort)) return { error: `--sort must be one of ${CREATOR_SORTS.join(", ")}.` };
92
+ query.sort = input.sort;
93
+ }
94
+ if (input.limit !== undefined) {
95
+ const n = Number(input.limit);
96
+ if (!Number.isInteger(n) || n < 1 || n > 100) return { error: "--limit must be a whole number from 1 to 100." };
97
+ query.limit = String(n);
98
+ }
99
+ return { query };
100
+ }
101
+
102
+ const SEARCH_FIX = "list the niche ids with `npx vfrey topics niches`.";
103
+ const PROFILE_FIX = "check the platform and handle, like `npx vfrey creators show @name --platform tiktok`.";
104
+ const WATCH_FIX = "list your watchlists with `npx vfrey creators watch list`.";
105
+
106
+ // ---------- server calls ----------
107
+
108
+ function checked<T>(value: T | null | undefined, ok: (v: T) => boolean): T {
109
+ if (value === null || value === undefined || !ok(value)) throw new ApiFailure("The vfrey server sent an answer this version does not understand.", "update vfrey with `npx vfrey update`.");
110
+ return value;
111
+ }
112
+
113
+ export async function searchCreators(deps: CreatorsDeps, query: Record<string, string>, api?: string | null): Promise<CreatorSummaryView[]> {
114
+ try {
115
+ return checked(await openSession(deps, api).creatorSearch(query), (a) => Array.isArray(a.creators)).creators;
116
+ } catch (error) {
117
+ throw withInputFix(error, SEARCH_FIX);
118
+ }
119
+ }
120
+
121
+ export async function fetchProfile(deps: CreatorsDeps, platform: string, handle: string, api?: string | null): Promise<CreatorProfileView> {
122
+ try {
123
+ return checked(await openSession(deps, api).creatorProfile(platform, normalizeHandle(handle)), (a) => typeof a.handle === "string");
124
+ } catch (error) {
125
+ throw withInputFix(error, PROFILE_FIX);
126
+ }
127
+ }
128
+
129
+ export async function fetchAlerts(deps: CreatorsDeps, api?: string | null): Promise<CreatorAlertsResponse> {
130
+ return checked(await openSession(deps, api).creatorAlerts(), (a) => Array.isArray(a.alerts));
131
+ }
132
+
133
+ export async function fetchWatchlists(deps: CreatorsDeps, api?: string | null): Promise<WatchlistView[]> {
134
+ return checked(await openSession(deps, api).watchlists(), (a) => Array.isArray(a.watchlists)).watchlists;
135
+ }
136
+
137
+ export async function fetchWatchlistItems(deps: CreatorsDeps, id: string, api?: string | null): Promise<{ watchlist: { id: string; name: string }; items: WatchlistItemView[] }> {
138
+ try {
139
+ return checked(await openSession(deps, api).watchlistItems(id), (a) => Array.isArray(a.items));
140
+ } catch (error) {
141
+ throw withInputFix(error, WATCH_FIX);
142
+ }
143
+ }
144
+
145
+ /** A watchlist by id or by (case-insensitive) name. */
146
+ export function findWatchlist(lists: WatchlistView[], key: string): WatchlistView | undefined {
147
+ const k = key.trim().toLowerCase();
148
+ return lists.find((w) => w.id === key.trim()) ?? lists.find((w) => w.name.trim().toLowerCase() === k);
149
+ }
150
+
151
+ /** Adds a creator to a watchlist by name or id, making the watchlist when the name is new. */
152
+ export async function addToWatchlist(deps: CreatorsDeps, key: string, platform: string, handle: string, api?: string | null): Promise<{ watchlist: { id: string; name: string }; created: boolean; madeList: boolean }> {
153
+ const client = openSession(deps, api);
154
+ const lists = checked(await client.watchlists(), (a) => Array.isArray(a.watchlists)).watchlists;
155
+ let list: { id: string; name: string } | undefined = findWatchlist(lists, key);
156
+ let madeList = false;
157
+ if (!list) {
158
+ list = checked(await client.createWatchlist(key.trim()), (a) => typeof a.id === "string");
159
+ madeList = true;
160
+ }
161
+ try {
162
+ const added = await client.addWatch(list.id, { platform, handle: normalizeHandle(handle) });
163
+ return { watchlist: list, created: added.created === true, madeList };
164
+ } catch (error) {
165
+ throw withInputFix(error, "check the platform and handle, like `npx vfrey creators watch add @name --platform tiktok`.");
166
+ }
167
+ }
168
+
169
+ export async function removeFromWatchlist(deps: CreatorsDeps, key: string, platform: string, handle: string, api?: string | null): Promise<{ id: string; name: string }> {
170
+ const client = openSession(deps, api);
171
+ const lists = checked(await client.watchlists(), (a) => Array.isArray(a.watchlists)).watchlists;
172
+ const list = findWatchlist(lists, key);
173
+ if (!list) throw new ApiFailure(`No watchlist called "${clean(key)}".`, WATCH_FIX, 404, "not_found");
174
+ try {
175
+ await client.removeWatch(list.id, platform, normalizeHandle(handle));
176
+ } catch (error) {
177
+ throw withInputFix(error, `see who is on it with \`npx vfrey creators watch list --watchlist ${clean(list.name)}\`.`);
178
+ }
179
+ return list;
180
+ }
181
+
182
+ export async function exportWatchlist(deps: CreatorsDeps, key: string, api?: string | null): Promise<string> {
183
+ const client = openSession(deps, api);
184
+ const lists = checked(await client.watchlists(), (a) => Array.isArray(a.watchlists)).watchlists;
185
+ const list = findWatchlist(lists, key);
186
+ if (!list) throw new ApiFailure(`No watchlist called "${clean(key)}".`, WATCH_FIX, 404, "not_found");
187
+ return client.watchlistCsv(list.id);
188
+ }
189
+
190
+ // ---------- formatting: unknown is "—", never 0 ----------
191
+
192
+ const DASH = "—";
193
+ const count = (n: number | null | undefined): string => (typeof n === "number" && Number.isFinite(n) ? formatViews(n) : DASH);
194
+ const pct = (fraction: number | null | undefined): string => (typeof fraction === "number" && Number.isFinite(fraction) ? `${(fraction * 100).toFixed(1).replace(/\.0$/, "")}%` : DASH);
195
+ const signedPct = (fraction: number | null | undefined): string => (typeof fraction === "number" && Number.isFinite(fraction) ? `${fraction >= 0 ? "+" : ""}${(fraction * 100).toFixed(1).replace(/\.0$/, "")}%` : DASH);
196
+ const httpUrl = (url: unknown): string => (/^https?:\/\//i.test(clean(url)) ? clean(url) : "");
197
+ const nicheList = (c: { niches?: string[] }): string => (c.niches ?? []).map((n) => clean(n)).join(", ") || DASH;
198
+ const growthLabel = (g: { abs?: number; pct?: number } | undefined, pctFallback?: number): string => {
199
+ if (typeof g?.pct === "number") return signedPct(g.pct);
200
+ if (typeof g?.abs === "number") return `${g.abs >= 0 ? "+" : ""}${g.abs} followers`;
201
+ return typeof pctFallback === "number" ? signedPct(pctFallback) : DASH;
202
+ };
203
+
204
+ export function engagementLabel(c: { engagementRate?: number | null; engagementBasis?: string | null }): string {
205
+ if (typeof c.engagementRate !== "number") return DASH;
206
+ return `${pct(c.engagementRate)} of ${c.engagementBasis === "followers" ? "followers" : c.engagementBasis === "views" ? "views" : "views or followers (basis unknown)"}`;
207
+ }
208
+
209
+ export function postsPerWeekLabel(c: { postsPerWeek?: number | null }, basis?: string | null): string {
210
+ if (typeof c.postsPerWeek !== "number") return DASH;
211
+ const n = Number(c.postsPerWeek.toFixed(1));
212
+ return `${n}${basis === "30d" ? " (last 30 days)" : basis === "sample" ? " (over the recent posts only)" : ""}`;
213
+ }
214
+
215
+ export function formatSearch(creators: CreatorSummaryView[]): string {
216
+ if (creators.length === 0) return "No creators match. Loosen a filter, or try another niche.\n";
217
+ const rows = creators.map((c) => [
218
+ `@${clean(c.handle)}`,
219
+ platformName(c.platform),
220
+ count(c.followers),
221
+ typeof c.engagementRate === "number" ? `${pct(c.engagementRate)}${c.engagementBasis === "followers" ? " fol" : c.engagementBasis === "views" ? " views" : ""}` : DASH,
222
+ count(c.medianViews),
223
+ typeof c.postsPerWeek === "number" ? String(Number(c.postsPerWeek.toFixed(1))) : DASH,
224
+ signedPct(c.growth30d),
225
+ nicheList(c),
226
+ ]);
227
+ const head = ["Creator", "Platform", "Followers", "Engagement", "Med. views", "Posts/wk", "30d growth", "Niches"];
228
+ const width = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i]!.length)));
229
+ const line = (r: string[]) => r.map((cell, i) => cell.padEnd(width[i]!)).join(" ").trimEnd();
230
+ return [
231
+ `Creators: ${creators.length}`,
232
+ "",
233
+ line(head),
234
+ ...rows.map(line),
235
+ "",
236
+ "— means the number is not known yet. Engagement says what it is a share of (views or followers); do not compare across the two.",
237
+ "Read one with `npx vfrey creators show @handle --platform tiktok`. Search uses credits; see your balance with `npx vfrey whoami`.",
238
+ "",
239
+ ].join("\n");
240
+ }
241
+
242
+ export function formatProfile(p: CreatorProfileView): string {
243
+ const s = p.stats ?? {};
244
+ const lines: string[] = [];
245
+ lines.push(`@${clean(p.handle)} · ${platformName(p.platform)}${p.displayName ? ` · ${clean(p.displayName)}` : ""}${p.verified ? " · verified" : ""}`);
246
+ lines.push(`Followers ${count(p.followers)} · Following ${count(p.following)} · Posts ${count(p.postCount)} · Likes ${count(p.totalLikes)}`);
247
+ lines.push(`Niches: ${nicheList(p)}`);
248
+ if (p.bio) lines.push(`Bio: ${clean(p.bio)}`);
249
+ lines.push("");
250
+ lines.push(`Stats${typeof s.sampleSize === "number" ? ` (from ${s.sampleSize} recent posts)` : ""}`);
251
+ lines.push(` Median views ${count(p.medianViews)}`);
252
+ lines.push(` Engagement ${engagementLabel(p)}`);
253
+ lines.push(` Posts per week ${postsPerWeekLabel(p, s.postsPerWeekBasis)}`);
254
+ lines.push(` Follower growth 7d ${growthLabel(s.growth7d, p.growth7d)} · 30d ${growthLabel(s.growth30d, p.growth30d)}`);
255
+ lines.push(` Median length ${typeof s.medianLengthSec === "number" ? `${Math.round(s.medianLengthSec)}s` : DASH}`);
256
+ if (s.postingTimes && s.postingTimes.bestHours?.length) lines.push(` Best hours ${s.postingTimes.bestHours.slice(0, 3).map((h) => `${String(h).padStart(2, "0")}:00`).join(", ")} (${clean(s.postingTimes.timeZone)})`);
257
+ lines.push("");
258
+ lines.push(`Formats (${p.formats?.length ?? 0})`);
259
+ if (!p.formats?.length) lines.push(" None found yet.");
260
+ for (const f of p.formats ?? []) lines.push(` ${clean(f.name)} · ${f.postCount} posts · ${(f.terms ?? []).map((t) => clean(t)).join(", ")}`);
261
+ lines.push("");
262
+ lines.push(`Hooks (${p.hooks?.length ?? 0})`);
263
+ if (!p.hooks?.length) lines.push(" None found yet.");
264
+ for (const h of p.hooks ?? []) lines.push(` "${clean(h.text)}"`);
265
+ lines.push("");
266
+ lines.push(`Top posts (${p.topPosts?.length ?? 0})`);
267
+ if (!p.topPosts?.length) lines.push(" None found yet.");
268
+ for (const t of p.topPosts ?? []) {
269
+ const posted = Date.parse(t.postedAt);
270
+ lines.push(` ${httpUrl(t.url) || "(no link)"} ${count(t.metrics?.views)} views · ${count(t.metrics?.likes)} likes · posted ${Number.isFinite(posted) ? clean(t.postedAt).slice(0, 10) : DASH}${t.caption ? ` · ${clean(t.caption).slice(0, 80)}` : ""}`);
271
+ }
272
+ lines.push("");
273
+ lines.push(p.businessContact ? `Business contact (from their public bio): ${clean(p.businessContact.value)}` : "Business contact: none in their public bio.");
274
+ if (p.profileAt) lines.push(`Profile data from ${clean(p.profileAt)}${p.refreshed ? " (refreshed now)" : ""}.`);
275
+ lines.push("Use their formats and hooks to learn what works, then design your own scene. Do not copy their videos.");
276
+ return lines.join("\n") + "\n";
277
+ }
278
+
279
+ export function formatWatchlists(lists: WatchlistView[]): string {
280
+ if (lists.length === 0) return "No watchlists yet. Add a creator with `npx vfrey creators watch add @handle --platform tiktok`.\n";
281
+ const width = Math.max(...lists.map((l) => clean(l.name).length));
282
+ return [...lists.map((l) => `${clean(l.name).padEnd(width)} ${l.items} creator${l.items === 1 ? "" : "s"} (${clean(l.id)})`), "", "See one with `npx vfrey creators watch list --watchlist NAME`. Watchlists are free.", ""].join("\n");
283
+ }
284
+
285
+ export function formatWatchItems(answer: { watchlist: { id: string; name: string }; items: WatchlistItemView[] }): string {
286
+ const lines = [`${clean(answer.watchlist.name)} (${clean(answer.watchlist.id)}): ${answer.items.length}`];
287
+ if (answer.items.length === 0) lines.push("Nobody yet.");
288
+ for (const i of answer.items) lines.push(` @${clean(i.handle)} ${platformName(i.platform)} ${count(i.followers)} followers${i.displayName ? ` ${clean(i.displayName)}` : ""}`);
289
+ lines.push("", `Export with \`npx vfrey creators export --watchlist ${clean(answer.watchlist.id)} > watchlist.csv\`.`);
290
+ return lines.join("\n") + "\n";
291
+ }
292
+
293
+ export function formatAlerts(answer: CreatorAlertsResponse): string {
294
+ const lines = [`Breakouts on creators you watch: ${answer.alerts.length}`, `A post counts when it has ${answer.rule?.multiple ?? 5}x the creator's median views within ${answer.rule?.withinHours ?? 48} hours.`];
295
+ if (answer.alerts.length === 0) lines.push("", "No breakouts right now.");
296
+ answer.alerts.forEach((b: BreakoutView, i) => {
297
+ lines.push("", `${String(i + 1).padStart(2)}. @${clean(b.creator.handle)} (${platformName(b.creator.platform)}) ${b.multiple}x their median`);
298
+ lines.push(` ${httpUrl(b.url) || "(no link)"} ${formatViews(b.views)} views (median ${formatViews(b.medianViews)}) · ${formatAge(b.ageHours)} old`);
299
+ if (b.caption) lines.push(` ${clean(b.caption).slice(0, 100)}`);
300
+ });
301
+ lines.push("", "Alerts use credits. See your balance with `npx vfrey whoami`.");
302
+ return lines.join("\n") + "\n";
303
+ }
304
+
305
+ // ---------- "Study formats": what the agent reads before it designs a scene ----------
306
+
307
+ /** A file name safe on every system: the handle, lowercased, nothing but a-z 0-9 . _ - */
308
+ export function studyFileName(handle: string): string | null {
309
+ const h = normalizeHandle(handle);
310
+ return /^[a-z0-9._-]{1,80}$/.test(h) && !h.startsWith(".") ? `${h}.md` : null;
311
+ }
312
+
313
+ export function studyMarkdown(p: CreatorProfileView, now: Date = new Date()): string {
314
+ const out: string[] = [];
315
+ out.push(`# Formats and hooks: @${clean(p.handle)} (${platformName(p.platform)})`, "");
316
+ out.push("Read this before you design a scene. Learn what works: the shape of the video, how it opens, how long it runs. Write your own script and visuals. Do not copy their videos, captions or footage.", "");
317
+ out.push(`Studied ${now.toISOString().slice(0, 10)} from vfrey Creators. Followers ${count(p.followers)}, median views ${count(p.medianViews)}, engagement ${engagementLabel(p)}, median length ${typeof p.stats?.medianLengthSec === "number" ? `${Math.round(p.stats.medianLengthSec)}s` : DASH}.`, "");
318
+ out.push("## Formats", "");
319
+ if (!p.formats?.length) out.push("None found yet.");
320
+ for (const f of p.formats ?? []) out.push(`- ${clean(f.name)}: ${f.postCount} posts. Terms: ${(f.terms ?? []).map((t) => clean(t)).join(", ")}.`);
321
+ out.push("", "## Hooks (the first sentence of their captions)", "");
322
+ if (!p.hooks?.length) out.push("None found yet.");
323
+ for (const h of p.hooks ?? []) out.push(`- ${clean(h.text)}`);
324
+ out.push("", "## Top posts (for reference)", "");
325
+ if (!p.topPosts?.length) out.push("None found yet.");
326
+ for (const t of p.topPosts ?? []) out.push(`- ${httpUrl(t.url) || "(no link)"}: ${count(t.metrics?.views)} views${typeof t.durationSec === "number" ? `, ${Math.round(t.durationSec)}s` : ""}${t.caption ? `. ${clean(t.caption).slice(0, 120)}` : ""}`);
327
+ return out.join("\n") + "\n";
328
+ }
329
+
330
+ // ---------- the command ----------
331
+
332
+ const USAGE = [
333
+ "usage: npx vfrey creators search [--niche ID] [--platform tiktok,youtube] [--followers 10k-200k] [--posts-per-week N] [--engagement 4%] [--sort engagement|growth|views|followers] [--limit N] [--json]",
334
+ " npx vfrey creators show @handle --platform P [--json]",
335
+ " npx vfrey creators watch add|remove @handle --platform P [--watchlist NAME] | watch list [--watchlist NAME] [--json]",
336
+ " npx vfrey creators export --watchlist ID > file.csv",
337
+ " npx vfrey creators alerts [--json].",
338
+ ].join("\n");
339
+
340
+ function refuse(io: ApiIo, message: string, fix: string): number {
341
+ io.stderr(`✗ ${message}\nFix: ${fix}\n`);
342
+ return 1;
343
+ }
344
+
345
+ function failure(io: ApiIo, error: unknown): number {
346
+ if (error instanceof ApiFailure || error instanceof ApiConfigError) return refuse(io, error.message, error.fix);
347
+ return refuse(io, error instanceof Error ? clean(error.message) : "Something went wrong.", "run `npx vfrey doctor` and check the line above.");
348
+ }
349
+
350
+ /** `--name value`, `--name=value`, bare words as positionals. Unknown flags are an error. */
351
+ function parseArgs(args: string[], boolFlags: string[], valueFlags: string[]): { bools: Set<string>; values: Record<string, string>; positionals: string[]; error?: string } {
352
+ const bools = new Set<string>();
353
+ const values: Record<string, string> = {};
354
+ const positionals: string[] = [];
355
+ for (let i = 0; i < args.length; i += 1) {
356
+ const arg = args[i]!;
357
+ if (!arg.startsWith("--")) {
358
+ positionals.push(arg);
359
+ continue;
360
+ }
361
+ const eq = arg.indexOf("=");
362
+ const name = eq > 0 ? arg.slice(0, eq) : arg;
363
+ if (boolFlags.includes(name)) bools.add(name);
364
+ else if (valueFlags.includes(name)) {
365
+ const next = args[i + 1];
366
+ const value = eq > 0 ? arg.slice(eq + 1) : next !== undefined && !next.startsWith("--") ? args[(i += 1)]! : "";
367
+ if (!value) return { bools, values, positionals, error: `${name} needs a value.` };
368
+ values[name] = value;
369
+ } else return { bools, values, positionals, error: `Unknown option "${arg}".` };
370
+ }
371
+ return { bools, values, positionals };
372
+ }
373
+
374
+ function platformOf(value: string | undefined): string | { error: string } {
375
+ const p = value?.trim().toLowerCase();
376
+ if (!p) return { error: "Say which platform: --platform tiktok, instagram or youtube." };
377
+ return (CREATOR_PLATFORM_IDS as readonly string[]).includes(p) ? p : { error: `--platform must be one of ${CREATOR_PLATFORM_IDS.join(", ")}.` };
378
+ }
379
+
380
+ // eslint-disable-next-line no-control-regex
381
+ const CSV_UNSAFE = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
382
+
383
+ export async function runCreators(args: string[], io: ApiIo): Promise<number> {
384
+ const sub = args[0];
385
+ const rest = args.slice(1);
386
+ const deps: CreatorsDeps = { env: io.env, ...(io.homedir ? { homedir: io.homedir } : {}), ...(io.platform ? { platform: io.platform } : {}), ...(io.run ? { run: io.run } : {}), ...(io.fetch ? { fetch: io.fetch } : {}), ...(io.timeoutMs ? { timeoutMs: io.timeoutMs } : {}) };
387
+ try {
388
+ if (sub === "search") {
389
+ const p = parseArgs(rest, ["--json"], ["--niche", "--platform", "--followers", "--posts-per-week", "--engagement", "--sort", "--limit", "--api"]);
390
+ if (p.error || p.positionals.length) return refuse(io, p.error ?? `Unexpected "${p.positionals[0]}".`, USAGE);
391
+ const built = buildSearchQuery({ niche: p.values["--niche"], platform: p.values["--platform"], followers: p.values["--followers"], postsPerWeek: p.values["--posts-per-week"], engagement: p.values["--engagement"], sort: p.values["--sort"], limit: p.values["--limit"] });
392
+ if ("error" in built) return refuse(io, built.error, USAGE);
393
+ const creators = await searchCreators(deps, built.query, p.values["--api"]);
394
+ io.stdout(p.bools.has("--json") ? JSON.stringify({ creators }, null, 2) + "\n" : formatSearch(creators));
395
+ return 0;
396
+ }
397
+ if (sub === "show") {
398
+ const p = parseArgs(rest, ["--json"], ["--platform", "--api"]);
399
+ if (p.error) return refuse(io, p.error, USAGE);
400
+ const handle = p.positionals[0] ? normalizeHandle(p.positionals[0]) : "";
401
+ if (!handle || p.positionals.length > 1) return refuse(io, "Say which creator: creators show @handle --platform tiktok.", USAGE);
402
+ const platform = platformOf(p.values["--platform"]);
403
+ if (typeof platform !== "string") return refuse(io, platform.error, USAGE);
404
+ const profile = await fetchProfile(deps, platform, handle, p.values["--api"]);
405
+ io.stdout(p.bools.has("--json") ? JSON.stringify(profile, null, 2) + "\n" : formatProfile(profile));
406
+ return 0;
407
+ }
408
+ if (sub === "watch") {
409
+ const action = rest[0];
410
+ if (action !== "add" && action !== "remove" && action !== "list") return refuse(io, "Say what to do: watch add, watch remove or watch list.", USAGE);
411
+ const p = parseArgs(rest.slice(1), ["--json"], ["--platform", "--watchlist", "--api"]);
412
+ if (p.error) return refuse(io, p.error, USAGE);
413
+ const key = p.values["--watchlist"]?.trim();
414
+ if (action === "list") {
415
+ if (p.positionals.length) return refuse(io, `Unexpected "${p.positionals[0]}".`, USAGE);
416
+ const lists = await fetchWatchlists(deps, p.values["--api"]);
417
+ if (!key) {
418
+ io.stdout(p.bools.has("--json") ? JSON.stringify({ watchlists: lists }, null, 2) + "\n" : formatWatchlists(lists));
419
+ return 0;
420
+ }
421
+ const list = findWatchlist(lists, key);
422
+ if (!list) return refuse(io, `No watchlist called "${clean(key)}".`, WATCH_FIX);
423
+ const items = await fetchWatchlistItems(deps, list.id, p.values["--api"]);
424
+ io.stdout(p.bools.has("--json") ? JSON.stringify(items, null, 2) + "\n" : formatWatchItems(items));
425
+ return 0;
426
+ }
427
+ const handle = p.positionals[0] ? normalizeHandle(p.positionals[0]) : "";
428
+ if (!handle || p.positionals.length > 1) return refuse(io, `Say which creator: creators watch ${action} @handle --platform tiktok.`, USAGE);
429
+ const platform = platformOf(p.values["--platform"]);
430
+ if (typeof platform !== "string") return refuse(io, platform.error, USAGE);
431
+ if (action === "add") {
432
+ const done = await addToWatchlist(deps, key || DEFAULT_WATCHLIST, platform, handle, p.values["--api"]);
433
+ io.stdout(p.bools.has("--json") ? JSON.stringify(done, null, 2) + "\n" : `✓ ${done.created ? "Added" : "Already on"} @${clean(handle)} (${platformName(platform)}) ${done.created ? "to" : "in"} "${clean(done.watchlist.name)}"${done.madeList ? " (new watchlist)" : ""}. Watching is free; alerts and exports use credits.\n`);
434
+ } else {
435
+ const list = await removeFromWatchlist(deps, key || DEFAULT_WATCHLIST, platform, handle, p.values["--api"]);
436
+ io.stdout(p.bools.has("--json") ? JSON.stringify({ ok: true, watchlist: list }, null, 2) + "\n" : `✓ Removed @${clean(handle)} (${platformName(platform)}) from "${clean(list.name)}".\n`);
437
+ }
438
+ return 0;
439
+ }
440
+ if (sub === "export") {
441
+ const p = parseArgs(rest, [], ["--watchlist", "--api"]);
442
+ if (p.error || p.positionals.length) return refuse(io, p.error ?? `Unexpected "${p.positionals[0]}".`, USAGE);
443
+ const key = p.values["--watchlist"]?.trim();
444
+ if (!key) {
445
+ const lists = await fetchWatchlists(deps, p.values["--api"]);
446
+ return refuse(io, "Pick a watchlist to export.", `run \`npx vfrey creators export --watchlist ID > file.csv\`. Watchlists: ${lists.slice(0, 12).map((l) => `${clean(l.name)} (${clean(l.id)})`).join(", ") || "none yet"}.`);
447
+ }
448
+ const csv = await exportWatchlist(deps, key, p.values["--api"]);
449
+ io.stdout(csv.replace(CSV_UNSAFE, "").replace(/vfr_[A-Za-z0-9_-]+/g, "[token]"));
450
+ return 0;
451
+ }
452
+ if (sub === "alerts") {
453
+ const p = parseArgs(rest, ["--json"], ["--api"]);
454
+ if (p.error || p.positionals.length) return refuse(io, p.error ?? `Unexpected "${p.positionals[0]}".`, USAGE);
455
+ const answer = await fetchAlerts(deps, p.values["--api"]);
456
+ io.stdout(p.bools.has("--json") ? JSON.stringify(answer, null, 2) + "\n" : formatAlerts(answer));
457
+ return 0;
458
+ }
459
+ return refuse(io, sub ? `Unknown creators command "${clean(sub)}".` : "Say what to do with creators.", USAGE);
460
+ } catch (error) {
461
+ return failure(io, error);
462
+ }
463
+ }
@@ -0,0 +1,152 @@
1
+ // Per-user token storage. Never the project's .vfrey folder.
2
+ // macOS: the Keychain through `security`. Otherwise (or when that fails): `<machine home>/credentials.json`, mode 0600, keyed by API host.
3
+ // VFREY_TOKEN overrides both. VFREY_CREDENTIALS_STORE=file skips the Keychain.
4
+ import { spawnSync } from "node:child_process";
5
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { vfreyHome } from "../home.js";
9
+
10
+ type Env = Record<string, string | undefined>;
11
+
12
+ export interface RunResult {
13
+ status: number | null;
14
+ stdout: string;
15
+ stderr: string;
16
+ }
17
+ /** Runs one program. Injectable so tests never touch the real Keychain. */
18
+ export type Runner = (command: string, args: string[]) => RunResult;
19
+
20
+ export const defaultRunner: Runner = (command, args) => {
21
+ const result = spawnSync(command, args, { encoding: "utf8", timeout: 10_000 });
22
+ return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
23
+ };
24
+
25
+ export interface CredentialDeps {
26
+ env: Env;
27
+ homedir?: string;
28
+ platform?: NodeJS.Platform;
29
+ run?: Runner;
30
+ }
31
+
32
+ /** Non-secret facts kept beside the token so `doctor` can answer without the network or the Keychain. */
33
+ export interface AccountHint {
34
+ org: string;
35
+ plan: string;
36
+ }
37
+
38
+ interface FileEntry extends Partial<AccountHint> {
39
+ token?: string;
40
+ }
41
+
42
+ export type TokenSource = "env" | "keychain" | "file";
43
+ export type SaveWhere = "keychain" | "file";
44
+
45
+ const SERVICE = "vfrey";
46
+
47
+ export function credentialsPath(deps: CredentialDeps): string {
48
+ return path.join(vfreyHome(deps.env, deps.homedir ?? os.homedir()), "credentials.json");
49
+ }
50
+
51
+ function useKeychain(deps: CredentialDeps): boolean {
52
+ return (deps.platform ?? process.platform) === "darwin" && deps.env.VFREY_CREDENTIALS_STORE !== "file";
53
+ }
54
+
55
+ function readFile(deps: CredentialDeps): Record<string, FileEntry> {
56
+ const file = credentialsPath(deps);
57
+ if (!existsSync(file)) return {};
58
+ try {
59
+ const parsed: unknown = JSON.parse(readFileSync(file, "utf8"));
60
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record<string, FileEntry>) : {};
61
+ } catch {
62
+ return {};
63
+ }
64
+ }
65
+
66
+ function writeFile(deps: CredentialDeps, data: Record<string, FileEntry>): void {
67
+ const file = credentialsPath(deps);
68
+ const dir = path.dirname(file);
69
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
70
+ const temp = `${file}.${process.pid}.tmp`;
71
+ writeFileSync(temp, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
72
+ chmodSync(temp, 0o600);
73
+ renameSync(temp, file);
74
+ }
75
+
76
+ function keychainGet(host: string, deps: CredentialDeps): string | null {
77
+ const result = (deps.run ?? defaultRunner)("security", ["find-generic-password", "-s", SERVICE, "-a", host, "-w"]);
78
+ const token = result.stdout.trim();
79
+ return result.status === 0 && token ? token : null;
80
+ }
81
+
82
+ /** The token to use for this API host, and where it came from. VFREY_TOKEN wins. */
83
+ export function loadToken(host: string, deps: CredentialDeps): { token: string; source: TokenSource } | null {
84
+ const fromEnv = deps.env.VFREY_TOKEN?.trim();
85
+ if (fromEnv) return { token: fromEnv, source: "env" };
86
+ return loadStoredToken(host, deps);
87
+ }
88
+
89
+ /** The stored token only, ignoring VFREY_TOKEN. `logout` works on this. */
90
+ export function loadStoredToken(host: string, deps: CredentialDeps): { token: string; source: "keychain" | "file" } | null {
91
+ if (useKeychain(deps)) {
92
+ const token = keychainGet(host, deps);
93
+ if (token) return { token, source: "keychain" };
94
+ }
95
+ const token = readFile(deps)[host]?.token;
96
+ return token ? { token, source: "file" } : null;
97
+ }
98
+
99
+ /** Stores the token (Keychain first on macOS, else the file) and the non-secret hint. */
100
+ export function saveToken(host: string, token: string, hint: AccountHint, deps: CredentialDeps): { where: SaveWhere; file: string } {
101
+ const file = credentialsPath(deps);
102
+ const data = readFile(deps);
103
+ if (useKeychain(deps)) {
104
+ const result = (deps.run ?? defaultRunner)("security", ["add-generic-password", "-s", SERVICE, "-a", host, "-w", token, "-U"]);
105
+ if (result.status === 0) {
106
+ data[host] = { ...hint };
107
+ try {
108
+ writeFile(deps, data);
109
+ } catch {
110
+ /* the hint is a courtesy for doctor */
111
+ }
112
+ return { where: "keychain", file };
113
+ }
114
+ }
115
+ data[host] = { token, ...hint };
116
+ writeFile(deps, data);
117
+ return { where: "file", file };
118
+ }
119
+
120
+ /** Refreshes the hint without touching the token. */
121
+ export function saveHint(host: string, hint: AccountHint, deps: CredentialDeps): void {
122
+ const data = readFile(deps);
123
+ if (!data[host]) return;
124
+ data[host] = { ...data[host], ...hint };
125
+ try {
126
+ writeFile(deps, data);
127
+ } catch {
128
+ /* a courtesy */
129
+ }
130
+ }
131
+
132
+ export function readHint(host: string, deps: CredentialDeps): AccountHint | null {
133
+ const entry = readFile(deps)[host];
134
+ return entry?.org && entry.plan ? { org: entry.org, plan: entry.plan } : null;
135
+ }
136
+
137
+ /** Removes the stored token and hint from both places. Returns whether anything was removed. */
138
+ export function deleteToken(host: string, deps: CredentialDeps): boolean {
139
+ let removed = false;
140
+ if (useKeychain(deps)) {
141
+ const result = (deps.run ?? defaultRunner)("security", ["delete-generic-password", "-s", SERVICE, "-a", host]);
142
+ if (result.status === 0) removed = true;
143
+ }
144
+ const data = readFile(deps);
145
+ if (data[host]) {
146
+ removed = true;
147
+ delete data[host];
148
+ if (Object.keys(data).length === 0) rmSync(credentialsPath(deps), { force: true });
149
+ else writeFile(deps, data);
150
+ }
151
+ return removed;
152
+ }