@vfrey/vfrey 0.0.1 → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +33 -0
- package/README.md +78 -3
- package/accounts.example.json +61 -0
- package/accounts.first-run.json +41 -0
- package/bin/vfrey.mjs +36 -0
- package/integration-tools/README.md +43 -0
- package/integration-tools/image/pillow.md +32 -0
- package/integration-tools/image/playwright.md +33 -0
- package/integration-tools/image/resvg.md +36 -0
- package/integration-tools/image/satori.md +36 -0
- package/integration-tools/image/sharp.md +38 -0
- package/integration-tools/video/blender.md +34 -0
- package/integration-tools/video/ffmpeg.md +41 -0
- package/integration-tools/video/manim.md +37 -0
- package/integration-tools/video/motion-canvas.md +37 -0
- package/integration-tools/video/remotion.md +50 -0
- package/integration-tools/video/revideo.md +32 -0
- package/package.json +74 -6
- package/skills/vfrey-orchestrator/SKILL.md +15 -0
- package/skills/vfrey-orchestrator/channel-video/SKILL.md +41 -0
- package/skills/vfrey-product/SKILL.md +63 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-one.svg +14 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-two.svg +22 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/index.html +70 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-one.svg +14 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-two.svg +22 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/index.tsx +49 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/script.json +15 -0
- package/skills/vfrey-product/examples/octopus-facts/index.html +60 -0
- package/skills/vfrey-product/examples/octopus-facts/octopus.svg +8 -0
- package/skills/vfrey-product/examples/octopus-facts/script.json +14 -0
- package/skills/vfrey-product/push/SKILL.md +15 -0
- package/skills/vfrey-product/scenes/NOTICE +5 -0
- package/skills/vfrey-product/scenes/blocks.md +57 -0
- package/skills/vfrey-product/scenes/craft.md +118 -0
- package/skills/vfrey-product/scenes/motion.md +88 -0
- package/src/analytics/dashboard.ts +109 -0
- package/src/analytics/index.ts +260 -0
- package/src/api/client.ts +282 -0
- package/src/api/commands.ts +330 -0
- package/src/api/config.ts +34 -0
- package/src/api/creators.ts +463 -0
- package/src/api/credentials.ts +152 -0
- package/src/api/mail.ts +333 -0
- package/src/api/topics.ts +207 -0
- package/src/api/types.ts +205 -0
- package/src/cli.ts +467 -0
- package/src/cloud/executor.ts +281 -0
- package/src/cloud/job.ts +106 -0
- package/src/cloud/render-job.ts +187 -0
- package/src/config.ts +309 -0
- package/src/dashboard-link.ts +180 -0
- package/src/dashboard-start.ts +103 -0
- package/src/desk/channel.ts +64 -0
- package/src/desk/cli.ts +216 -0
- package/src/desk/creators-route.ts +129 -0
- package/src/desk/fonts/JetBrainsMono-OFL.txt +93 -0
- package/src/desk/fonts/google-sans-flex-latin.woff2 +0 -0
- package/src/desk/fonts/jetbrains-mono-medium.woff2 +0 -0
- package/src/desk/http.ts +460 -0
- package/src/desk/mail-route.ts +103 -0
- package/src/desk/overlay.ts +112 -0
- package/src/desk/preview.ts +422 -0
- package/src/desk/screens.ts +149 -0
- package/src/desk/secrets.ts +143 -0
- package/src/desk/series.ts +127 -0
- package/src/desk/state.ts +77 -0
- package/src/desk/studio-route.ts +119 -0
- package/src/desk/tick-runner.ts +66 -0
- package/src/desk/tick.ts +56 -0
- package/src/desk/topics-route.ts +41 -0
- package/src/desk/ui/app.css +505 -0
- package/src/desk/ui/app.js +185 -0
- package/src/desk/ui/apple-icon.png +0 -0
- package/src/desk/ui/components.js +79 -0
- package/src/desk/ui/core.js +156 -0
- package/src/desk/ui/favicon.ico +0 -0
- package/src/desk/ui/icon.svg +1 -0
- package/src/desk/ui/icons.js +49 -0
- package/src/desk/ui/index.html +41 -0
- package/src/desk/ui/metrics-math.d.ts +22 -0
- package/src/desk/ui/metrics-math.js +135 -0
- package/src/desk/ui/theme.js +5 -0
- package/src/desk/ui/views-creators.js +209 -0
- package/src/desk/ui/views-keys.js +114 -0
- package/src/desk/ui/views-mail.js +188 -0
- package/src/desk/ui/views-misc.js +123 -0
- package/src/desk/ui/views-review.js +119 -0
- package/src/desk/ui/views-runs.js +350 -0
- package/src/desk/ui/views-screens.js +163 -0
- package/src/desk/ui/views-series.js +318 -0
- package/src/desk/ui/views-topics.js +87 -0
- package/src/desk/verbs.ts +361 -0
- package/src/doctor.ts +212 -0
- package/src/drain.ts +14 -0
- package/src/engage/index.ts +217 -0
- package/src/generate/index.ts +110 -0
- package/src/generate/script.ts +152 -0
- package/src/generate/title.ts +78 -0
- package/src/help.ts +233 -0
- package/src/home.ts +45 -0
- package/src/llm/client.ts +177 -0
- package/src/log.ts +7 -0
- package/src/oauth/tokens.ts +189 -0
- package/src/oauth/vault.ts +72 -0
- package/src/pipeline/review.ts +642 -0
- package/src/pipeline/run.ts +443 -0
- package/src/pipeline/series-guard.ts +26 -0
- package/src/pipeline/source-fetch.ts +69 -0
- package/src/pipeline/stages.ts +728 -0
- package/src/pipeline/topics-source.ts +90 -0
- package/src/product/commands.ts +296 -0
- package/src/product/config-file.ts +38 -0
- package/src/product/engine-choice.ts +89 -0
- package/src/product/engines.ts +103 -0
- package/src/product/insights.ts +50 -0
- package/src/product/keys.ts +67 -0
- package/src/product/paths.ts +18 -0
- package/src/product/probes.ts +88 -0
- package/src/product/push-flow.ts +90 -0
- package/src/product/render-video.ts +21 -0
- package/src/product/short.ts +62 -0
- package/src/product/update.ts +32 -0
- package/src/project/canonical.ts +14 -0
- package/src/project/commands.ts +177 -0
- package/src/project/dashboard-status.ts +28 -0
- package/src/project/discover.ts +19 -0
- package/src/project/index-file.ts +73 -0
- package/src/project/marker.ts +87 -0
- package/src/project/migrate.ts +201 -0
- package/src/project/names.ts +27 -0
- package/src/project/notice.ts +16 -0
- package/src/project/open.ts +88 -0
- package/src/project/scaffold.ts +112 -0
- package/src/project/templates.ts +42 -0
- package/src/project/videos.ts +64 -0
- package/src/publish/facebook.ts +91 -0
- package/src/publish/index.ts +12 -0
- package/src/publish/instagram.ts +97 -0
- package/src/publish/rate.ts +62 -0
- package/src/publish/session.ts +242 -0
- package/src/publish/tiktok.ts +202 -0
- package/src/publish/youtube.ts +177 -0
- package/src/queue/desk-ops.ts +1170 -0
- package/src/queue/seal.ts +3 -0
- package/src/queue/store.ts +1006 -0
- package/src/remotion/AiLabel.tsx +26 -0
- package/src/remotion/Root.tsx +73 -0
- package/src/remotion/defaults.ts +26 -0
- package/src/remotion/films/claude/ClaudeFilm.tsx +1014 -0
- package/src/remotion/films/engine/EngineMark.tsx +17 -0
- package/src/remotion/films/rea/ReaShort.tsx +552 -0
- package/src/remotion/films/rea/drawings.tsx +907 -0
- package/src/remotion/films/rea/timing.ts +144 -0
- package/src/remotion/index.ts +4 -0
- package/src/remotion/templates/EditorialSplit.tsx +39 -0
- package/src/remotion/templates/KineticType.tsx +39 -0
- package/src/remotion/templates/SignalBoard.tsx +50 -0
- package/src/remotion/timing.ts +22 -0
- package/src/render/chrome.ts +78 -0
- package/src/render/engine-mark.ts +29 -0
- package/src/render/framey-template.ts +130 -0
- package/src/render/html-engine.ts +149 -0
- package/src/render/node-crypto-stub.ts +11 -0
- package/src/render/probe.ts +29 -0
- package/src/render/props.ts +17 -0
- package/src/render/vertical-short.ts +35 -0
- package/src/render/webpack.ts +33 -0
- package/src/render/worker.ts +65 -0
- package/src/scene/assets/fonts/TitanOne-OFL.txt +94 -0
- package/src/scene/assets/fonts/titan-one.woff2 +0 -0
- package/src/scene/assets/gsap/LICENSE.txt +8 -0
- package/src/scene/assets/gsap/gsap.min.js +11 -0
- package/src/scene/audio.ts +54 -0
- package/src/scene/blocks/LICENSE +190 -0
- package/src/scene/blocks/NOTICE +5 -0
- package/src/scene/blocks/bar-chart-race/bar-chart-race.html +585 -0
- package/src/scene/blocks/beat-freeze-cut/beat-freeze-cut.html +874 -0
- package/src/scene/blocks/camcorder-hud/camcorder-hud.html +164 -0
- package/src/scene/blocks/camera-dolly-zoom/camera-dolly-zoom.html +355 -0
- package/src/scene/blocks/code-diff/code-diff.html +817 -0
- package/src/scene/blocks/code-snippet-apple-terminal-pro/code-snippet-apple-terminal-pro.html +248 -0
- package/src/scene/blocks/code-typing/code-typing.html +764 -0
- package/src/scene/blocks/data-chart/data-chart.html +452 -0
- package/src/scene/blocks/flowchart-vertical/flowchart-vertical.html +454 -0
- package/src/scene/blocks/hw-scribble-transition/hw-scribble-transition.html +153 -0
- package/src/scene/blocks/index.json +824 -0
- package/src/scene/blocks/lt-accent-underline/lt-accent-underline.html +107 -0
- package/src/scene/blocks/lt-bold-block/lt-bold-block.html +109 -0
- package/src/scene/blocks/lt-clean-bar/lt-clean-bar.html +112 -0
- package/src/scene/blocks/lt-dark-card/lt-dark-card.html +111 -0
- package/src/scene/blocks/lt-kicker-name/lt-kicker-name.html +114 -0
- package/src/scene/blocks/lt-mask-reveal/lt-mask-reveal.html +120 -0
- package/src/scene/blocks/lt-neon-border/lt-neon-border.html +391 -0
- package/src/scene/blocks/lt-stack-bars/lt-stack-bars.html +109 -0
- package/src/scene/blocks/macos-notification/macos-notification.html +194 -0
- package/src/scene/blocks/mk-background/mk-background.html +236 -0
- package/src/scene/blocks/mk-callout-highlight/mk-callout-highlight.html +183 -0
- package/src/scene/blocks/mk-clone-wall-transition/mk-clone-wall-transition.html +226 -0
- package/src/scene/blocks/mk-line-graph/mk-line-graph.html +293 -0
- package/src/scene/blocks/mk-progress-stat/mk-progress-stat.html +186 -0
- package/src/scene/blocks/mk-specs-list/mk-specs-list.html +223 -0
- package/src/scene/blocks/news-ticker/news-ticker.html +409 -0
- package/src/scene/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html +126 -0
- package/src/scene/blocks/oscilloscope-trace/oscilloscope-trace.html +519 -0
- package/src/scene/blocks/snow/snow.html +205 -0
- package/src/scene/blocks/split-flap-board/split-flap-board.html +347 -0
- package/src/scene/blocks/spotify-card/spotify-card.html +294 -0
- package/src/scene/blocks/starfield/starfield.html +188 -0
- package/src/scene/blocks/thread-message-stack/thread-message-stack.html +239 -0
- package/src/scene/blocks/week-in-merges/week-in-merges.html +470 -0
- package/src/scene/blocks/x-post/x-post.html +437 -0
- package/src/scene/blocks/yt-comment-card/yt-comment-card.html +319 -0
- package/src/scene/blocks/yt-lcd-background/yt-lcd-background.html +307 -0
- package/src/scene/blocks/yt-prism-title/yt-prism-title.html +205 -0
- package/src/scene/blocks.ts +229 -0
- package/src/scene/command.ts +272 -0
- package/src/scene/figure.ts +181 -0
- package/src/scene/glyphs.ts +107 -0
- package/src/scene/init.ts +86 -0
- package/src/scene/inspect/css-tree.d.ts +6 -0
- package/src/scene/inspect/page-scripts.ts +272 -0
- package/src/scene/inspect/report.ts +405 -0
- package/src/scene/inspect/run.ts +200 -0
- package/src/scene/inspect/source.ts +236 -0
- package/src/scene/label.ts +131 -0
- package/src/scene/net-guard.ts +74 -0
- package/src/scene/render.ts +292 -0
- package/src/scene/runtime/browser.ts +199 -0
- package/src/scene/runtime/core.ts +181 -0
- package/src/scene/runtime/react.tsx +115 -0
- package/src/scene/runtime/transitions.ts +133 -0
- package/src/scene/sanitize.ts +181 -0
- package/src/scene/scene.ts +152 -0
- package/src/scene/studio-command.ts +48 -0
- package/src/scene/studio-skin.ts +59 -0
- package/src/scene/studio.ts +337 -0
- package/src/scene/supply.ts +111 -0
- package/src/scene/timing.ts +288 -0
- package/src/scene/transitions/LICENSE +190 -0
- package/src/scene/transitions/NOTICE +5 -0
- package/src/scene/transitions/SOURCE.json +24 -0
- package/src/scene/transitions/transitions.global.js +38 -0
- package/src/scene/transitions.ts +26 -0
- package/src/service/all-projects.ts +153 -0
- package/src/service/commands.ts +256 -0
- package/src/service/due.ts +82 -0
- package/src/service/lock.ts +101 -0
- package/src/service/log-file.ts +56 -0
- package/src/service/state.ts +41 -0
- package/src/service/units.ts +90 -0
- package/src/service/worker.ts +51 -0
- package/src/skill-agents.ts +74 -0
- package/src/skill-install.ts +334 -0
- package/src/sync-write.ts +21 -0
- package/src/tick-report.ts +205 -0
- package/src/tts.ts +124 -0
- package/vendor/@vfrey/contracts/dist/account.d.ts +97 -0
- package/vendor/@vfrey/contracts/dist/account.js +27 -0
- package/vendor/@vfrey/contracts/dist/checks.d.ts +8 -0
- package/vendor/@vfrey/contracts/dist/checks.js +1 -0
- package/vendor/@vfrey/contracts/dist/credentials/crypto.d.ts +4 -0
- package/vendor/@vfrey/contracts/dist/credentials/crypto.js +38 -0
- package/vendor/@vfrey/contracts/dist/domain/advice-persona.d.ts +10 -0
- package/vendor/@vfrey/contracts/dist/domain/advice-persona.js +24 -0
- package/vendor/@vfrey/contracts/dist/domain/backoff.d.ts +13 -0
- package/vendor/@vfrey/contracts/dist/domain/backoff.js +27 -0
- package/vendor/@vfrey/contracts/dist/domain/cadence.d.ts +29 -0
- package/vendor/@vfrey/contracts/dist/domain/cadence.js +95 -0
- package/vendor/@vfrey/contracts/dist/domain/creators.d.ts +146 -0
- package/vendor/@vfrey/contracts/dist/domain/creators.js +2 -0
- package/vendor/@vfrey/contracts/dist/domain/disclosure.d.ts +127 -0
- package/vendor/@vfrey/contracts/dist/domain/disclosure.js +250 -0
- package/vendor/@vfrey/contracts/dist/domain/errors.d.ts +34 -0
- package/vendor/@vfrey/contracts/dist/domain/errors.js +68 -0
- package/vendor/@vfrey/contracts/dist/domain/redact.d.ts +1 -0
- package/vendor/@vfrey/contracts/dist/domain/redact.js +21 -0
- package/vendor/@vfrey/contracts/dist/domain/revenue.d.ts +13 -0
- package/vendor/@vfrey/contracts/dist/domain/revenue.js +16 -0
- package/vendor/@vfrey/contracts/dist/domain/safety.d.ts +20 -0
- package/vendor/@vfrey/contracts/dist/domain/safety.js +61 -0
- package/vendor/@vfrey/contracts/dist/domain/similarity.d.ts +13 -0
- package/vendor/@vfrey/contracts/dist/domain/similarity.js +100 -0
- package/vendor/@vfrey/contracts/dist/domain/templates.d.ts +18 -0
- package/vendor/@vfrey/contracts/dist/domain/templates.js +67 -0
- package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.d.ts +19 -0
- package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.js +52 -0
- package/vendor/@vfrey/contracts/dist/domain/topics.d.ts +78 -0
- package/vendor/@vfrey/contracts/dist/domain/topics.js +4 -0
- package/vendor/@vfrey/contracts/dist/domain/types.d.ts +216 -0
- package/vendor/@vfrey/contracts/dist/domain/types.js +1 -0
- package/vendor/@vfrey/contracts/dist/domain/variation.d.ts +57 -0
- package/vendor/@vfrey/contracts/dist/domain/variation.js +278 -0
- package/vendor/@vfrey/contracts/dist/events.d.ts +20 -0
- package/vendor/@vfrey/contracts/dist/events.js +1 -0
- package/vendor/@vfrey/contracts/dist/http.d.ts +11 -0
- package/vendor/@vfrey/contracts/dist/http.js +37 -0
- package/vendor/@vfrey/contracts/dist/index.d.ts +22 -0
- package/vendor/@vfrey/contracts/dist/index.js +22 -0
- package/vendor/@vfrey/contracts/dist/log.d.ts +2 -0
- package/vendor/@vfrey/contracts/dist/log.js +10 -0
- package/vendor/@vfrey/contracts/dist/mail.d.ts +55 -0
- package/vendor/@vfrey/contracts/dist/mail.js +1 -0
- package/vendor/@vfrey/contracts/package.json +18 -0
- package/vendor/@vfrey/ingest/dist/index.d.ts +41 -0
- package/vendor/@vfrey/ingest/dist/index.js +221 -0
- package/vendor/@vfrey/ingest/dist/parse.d.ts +5 -0
- package/vendor/@vfrey/ingest/dist/parse.js +165 -0
- package/vendor/@vfrey/ingest/package.json +21 -0
- package/vendor/framey/LICENSE +190 -0
- package/vendor/framey/NOTICE +15 -0
- package/vendor/framey/SOURCE.json +24 -0
- package/vendor/framey/STUDIO-SOURCE.json +9 -0
- package/vendor/framey/bin/framey-localize-fonts.mjs +12 -0
- package/vendor/framey/bin/framey.mjs +16 -0
- package/vendor/framey/dist/add-VMJD5KBS.js +75 -0
- package/vendor/framey/dist/animatedGifPrep-QPKGJ6PM.js +43 -0
- package/vendor/framey/dist/api.d.ts +211 -0
- package/vendor/framey/dist/api.js +76 -0
- package/vendor/framey/dist/appHistory-WW56FZTT.js +31 -0
- package/vendor/framey/dist/asset-WQKGOELE.js +26 -0
- package/vendor/framey/dist/audio-RSJWVWRA.js +1936 -0
- package/vendor/framey/dist/batchRender-3HPICDCT.js +366 -0
- package/vendor/framey/dist/beats-HL2BDUZI.js +273 -0
- package/vendor/framey/dist/benchmark-RWCWHI5Z.js +230 -0
- package/vendor/framey/dist/browser-ECYET3Q2.js +206 -0
- package/vendor/framey/dist/bundleWithLocalizedFonts-ALYBQEQT.js +52 -0
- package/vendor/framey/dist/captureCompositionFrame-4DA6SEH2.js +94 -0
- package/vendor/framey/dist/catalog-5JVDEUWK.js +979 -0
- package/vendor/framey/dist/catch-up-NEBCL5NO.js +106 -0
- package/vendor/framey/dist/check-F5N544ML.js +111 -0
- package/vendor/framey/dist/checkBrowser-LZ66CMJG.js +110 -0
- package/vendor/framey/dist/chunk-2K7Z2HVP.js +372 -0
- package/vendor/framey/dist/chunk-2UZQHDM7.js +323 -0
- package/vendor/framey/dist/chunk-2X4PQEYM.js +34 -0
- package/vendor/framey/dist/chunk-3422KMTD.js +168 -0
- package/vendor/framey/dist/chunk-3FYD4YEK.js +72 -0
- package/vendor/framey/dist/chunk-3GXQPCJC.js +682 -0
- package/vendor/framey/dist/chunk-3LM6YPBR.js +179 -0
- package/vendor/framey/dist/chunk-442JNXRM.js +20 -0
- package/vendor/framey/dist/chunk-4BNN5FGX.js +182 -0
- package/vendor/framey/dist/chunk-4P7NX5BM.js +11532 -0
- package/vendor/framey/dist/chunk-4UMPIPCQ.js +88 -0
- package/vendor/framey/dist/chunk-4YYJOFKS.js +28 -0
- package/vendor/framey/dist/chunk-54HJ63Q2.js +29 -0
- package/vendor/framey/dist/chunk-5ETJX6WM.js +89 -0
- package/vendor/framey/dist/chunk-5SG54JRS.js +68 -0
- package/vendor/framey/dist/chunk-634HSJ3J.js +58 -0
- package/vendor/framey/dist/chunk-66LREXGS.js +48 -0
- package/vendor/framey/dist/chunk-6BEFEWFB.js +25 -0
- package/vendor/framey/dist/chunk-6BNTGNGD.js +286 -0
- package/vendor/framey/dist/chunk-6DDPOHMK.js +140 -0
- package/vendor/framey/dist/chunk-6GV5XSOR.js +214 -0
- package/vendor/framey/dist/chunk-6JR2XUEE.js +127 -0
- package/vendor/framey/dist/chunk-73QVATIM.js +362 -0
- package/vendor/framey/dist/chunk-7FMUTGOH.js +279 -0
- package/vendor/framey/dist/chunk-7JQFJ6JJ.js +291 -0
- package/vendor/framey/dist/chunk-AHPGAESX.js +113 -0
- package/vendor/framey/dist/chunk-AHX34EFP.js +15 -0
- package/vendor/framey/dist/chunk-AJ5CSAEZ.js +487 -0
- package/vendor/framey/dist/chunk-B7RJV74O.js +5047 -0
- package/vendor/framey/dist/chunk-BL4U6U52.js +254 -0
- package/vendor/framey/dist/chunk-BSQ7XANM.js +210 -0
- package/vendor/framey/dist/chunk-BZ56MTDX.js +6 -0
- package/vendor/framey/dist/chunk-C5BO7PMT.js +282 -0
- package/vendor/framey/dist/chunk-CFUCHO4T.js +358 -0
- package/vendor/framey/dist/chunk-CZGHFATT.js +31 -0
- package/vendor/framey/dist/chunk-DLA44CMS.js +5649 -0
- package/vendor/framey/dist/chunk-DPVXBVLL.js +31292 -0
- package/vendor/framey/dist/chunk-DQBY7FRR.js +397 -0
- package/vendor/framey/dist/chunk-EAMLBAHA.js +154 -0
- package/vendor/framey/dist/chunk-EOVYQ3IG.js +143 -0
- package/vendor/framey/dist/chunk-EVMY7IPL.js +612 -0
- package/vendor/framey/dist/chunk-FG4BBFZI.js +122 -0
- package/vendor/framey/dist/chunk-FI4I7CME.js +56 -0
- package/vendor/framey/dist/chunk-FLREUEHW.js +2081 -0
- package/vendor/framey/dist/chunk-FUFP7A7S.js +1126 -0
- package/vendor/framey/dist/chunk-FV3PIKGH.js +56 -0
- package/vendor/framey/dist/chunk-FXMKHHPO.js +123 -0
- package/vendor/framey/dist/chunk-GGZAM5VH.js +1187 -0
- package/vendor/framey/dist/chunk-GKZEVJIB.js +39 -0
- package/vendor/framey/dist/chunk-GMECXFAN.js +57 -0
- package/vendor/framey/dist/chunk-GNEFDLFB.js +621 -0
- package/vendor/framey/dist/chunk-GPMLSOEG.js +19274 -0
- package/vendor/framey/dist/chunk-GPYVT5EE.js +59 -0
- package/vendor/framey/dist/chunk-H6IEMZEV.js +40 -0
- package/vendor/framey/dist/chunk-HEFQCIXR.js +170 -0
- package/vendor/framey/dist/chunk-HFQ4CWHA.js +118 -0
- package/vendor/framey/dist/chunk-HPDTCOKE.js +304 -0
- package/vendor/framey/dist/chunk-HTUHQKDB.js +548 -0
- package/vendor/framey/dist/chunk-HU2SPFNG.js +103 -0
- package/vendor/framey/dist/chunk-HVW7ABXD.js +19 -0
- package/vendor/framey/dist/chunk-IG7NDKXO.js +26 -0
- package/vendor/framey/dist/chunk-II3J3WXK.js +246 -0
- package/vendor/framey/dist/chunk-IR35BCLK.js +13 -0
- package/vendor/framey/dist/chunk-ISN5GRYI.js +20 -0
- package/vendor/framey/dist/chunk-IUONB55J.js +669 -0
- package/vendor/framey/dist/chunk-IVSUQR2B.js +161 -0
- package/vendor/framey/dist/chunk-IXU6PQSW.js +259 -0
- package/vendor/framey/dist/chunk-IZCW2NKT.js +29 -0
- package/vendor/framey/dist/chunk-JGXJU5PF.js +68 -0
- package/vendor/framey/dist/chunk-JHOCK3DG.js +27 -0
- package/vendor/framey/dist/chunk-JVBRJMOP.js +121 -0
- package/vendor/framey/dist/chunk-K4MJX7SR.js +17 -0
- package/vendor/framey/dist/chunk-K4WKGXRC.js +18 -0
- package/vendor/framey/dist/chunk-KGPLIH7B.js +127 -0
- package/vendor/framey/dist/chunk-KLIKCX6Q.js +63 -0
- package/vendor/framey/dist/chunk-KXTKKBOC.js +61 -0
- package/vendor/framey/dist/chunk-KZI44OPA.js +185 -0
- package/vendor/framey/dist/chunk-LEKZNRKI.js +45 -0
- package/vendor/framey/dist/chunk-LFGJWOVA.js +61 -0
- package/vendor/framey/dist/chunk-LLMSX6V6.js +17 -0
- package/vendor/framey/dist/chunk-LTGTN2TH.js +195 -0
- package/vendor/framey/dist/chunk-M2WBOQ2Q.js +54 -0
- package/vendor/framey/dist/chunk-MI5FTLYK.js +399 -0
- package/vendor/framey/dist/chunk-MLQP2QUF.js +1809 -0
- package/vendor/framey/dist/chunk-MMQ52NIZ.js +956 -0
- package/vendor/framey/dist/chunk-ND46XKK3.js +742 -0
- package/vendor/framey/dist/chunk-NYEXNSHM.js +504 -0
- package/vendor/framey/dist/chunk-O4YRB27L.js +57 -0
- package/vendor/framey/dist/chunk-O7BGSQ5F.js +92 -0
- package/vendor/framey/dist/chunk-OFQWTKPW.js +117 -0
- package/vendor/framey/dist/chunk-P5ZBLXJ6.js +91 -0
- package/vendor/framey/dist/chunk-P6LJ4Y32.js +4662 -0
- package/vendor/framey/dist/chunk-PKZ7RK5N.js +281 -0
- package/vendor/framey/dist/chunk-PURHAXFL.js +15 -0
- package/vendor/framey/dist/chunk-Q5X2B5WT.js +29 -0
- package/vendor/framey/dist/chunk-QAMKOG67.js +248 -0
- package/vendor/framey/dist/chunk-R6YBK4XX.js +514 -0
- package/vendor/framey/dist/chunk-RGLQURIX.js +2997 -0
- package/vendor/framey/dist/chunk-RNV3DM4S.js +3573 -0
- package/vendor/framey/dist/chunk-ROEW726S.js +4638 -0
- package/vendor/framey/dist/chunk-RQVDTPGD.js +6 -0
- package/vendor/framey/dist/chunk-RVZHGOGR.js +26 -0
- package/vendor/framey/dist/chunk-RXATE6FZ.js +385 -0
- package/vendor/framey/dist/chunk-S3S4TGEH.js +424 -0
- package/vendor/framey/dist/chunk-SDYDNHEE.js +11202 -0
- package/vendor/framey/dist/chunk-SSWBU67I.js +173 -0
- package/vendor/framey/dist/chunk-ST35C4K6.js +28 -0
- package/vendor/framey/dist/chunk-SZNTYTIV.js +990 -0
- package/vendor/framey/dist/chunk-T2EUWCJE.js +29 -0
- package/vendor/framey/dist/chunk-T6AGUTZJ.js +444 -0
- package/vendor/framey/dist/chunk-THJALHFJ.js +11131 -0
- package/vendor/framey/dist/chunk-TIQB63MX.js +18128 -0
- package/vendor/framey/dist/chunk-U242RTCT.js +134 -0
- package/vendor/framey/dist/chunk-UJZREAV6.js +3901 -0
- package/vendor/framey/dist/chunk-UMD3V7CU.js +264 -0
- package/vendor/framey/dist/chunk-UOFAE5RF.js +42 -0
- package/vendor/framey/dist/chunk-UTEIUGAZ.js +29 -0
- package/vendor/framey/dist/chunk-VAS6HQX3.js +138 -0
- package/vendor/framey/dist/chunk-VC76R6W2.js +27 -0
- package/vendor/framey/dist/chunk-VFZ7W5W7.js +226 -0
- package/vendor/framey/dist/chunk-VHNLZAE2.js +348 -0
- package/vendor/framey/dist/chunk-VMC4WSH5.js +278 -0
- package/vendor/framey/dist/chunk-VORQIH64.js +108 -0
- package/vendor/framey/dist/chunk-VTXFRIHO.js +186 -0
- package/vendor/framey/dist/chunk-W2LGJSEN.js +182 -0
- package/vendor/framey/dist/chunk-WKBM7NDG.js +32 -0
- package/vendor/framey/dist/chunk-WMQNIQDJ.js +2057 -0
- package/vendor/framey/dist/chunk-WN4XZC5F.js +504 -0
- package/vendor/framey/dist/chunk-WQ6ZXHFQ.js +133 -0
- package/vendor/framey/dist/chunk-X7HBSTCZ.js +53 -0
- package/vendor/framey/dist/chunk-X7MTZ4T2.js +6 -0
- package/vendor/framey/dist/chunk-XPTOJZJF.js +308 -0
- package/vendor/framey/dist/chunk-XT6MXC7A.js +6 -0
- package/vendor/framey/dist/chunk-XT7X6QCP.js +6 -0
- package/vendor/framey/dist/chunk-XTMDWNEC.js +2820 -0
- package/vendor/framey/dist/chunk-XUHOIDKG.js +6 -0
- package/vendor/framey/dist/chunk-XX7LTW5Y.js +138 -0
- package/vendor/framey/dist/chunk-Y3YGTZEG.js +382 -0
- package/vendor/framey/dist/chunk-Y5SBSGN7.js +346 -0
- package/vendor/framey/dist/chunk-YCMVI4MZ.js +625 -0
- package/vendor/framey/dist/chunk-YOR4CTCE.js +261 -0
- package/vendor/framey/dist/chunk-YPRBGITP.js +2748 -0
- package/vendor/framey/dist/chunk-ZKDMAOFU.js +32 -0
- package/vendor/framey/dist/chunk-ZW6DURPF.js +1318 -0
- package/vendor/framey/dist/clean-ZJOGDKDM.js +336 -0
- package/vendor/framey/dist/cli.js +439 -0
- package/vendor/framey/dist/cloudrun-HGJ3B6ZV.js +767 -0
- package/vendor/framey/dist/commands/contrast-audit.browser.js +643 -0
- package/vendor/framey/dist/commands/layout-audit.browser.js +2914 -0
- package/vendor/framey/dist/commands/motion-sample.browser.js +74 -0
- package/vendor/framey/dist/commands/motion-signature.browser.js +475 -0
- package/vendor/framey/dist/compare-W7UY3BAZ.js +463 -0
- package/vendor/framey/dist/compiler-TM3W6GYF.js +136 -0
- package/vendor/framey/dist/component-QF5VGYMU.js +176 -0
- package/vendor/framey/dist/compositions-XT3PMNDF.js +238 -0
- package/vendor/framey/dist/contactSheet-7W6GYHBQ.js +24 -0
- package/vendor/framey/dist/deploy-6FJPWKF2.js +299 -0
- package/vendor/framey/dist/destroy-HE2RIPKZ.js +57 -0
- package/vendor/framey/dist/deterministicFonts-RADD3BSX.js +50 -0
- package/vendor/framey/dist/dist-4S2FGGVY.js +422 -0
- package/vendor/framey/dist/dist-BE4B2EZE.js +32 -0
- package/vendor/framey/dist/dist-EWXJGN63.js +129 -0
- package/vendor/framey/dist/docs-VKIRFEEX.js +131 -0
- package/vendor/framey/dist/doctor-2UNJ2G7U.js +74 -0
- package/vendor/framey/dist/dom-37K6YOH4.js +14 -0
- package/vendor/framey/dist/dotEnv-7PZQTVIM.js +54 -0
- package/vendor/framey/dist/events-ACY3P2PA.js +76 -0
- package/vendor/framey/dist/events-IHB3B2MK.js +69 -0
- package/vendor/framey/dist/ffmpeg-DR5LGGSK.js +63 -0
- package/vendor/framey/dist/figma-BODVUPHB.js +63 -0
- package/vendor/framey/dist/fontData.generated-NXAOI4GH.js +13 -0
- package/vendor/framey/dist/fontLocalizeCli.js +142 -0
- package/vendor/framey/dist/gpuPolicy-XLEXORZY.js +49 -0
- package/vendor/framey/dist/grade-compare-GAXFQ74C.js +661 -0
- package/vendor/framey/dist/gsapParser-IEUSLYDJ.js +2885 -0
- package/vendor/framey/dist/gsapParserAcorn-LZJKCFQI.js +30 -0
- package/vendor/framey/dist/help-VCB5X2HS.js +184 -0
- package/vendor/framey/dist/history-ENGZEVFT.js +471 -0
- package/vendor/framey/dist/hyperframe.manifest.json +22 -0
- package/vendor/framey/dist/hyperframe.runtime.iife.js +1132 -0
- package/vendor/framey/dist/hyperframes-player.global.js +478 -0
- package/vendor/framey/dist/hyperframes-slideshow.global.js +148 -0
- package/vendor/framey/dist/info-WPCJHBMU.js +182 -0
- package/vendor/framey/dist/init-G4WGLYGD.js +1176 -0
- package/vendor/framey/dist/inspect-5IQ6CNEQ.js +106 -0
- package/vendor/framey/dist/installer-ML4HWUV7.js +40 -0
- package/vendor/framey/dist/keyframes-Q6XJO7VW.js +2755 -0
- package/vendor/framey/dist/lambda-A7JC74JF.js +468 -0
- package/vendor/framey/dist/layout-YET6XNXA.js +95 -0
- package/vendor/framey/dist/lint-I7FZN2VJ.js +162 -0
- package/vendor/framey/dist/lintProject-PDB3UN72.js +30 -0
- package/vendor/framey/dist/manager-CD6LJ7ZN.js +45 -0
- package/vendor/framey/dist/manager-WA4F2GST.js +67 -0
- package/vendor/framey/dist/media-treatment-BXYS4WVA.js +1030 -0
- package/vendor/framey/dist/media-use-LWH3MANK.js +113 -0
- package/vendor/framey/dist/models-K363FYM4.js +195 -0
- package/vendor/framey/dist/motionShot-GPAVWXLH.js +640 -0
- package/vendor/framey/dist/normalize-FASIPBUM.js +24 -0
- package/vendor/framey/dist/normalize-audio-VDWYRHN6.js +268 -0
- package/vendor/framey/dist/open-E3TB6P6J.js +128 -0
- package/vendor/framey/dist/parakeetRunner-UP34UOI2.js +58 -0
- package/vendor/framey/dist/pipeline-VXIUYEBU.js +64 -0
- package/vendor/framey/dist/play-BUTDQ4DV.js +353 -0
- package/vendor/framey/dist/policies-OLDYGFK5.js +309 -0
- package/vendor/framey/dist/preflight-2KZ2GWAZ.js +55 -0
- package/vendor/framey/dist/present-GXYSM7NX.js +353 -0
- package/vendor/framey/dist/preview-EKZ33JOJ.js +1910 -0
- package/vendor/framey/dist/producer-SKU5EV3J.js +13 -0
- package/vendor/framey/dist/progress-ZWB4Z4ET.js +76 -0
- package/vendor/framey/dist/projectConfig-3MZAYVTS.js +60 -0
- package/vendor/framey/dist/publication-DTKWNF74.js +33 -0
- package/vendor/framey/dist/remove-background-OY5L7XN2.js +265 -0
- package/vendor/framey/dist/render-CDV4GDFX.js +187 -0
- package/vendor/framey/dist/render-YE5YAMOD.js +2620 -0
- package/vendor/framey/dist/render-batch-ZAMFPCLL.js +274 -0
- package/vendor/framey/dist/renderSetupWorker.js +120 -0
- package/vendor/framey/dist/resolver-D5BIPSEW.js +42 -0
- package/vendor/framey/dist/runtimeVersion.js +18 -0
- package/vendor/framey/dist/sam-WFZXI4F7.js +23 -0
- package/vendor/framey/dist/shaderTransitionWorker.js +44 -0
- package/vendor/framey/dist/sherpa-RQWLV7I3.js +79 -0
- package/vendor/framey/dist/sherpaWorker.js +84 -0
- package/vendor/framey/dist/sites-AMOVABTK.js +54 -0
- package/vendor/framey/dist/skill-ZHVLH7KX.js +15 -0
- package/vendor/framey/dist/slideshow-SJGK4RQS.js +24 -0
- package/vendor/framey/dist/snapshot-TUXL6LBQ.js +1515 -0
- package/vendor/framey/dist/src-JHQGB2S6.js +215 -0
- package/vendor/framey/dist/src-Z5PGEMXB.js +532 -0
- package/vendor/framey/dist/state-B6H3WHCF.js +28 -0
- package/vendor/framey/dist/staticProjectServer-DFVZOUGI.js +63 -0
- package/vendor/framey/dist/studio/assets/hyperframes-player-DLaKjw0L.js +478 -0
- package/vendor/framey/dist/studio/assets/index-BX3KHhGX.js +71 -0
- package/vendor/framey/dist/studio/assets/index-CYbvOVp-.css +1 -0
- package/vendor/framey/dist/studio/assets/index-Dp3QEUGL.js +402 -0
- package/vendor/framey/dist/studio/assets/index-qrTAXbqc.js +531 -0
- package/vendor/framey/dist/studio/favicon.svg +14 -0
- package/vendor/framey/dist/studio/icons/timeline/audio.svg +7 -0
- package/vendor/framey/dist/studio/icons/timeline/captions.svg +5 -0
- package/vendor/framey/dist/studio/icons/timeline/composition.svg +12 -0
- package/vendor/framey/dist/studio/icons/timeline/image.svg +18 -0
- package/vendor/framey/dist/studio/icons/timeline/music.svg +10 -0
- package/vendor/framey/dist/studio/icons/timeline/text.svg +3 -0
- package/vendor/framey/dist/studio/index.html +23 -0
- package/vendor/framey/dist/studio-api-2NECGRMB.js +179 -0
- package/vendor/framey/dist/studioSelectionClient-IUUPKUHZ.js +25 -0
- package/vendor/framey/dist/studioServer-N2OPUMOQ.js +1569 -0
- package/vendor/framey/dist/synthesize-PKTB5ATG.js +183 -0
- package/vendor/framey/dist/system-7F3UEHQT.js +27 -0
- package/vendor/framey/dist/telemetry-DQGPEKM6.js +145 -0
- package/vendor/framey/dist/telemetry-UN55N6U6.js +77 -0
- package/vendor/framey/dist/timeline-LHUUMK63.js +1295 -0
- package/vendor/framey/dist/tokens-OLECWDFK.js +92 -0
- package/vendor/framey/dist/transcribe-O7HQKMIV.js +331 -0
- package/vendor/framey/dist/transcribe-UJESPBT5.js +76 -0
- package/vendor/framey/dist/tts-YCCQHZD4.js +234 -0
- package/vendor/framey/dist/urlDownloader-H2LXLG4U.js +48 -0
- package/vendor/framey/dist/usage-HG4WMM7A.js +2167 -0
- package/vendor/framey/dist/validate-F2XMN6EV.js +87 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/GSAP-LICENSE.txt +8 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/CustomEase.min.js +11 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js +11 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/gsap.min.js +11 -0
- package/vendor/framey/dist/warmCheck.js +220 -0
- package/vendor/framey/dist/warmCheckClient.js +139 -0
- package/vendor/framey/dist/workspaceBuildCheck-XYD2YTFW.js +133 -0
- package/vendor/framey-player/LICENSE +190 -0
- package/vendor/framey-player/NOTICE +16 -0
- package/vendor/framey-player/SOURCE.json +6 -0
- package/vendor/framey-player/hyperframes-player.global.js +478 -0
- package/vendor/framey.pin +1 -0
- package/bin/index.js +0 -3
package/src/api/mail.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// vfrey Mail (spec 018): `mail draft | drafts | show | approve | send | discard | identity`, and the helpers the desk route shares.
|
|
2
|
+
// Drafting is free and sends nothing. Approving and sending are two separate commands a person runs, one draft at a time:
|
|
3
|
+
// no command here ever approves or sends as a side effect of another.
|
|
4
|
+
import { ApiFailure, clean } from "./client.js";
|
|
5
|
+
import { ApiConfigError } from "./config.js";
|
|
6
|
+
import type { ApiIo } from "./commands.js";
|
|
7
|
+
import { openSession, type TopicsDeps } from "./topics.js";
|
|
8
|
+
import { normalizeHandle, CREATOR_PLATFORM_IDS } from "./creators.js";
|
|
9
|
+
import { MAIL_KINDS, MAIL_STATUSES, type MailDraftBody, type MailDraftPatch, type MailDraftView, type MailIdentityBody, type MailIdentityView } from "./types.js";
|
|
10
|
+
|
|
11
|
+
export type MailDeps = TopicsDeps;
|
|
12
|
+
|
|
13
|
+
const DASH = "—";
|
|
14
|
+
const PERSON_ONLY = "an agent token can't approve or send; a person signs in with `npx vfrey login` and runs it.";
|
|
15
|
+
|
|
16
|
+
// ---------- every server refusal, in the person's words ----------
|
|
17
|
+
|
|
18
|
+
/** Maps one server refusal to a `✗` message and a `Fix:` that names the next command. `id` is the draft it was about. */
|
|
19
|
+
export function mailFailure(error: unknown, id?: string): unknown {
|
|
20
|
+
if (!(error instanceof ApiFailure)) return error;
|
|
21
|
+
const { status, code } = error;
|
|
22
|
+
const text = error.message;
|
|
23
|
+
const mk = (message: string, fix: string) => new ApiFailure(message, fix, status, code);
|
|
24
|
+
const show = id ? `\`npx vfrey mail show --id ${clean(id)}\`` : "`npx vfrey mail drafts`";
|
|
25
|
+
if (status === 401 || code === "not_live" || code === "not_signed_in") return error;
|
|
26
|
+
if (status === 402 && code === "plan_required") return mk("Mail is not in your plan: it needs the Mail add-on.", error.fix.replace(/^upgrade your plan/, "add the Mail add-on"));
|
|
27
|
+
if (status === 402) return error;
|
|
28
|
+
if (status === 403) return mk(text || "Only a person can do that.", PERSON_ONLY);
|
|
29
|
+
if (status === 404) {
|
|
30
|
+
if (/creator/i.test(text)) return mk(text, "look the creator up with `npx vfrey creators show @handle --platform tiktok`, or draft without --creator.");
|
|
31
|
+
return mk(text || "No such draft.", "list your drafts with `npx vfrey mail drafts`.");
|
|
32
|
+
}
|
|
33
|
+
if (status === 409) {
|
|
34
|
+
if (/Approve this draft/i.test(text)) return mk(text, `ask the person to check ${show}; only if they say so for this draft, they run \`npx vfrey mail approve --id ${id ? clean(id) : "ID"}\`.`);
|
|
35
|
+
if (/reply-to|Confirm your/i.test(text)) return mk(text, "open the confirmation link emailed to your reply-to address, then check it with `npx vfrey mail identity`. Resend the link by running `npx vfrey mail identity` with the same flags.");
|
|
36
|
+
if (/cannot be mailed|suppress/i.test(text)) return mk("This address cannot be mailed: its owner opted out or it bounced.", "use another recipient: `npx vfrey mail draft --to OTHER_EMAIL ...`. Do not try to get around it.");
|
|
37
|
+
if (/sender identity/i.test(text)) return mk(text, 'set it up first: `npx vfrey mail identity --from-name "Ana" --reply-to you@example.com --postal-address "1 Main St, City, Country"`.');
|
|
38
|
+
if (/already sent/i.test(text)) return mk(text, "nothing to do. See it with `npx vfrey mail drafts --status sent`.");
|
|
39
|
+
if (/changed/i.test(text)) return mk(text, `read it again with ${show}.`);
|
|
40
|
+
return mk(text, `check where it stands with ${show}.`);
|
|
41
|
+
}
|
|
42
|
+
if (status === 429) {
|
|
43
|
+
if (/limit of/i.test(text)) return mk(text, "send the rest after the reset time named above. The limit protects your sending reputation.");
|
|
44
|
+
if (/verification/i.test(text)) return mk(text, "wait an hour, then save your identity again.");
|
|
45
|
+
return mk(text || "Too many requests.", "wait a minute and try again.");
|
|
46
|
+
}
|
|
47
|
+
if (status === 503) {
|
|
48
|
+
if (code === "capacity_paused") return error;
|
|
49
|
+
return mk(text || "Mail is paused or not set up on this server.", "try again later. Nothing was sent or charged. If you run your own server, check the mail secret and provider settings.");
|
|
50
|
+
}
|
|
51
|
+
if (status === 400) return mk(text, "fix what it names, then run the command again. `npx vfrey mail --help` lists the flags.");
|
|
52
|
+
return error;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function checked<T>(value: T | null | undefined, ok: (v: T) => boolean): T {
|
|
56
|
+
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`.");
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
const isDraft = (a: { draft?: MailDraftView }) => typeof a.draft?.id === "string";
|
|
60
|
+
|
|
61
|
+
// ---------- server calls (one per command; none calls another) ----------
|
|
62
|
+
|
|
63
|
+
export async function createMailDraft(deps: MailDeps, body: MailDraftBody, api?: string | null): Promise<MailDraftView> {
|
|
64
|
+
try {
|
|
65
|
+
return checked(await openSession(deps, api).mailDraft(body), isDraft).draft;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw mailFailure(error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function listMailDrafts(deps: MailDeps, query: { status?: string; limit?: number } = {}, api?: string | null): Promise<MailDraftView[]> {
|
|
72
|
+
try {
|
|
73
|
+
return checked(await openSession(deps, api).mailDrafts(query), (a) => Array.isArray(a.drafts)).drafts;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw mailFailure(error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function getMailDraft(deps: MailDeps, id: string, api?: string | null): Promise<MailDraftView> {
|
|
80
|
+
try {
|
|
81
|
+
return checked(await openSession(deps, api).mailGet(id), isDraft).draft;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw mailFailure(error, id);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function editMailDraft(deps: MailDeps, id: string, patch: MailDraftPatch, api?: string | null): Promise<MailDraftView> {
|
|
88
|
+
try {
|
|
89
|
+
return checked(await openSession(deps, api).mailEdit(id, patch), isDraft).draft;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw mailFailure(error, id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Approves one draft. Sends nothing. Only the `approve` subcommand and the desk's Approve button reach this. */
|
|
96
|
+
export async function approveMailDraft(deps: MailDeps, id: string, api?: string | null): Promise<MailDraftView> {
|
|
97
|
+
try {
|
|
98
|
+
return checked(await openSession(deps, api).mailApprove(id), isDraft).draft;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw mailFailure(error, id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Sends one already-approved draft. Only the `send` subcommand and the desk's Send button reach this. */
|
|
105
|
+
export async function sendMailDraft(deps: MailDeps, id: string, api?: string | null): Promise<MailDraftView> {
|
|
106
|
+
try {
|
|
107
|
+
return checked(await openSession(deps, api).mailSend(id), isDraft).draft;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw mailFailure(error, id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function discardMailDraft(deps: MailDeps, id: string, api?: string | null): Promise<MailDraftView> {
|
|
114
|
+
try {
|
|
115
|
+
return checked(await openSession(deps, api).mailDiscard(id), isDraft).draft;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw mailFailure(error, id);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function getMailIdentity(deps: MailDeps, api?: string | null): Promise<MailIdentityView | null> {
|
|
122
|
+
try {
|
|
123
|
+
const answer = checked(await openSession(deps, api).mailIdentity(), (a) => "identity" in a);
|
|
124
|
+
return answer.identity;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw mailFailure(error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface IdentityInput {
|
|
131
|
+
fromName?: string | undefined;
|
|
132
|
+
replyTo?: string | undefined;
|
|
133
|
+
postalAddress?: string | undefined;
|
|
134
|
+
org?: string | undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Saves the identity. Fields not given keep what is already saved. A new reply-to is emailed a link and sends nothing until it is opened. */
|
|
138
|
+
export async function saveMailIdentity(deps: MailDeps, input: IdentityInput, api?: string | null): Promise<{ identity: MailIdentityView; verificationEmailed: boolean }> {
|
|
139
|
+
const client = openSession(deps, api);
|
|
140
|
+
try {
|
|
141
|
+
const existing = checked(await client.mailIdentity(), (a) => "identity" in a).identity;
|
|
142
|
+
const body = {
|
|
143
|
+
fromName: input.fromName ?? existing?.fromName ?? "",
|
|
144
|
+
replyTo: input.replyTo ?? existing?.replyTo ?? "",
|
|
145
|
+
postalAddress: input.postalAddress ?? existing?.postalAddress ?? "",
|
|
146
|
+
...(input.org !== undefined ? { senderOrg: input.org } : existing?.senderOrg ? { senderOrg: existing.senderOrg } : {}),
|
|
147
|
+
};
|
|
148
|
+
const missing = [!body.fromName && "--from-name", !body.replyTo && "--reply-to", !body.postalAddress && "--postal-address"].filter(Boolean);
|
|
149
|
+
if (missing.length) throw new ApiFailure(`Your sender identity needs ${missing.join(", ")}.`, 'run `npx vfrey mail identity --from-name "Ana" --reply-to you@example.com --postal-address "1 Main St, City, Country"`.', undefined, "bad_request");
|
|
150
|
+
return checked(await client.mailSaveIdentity(body as MailIdentityBody), (a) => typeof a.identity?.replyTo === "string");
|
|
151
|
+
} catch (error) {
|
|
152
|
+
throw mailFailure(error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------- formatting ----------
|
|
157
|
+
|
|
158
|
+
const clip = (t: string, n: number): string => (t.length > n ? `${t.slice(0, n - 1)}…` : t);
|
|
159
|
+
const when = (iso: string | null | undefined): string => {
|
|
160
|
+
const t = iso ? Date.parse(iso) : NaN;
|
|
161
|
+
return Number.isFinite(t) ? new Date(t).toISOString().slice(0, 16).replace("T", " ") + " UTC" : DASH;
|
|
162
|
+
};
|
|
163
|
+
const recipient = (d: MailDraftView): string => (d.toName ? `${clean(d.toName)} <${clean(d.to)}>` : clean(d.to)) || "(no recipient yet)";
|
|
164
|
+
|
|
165
|
+
export function statusLine(d: MailDraftView): string {
|
|
166
|
+
if (d.status === "sent") return `sent ${when(d.sentAt)}${d.deliveryStatus ? ` (${clean(d.deliveryStatus)})` : ""}`;
|
|
167
|
+
if (d.status === "approved") return `approved ${when(d.approvedAt)}, not sent yet`;
|
|
168
|
+
if (d.status === "failed") return `failed${d.error ? `: ${clean(d.error)}` : ""} (approve again to retry)`;
|
|
169
|
+
if (d.status === "discarded") return "discarded";
|
|
170
|
+
return "draft, not approved";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The next step for a draft in this status, as commands. Never says to run them for the person. */
|
|
174
|
+
export function nextSteps(d: MailDraftView): string[] {
|
|
175
|
+
const id = clean(d.id);
|
|
176
|
+
if (d.status === "draft" || d.status === "failed") return [`A person reads it, and only if they say so for this draft: \`npx vfrey mail approve --id ${id}\`, then \`npx vfrey mail send --id ${id}\`.`, `Approving sends nothing. Edit it in the dashboard (\`npx vfrey open\`) or drop it with \`npx vfrey mail discard --id ${id}\`.`];
|
|
177
|
+
if (d.status === "approved") return [`Send it, only if the person says so: \`npx vfrey mail send --id ${id}\`.`];
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function formatDraft(d: MailDraftView, head?: string): string {
|
|
182
|
+
const lines = [];
|
|
183
|
+
if (head) lines.push(head, "");
|
|
184
|
+
lines.push(`Id: ${clean(d.id)}`, `To: ${recipient(d)}`, `Subject: ${clean(d.subject)}`, `Kind: ${clean(d.kind)}${d.creatorRef ? ` · ${clean(d.creatorRef)}` : ""}`, `Status: ${statusLine(d)}`, "", d.text.replace(/\r/g, "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "").replace(/vfr_[A-Za-z0-9_-]+/g, "[token]").trimEnd(), "");
|
|
185
|
+
const next = nextSteps(d);
|
|
186
|
+
if (next.length) lines.push(...next, "");
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function formatDrafts(drafts: MailDraftView[], status?: string): string {
|
|
191
|
+
if (drafts.length === 0) return `No ${status ? `${status} ` : ""}drafts. Write one with \`npx vfrey mail draft --kind creator_collab --creator @handle --platform tiktok --need "..."\`.\n`;
|
|
192
|
+
const rows = drafts.map((d) => [clean(d.id), d.status, clip(recipient(d), 36), clip(clean(d.subject), 48), clean(d.kind), when(d.createdAt).slice(0, 10)]);
|
|
193
|
+
const head = ["Id", "Status", "To", "Subject", "Kind", "Created"];
|
|
194
|
+
const width = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i]!.length)));
|
|
195
|
+
const line = (r: string[]) => r.map((cell, i) => cell.padEnd(width[i]!)).join(" ").trimEnd();
|
|
196
|
+
return [`Drafts: ${drafts.length}`, "", line(head), ...rows.map(line), "", "Read one with `npx vfrey mail show --id ID`. Nothing here is sent until a person approves and sends that draft.", ""].join("\n");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function formatIdentity(identity: MailIdentityView | null, emailed?: boolean): string {
|
|
200
|
+
if (!identity) return 'No sender identity yet. Every message carries your name and postal address, so set it first:\n npx vfrey mail identity --from-name "Ana" --reply-to you@example.com --postal-address "1 Main St, City, Country" [--org "Ana Co"]\n';
|
|
201
|
+
const lines = [
|
|
202
|
+
`From name: ${clean(identity.fromName)}${identity.senderOrg ? ` (${clean(identity.senderOrg)})` : ""}`,
|
|
203
|
+
`Reply-to: ${clean(identity.replyTo)}`,
|
|
204
|
+
`Postal address: ${clean(identity.postalAddress)}`,
|
|
205
|
+
`Verified: ${identity.verified ? `yes, ${when(identity.verifiedAt)}` : "NO. Nothing can be sent until the reply-to address is confirmed."}`,
|
|
206
|
+
];
|
|
207
|
+
if (!identity.verified) lines.push("", emailed ? `We emailed a confirmation link to ${clean(identity.replyTo)}. Open it (it works for 24 hours), then check with \`npx vfrey mail identity\`.` : `Open the link we emailed to ${clean(identity.replyTo)}. Lost it? Run \`npx vfrey mail identity\` with the same flags to send a new one.`);
|
|
208
|
+
return lines.join("\n") + "\n";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------- the command ----------
|
|
212
|
+
|
|
213
|
+
const USAGE = [
|
|
214
|
+
"usage: npx vfrey mail draft --kind sponsor|brand|creator_collab|client_update --need TEXT [--creator @handle --platform P] [--to EMAIL] [--to-name N] [--tone T] [--json]",
|
|
215
|
+
" npx vfrey mail drafts [--status draft|approved|sent|failed|discarded] [--json]",
|
|
216
|
+
" npx vfrey mail show --id ID [--json]",
|
|
217
|
+
" npx vfrey mail approve --id ID (a person; sends nothing)",
|
|
218
|
+
" npx vfrey mail send --id ID (a person; the draft must be approved)",
|
|
219
|
+
" npx vfrey mail discard --id ID",
|
|
220
|
+
" npx vfrey mail identity [--from-name N --reply-to EMAIL --postal-address A --org NAME] [--json].",
|
|
221
|
+
].join("\n");
|
|
222
|
+
|
|
223
|
+
function refuse(io: ApiIo, message: string, fix: string): number {
|
|
224
|
+
io.stderr(`✗ ${message}\nFix: ${fix}\n`);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function failure(io: ApiIo, error: unknown): number {
|
|
229
|
+
if (error instanceof ApiFailure || error instanceof ApiConfigError) return refuse(io, error.message, error.fix);
|
|
230
|
+
return refuse(io, error instanceof Error ? clean(error.message) : "Something went wrong.", "run `npx vfrey doctor` and check the line above.");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function parseArgs(args: string[], boolFlags: string[], valueFlags: string[]): { bools: Set<string>; values: Record<string, string>; positionals: string[]; error?: string } {
|
|
234
|
+
const bools = new Set<string>();
|
|
235
|
+
const values: Record<string, string> = {};
|
|
236
|
+
const positionals: string[] = [];
|
|
237
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
238
|
+
const arg = args[i]!;
|
|
239
|
+
if (!arg.startsWith("--")) {
|
|
240
|
+
positionals.push(arg);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const eq = arg.indexOf("=");
|
|
244
|
+
const name = eq > 0 ? arg.slice(0, eq) : arg;
|
|
245
|
+
if (boolFlags.includes(name)) bools.add(name);
|
|
246
|
+
else if (valueFlags.includes(name)) {
|
|
247
|
+
const next = args[i + 1];
|
|
248
|
+
const value = eq > 0 ? arg.slice(eq + 1) : next !== undefined && !next.startsWith("--") ? args[(i += 1)]! : "";
|
|
249
|
+
if (!value.trim()) return { bools, values, positionals, error: `${name} needs a value.` };
|
|
250
|
+
values[name] = value;
|
|
251
|
+
} else return { bools, values, positionals, error: `Unknown option "${arg}".` };
|
|
252
|
+
}
|
|
253
|
+
return { bools, values, positionals };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const json = (v: unknown): string => JSON.stringify(v, null, 2) + "\n";
|
|
257
|
+
|
|
258
|
+
export async function runMail(args: string[], io: ApiIo): Promise<number> {
|
|
259
|
+
const sub = args[0];
|
|
260
|
+
const rest = args.slice(1);
|
|
261
|
+
const deps: MailDeps = { 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 } : {}) };
|
|
262
|
+
try {
|
|
263
|
+
if (sub === "draft") {
|
|
264
|
+
const p = parseArgs(rest, ["--json"], ["--kind", "--creator", "--platform", "--to", "--to-name", "--need", "--tone", "--api"]);
|
|
265
|
+
if (p.error || p.positionals.length) return refuse(io, p.error ?? `Unexpected "${p.positionals[0]}".`, USAGE);
|
|
266
|
+
const kind = p.values["--kind"]?.trim().toLowerCase();
|
|
267
|
+
if (!kind || !(MAIL_KINDS as readonly string[]).includes(kind)) return refuse(io, kind ? `--kind must be one of ${MAIL_KINDS.join(", ")}.` : "Say what kind of message: --kind sponsor, brand, creator_collab or client_update.", USAGE);
|
|
268
|
+
const need = p.values["--need"]?.trim();
|
|
269
|
+
if (!need) return refuse(io, "Say what the message should ask or say: --need TEXT.", 'for example `npx vfrey mail draft --kind creator_collab --creator @ana.cooks --platform tiktok --need "a paid 30 second demo of our app"`.');
|
|
270
|
+
const body: MailDraftBody = { kind, need };
|
|
271
|
+
const creator = p.values["--creator"];
|
|
272
|
+
const platform = p.values["--platform"]?.trim().toLowerCase();
|
|
273
|
+
if (creator) {
|
|
274
|
+
const handle = normalizeHandle(creator);
|
|
275
|
+
if (!platform || !(CREATOR_PLATFORM_IDS as readonly string[]).includes(platform)) return refuse(io, "Say which platform the creator is on: --platform tiktok, instagram or youtube.", USAGE);
|
|
276
|
+
if (!/^[a-z0-9._-]{1,80}$/i.test(handle)) return refuse(io, "That creator handle does not look right.", "use letters, numbers, dots, dashes and underscores, like @ana.cooks.");
|
|
277
|
+
body.creator = { platform, handle };
|
|
278
|
+
} else if (platform) return refuse(io, "--platform goes with --creator.", USAGE);
|
|
279
|
+
if (p.values["--to"]) body.to = p.values["--to"].trim();
|
|
280
|
+
if (p.values["--to-name"]) body.toName = p.values["--to-name"].trim();
|
|
281
|
+
if (p.values["--tone"]) body.tone = p.values["--tone"].trim();
|
|
282
|
+
const draft = await createMailDraft(deps, body, p.values["--api"]);
|
|
283
|
+
io.stdout(p.bools.has("--json") ? json({ draft }) : formatDraft(draft, `✓ Draft written. Nothing was sent, and nothing is sent until a person approves and sends it.`));
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
|
286
|
+
if (sub === "drafts") {
|
|
287
|
+
const p = parseArgs(rest, ["--json"], ["--status", "--api"]);
|
|
288
|
+
if (p.error || p.positionals.length) return refuse(io, p.error ?? `Unexpected "${p.positionals[0]}".`, USAGE);
|
|
289
|
+
const status = p.values["--status"]?.trim().toLowerCase();
|
|
290
|
+
if (status && !(MAIL_STATUSES as readonly string[]).includes(status)) return refuse(io, `--status must be one of ${MAIL_STATUSES.join(", ")}.`, USAGE);
|
|
291
|
+
const drafts = await listMailDrafts(deps, status ? { status } : {}, p.values["--api"]);
|
|
292
|
+
io.stdout(p.bools.has("--json") ? json({ drafts }) : formatDrafts(drafts, status));
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
if (sub === "show" || sub === "approve" || sub === "send" || sub === "discard") {
|
|
296
|
+
const p = parseArgs(rest, ["--json"], ["--id", "--api"]);
|
|
297
|
+
if (p.error || p.positionals.length) return refuse(io, p.error ?? `Unexpected "${p.positionals[0]}".`, USAGE);
|
|
298
|
+
const id = p.values["--id"]?.trim();
|
|
299
|
+
if (!id) return refuse(io, `Say which draft: mail ${sub} --id ID.`, "list your drafts with `npx vfrey mail drafts`.");
|
|
300
|
+
const want = p.bools.has("--json");
|
|
301
|
+
if (sub === "show") {
|
|
302
|
+
const draft = await getMailDraft(deps, id, p.values["--api"]);
|
|
303
|
+
io.stdout(want ? json({ draft }) : formatDraft(draft));
|
|
304
|
+
} else if (sub === "approve") {
|
|
305
|
+
const draft = await approveMailDraft(deps, id, p.values["--api"]);
|
|
306
|
+
io.stdout(want ? json({ draft }) : `✓ Approved ${clean(draft.id)} for ${recipient(draft)}. Nothing was sent.\nSend it, only if the person says so: \`npx vfrey mail send --id ${clean(draft.id)}\`.\n`);
|
|
307
|
+
} else if (sub === "send") {
|
|
308
|
+
const draft = await sendMailDraft(deps, id, p.values["--api"]);
|
|
309
|
+
io.stdout(want ? json({ draft }) : `✓ Sent ${clean(draft.id)} to ${recipient(draft)} at ${when(draft.sentAt)}.\nOne send was charged. Replies go to your reply-to address. See it with \`npx vfrey mail show --id ${clean(draft.id)}\`.\n`);
|
|
310
|
+
} else {
|
|
311
|
+
const draft = await discardMailDraft(deps, id, p.values["--api"]);
|
|
312
|
+
io.stdout(want ? json({ draft }) : `✓ Discarded ${clean(draft.id)}. It will not be sent.\n`);
|
|
313
|
+
}
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
if (sub === "identity") {
|
|
317
|
+
const p = parseArgs(rest, ["--json"], ["--from-name", "--reply-to", "--postal-address", "--org", "--api"]);
|
|
318
|
+
if (p.error || p.positionals.length) return refuse(io, p.error ?? `Unexpected "${p.positionals[0]}".`, USAGE);
|
|
319
|
+
const changing = ["--from-name", "--reply-to", "--postal-address", "--org"].some((f) => p.values[f] !== undefined);
|
|
320
|
+
if (!changing) {
|
|
321
|
+
const identity = await getMailIdentity(deps, p.values["--api"]);
|
|
322
|
+
io.stdout(p.bools.has("--json") ? json({ identity }) : formatIdentity(identity));
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
const saved = await saveMailIdentity(deps, { fromName: p.values["--from-name"], replyTo: p.values["--reply-to"]?.toLowerCase(), postalAddress: p.values["--postal-address"], org: p.values["--org"] }, p.values["--api"]);
|
|
326
|
+
io.stdout(p.bools.has("--json") ? json(saved) : `✓ Saved your sender identity.\n\n${formatIdentity(saved.identity, saved.verificationEmailed)}`);
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
329
|
+
return refuse(io, sub ? `Unknown mail command "${clean(sub)}".` : "Say what to do with mail.", USAGE);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return failure(io, error);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// vfrey Topics (spec 016): `vfrey topics`, `topics niches`, `topics request`, and the one session helper the series source and the desk route share.
|
|
2
|
+
// Every call goes through ApiClient; the token is read from the token store here and never leaves this process.
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { ApiClient, ApiFailure, clean, type FetchLike } from "./client.js";
|
|
5
|
+
import { ApiConfigError, apiHost, resolveApiUrl } from "./config.js";
|
|
6
|
+
import { loadToken, type CredentialDeps, type Runner } from "./credentials.js";
|
|
7
|
+
import type { ApiIo } from "./commands.js";
|
|
8
|
+
import type { NicheView, TopicsResponse } from "./types.js";
|
|
9
|
+
import type { Topic, TopicEvidence } from "@vfrey/contracts";
|
|
10
|
+
|
|
11
|
+
type Env = Record<string, string | undefined>;
|
|
12
|
+
|
|
13
|
+
/** What a topics call needs: where the server is and how to find the token. */
|
|
14
|
+
export interface TopicsDeps {
|
|
15
|
+
env: Env;
|
|
16
|
+
homedir?: string;
|
|
17
|
+
platform?: NodeJS.Platform;
|
|
18
|
+
run?: Runner;
|
|
19
|
+
fetch?: FetchLike;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function credsOf(deps: TopicsDeps): CredentialDeps {
|
|
24
|
+
return { env: deps.env, homedir: deps.homedir ?? os.homedir(), ...(deps.platform ? { platform: deps.platform } : {}), ...(deps.run ? { run: deps.run } : {}) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A client for the signed-in person. Throws ApiFailure (not live, not signed in) or ApiConfigError. */
|
|
28
|
+
export function openSession(deps: TopicsDeps, api?: string | null): ApiClient {
|
|
29
|
+
const base = resolveApiUrl(deps.env, api ?? null);
|
|
30
|
+
if (!base) {
|
|
31
|
+
throw new ApiFailure("vfrey accounts are not live yet.", "free features work without an account. If you run your own vfrey server, set VFREY_API_URL or pass --api <url>.", undefined, "not_live");
|
|
32
|
+
}
|
|
33
|
+
const found = loadToken(apiHost(base), credsOf(deps));
|
|
34
|
+
if (!found) throw new ApiFailure("You are not signed in.", "run `npx vfrey login`.", undefined, "not_signed_in");
|
|
35
|
+
return new ApiClient({ baseUrl: base, token: found.token, ...(deps.fetch ? { fetch: deps.fetch } : {}), ...(deps.timeoutMs ? { timeoutMs: deps.timeoutMs } : {}) });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Input mistakes the server names (400, 404, 409) get a Fix that fits them. */
|
|
39
|
+
export function withInputFix(error: unknown, fix: string): unknown {
|
|
40
|
+
if (error instanceof ApiFailure && (error.status === 400 || error.status === 404 || error.status === 409)) {
|
|
41
|
+
return new ApiFailure(error.message, fix, error.status, error.code);
|
|
42
|
+
}
|
|
43
|
+
return error;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function fetchTopics(deps: TopicsDeps, query: { niche: string; limit?: number; since?: string }, api?: string | null): Promise<TopicsResponse> {
|
|
47
|
+
const client = openSession(deps, api);
|
|
48
|
+
try {
|
|
49
|
+
const answer = await client.topics(query);
|
|
50
|
+
if (!answer || !Array.isArray(answer.topics)) {
|
|
51
|
+
throw new ApiFailure("The vfrey server sent an answer this version does not understand.", "update vfrey with `npx vfrey update`.");
|
|
52
|
+
}
|
|
53
|
+
return answer;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw withInputFix(error, "list the niche ids with `npx vfrey topics niches`.");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function fetchNiches(deps: TopicsDeps, api?: string | null): Promise<NicheView[]> {
|
|
60
|
+
const answer = await openSession(deps, api).niches();
|
|
61
|
+
if (!answer || !Array.isArray(answer.niches)) {
|
|
62
|
+
throw new ApiFailure("The vfrey server sent an answer this version does not understand.", "update vfrey with `npx vfrey update`.");
|
|
63
|
+
}
|
|
64
|
+
return answer.niches;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------- formatting ----------
|
|
68
|
+
|
|
69
|
+
export function formatViews(views: number | undefined | null): string {
|
|
70
|
+
if (typeof views !== "number" || !Number.isFinite(views)) return "—";
|
|
71
|
+
if (views >= 1_000_000) return `${(views / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
|
|
72
|
+
if (views >= 1_000) return `${(views / 1_000).toFixed(1).replace(/\.0$/, "")}K`;
|
|
73
|
+
return String(Math.round(views));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function formatAge(hours: number | undefined | null): string {
|
|
77
|
+
if (typeof hours !== "number" || !Number.isFinite(hours) || hours < 0) return "—";
|
|
78
|
+
if (hours < 1) return "<1h";
|
|
79
|
+
if (hours < 48) return `${Math.round(hours)}h`;
|
|
80
|
+
return `${Math.round(hours / 24)}d`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const httpUrl = (url: unknown): string => {
|
|
84
|
+
const text = clean(url);
|
|
85
|
+
return /^https?:\/\//i.test(text) ? text : "";
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function evidenceLine(item: TopicEvidence): string {
|
|
89
|
+
const url = httpUrl(item.url) || "(no link)";
|
|
90
|
+
return `${url} ${formatViews(item.views)} views · ${formatAge(item.ageHours)} old${item.why ? ` · ${clean(item.why)}` : ""}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function formatTopics(answer: TopicsResponse, nicheId: string): string {
|
|
94
|
+
const name = answer.niche ? clean(answer.niche.name) : clean(nicheId);
|
|
95
|
+
const lines = [`Topics for ${name} (${clean(answer.niche?.id ?? nicheId)}): ${answer.topics.length}`];
|
|
96
|
+
if (answer.topics.length === 0) lines.push("No topics yet. The niche is collected on a schedule; try again later.");
|
|
97
|
+
answer.topics.forEach((topic: Topic, index) => {
|
|
98
|
+
lines.push("", `${String(index + 1).padStart(2)}. ${clean(topic.title)} score ${Number(topic.score).toFixed(1)} ${(topic.platforms ?? []).map((p) => clean(p)).join(", ")}`);
|
|
99
|
+
for (const item of topic.evidence ?? []) lines.push(` ${evidenceLine(item)}`);
|
|
100
|
+
});
|
|
101
|
+
const run = answer.lastRun;
|
|
102
|
+
if (run && !/^(ok|success|succeeded|done)$/i.test(run.status)) lines.push("", `Note: the last collection was ${clean(run.status)} (${clean(run.at)}); these are the last stored topics.`);
|
|
103
|
+
else if (answer.lastCollectedAt) lines.push("", `Collected ${clean(answer.lastCollectedAt)}.`);
|
|
104
|
+
lines.push("Topics use credits. See your balance with `npx vfrey whoami`.");
|
|
105
|
+
return lines.join("\n") + "\n";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function formatNiches(niches: NicheView[]): string {
|
|
109
|
+
if (niches.length === 0) return "No niches yet.\n";
|
|
110
|
+
const width = Math.max(...niches.map((n) => clean(n.id).length));
|
|
111
|
+
const lines = niches.map((n) => `${clean(n.id).padEnd(width)} ${clean(n.name)}${n.custom ? " (yours)" : ""} ${(n.platforms ?? []).map((p) => clean(p)).join(", ")} [${(n.keywords ?? []).map((k) => clean(k)).join(", ")}]`);
|
|
112
|
+
return [...lines, "", "Read one with `npx vfrey topics --niche ID`. Listing niches is free."].join("\n") + "\n";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------- the command ----------
|
|
116
|
+
|
|
117
|
+
const USAGE = "usage: npx vfrey topics [--niche ID] [--limit N] [--since ISO] [--json] | topics niches [--json] | topics request --name N --keywords a,b [--hashtags a,b] [--platforms tiktok,youtube].";
|
|
118
|
+
|
|
119
|
+
function refuse(io: ApiIo, message: string, fix: string): number {
|
|
120
|
+
io.stderr(`✗ ${message}\nFix: ${fix}\n`);
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function failure(io: ApiIo, error: unknown): number {
|
|
125
|
+
if (error instanceof ApiFailure || error instanceof ApiConfigError) return refuse(io, error.message, error.fix);
|
|
126
|
+
return refuse(io, error instanceof Error ? clean(error.message) : "Something went wrong.", "run `npx vfrey doctor` and check the line above.");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Reads `--name value` and `--name=value` flags. Unknown flags are an error. */
|
|
130
|
+
function parseFlags(args: string[], boolFlags: string[], valueFlags: string[]): { bools: Set<string>; values: Record<string, string>; error?: string } {
|
|
131
|
+
const bools = new Set<string>();
|
|
132
|
+
const values: Record<string, string> = {};
|
|
133
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
134
|
+
const arg = args[i]!;
|
|
135
|
+
const eq = arg.indexOf("=");
|
|
136
|
+
const name = eq > 0 ? arg.slice(0, eq) : arg;
|
|
137
|
+
if (boolFlags.includes(name)) bools.add(name);
|
|
138
|
+
else if (valueFlags.includes(name)) {
|
|
139
|
+
const value = eq > 0 ? arg.slice(eq + 1) : args[i + 1] && !args[i + 1]!.startsWith("--") ? args[(i += 1)]! : "";
|
|
140
|
+
if (!value) return { bools, values, error: `${name} needs a value.` };
|
|
141
|
+
values[name] = value;
|
|
142
|
+
} else return { bools, values, error: `Unknown option "${arg}".` };
|
|
143
|
+
}
|
|
144
|
+
return { bools, values };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const list = (text: string): string[] => text.split(",").map((part) => part.trim()).filter(Boolean);
|
|
148
|
+
|
|
149
|
+
export async function runTopics(args: string[], io: ApiIo): Promise<number> {
|
|
150
|
+
const sub = args[0] === "niches" || args[0] === "request" ? args[0] : null;
|
|
151
|
+
const rest = sub ? args.slice(1) : args;
|
|
152
|
+
const deps: TopicsDeps = { 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 } : {}) };
|
|
153
|
+
try {
|
|
154
|
+
if (sub === "niches") {
|
|
155
|
+
const p = parseFlags(rest, ["--json"], ["--api"]);
|
|
156
|
+
if (p.error) return refuse(io, p.error, USAGE);
|
|
157
|
+
const niches = await fetchNiches(deps, p.values["--api"]);
|
|
158
|
+
io.stdout(p.bools.has("--json") ? JSON.stringify({ niches }, null, 2) + "\n" : formatNiches(niches));
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
if (sub === "request") {
|
|
162
|
+
const p = parseFlags(rest, ["--json"], ["--name", "--keywords", "--hashtags", "--platforms", "--api"]);
|
|
163
|
+
if (p.error) return refuse(io, p.error, USAGE);
|
|
164
|
+
const name = p.values["--name"]?.trim();
|
|
165
|
+
const keywords = list(p.values["--keywords"] ?? "");
|
|
166
|
+
if (!name || keywords.length === 0) return refuse(io, "A niche request needs --name and --keywords.", USAGE);
|
|
167
|
+
const body: { name: string; keywords: string[]; hashtags?: string[]; platforms?: never } = { name, keywords };
|
|
168
|
+
if (p.values["--hashtags"]) body.hashtags = list(p.values["--hashtags"]);
|
|
169
|
+
const client = openSession(deps, p.values["--api"]);
|
|
170
|
+
let answer;
|
|
171
|
+
try {
|
|
172
|
+
answer = await client.requestNiche({ ...body, ...(p.values["--platforms"] ? { platforms: list(p.values["--platforms"]) as never } : {}) });
|
|
173
|
+
} catch (error) {
|
|
174
|
+
throw withInputFix(error, "check the name, keywords and platforms, or list your niches with `npx vfrey topics niches`.");
|
|
175
|
+
}
|
|
176
|
+
if (p.bools.has("--json")) io.stdout(JSON.stringify(answer, null, 2) + "\n");
|
|
177
|
+
else {
|
|
178
|
+
const cost = typeof answer.creditsCharged === "number" ? ` It cost ${answer.creditsCharged} credits.` : "";
|
|
179
|
+
io.stdout(`✓ Requested niche "${clean(answer.niche.name)}" (${clean(answer.niche.id)}).${cost} It is collected on the next run.\nRead it later with \`npx vfrey topics --niche ${clean(answer.niche.id)}\`. See your credits with \`npx vfrey whoami\`.\n`);
|
|
180
|
+
}
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
const p = parseFlags(rest, ["--json"], ["--niche", "--limit", "--since", "--api"]);
|
|
184
|
+
if (p.error) return refuse(io, p.error, USAGE);
|
|
185
|
+
const query: { niche: string; limit?: number; since?: string } = { niche: "" };
|
|
186
|
+
if (p.values["--limit"] !== undefined) {
|
|
187
|
+
const limit = Number(p.values["--limit"]);
|
|
188
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) return refuse(io, "--limit must be a whole number from 1 to 100.", USAGE);
|
|
189
|
+
query.limit = limit;
|
|
190
|
+
}
|
|
191
|
+
if (p.values["--since"] !== undefined) {
|
|
192
|
+
if (Number.isNaN(new Date(p.values["--since"]).getTime())) return refuse(io, "--since must be an ISO date, like 2026-10-26T00:00:00Z.", USAGE);
|
|
193
|
+
query.since = p.values["--since"];
|
|
194
|
+
}
|
|
195
|
+
const nicheId = p.values["--niche"]?.trim();
|
|
196
|
+
if (!nicheId) {
|
|
197
|
+
const niches = await fetchNiches(deps, p.values["--api"]);
|
|
198
|
+
return refuse(io, "Pick a niche to read topics for.", `run \`npx vfrey topics --niche ID\`. Niches: ${niches.slice(0, 12).map((n) => clean(n.id)).join(", ") || "none yet"}.`);
|
|
199
|
+
}
|
|
200
|
+
query.niche = nicheId;
|
|
201
|
+
const answer = await fetchTopics(deps, query, p.values["--api"]);
|
|
202
|
+
io.stdout(p.bools.has("--json") ? JSON.stringify(answer, null, 2) + "\n" : formatTopics(answer, nicheId));
|
|
203
|
+
return 0;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return failure(io, error);
|
|
206
|
+
}
|
|
207
|
+
}
|