@officexapp/vidfarm-devcli 0.21.43 → 0.21.46
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/.agents/skills/editor-capabilities/SKILL.md +4 -0
- package/.agents/skills/vidfarm/SKILL.md +95 -17
- package/.agents/skills/vidfarm/harnesses/explainer.HARNESS.md +1 -1
- package/.agents/skills/vidfarm/harnesses/product-demo.HARNESS.md +2 -0
- package/.agents/skills/vidfarm/harnesses/short-form.HARNESS.md +1 -0
- package/.agents/skills/vidfarm/recipes/local-edit-render-approve.md +1 -1
- package/.agents/skills/vidfarm/recipes/onboard-a-new-director.md +1 -1
- package/.agents/skills/vidfarm/references/agent-included-imagegen.md +75 -0
- package/.agents/skills/vidfarm/references/assets-and-sourcing.md +152 -2
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +22 -9
- package/.agents/skills/vidfarm/references/browser-harness.md +93 -0
- package/.agents/skills/vidfarm/references/content-ideas.md +232 -10
- package/.agents/skills/vidfarm/references/core-workflows.md +11 -1
- package/.agents/skills/vidfarm/references/editor-workflows.md +39 -0
- package/.agents/skills/vidfarm/references/onboarding.md +1 -1
- package/.agents/skills/vidfarm/references/primitives.md +51 -0
- package/.agents/skills/vidfarm-media/SKILL.md +2 -0
- package/SKILL.director.md +775 -42
- package/SKILL.md +157 -115
- package/crowdsourcing.md +417 -3
- package/dist/src/cli.js +750 -34
- package/dist/src/devcli/agent-imagegen.js +181 -0
- package/dist/src/devcli/browser-harness.js +384 -0
- package/dist/src/devcli/clip-store.js +41 -3
- package/dist/src/devcli/consult.js +14 -0
- package/dist/src/devcli/cost-mode.js +23 -3
- package/dist/src/devcli/doctor.js +52 -3
- package/dist/src/devcli/hyperframes-cli.js +11 -1
- package/dist/src/devcli/local-render.js +4 -7
- package/dist/src/devcli/marketplace-gigs.js +623 -0
- package/dist/src/devcli/qa-check.js +89 -1
- package/dist/src/devcli/shared-folder.js +387 -0
- package/dist/src/devcli/skill-docs.js +61 -7
- package/dist/src/devcli/stills.js +4 -8
- package/dist/src/lib/ffprobe-path.js +64 -0
- package/dist/src/lib/render-media-prep.js +2 -11
- package/dist/src/services/clip-curation/ffmpeg.js +4 -15
- package/dist/src/services/clip-curation/index.js +1 -1
- package/dist/src/services/clip-curation/local-agent.js +6 -2
- package/dist/src/services/clip-curation/media-select.js +146 -3
- package/experimental/google-news-to-video.md +235 -0
- package/package.json +8 -150
- package/public/assets/file-directory-app.js +35 -35
- package/public/assets/homepage-client-app.js +15 -15
- package/public/serve-shells/library-files.html +5 -1
- package/public/serve-shells/library-raws.html +10 -1
- package/public/serve-shells/tools-clipper.html +5 -1
- package/public/serve-shells/tools-image.html +5 -1
- package/public/serve-shells/tools-video.html +5 -1
package/dist/src/cli.js
CHANGED
|
@@ -29,14 +29,17 @@ import { runDoctorCommand } from "./devcli/doctor.js";
|
|
|
29
29
|
import { findFreePort } from "./devcli/port-utils.js";
|
|
30
30
|
import { scanLocalServers } from "./devcli/process-scan.js";
|
|
31
31
|
import { runSkillsCommand } from "./devcli/skills.js";
|
|
32
|
-
import { AmbiguousDocRef, DEFAULT_PACK, bundledPackDir, listPackDocs, listPackTopics, loadIdeaBank, readPackDoc, readPackTopic, resolvePackTopic, searchPackDocs } from "./devcli/skill-docs.js";
|
|
32
|
+
import { AmbiguousDocRef, DEFAULT_PACK, bundledPackDir, listPackDocs, listPackTopics, loadAngleBank, loadAwarenessLadder, loadIdeaBank, readPackDoc, readPackTopic, resolvePackTopic, searchPackDocs } from "./devcli/skill-docs.js";
|
|
33
33
|
import { initTelemetry, reportCliCrash } from "./devcli/telemetry.js";
|
|
34
34
|
import { resolveLocalDataDir, localBackendAvailable, LocalModeUnavailableError, localApiRequest } from "./devcli/local-backend.js";
|
|
35
|
+
import { NativeModuleUnavailableError } from "./devcli/clip-store.js";
|
|
35
36
|
import { startLocalFrontendServer, serveShellsPresent } from "./devcli/local-frontend-server.js";
|
|
36
37
|
import { readStoredAuth, writeStoredAuth, clearStoredAuth, hostsMatch } from "./devcli/auth-store.js";
|
|
37
38
|
import { CostModeBlockedError, assertBilledAllowed, clearStoredCostMode, costModeExplainer, costModeSummaryLine, normalizeCostMode, resolveCostMode, writeStoredCostMode, COST_MODE_BLURB, COST_MODE_DISPLAY_LIST, costModeDisplayName } from "./devcli/cost-mode.js";
|
|
38
39
|
import { INTERACTION_MODES, INTERACTION_MODE_BLURB, clearStoredInteractionMode, interactionModeExplainer, interactionModeSummaryLine, interactiveOfferLine, normalizeInteractionMode, resolveInteractionMode, writeStoredInteractionMode } from "./devcli/interaction-mode.js";
|
|
39
40
|
import { buildImageHandoff, buildRawsHandoff, formatHandoff } from "./devcli/handoff.js";
|
|
41
|
+
import { AGENT_IMAGEGEN_ALTERNATIVE, KNOWN_HOST_AGENTS, buildAgentImageBrief, detectHostAgent, formatAgentImageBrief } from "./devcli/agent-imagegen.js";
|
|
42
|
+
import { runBrowserHarnessCommand, runBrowseCommand } from "./devcli/browser-harness.js";
|
|
40
43
|
import { buildConsultBrief, CONSULT_ARTIFACTS, consultContextState, consultCountRange, defaultConsultCount, findOfferFile, formatConsultBrief, looksLikeOfferPath, planConsultation, resolveConsultStep, resolveOfferPath } from "./devcli/consult.js";
|
|
41
44
|
import { resolveRootFileCandidates } from "./lib/package-root.js";
|
|
42
45
|
import { STORYBOARD_FILENAME, readStoryboard, renderStoryboardScaffold, storyboardStage } from "./devcli/storyboard.js";
|
|
@@ -121,7 +124,31 @@ Account (persisted login — points the CLI + \`serve\` at cloud prod):
|
|
|
121
124
|
A manual clip-sourcing brief: what to search, how to
|
|
122
125
|
download (a downloader SEARCH, not a rotting link), and
|
|
123
126
|
the vidfarm clipper / raws scan follow-up. Try browser control
|
|
124
|
-
or "vidfarm raws scan" first.
|
|
127
|
+
("vidfarm browse", below) or "vidfarm raws scan" first.
|
|
128
|
+
agent-image "<subject>" [--items "a,b,c"] [--single] [--out ./sheet.png]
|
|
129
|
+
FREE CUSTOM ART — image generation YOUR OWN agent
|
|
130
|
+
subscription already includes. Antigravity / Gemini CLI
|
|
131
|
+
(Nano Banana / Imagen) and Codex / ChatGPT (gpt-image)
|
|
132
|
+
include one; Claude Code does NOT (Anthropic ships no
|
|
133
|
+
image model). Prints the prompt, the key-plate rules and
|
|
134
|
+
the cut + import commands. Vidfarm bills nothing and the
|
|
135
|
+
provider bills nothing extra, so it is allowed in EVERY
|
|
136
|
+
cost mode — minimize included.
|
|
137
|
+
--check (or no args) = is it included here? --json
|
|
138
|
+
(aliases: agent-imagegen, own-image, native-image)
|
|
139
|
+
browser [status|setup|doctor] FREE BROWSER SOURCING — set up browser-harness so I can
|
|
140
|
+
drive YOUR Chrome myself. It substitutes for the PAID
|
|
141
|
+
video-search / image-search / news-search / download-video
|
|
142
|
+
primitives at $0, on your own logged-in session and IP.
|
|
143
|
+
setup = install via uv + register the skill + the one
|
|
144
|
+
Chrome step (chrome://inspect) you must do once.
|
|
145
|
+
status = what's installed · doctor = connection check.
|
|
146
|
+
Suggest this to every free-plan user before saying "I can't".
|
|
147
|
+
browse <videos|images|news|page> "<query|url>"
|
|
148
|
+
Mint the browse recipe for one sourcing job: the Google
|
|
149
|
+
surface to open, what to pull off the page, and the vidfarm
|
|
150
|
+
command it feeds. --run pipes it straight into browser-harness,
|
|
151
|
+
--json for an agent, --legacy for the old tbm=… params.
|
|
125
152
|
consult [step] THE CONSULTATION / BRAINSTORM CHAIN, KEYLESS AND LOCAL.
|
|
126
153
|
The cloud runs these as brainstorm/* primitives that need
|
|
127
154
|
a provider key or wallet credits. This prints the IDENTICAL
|
|
@@ -131,6 +158,9 @@ Account (persisted login — points the CLI + \`serve\` at cloud prod):
|
|
|
131
158
|
Every step reads and updates CONTEXT.md in --dir, the
|
|
132
159
|
durable answer file, so a later session starts warm.
|
|
133
160
|
Run the whole chain from ONE working folder.
|
|
161
|
+
OFFER "vidfarm ideas" FIRST — 20+ video ideas for one
|
|
162
|
+
line of input is an easier first win than an interview,
|
|
163
|
+
and the picks make the interview answers better.
|
|
134
164
|
(aliases: consultation, brainstorm)
|
|
135
165
|
coldstart [--short] The interview → OFFER.md. Ask the human the questions a
|
|
136
166
|
few at a time; never invent their answers. --short is a
|
|
@@ -727,6 +757,31 @@ Speech (TTS/STT) — LOCAL-FIRST on your own AI key; --cloud is the explicit bac
|
|
|
727
757
|
key adds photos/vectors/stock video.
|
|
728
758
|
--provider <p> Force pixabay|openverse|iconify|iconscout
|
|
729
759
|
--limit <n> | --json
|
|
760
|
+
video-search "<query>" GOOGLE VIDEO SEARCH — find the actual SHOT anywhere on the web
|
|
761
|
+
(TikTok, YouTube, Pexels/Pixabay/Mixkit, news, archive.org).
|
|
762
|
+
This is how you SOURCE raws: take a result url straight into
|
|
763
|
+
'vidfarm raws scan <url>' (mine clips) or 'download-video <url>'
|
|
764
|
+
(collect the file). Search the SHOT, not the topic:
|
|
765
|
+
[subject]+[action]+[shot/style]+[quality/orientation], e.g.
|
|
766
|
+
"office worker typing laptop close up 4K". Google operators work:
|
|
767
|
+
site:pexels.com/videos · site:tiktok.com · "exact phrase" · -dance
|
|
768
|
+
PAID PLANS ONLY · flat $0.0003 per call whatever the result count,
|
|
769
|
+
so ask for a WIDE page instead of paging twice.
|
|
770
|
+
--limit <n> Results (default 25, max 50)
|
|
771
|
+
--fresh <d|w|m|y> Only the last day/week/month/year
|
|
772
|
+
--duration <d> short|medium|long --resolution high|standard
|
|
773
|
+
--region <r> wt-wt (default)|us-en|ph-en|… --json
|
|
774
|
+
image-search "<query>" GOOGLE IMAGE SEARCH — reference stills, textures, logos, product
|
|
775
|
+
shots. Same operators. PAID PLANS ONLY · flat $0.0003 per call.
|
|
776
|
+
For licence-checked, royalty-free assets prefer 'vidfarm media'
|
|
777
|
+
(free): image-search returns LINKS with no licence attached.
|
|
778
|
+
--limit <n> (default 25, max 100) --type-image photo|clipart|gif|transparent
|
|
779
|
+
--color <c> --size <s> --layout Square|Tall|Wide --region <r> --json
|
|
780
|
+
news-search "<query>" GOOGLE NEWS — recent real events to build TIMELY content on.
|
|
781
|
+
News finds the STORY; run video-search after for the VISUALS.
|
|
782
|
+
PAID PLANS ONLY · flat $0.0003 per call.
|
|
783
|
+
Method: https://vidfarm.cc/experimental/google-news-to-video.md
|
|
784
|
+
--limit <n> (default 25, max 100) --fresh <d|w|m|y> --region <r> --json
|
|
730
785
|
iconscout "<query>" DESIGNER icons, STICKERS, illustrations, 3D and Lottie —
|
|
731
786
|
THE CHEAP ALTERNATIVE TO AI IMAGE GENERATION. Reach for this
|
|
732
787
|
BEFORE 'generate' / 'image': an AI sticker costs cents per
|
|
@@ -921,6 +976,9 @@ Approve & schedule (publish a finished MP4 as a shareable post):
|
|
|
921
976
|
approve Approve media into a preview/share page → POST /api/v1/approved/posts
|
|
922
977
|
--video <url|file> Primary MP4 (URL, /files path, or local file → durable My Files)
|
|
923
978
|
--media <url|file> Extra media (repeatable; local files → durable My Files)
|
|
979
|
+
--thumbnail <url|file> Poster image for the share link's social card. Omit it and
|
|
980
|
+
the server extracts a frame (~1s in) from --video and files
|
|
981
|
+
it under /files/approved
|
|
924
982
|
--folder <path> Destination subfolder for uploaded local files (default: approved)
|
|
925
983
|
--temp Upload local files to the 30-day TEMP store instead of
|
|
926
984
|
durable My Files (throwaway preview; share video dies at day 30)
|
|
@@ -956,18 +1014,28 @@ Account:
|
|
|
956
1014
|
(iconscout takes BOTH values as one secret: <client_id>:<client_secret>)
|
|
957
1015
|
|
|
958
1016
|
Agent skill (the director knowledge — a full copy SHIPS INSIDE this CLI):
|
|
959
|
-
ideas [topic] "What should I post?" — the
|
|
960
|
-
(the rise of ·
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1017
|
+
ideas [topic] "What should I post?" — the THREE IDEA BANKS (local — offline, free, no AI)
|
|
1018
|
+
50 frames (the rise of · then vs now · …), 5
|
|
1019
|
+
AWARENESS STAGES (what the viewer already knows,
|
|
1020
|
+
and what the video may ask for), and 44 PROBLEM
|
|
1021
|
+
ANGLES (which side of the problem it comes from).
|
|
1022
|
+
An idea is one point in that grid; vary TWO axes
|
|
1023
|
+
and 30 videos never read as repeats. It hands over
|
|
1024
|
+
the banks — you fill them with the director's
|
|
1025
|
+
offer, then write hook/loop/payoff/bait per pick.
|
|
1026
|
+
RUN THIS BEFORE "consult" — one line of input buys
|
|
1027
|
+
20+ ideas, and it is the easiest first win a new
|
|
1028
|
+
director can get. The interview comes after.
|
|
966
1029
|
--topic "<offer>" Print each frame already filled with your topic
|
|
967
|
-
--family <name> One family only (arc, contrast, pivot, …)
|
|
968
|
-
--families List the families and their sizes
|
|
1030
|
+
--family <name> One frame family only (arc, contrast, pivot, …)
|
|
1031
|
+
--families List the frame families and their sizes
|
|
1032
|
+
--stages The awareness ladder — all 5 stages in full
|
|
1033
|
+
--stage <name|n> One stage (unaware · problem-aware · … · most-aware)
|
|
1034
|
+
--angles The 44 problem angles, by family
|
|
1035
|
+
--angle <name> One angle family or one named angle
|
|
1036
|
+
--grid Stage × angle × frame — one row per video
|
|
969
1037
|
--count <n> Sample N, spread across families (default 20 with --topic)
|
|
970
|
-
--json Machine-readable
|
|
1038
|
+
--json Machine-readable banks (+ starter lines)
|
|
971
1039
|
skill topics The craft this pack knows, by SPOKEN name — (local — offline, no account)
|
|
972
1040
|
meme-recaption, product-explainer, captions,
|
|
973
1041
|
first-frame, blurred-plate, density, avatar, …
|
|
@@ -1079,7 +1147,22 @@ Marketplace (paid, cloud-only — the bazaar never renders locally):
|
|
|
1079
1147
|
directory save-url <url> Save a durable media URL INTO My Files at a folder → POST /api/v1/user/me/attachments/from-url
|
|
1080
1148
|
--folder <path> Destination folder under /files (e.g. inpaints, promos)
|
|
1081
1149
|
--as <name> Name the saved file · --notes <text> vector-embedded notes
|
|
1150
|
+
directory share <path> Mint a PUBLIC link onto one subfolder (any root) → POST /api/v1/user/me/directory/shares
|
|
1151
|
+
--mode read|upload|edit read = browse + vector search · upload = + add files (never delete) · edit = + rename/delete
|
|
1152
|
+
--label <text> Name the link. PAID to mint; visiting one needs no account.
|
|
1153
|
+
directory shares [path] List your share links → GET /api/v1/user/me/directory/shares
|
|
1154
|
+
directory share-update <token> --mode/--label/--disable/--enable → PATCH /api/v1/user/me/directory/shares/:token
|
|
1155
|
+
directory unshare <token> Delete a share link for good → DELETE /api/v1/user/me/directory/shares/:token
|
|
1082
1156
|
(alias: dir · save-url|from-url|import-url — CLI twin of the "Save to Files" picker)
|
|
1157
|
+
gigs <sub> Your vending machines, BOTH sides (client + gigworker)
|
|
1158
|
+
machines · tasks · add-task · proofs · approve · reject · ring-bell (client, paid)
|
|
1159
|
+
work · feed · join-feed · join · claim · submit · mine (gigworker, free — bring DOLLARPLATOON_API_KEY)
|
|
1160
|
+
e.g. vidfarm gigs add-task --task "60s explainer for acme.com" --price 0.50 --upload-link <share url>
|
|
1161
|
+
vidfarm gigs proofs --status pending · vidfarm gigs approve PRF_01H…
|
|
1162
|
+
(see: vidfarm gigs help)
|
|
1163
|
+
shared <sub> <link> USE a link someone shared with you — NO account, NO API key
|
|
1164
|
+
info · ls · search · mkdir · put · get e.g. vidfarm shared put <link> clip.mp4 --subfolder batch-01
|
|
1165
|
+
(the gigworker/agent side of directory share; see: vidfarm shared help)
|
|
1083
1166
|
put-file / get-file / files / annotate-file are the My Files (persistent) set;
|
|
1084
1167
|
upload is the throwaway temp store for dropping media into a composition.
|
|
1085
1168
|
Annotate assets you'll want back — esp. recurring characters, kept under
|
|
@@ -1118,6 +1201,12 @@ Cost spectrum (default to the cheapest approach that works; see SKILL.director.m
|
|
|
1118
1201
|
In minimize AND hybrid, 'vidfarm tts' DEFAULTS to the free local Kokoro voice — ask for
|
|
1119
1202
|
a premium voice (--style/--voice/--provider/--own-key/--cloud), or be in rich-ai /
|
|
1120
1203
|
pure-videogen, to opt out.
|
|
1204
|
+
minimize is NOT "no AI images". If the agent driving this terminal has image generation
|
|
1205
|
+
included in its own subscription (Google Antigravity / Gemini CLI, OpenAI Codex /
|
|
1206
|
+
ChatGPT — Claude Code does NOT, Anthropic ships no image model), it generates the
|
|
1207
|
+
graphic itself: nothing is billed by Vidfarm and nothing extra by the provider, so it
|
|
1208
|
+
is allowed in EVERY mode. 'vidfarm agent-image --check' answers "do I have it?", and
|
|
1209
|
+
'vidfarm agent-image "<subject>" --items "a,b,c"' prints the prompt + cut/import steps.
|
|
1121
1210
|
|
|
1122
1211
|
Escape hatch — call ANY route directly:
|
|
1123
1212
|
api <METHOD> <path> Raw REST call with auth + pretty errors
|
|
@@ -1147,9 +1236,8 @@ Publish-mode options:
|
|
|
1147
1236
|
--message <text> Message attached to the published version snapshot
|
|
1148
1237
|
--no-snapshot Only update the working copy; skip the version snapshot
|
|
1149
1238
|
`;
|
|
1150
|
-
// ANSI palette
|
|
1151
|
-
//
|
|
1152
|
-
// this module is still evaluating.
|
|
1239
|
+
// ANSI palette. (Its position no longer matters — main() now runs as the last
|
|
1240
|
+
// statement in this file — but it stays up here because everything below reads it.)
|
|
1153
1241
|
const FRONTEND = "\x1b[1m\x1b[36m"; // bold cyan — reserved for openable URLs
|
|
1154
1242
|
const BOLD = "\x1b[1m";
|
|
1155
1243
|
const DIM = "\x1b[2m";
|
|
@@ -1157,24 +1245,16 @@ const GREEN = "\x1b[32m";
|
|
|
1157
1245
|
const YELLOW = "\x1b[33m";
|
|
1158
1246
|
const RED = "\x1b[31m";
|
|
1159
1247
|
const RESET = "\x1b[0m";
|
|
1160
|
-
// Referenced by runMarketplaceCommand's SYNCHRONOUS browse path
|
|
1161
|
-
// initialized before `void main()` starts dispatching below.
|
|
1248
|
+
// Referenced by runMarketplaceCommand's SYNCHRONOUS browse path.
|
|
1162
1249
|
const DOLLARPLATOON_SKILL_HINT = `Gig inbox / proofs / payouts live on Dollar Platoon's own API — grab the skill:
|
|
1163
1250
|
npx skills add https://github.com/OfficeXApp/dollarplatoon-skill
|
|
1164
1251
|
(or from the vidfarm mirror: vidfarm skills add dollarplatoon-skill)`;
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
}
|
|
1172
|
-
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
1173
|
-
// Report only unexpected crashes (bugs), never the deliberate user-facing
|
|
1174
|
-
// `throw new Error(...)` above; no-op unless a devcli DSN is configured.
|
|
1175
|
-
await reportCliCrash(error);
|
|
1176
|
-
process.exit(1);
|
|
1177
|
-
});
|
|
1252
|
+
// main() is INVOKED AT THE BOTTOM OF THIS FILE, not here. Command handlers read
|
|
1253
|
+
// module-level `const`s (help texts, tables, palettes) synchronously, and a
|
|
1254
|
+
// const declared below the invocation point is still in its temporal dead zone
|
|
1255
|
+
// when the handler runs — `vidfarm directory help` used to die with "Cannot
|
|
1256
|
+
// access 'DIRECTORY_HELP' before initialization". Starting main() last means
|
|
1257
|
+
// every declaration in this module is initialized before any command dispatches.
|
|
1178
1258
|
// AI agents habitually save credentials to a project-local .env instead of
|
|
1179
1259
|
// exporting them, so honor VIDFARM_API_KEY (and VIDFARM_TARGET/VIDFARM_HOST)
|
|
1180
1260
|
// from ./.env when the variable isn't already in the environment. Real env
|
|
@@ -1323,6 +1403,22 @@ async function main() {
|
|
|
1323
1403
|
case "media":
|
|
1324
1404
|
await runMediaCommand(rest);
|
|
1325
1405
|
return;
|
|
1406
|
+
case "video-search":
|
|
1407
|
+
case "videosearch":
|
|
1408
|
+
case "search-video":
|
|
1409
|
+
await runWebSearchCommand("video", rest);
|
|
1410
|
+
return;
|
|
1411
|
+
case "image-search":
|
|
1412
|
+
case "imagesearch":
|
|
1413
|
+
case "search-image":
|
|
1414
|
+
await runWebSearchCommand("image", rest);
|
|
1415
|
+
return;
|
|
1416
|
+
case "news-search":
|
|
1417
|
+
case "newssearch":
|
|
1418
|
+
case "news":
|
|
1419
|
+
case "search-news":
|
|
1420
|
+
await runWebSearchCommand("news", rest);
|
|
1421
|
+
return;
|
|
1326
1422
|
case "iconscout":
|
|
1327
1423
|
case "icons":
|
|
1328
1424
|
case "stickers":
|
|
@@ -1423,6 +1519,23 @@ async function main() {
|
|
|
1423
1519
|
case "hand-off":
|
|
1424
1520
|
await runHandoffCommand(rest);
|
|
1425
1521
|
return;
|
|
1522
|
+
// Image generation the AGENT already pays for — free in every cost mode.
|
|
1523
|
+
case "agent-image":
|
|
1524
|
+
case "agent-imagegen":
|
|
1525
|
+
case "own-image":
|
|
1526
|
+
case "native-image":
|
|
1527
|
+
await runAgentImageCommand(rest);
|
|
1528
|
+
return;
|
|
1529
|
+
// Free browser sourcing — the agent drives the user's own Chrome instead of
|
|
1530
|
+
// paying for video-search / image-search / news-search / download-video.
|
|
1531
|
+
case "browser":
|
|
1532
|
+
case "browser-harness":
|
|
1533
|
+
case "bh":
|
|
1534
|
+
await runBrowserHarnessCommand(rest);
|
|
1535
|
+
return;
|
|
1536
|
+
case "browse":
|
|
1537
|
+
await runBrowseCommand(rest);
|
|
1538
|
+
return;
|
|
1426
1539
|
// The brainstorm chain with no AI key: the prompt goes to the agent that is
|
|
1427
1540
|
// already here, not to a provider. "consultation" is the director's word.
|
|
1428
1541
|
case "consult":
|
|
@@ -1493,6 +1606,15 @@ async function main() {
|
|
|
1493
1606
|
case "market":
|
|
1494
1607
|
await runMarketplaceCommand(rest);
|
|
1495
1608
|
return;
|
|
1609
|
+
// Both sides of the vending machines: a paid client posting tasks and
|
|
1610
|
+
// reviewing proofs, and a FREE gigworker with their own dollarplatoon key.
|
|
1611
|
+
case "gigs":
|
|
1612
|
+
case "gig":
|
|
1613
|
+
case "vending": {
|
|
1614
|
+
const { runGigsCommand } = await import("./devcli/marketplace-gigs.js");
|
|
1615
|
+
await runGigsCommand(rest);
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1496
1618
|
case "get-file":
|
|
1497
1619
|
await runGetFileCommand(rest);
|
|
1498
1620
|
return;
|
|
@@ -1507,6 +1629,15 @@ async function main() {
|
|
|
1507
1629
|
case "dir":
|
|
1508
1630
|
await runDirectoryCommand(rest);
|
|
1509
1631
|
return;
|
|
1632
|
+
// The VISITOR side of a share link — no account, no API key. A gigworker or
|
|
1633
|
+
// their agent works a folder someone else shared with them.
|
|
1634
|
+
case "shared":
|
|
1635
|
+
case "shared-folder":
|
|
1636
|
+
case "dropbox": {
|
|
1637
|
+
const { runSharedCommand } = await import("./devcli/shared-folder.js");
|
|
1638
|
+
await runSharedCommand(rest);
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1510
1641
|
case "migrate-local": {
|
|
1511
1642
|
if (!localBackendAvailable())
|
|
1512
1643
|
throw new LocalModeUnavailableError();
|
|
@@ -1812,6 +1943,7 @@ function guardBilled(ctx, input) {
|
|
|
1812
1943
|
json: ctx.json,
|
|
1813
1944
|
label: input.label,
|
|
1814
1945
|
estimate: input.estimate,
|
|
1946
|
+
agentAlternative: input.agentAlternative,
|
|
1815
1947
|
freeAlternative: input.freeAlternative
|
|
1816
1948
|
? `${input.freeAlternative}${interactiveNote}`
|
|
1817
1949
|
: (interactiveNote ? interactiveNote.trimStart() : undefined)
|
|
@@ -2610,12 +2742,14 @@ Rules:
|
|
|
2610
2742
|
- For replacement graphics, screenshots, or still-like scenes, prefer AI image generation plus Ken Burns before paying for AI video unless static_vs_pivot says motion footage is load-bearing.
|
|
2611
2743
|
- If narration must be customized, default to premium ElevenLabs first, then the user's own ElevenLabs path, then BYOK OpenAI/Gemini/OpenRouter. If captions or scenes were timed to the old VO, retime them to the new narration.
|
|
2612
2744
|
- NO HTML SLOP. You are editing HTML, but the output is a social video, not a web page. THE TEST IS THE NATIVE-EDITOR TEST: could you have made this element with the tools inside TikTok's own editor? That toolset is a font, a color, a stroke/outline, a soft shadow, a tight text box, alignment, opacity, rotation, animation presets — plus stickers, emoji, drawn marks and clips. It has NO padded capsule, NO border, NO gradient fill, NO blur panel, NO card. If you reached past it, cut it. Never author landing-page furniture: CTA "buttons" (a filled/gradient rounded capsule with action copy like "Sign Up for a Free Trial →"), benefit chip/badge rows ("✓ No Credit Card Needed"), bordered/shadowed/frosted cards holding a headline + URL, gradient text fills, feature grids, bulleted lists, or web-default fonts (Inter/Roboto/Arial/system-ui). AND NOT A SINGLE PILL EITHER: one lonely rounded, padded, filled capsule around a static stat or label — "10 hrs / week", "STEP 2", "EP.01", "+40%" — is a web badge, and being the only one on screen does not make it native. The ONLY legitimate capsule in a video is the active-word spotlight/karaoke caption highlight, because it moves with the spoken word. Emphasize a stat the way the editor would: bigger, heavier, ALL-CAPS, an accent color, a hand-drawn circle or underline, or its own beat on screen. Rule of thumb on anything holding words: border-radius over ~8px PLUS a background fill PLUS padding = a badge; drop the fill or drop the radius until the band hugs the glyphs. None of this appears in a real TikTok, and nothing in a video is clickable — say it as timed text on the footage instead. Arrows, scribble/underline marks, italics, ALL-CAPS, single-word color pops, emoji, transparent cut-out stickers, and mock social UI (iMessage bubbles, comment cards) are all fine. Captions use an imported family (Montserrat default / TikTok Sans / Abel / Source Code Pro / Yesteryear) at weight 700-900, ~36-64px on a 1080-wide frame, inside the 8%-85% safe zone, with exactly one of four backgrounds: outline, plain, an active-word spotlight/karaoke pill, or a tight-hugging solid band (radius <=8px, no border/shadow/gradient/blur).
|
|
2745
|
+
- NO LAYOUT TEMPLATES — JUDGE THE WHOLE FRAME, NOT JUST THE ELEMENT. Every rule above judges one element, and a frame can pass element-by-element and still be a web page. The archetype is the MODAL: the backdrop dimmed and blurred out of focus, and floating on top of it a rounded bordered box holding a big headline, a smaller support line, and a fat CTA button. THE STACK IS THE TELL, NOT THE BOX — delete the border, the fill and the capsule, keep headline then subheadline then CTA centred in a well with even margins, and it STILL reads as a landing page, because a viewer recognizes the SHAPE before reading a single word. Banned at frame level: a modal/dialog staged on top of a backdrop that has been dimmed, blurred, greyed or scaled back (nothing in a video pops "above" the video); the hero triplet and its cousins (title + kicker + logo lockup, question + answer + URL); a full-frame dark wash used to stage a floating block (a legibility band on ONE caption is legal, a page-wide wash to stage a panel is not; likewise a blurred backdrop is fine alone — a blurred fill behind a 16:9 clip in a 9:16 frame is a real technique — but blur PLUS dimming is modal staging); nav strip / hero / three-up feature row / testimonial block / footer fine print; a blurred website screenshot used as the background plate (if the backdrop is a web page, the frame is a screen recording of a web page — show the real product UI full-bleed and in focus, or don't show it); a centred content column with even gutters and document margins. THE FIX IS ALWAYS TO UNSTACK IT INTO TIME: the headline is the hook at start:0, the support line lands on the next cut, the CTA is SPOKEN or a bare caption on the final frame. You lose nothing — a viewer reads one line at a time anyway — and you gain the pacing that makes it look shot rather than designed. Self-check before you place any text group: am I arranging words relative to EACH OTHER, or relative to the PICTURE? Relative to each other is a layout, which is web. Two on-screen text runs at once is the ceiling. Verify on real pixels: \`vidfarm stills . --at <t>\` — if the still could be a screenshot of a website, rebuild the beat. \`vidfarm qa\` catches only the mechanical half (layout-template, modal-scrim); the frame-level judgement is yours.
|
|
2613
2746
|
- STRUCTURE BEFORE POLISH — THE FOUR CHARGES, WRITTEN BEFORE YOU TOUCH THE TIMELINE. Most agent-made videos fail on structure, not polish, because the timeline is the fun part so it gets built first and the words get retrofitted. Invert it: (1) HOOK — write the opening line as text first: a complete clause (subject + verb), no jargon, naming a SITUATION ("I've quit six businesses") not a label ("anonymity"); it goes on screen at start:0, because caption chunk 1 is read before any audio and muted autoplay is the default. Banned openings: throat-clearing ("so I was thinking", "here's the thing"), a logo, a title card, a fade from black, context before the claim. (2) LOOP — one open question by 0:10, said ON SCREEN, closing INSIDE this video (state the timestamp it closes at; if you can't, there is no loop), and the withheld answer must be one the viewer CANNOT supply themselves — a formally-correct loop with a guessable answer passes every mechanical check and dies in the field. (3) PAYOFF — shown, not summarized, ≥5 uninterrupted seconds, landing BEFORE the final beat; the payoff is not the CTA. (4) BAIT — one ask in the final beat and in the post caption; a keyword comment ask ("comment CLIPPER and I'll send the breakdown") is standard and allowed, but never "follow for part two", ragebait, or an earnings/health claim traded for the reply. Then build the timeline. Re-theming a decomposed template: viral_dna already names the source's hook/retention/payoff — rebuild each charge for the new subject, never flatten the loop into a product statement. Full craft harness: the vidfarm skill's references/hooks-and-virality.md. Checkable form: \`vidfarm harness show hooks\`.
|
|
2614
2747
|
- ORIENT THE COLD VIEWER IN THE FIRST 3 SECONDS — THE VIEWER HAS NO CONTEXT AND DID NOT CHOOSE THIS VIDEO. Distinct from the hook: the hook makes them WANT to watch, orientation makes the watching POSSIBLE. A stranger mid-scroll must be able to answer three things by ~3s — what am I looking at (the CATEGORY noun), who is it for, and why is this on my screen (the situation). The failure is not a bad first frame, it is a good video that BEGINS AT BEAT TWO, and the author cannot see it because the author already knows what the thing is. Signatures, each a rebuild not a polish: a pronoun with no referent ("it just works", "this changes everything", "here's how they do it"); starting at step three (the process already running, the dashboard already full); a metaphor whose subject only lands at 6s; insider vocabulary, a product's own feature name, or an ACRONYM in the first line; a detail crop that reads as texture until you know the whole. Instead, the opening beat is BOTH channels at once: an EASY IMAGE (one large subject, already moving, legible at a glance and at thumbnail scale — a relevant die-cut sticker names the category before a word is read) AND an EASY LINE (first spoken sentence one clause, <=12 words, everyday words, concrete noun + verb, no subordinate clause, brand name said once plainly, and the CATEGORY named: "X is a language app that…"). Give the SITUATION, not the label — "the end of the month, and your receipts are in a shoebox" orients, "expense automation" does not. THIS IS NOT AN INTRO AND COSTS NO EXTRA SECONDS: it replaces the wind-up sentence, it never precedes it, and it never licenses a logo, a title card or a fade from black. Test it on the render, not the script: play the first 3 seconds ONLY to somebody with no context and stop — they should say what kind of thing it is and roughly who it is for. "Something about audio" is a fail. Fullest form: \`vidfarm harness show product-explainer\` (Rule 0).
|
|
2615
2748
|
- THE FIRST FRAME IS THE THUMBNAIL. Frame 0 is one frame of ~30 in the first second, but every feed card, share link, and paused player freezes on it — more people see that frame than watch the video. It must never be black, empty, mid-fade, or mid-animation: a real visual at start:0 (\`vidfarm retime . --layer <key> --start 0\`), the hook words already on screen at t=0, and NO entrance transition on the FIRST clip (\`vidfarm transitions set . --layer <key> --in none\`; junction transitions between later clips are fine). Look at the actual pixels before you render: \`vidfarm stills . --at 0\`.
|
|
2616
2749
|
- REVIEW THE WHOLE VIDEO AS ONE OBJECT — AND NEVER JUDGE IT BY ONE FRAME. Assume your own finished video has a defect you cannot see: across a 32-video batch, EVERY first-pass video had a real defect its own author had already reported as "verified, looks good". The cause is structural — you build scene by scene, each scene correct while it is the whole world, so every scene passes alone and the video fails as a SEQUENCE: margins shift between beats, headline sizes drift, the accent color wanders, one asset is flat vector and the next is photographic, every beat is the same length, a join lands like a slap. Nobody watches a scene; they watch the sequence. So before you call anything done, tile ~12 stills into ONE contact sheet and READ IT AS AN IMAGE — one command: \`vidfarm stills . --sheet\` (add \`--at 0,2,4,…\` to pick timestamps; writes stills/contact-sheet.png). Check: visual balance (no dead band under top-anchored content), consistent spacing/margins, ONE type scale, ONE accent color, ONE illustration style, deliberate pacing rather than N identical beats, nothing jarring at the joins, no frame where two elements compete for the eye — and the summary question, does it look like one person made it in one sitting? Fix drift by defining the SYSTEM (type scale, margin, palette, default beat) and applying it to every scene, not by patching the one scene that stood out. The defects that actually ship, in observed frequency order: large flat dead regions · a placeholder empty state that reads as a failed render · two contradictory numbers in one frame · a CTA still building at the last frame (settle it >=2s before the end) · two headlines superimposed at a scene handoff (exit at nextIn-0.18, duration 0.24, ease power2.out) · type colliding with a busy background exactly as it is spoken. If a frame looks empty, sample 0.2s apart to see whether it RESTS there — a transient wipe frame is fine, >0.5s is a hole. AND ALWAYS COMPARE TWO FRAMES FROM DIFFERENT SCENES: a frozen render (an overlay/watermark pass missing \`-loop 1\`, or assets outside the composition root so the timeline never runs) makes every frame identical while duration, frame count and audio hash all still pass, and frame 0 looks perfect. Verify audio by MEASUREMENT, never "it sounds fine" (you cannot hear it): ~12-15 dB speech-over-bed separation across the actual word spans, peak <0 dBFS. Never \`adelay\` the VO — whisper word timings and every caption built from them are relative to the raw vo.wav; use apad+atrim. Report what you MEASURED separately from what you JUDGED. Full method: the vidfarm skill's references/reviewing-renders.md.
|
|
2617
2750
|
- THE HARNESS IS A FIRST-CLASS ARTIFACT — KNOW THE THREE PHRASINGS. A HARNESS.md is the reusable AI harness for ONE format or template: what makes it special, written down so an agent reproduces it without the director in the room. "Create me a harness" → \`vidfarm harness init short-form --out ./HARNESS.md\` (bases: short-form, hooks, ugc-testimonial, explainer, product-demo, product-explainer), then EDIT it with them. "Update the harness for this format" → open the file and write the new rule in WITH ITS REASON on the same line. "Give me the harness for this template_id" → they mean the DECOMPOSITION: \`vidfarm harness derive <forkId>\` distils this fork's viral/visual/structural/audio/build DNA into an editable HARNESS.md, keyed the same way the decompose JSON is (\`## Viral DNA\` → \`viral_dna\`, …); \`vidfarm harness show <ref> --dna visual\` prints one strand. \`vidfarm qa .\` picks up ./HARNESS.md automatically; \`--harness <name|path>\` adds more (they stack, and any file of theirs anywhere is valid). Its \`checks:\` are machine-settled; its \`- [ ]\` items come back for YOU to answer honestly in your report — never claim a pass on the half the CLI can't judge. ONE-TIME OR BULK? Ask before you build: if the director wants volume (daily posting, N variants, hook tests), that's SCRIPTING MODE — pin this fork as the base, vary exactly ONE thing per variant, and install a harness, because nobody watches variant #37 as carefully as #1. When a batch teaches you something, write it back into the harness — the compositions are disposable, the harness compounds. (Formerly QA_REGIME.md; \`vidfarm regime …\` still works as an alias. NOTE: the \`.harness/\` directory beside this file is machine-generated context regenerated on every pull — never hand-edit it. HARNESS.md is the one you own.)
|
|
2618
2751
|
- DEDUPLICATE BEFORE YOU PUBLISH — AND ASK FIRST. Social platforms fingerprint every upload, so the same render posted twice (a second account, another platform, a re-post next month) gets the later copy suppressed as duplicate/reused content. BEFORE you render for publication, and before any bulk run, ASK the director: "do you want deduplicated copies for posting, and how many?" Ask THEN, not after — dedupe is a post-render ffmpeg pass, so the correct order is RENDER ONCE → DEDUPE N, and deciding late means paying for a second render. Run it on the EXPORTED file: \`vidfarm dedupe ./final.mp4\` (one copy) or \`vidfarm dedupe ./final.mp4 --variants N --seed <slug> --out-dir ./posts\` (N copies, one per account/slot). Free, offline, no wallet — it never re-renders the composition. The default \`standard\` preset is skew 2%, zoom 3%, rotate 2°, speed +2%, saturation +4%, plus contrast/brightness/hue/grain, a container-metadata strip and a per-variant CRF walk; invisible to a viewer, and each variant differs from the original AND from its siblings. Post each variant to a DIFFERENT account — two accounts posting the same variant defeats the point. A rotate forces a bigger centre-crop to hide the black corners (~6.7% on a tall frame at 2°) and the CLI says so; pass \`--rotate 0\` when framing matters more. Cloud twin: \`POST /api/v1/primitives/media/dedupe\`.
|
|
2752
|
+
- USE THE REAL SONG — A POPULAR/COPYRIGHTED TRACK BELONGS IN THE RENDER, AND YOU DO NOT REFUSE IT, WARN AT LENGTH ABOUT IT, OR QUIETLY SWAP IN A GENERIC ROYALTY-FREE BED. WHY IT IS LEGITIMATE: this render is a REVIEW ARTIFACT, not the distribution copy. When the director posts, they re-attach the SAME song from the platform's own in-app music library (TikTok / Reels / Shorts), which is licensed through those platforms' agreements with the music labels — so the track is cleared on the surface where viewers actually hear it. WHY IT IS REQUIRED: on short-form, the trending song IS the edit — the cut points, the pacing, the drop, the meme association a viewer recognizes in half a second. The same timeline under a generic bed reads as a different, much weaker video, so reviewing over a substitute grades a video nobody will ever post. Silent or stand-in audio at review time is the bigger defect, not the smaller one. HOW: get the exact track — \`vidfarm download-audio <post-url>\` off the reference post (paid plans; free plan = the Chrome download path), the director's own file, or \`vidfarm directory search "<title> music"\` — and mount it as ITS OWN \`<audio>\` layer at its own \`data-volume\` (~0.1-0.2 under narration, or full level when the song carries the piece). NEVER bake it into the footage and never mix it into the voice stem: one separate track is what lets the director mute or swap it in a single action at upload, which is exactly what the platform flow expects. Name the track you used in your report. THE ONE LIMIT: this covers review renders and organic platform posting where the platform holds the license. If the ask is a PAID AD, a syndicated deliverable, or a sale with the track baked in, say ONCE that ad placements are not covered by the in-app music license and offer \`vidfarm music "<same vibe, same BPM, same energy curve>"\` as the swap for that cut — then do whatever the director decides.
|
|
2619
2753
|
- QA EVERY VIDEO BEFORE YOU RENDER: run \`vidfarm qa .\` in this directory. It's free, instant, and local — a blocklist for the slop above plus the first frame, the font regime, and the safe zone, with a concrete fix per finding. It's feedback, not a gate (exits 0 even on findings, never runs automatically) and a blocklist, not an allowlist, so stylized or hand-made work passes untouched. Fix what's real, ignore what's a deliberate style call. \`--json\` for scripted batches.
|
|
2620
2754
|
|
|
2621
2755
|
The three paintbrushes (Vidfarm is thrift-first — do NOT spend AI credits on every scene):
|
|
@@ -4627,6 +4761,7 @@ async function runGenerateCommand(argv) {
|
|
|
4627
4761
|
: {
|
|
4628
4762
|
label: "AI image generation",
|
|
4629
4763
|
estimate: "cheap, ~$0.01–$0.05",
|
|
4764
|
+
agentAlternative: AGENT_IMAGEGEN_ALTERNATIVE,
|
|
4630
4765
|
freeAlternative: 'free stock image/vector — vidfarm media search "<meaning>" --type image|vector ($0). ' +
|
|
4631
4766
|
"Openverse (CC images) is keyless and always on — check it first; a free Pixabay key adds " +
|
|
4632
4767
|
"photos/vectors and may already be saved (check vidfarm provider-keys). " +
|
|
@@ -4878,7 +5013,11 @@ async function runInpaintCommand(argv) {
|
|
|
4878
5013
|
}
|
|
4879
5014
|
});
|
|
4880
5015
|
const ctx = commonContext(parsed.values);
|
|
4881
|
-
guardBilled(ctx, {
|
|
5016
|
+
guardBilled(ctx, {
|
|
5017
|
+
label: "AI image inpaint",
|
|
5018
|
+
estimate: "cheap, ~$0.01–$0.05",
|
|
5019
|
+
agentAlternative: AGENT_IMAGEGEN_ALTERNATIVE
|
|
5020
|
+
});
|
|
4882
5021
|
const sourceArg = parsed.values.source ?? parsed.positionals[0];
|
|
4883
5022
|
if (!sourceArg)
|
|
4884
5023
|
throw new Error("inpaint requires a source image: `vidfarm inpaint <image|url> --mask <mask.png> --prompt \"...\"`.");
|
|
@@ -5014,7 +5153,11 @@ async function runCreateOverlayCommand(argv) {
|
|
|
5014
5153
|
}
|
|
5015
5154
|
});
|
|
5016
5155
|
const ctx = commonContext(parsed.values);
|
|
5017
|
-
guardBilled(ctx, {
|
|
5156
|
+
guardBilled(ctx, {
|
|
5157
|
+
label: "AI transparent overlay generation",
|
|
5158
|
+
estimate: "cheap, ~$0.01–$0.05",
|
|
5159
|
+
agentAlternative: AGENT_IMAGEGEN_ALTERNATIVE
|
|
5160
|
+
});
|
|
5018
5161
|
const prompt = (parsed.values.prompt ?? parsed.positionals.join(" ")).trim();
|
|
5019
5162
|
if (!prompt) {
|
|
5020
5163
|
throw new Error("create-overlay requires a subject prompt: `vidfarm create-overlay \"a cartoon rocket ship\" [--key-color #00FF00] [--out overlay.png]`. Describe ONLY the subject — the flat key-color background is added for you.");
|
|
@@ -6095,6 +6238,7 @@ async function runCutoutCommand(argv) {
|
|
|
6095
6238
|
guardBilled(ctx, {
|
|
6096
6239
|
label: "AI image generation (cutout plate)",
|
|
6097
6240
|
estimate: "cheap, ~$0.01–$0.05",
|
|
6241
|
+
agentAlternative: AGENT_IMAGEGEN_ALTERNATIVE,
|
|
6098
6242
|
freeAlternative: "pass an existing graphic instead — `vidfarm cutout <image|url>` keys+trims for $0.00",
|
|
6099
6243
|
interactiveAlternative: 'vidfarm handoff image --theme "<subject>" --single — the user generates the plate in a free web tool ' +
|
|
6100
6244
|
"and hands the PNG back; `vidfarm cutout <file>` then keys + trims it for $0"
|
|
@@ -6644,6 +6788,7 @@ async function runStickerPackCommand(argv) {
|
|
|
6644
6788
|
guardBilled(ctx, {
|
|
6645
6789
|
label: `AI image generation (sticker sheet of ${count} items)`,
|
|
6646
6790
|
estimate: "cheap, ~$0.01–$0.05 for the WHOLE pack (one image job, not one per sticker)",
|
|
6791
|
+
agentAlternative: AGENT_IMAGEGEN_ALTERNATIVE,
|
|
6647
6792
|
freeAlternative: "pass a greenscreen sheet you already have — `vidfarm sticker-pack <sheet.png>` cuts it up for $0.00",
|
|
6648
6793
|
interactiveAlternative: `vidfarm handoff image --theme "${generatePrompt.replace(/"/g, "'")}"${itemNames.length ? ` --items "${itemNames.join(",")}"` : ""} — ` +
|
|
6649
6794
|
"the user generates the SAME sheet in a free web tool (often better art than the API tier) and hands it " +
|
|
@@ -7820,6 +7965,121 @@ async function runMediaCommand(argv) {
|
|
|
7820
7965
|
}
|
|
7821
7966
|
console.log(`${DIM}Attribution shown for CC-BY-style items — include it when you use them. Pixabay/CC0 need none.${RESET}`);
|
|
7822
7967
|
}
|
|
7968
|
+
// Google search — video / image / news.
|
|
7969
|
+
//
|
|
7970
|
+
// The catalog (`vidfarm media`) answers "give me A clip of X". Google answers
|
|
7971
|
+
// "give me THE shot": it indexes TikTok, YouTube, every stock library, and the
|
|
7972
|
+
// news at once, so one query returns candidate SOURCE URLs that feed straight
|
|
7973
|
+
// into `raws scan` (mine clips out of them) or `download-video` (collect one).
|
|
7974
|
+
//
|
|
7975
|
+
// Flat $0.0003 per CALL — the same price for 1 or 100 links — so the CLI nudges
|
|
7976
|
+
// toward one wide page rather than repeated narrow ones. No cost-mode gate: at
|
|
7977
|
+
// this price a search is cheaper than the agent turn that decides to skip it.
|
|
7978
|
+
async function runWebSearchCommand(kind, argv) {
|
|
7979
|
+
const parsed = parseArgs({
|
|
7980
|
+
args: argv,
|
|
7981
|
+
allowPositionals: true,
|
|
7982
|
+
options: {
|
|
7983
|
+
...commonOptions(),
|
|
7984
|
+
limit: { type: "string" },
|
|
7985
|
+
fresh: { type: "string" },
|
|
7986
|
+
region: { type: "string" },
|
|
7987
|
+
duration: { type: "string" },
|
|
7988
|
+
resolution: { type: "string" },
|
|
7989
|
+
color: { type: "string" },
|
|
7990
|
+
size: { type: "string" },
|
|
7991
|
+
"type-image": { type: "string" },
|
|
7992
|
+
layout: { type: "string" },
|
|
7993
|
+
safesearch: { type: "string" }
|
|
7994
|
+
}
|
|
7995
|
+
});
|
|
7996
|
+
const positionals = parsed.positionals.slice();
|
|
7997
|
+
if (positionals[0] && ["search", "find"].includes(positionals[0].toLowerCase()))
|
|
7998
|
+
positionals.shift();
|
|
7999
|
+
const query = positionals.join(" ").trim();
|
|
8000
|
+
if (!query) {
|
|
8001
|
+
console.error(`${RED}${kind}-search: a search query is required${RESET}`);
|
|
8002
|
+
if (kind === "video") {
|
|
8003
|
+
console.error(`${DIM}Search the SHOT, not the topic: vidfarm video-search "office worker typing laptop close up 4K"${RESET}`);
|
|
8004
|
+
console.error(`${DIM}Operators work: site:pexels.com/videos · site:tiktok.com "packing orders" -dance${RESET}`);
|
|
8005
|
+
}
|
|
8006
|
+
else if (kind === "news") {
|
|
8007
|
+
console.error(`${DIM}e.g. vidfarm news-search "AI startup funding announced" --fresh w${RESET}`);
|
|
8008
|
+
}
|
|
8009
|
+
else {
|
|
8010
|
+
console.error(`${DIM}e.g. vidfarm image-search "warehouse interior wide shot" --limit 40${RESET}`);
|
|
8011
|
+
}
|
|
8012
|
+
process.exit(2);
|
|
8013
|
+
}
|
|
8014
|
+
const ctx = commonContext(parsed.values);
|
|
8015
|
+
const maxCap = kind === "video" ? 50 : 100;
|
|
8016
|
+
const limit = Math.min(Math.max(Number(parsed.values.limit ?? "25") || 25, 1), maxCap);
|
|
8017
|
+
const qs = new URLSearchParams({ q: query, max_results: String(limit) });
|
|
8018
|
+
if (parsed.values.region)
|
|
8019
|
+
qs.set("region", String(parsed.values.region));
|
|
8020
|
+
if (parsed.values.fresh)
|
|
8021
|
+
qs.set("timelimit", String(parsed.values.fresh));
|
|
8022
|
+
if (parsed.values.safesearch)
|
|
8023
|
+
qs.set("safesearch", String(parsed.values.safesearch));
|
|
8024
|
+
if (kind === "video") {
|
|
8025
|
+
if (parsed.values.duration)
|
|
8026
|
+
qs.set("duration", String(parsed.values.duration));
|
|
8027
|
+
if (parsed.values.resolution)
|
|
8028
|
+
qs.set("resolution", String(parsed.values.resolution));
|
|
8029
|
+
}
|
|
8030
|
+
if (kind === "image") {
|
|
8031
|
+
if (parsed.values.color)
|
|
8032
|
+
qs.set("color", String(parsed.values.color));
|
|
8033
|
+
if (parsed.values.size)
|
|
8034
|
+
qs.set("size", String(parsed.values.size));
|
|
8035
|
+
if (parsed.values["type-image"])
|
|
8036
|
+
qs.set("type_image", String(parsed.values["type-image"]));
|
|
8037
|
+
if (parsed.values.layout)
|
|
8038
|
+
qs.set("layout", String(parsed.values.layout));
|
|
8039
|
+
}
|
|
8040
|
+
const res = await apiRequest({
|
|
8041
|
+
method: "GET", host: ctx.host, path: `/api/v1/primitives/${kind}-search?${qs.toString()}`, auth: ctx.auth
|
|
8042
|
+
});
|
|
8043
|
+
assertApiOk(res, `${kind} search`);
|
|
8044
|
+
const body = (res.json ?? {});
|
|
8045
|
+
if (ctx.json) {
|
|
8046
|
+
printJson(body);
|
|
8047
|
+
return;
|
|
8048
|
+
}
|
|
8049
|
+
const results = Array.isArray(body.results) ? body.results : [];
|
|
8050
|
+
console.log(`${DIM}${results.length} ${kind} result(s) for "${query}" · $${body.charged_usd ?? 0} this call${RESET}`);
|
|
8051
|
+
if (!results.length) {
|
|
8052
|
+
console.log(`${DIM}No results. Loosen the query — a site: filter plus a long phrase often returns nothing. Try fewer operators, or drop --fresh.${RESET}`);
|
|
8053
|
+
return;
|
|
8054
|
+
}
|
|
8055
|
+
for (const item of results) {
|
|
8056
|
+
if (kind === "image") {
|
|
8057
|
+
console.log(`${GREEN}${item.image_url}${RESET}`);
|
|
8058
|
+
const dims = item.width && item.height ? ` · ${item.width}x${item.height}` : "";
|
|
8059
|
+
console.log(`${DIM} ${item.title}${dims}${item.source_page ? ` · ${item.source_page}` : ""}${RESET}`);
|
|
8060
|
+
continue;
|
|
8061
|
+
}
|
|
8062
|
+
if (kind === "news") {
|
|
8063
|
+
console.log(`${GREEN}${item.url}${RESET}`);
|
|
8064
|
+
console.log(`${DIM} ${item.title}${item.source ? ` · ${item.source}` : ""}${item.date ? ` · ${item.date}` : ""}${RESET}`);
|
|
8065
|
+
continue;
|
|
8066
|
+
}
|
|
8067
|
+
console.log(`${GREEN}${item.url}${RESET}`);
|
|
8068
|
+
const meta = [item.duration, item.publisher, item.uploader].filter(Boolean).join(" · ");
|
|
8069
|
+
console.log(`${DIM} ${item.title}${meta ? ` · ${meta}` : ""}${RESET}`);
|
|
8070
|
+
}
|
|
8071
|
+
if (kind === "video") {
|
|
8072
|
+
console.log(`${DIM}Next: 'vidfarm raws scan <url>' mines clips out of a source · 'vidfarm download-video <url>' collects the file.${RESET}`);
|
|
8073
|
+
console.log(`${RED}A public video is NOT a licensed video — check rights before you reuse anything. Licence-checked free footage: 'vidfarm media search "<meaning>" --type video'.${RESET}`);
|
|
8074
|
+
}
|
|
8075
|
+
else if (kind === "news") {
|
|
8076
|
+
console.log(`${DIM}News finds the STORY. Run 'vidfarm video-search "<event> footage"' for the VISUALS — searching both at once finds neither.${RESET}`);
|
|
8077
|
+
console.log(`${DIM}Method: https://vidfarm.cc/experimental/google-news-to-video.md${RESET}`);
|
|
8078
|
+
}
|
|
8079
|
+
else {
|
|
8080
|
+
console.log(`${RED}These are LINKS with no licence attached. For royalty-free assets use 'vidfarm media search "<meaning>" --type image' instead.${RESET}`);
|
|
8081
|
+
}
|
|
8082
|
+
}
|
|
7823
8083
|
// IconScout — designer icons / STICKERS / illustrations / 3D / Lottie.
|
|
7824
8084
|
//
|
|
7825
8085
|
// This is the CHEAP path, and the CLI says so loudly: an AI image attempt costs
|
|
@@ -8896,6 +9156,7 @@ async function runApproveCommand(argv) {
|
|
|
8896
9156
|
...commonOptions(),
|
|
8897
9157
|
video: { type: "string" },
|
|
8898
9158
|
media: { type: "string", multiple: true },
|
|
9159
|
+
thumbnail: { type: "string" },
|
|
8899
9160
|
caption: { type: "string" },
|
|
8900
9161
|
title: { type: "string" },
|
|
8901
9162
|
pinned: { type: "string" },
|
|
@@ -8937,6 +9198,11 @@ async function runApproveCommand(argv) {
|
|
|
8937
9198
|
media.push({ url: await resolveApproveMedia(String(parsed.values.video)), kind: "video", role: "primary" });
|
|
8938
9199
|
for (const url of parsed.values.media ?? [])
|
|
8939
9200
|
media.push({ url: await resolveApproveMedia(String(url)) });
|
|
9201
|
+
// The social-card poster of the share page. Omit it and the server extracts a
|
|
9202
|
+
// frame (~1s in) from --video and files it under /files/approved.
|
|
9203
|
+
if (parsed.values.thumbnail) {
|
|
9204
|
+
media.push({ url: await resolveApproveMedia(String(parsed.values.thumbnail)), kind: "image", role: "thumbnail" });
|
|
9205
|
+
}
|
|
8940
9206
|
if (media.length === 0) {
|
|
8941
9207
|
console.warn(`${DIM}[vidfarm] no --video / --media given; approving a text-only post.${RESET}`);
|
|
8942
9208
|
}
|
|
@@ -9302,6 +9568,15 @@ async function runCostModeCommand(argv) {
|
|
|
9302
9568
|
if (resolved.mode === "minimize") {
|
|
9303
9569
|
console.log(`${DIM}In minimize, ${interaction.mode === "interactive" ? "you're already on" : "offer"} the sweet spot: minimize + interactive = $0 videos that still get custom art (free web image gen → vidfarm sticker-pack) and hand-picked clips (vidfarm handoff raws).${RESET}`);
|
|
9304
9570
|
}
|
|
9571
|
+
// The one exception worth naming out loud: image generation the AGENT already
|
|
9572
|
+
// owns is free custom art with NO manual step, so minimize keeps it.
|
|
9573
|
+
if (resolved.mode === "minimize" || resolved.mode === "hybrid") {
|
|
9574
|
+
const detection = detectHostAgent();
|
|
9575
|
+
const verdict = detection.host
|
|
9576
|
+
? `${detection.host.name} — image generation ${detection.availability}`
|
|
9577
|
+
: "host agent not detected";
|
|
9578
|
+
console.log(`${DIM}Image generation included in YOUR agent's own subscription is free in every mode, minimize included (${verdict}). Check: ${BOLD}vidfarm agent-image --check${RESET}${DIM} · brief: vidfarm agent-image "<subject>".${RESET}`);
|
|
9579
|
+
}
|
|
9305
9580
|
}
|
|
9306
9581
|
// `vidfarm storyboard [dir]` — the PLAN pass, before any animation work.
|
|
9307
9582
|
// Storyboarding is a first-class part of the composition format now: a project
|
|
@@ -9853,6 +10128,106 @@ async function runHandoffCommand(argv) {
|
|
|
9853
10128
|
}
|
|
9854
10129
|
throw new Error('handoff needs a kind: `vidfarm handoff image --theme "…" --items "a,b,c"` (free web image gen → sticker pack) or `vidfarm handoff raws --keywords "…"` (manual clip sourcing).');
|
|
9855
10130
|
}
|
|
10131
|
+
// `vidfarm agent-image "<subject>"` — the brief for image generation the AGENT
|
|
10132
|
+
// runs on its OWN subscription. Vidfarm charges nothing and the provider charges
|
|
10133
|
+
// nothing extra, so this rung is allowed in EVERY cost mode, `minimize`
|
|
10134
|
+
// included. Antigravity / Gemini CLI and Codex / ChatGPT include an image tool;
|
|
10135
|
+
// Claude Code does not (Anthropic ships no image model) — those runs fall back
|
|
10136
|
+
// to stock, IconScout, a human hand-off, or a confirmed billed generation.
|
|
10137
|
+
async function runAgentImageCommand(argv) {
|
|
10138
|
+
const parsed = parseArgs({
|
|
10139
|
+
args: argv,
|
|
10140
|
+
allowPositionals: true,
|
|
10141
|
+
options: {
|
|
10142
|
+
...commonOptions(),
|
|
10143
|
+
theme: { type: "string" },
|
|
10144
|
+
items: { type: "string" },
|
|
10145
|
+
style: { type: "string" },
|
|
10146
|
+
"key-color": { type: "string" },
|
|
10147
|
+
"out-dir": { type: "string" },
|
|
10148
|
+
out: { type: "string" },
|
|
10149
|
+
folder: { type: "string" },
|
|
10150
|
+
grid: { type: "string" },
|
|
10151
|
+
single: { type: "boolean", default: false },
|
|
10152
|
+
zoned: { type: "boolean", default: false },
|
|
10153
|
+
check: { type: "boolean", default: false }
|
|
10154
|
+
}
|
|
10155
|
+
});
|
|
10156
|
+
const json = Boolean(parsed.values.json);
|
|
10157
|
+
const detection = detectHostAgent();
|
|
10158
|
+
// `--check` (and a bare invocation) answers only "do I have this at all?".
|
|
10159
|
+
if (parsed.values.check || (!parsed.values.theme && !parsed.values.items && !parsed.positionals.length)) {
|
|
10160
|
+
if (json) {
|
|
10161
|
+
return printJson({
|
|
10162
|
+
ok: true,
|
|
10163
|
+
host: detection.host?.id ?? null,
|
|
10164
|
+
host_name: detection.host?.name ?? null,
|
|
10165
|
+
availability: detection.availability,
|
|
10166
|
+
matched_env: detection.matched,
|
|
10167
|
+
known_hosts: KNOWN_HOST_AGENTS.map((h) => ({ id: h.id, name: h.name, availability: h.availability, note: h.note }))
|
|
10168
|
+
});
|
|
10169
|
+
}
|
|
10170
|
+
console.log(`${BOLD}Agent-included image generation${RESET}`);
|
|
10171
|
+
console.log(`${DIM}Vidfarm does not need to sell you an image model if the agent driving this terminal already has one.${RESET}\n`);
|
|
10172
|
+
if (detection.host) {
|
|
10173
|
+
const flag = detection.availability === "included" ? GREEN : detection.availability === "none" ? YELLOW : DIM;
|
|
10174
|
+
console.log(`Detected: ${flag}${detection.host.name}${RESET} — image generation ${BOLD}${detection.availability}${RESET}`);
|
|
10175
|
+
console.log(`${DIM}${detection.host.note}${RESET}`);
|
|
10176
|
+
console.log(`${DIM}Matched env: ${detection.matched.join(", ")}${RESET}\n`);
|
|
10177
|
+
}
|
|
10178
|
+
else {
|
|
10179
|
+
console.log(`${DIM}Host agent not detected from the environment.${RESET}\n`);
|
|
10180
|
+
}
|
|
10181
|
+
for (const h of KNOWN_HOST_AGENTS) {
|
|
10182
|
+
console.log(` ${h.availability === "included" ? GREEN + "✓" : h.availability === "none" ? YELLOW + "✗" : DIM + "?"}${RESET} ${h.name} ${DIM}— ${h.availability}${RESET}`);
|
|
10183
|
+
}
|
|
10184
|
+
console.log(`\n${DIM}The env check is a hint only. The authority is your own tool list — if you have an image tool, you have this.${RESET}`);
|
|
10185
|
+
console.log(`${DIM}Brief: ${BOLD}vidfarm agent-image "<subject>" --items "a,b,c"${RESET}${DIM} · free in EVERY cost mode, minimize included.${RESET}`);
|
|
10186
|
+
return;
|
|
10187
|
+
}
|
|
10188
|
+
const splitList = (raw) => (raw ?? "").split(/\s*[;,]\s*/).map((s) => s.trim()).filter(Boolean);
|
|
10189
|
+
const theme = parsed.values.theme?.trim() || parsed.positionals.join(" ").trim();
|
|
10190
|
+
const items = splitList(parsed.values.items);
|
|
10191
|
+
if (!theme && !items.length) {
|
|
10192
|
+
throw new Error('agent-image needs a subject: `vidfarm agent-image "cozy farm props" --items "barn,tractor,hay bale"`.');
|
|
10193
|
+
}
|
|
10194
|
+
const brief = buildAgentImageBrief({
|
|
10195
|
+
theme: theme || items.join(", "),
|
|
10196
|
+
items,
|
|
10197
|
+
style: parsed.values.style,
|
|
10198
|
+
keyColor: parsed.values["key-color"],
|
|
10199
|
+
outDir: parsed.values["out-dir"],
|
|
10200
|
+
outFile: parsed.values.out,
|
|
10201
|
+
folder: parsed.values.folder,
|
|
10202
|
+
grid: parsed.values.grid,
|
|
10203
|
+
pack: parsed.values.single ? false : undefined,
|
|
10204
|
+
zoned: Boolean(parsed.values.zoned),
|
|
10205
|
+
detection
|
|
10206
|
+
});
|
|
10207
|
+
if (json) {
|
|
10208
|
+
return printJson({
|
|
10209
|
+
ok: true,
|
|
10210
|
+
host: detection.host?.id ?? null,
|
|
10211
|
+
availability: brief.availability,
|
|
10212
|
+
out_file: brief.outFile,
|
|
10213
|
+
prompt: brief.prompt,
|
|
10214
|
+
steps: brief.steps,
|
|
10215
|
+
follow_up: brief.followUp,
|
|
10216
|
+
fallbacks: brief.fallbacks,
|
|
10217
|
+
key_color: brief.keyColor,
|
|
10218
|
+
key_color_note: brief.keyColorNote
|
|
10219
|
+
});
|
|
10220
|
+
}
|
|
10221
|
+
if (detection.availability === "none") {
|
|
10222
|
+
console.log(`${YELLOW}${BOLD}This host has no image tool of its own — read the fallbacks at the bottom first.${RESET}`);
|
|
10223
|
+
console.log(`${DIM}The prompt is still below: it is the same prompt for a human hand-off or a billed generation.${RESET}\n`);
|
|
10224
|
+
}
|
|
10225
|
+
else {
|
|
10226
|
+
console.log(`${BOLD}Generate this yourself — it is already in your subscription.${RESET}`);
|
|
10227
|
+
console.log(`${DIM}Costs $0 on Vidfarm and $0 extra on the provider, so it is allowed in cost mode minimize.${RESET}\n`);
|
|
10228
|
+
}
|
|
10229
|
+
console.log(formatAgentImageBrief(brief, detection));
|
|
10230
|
+
}
|
|
9856
10231
|
// `vidfarm consult [step]` — the consultation / brainstorm chain, run KEYLESS
|
|
9857
10232
|
// and LOCAL. The cloud version of each step is a `brainstorm/*` primitive that
|
|
9858
10233
|
// needs a provider key or wallet credits; this prints the identical prompt for
|
|
@@ -9892,7 +10267,11 @@ async function runConsultCommand(argv) {
|
|
|
9892
10267
|
}
|
|
9893
10268
|
const first = plan.find((row) => !row.done) ?? plan[0];
|
|
9894
10269
|
console.log(`\n ${context.exists ? `${GREEN}✓${RESET}` : `${DIM}·${RESET}`} ${"saved answers".padEnd(28)} ${DIM}→ ${context.file}${context.exists ? " (read it before you ask anything)" : " (written by the first step)"}${RESET}`);
|
|
9895
|
-
|
|
10270
|
+
// The easier win comes first. A director who has seen 20 titled ideas
|
|
10271
|
+
// answers the interview better than one asked to think cold.
|
|
10272
|
+
console.log(`\nBefore the interview: ${BOLD}vidfarm ideas --topic "<your offer in one line>"${RESET}`);
|
|
10273
|
+
console.log(`${DIM}20+ video ideas, offline and free, for one sentence of input → content-ideas.md. Do that first.${RESET}`);
|
|
10274
|
+
console.log(`\nThen here: ${BOLD}${first.command}${RESET}`);
|
|
9896
10275
|
console.log(`${DIM}In a hurry: vidfarm consult coldstart --short (6 fixed questions instead of the full interview)${RESET}`);
|
|
9897
10276
|
console.log(`${DIM}Run every step from ONE working folder (--dir), so the artifacts and ${context.file} stay together.${RESET}`);
|
|
9898
10277
|
console.log(`${DIM}Also: vidfarm consult placement --video <path|url> (native product placement inside an existing video)${RESET}`);
|
|
@@ -10733,6 +11112,30 @@ const DIRECTORY_HELP = `vidfarm directory — browse the unified file tree (/fil
|
|
|
10733
11112
|
e.g. vidfarm directory save-url "https://…/inpaint-out.png" --folder inpaints --as hero.png
|
|
10734
11113
|
(server fetches the URL — handles cross-origin S3; the CLI twin of the pop-panel "Save to Files")
|
|
10735
11114
|
|
|
11115
|
+
directory share <path> Mint a PUBLIC link onto one subfolder → POST /api/v1/user/me/directory/shares
|
|
11116
|
+
--mode read|upload|edit read = browse + vector search (default) · upload = + add files/folders,
|
|
11117
|
+
never delete · edit = + rename & delete. Scoped to <path> and everything
|
|
11118
|
+
under it — never a sibling or a parent.
|
|
11119
|
+
--label <text> Name the link so you can tell your links apart
|
|
11120
|
+
--json Raw JSON response
|
|
11121
|
+
Works on ANY root (/files·/temp·/raws·/approved; /projects is read-only so it takes --mode read).
|
|
11122
|
+
Only /files and /temp accept uploads — the other roots have no plain-upload backend, so an upload
|
|
11123
|
+
link there only creates folders. PAID: minting a link needs a paid plan; VISITING one needs no account.
|
|
11124
|
+
e.g. vidfarm directory share /files/crowdsourced --mode upload --label "Contributor drop box"
|
|
11125
|
+
vidfarm directory share /raws/b-roll --mode read --label "Editor library"
|
|
11126
|
+
|
|
11127
|
+
directory shares [path] List your share links (optionally for one folder) → GET /api/v1/user/me/directory/shares
|
|
11128
|
+
--path <path> --json
|
|
11129
|
+
|
|
11130
|
+
directory share-update <token> Change or switch off a link → PATCH /api/v1/user/me/directory/shares/:token
|
|
11131
|
+
--mode read|upload|edit Re-scope what holders can do
|
|
11132
|
+
--label <text> Rename the link
|
|
11133
|
+
--disable | --enable Cut off access instantly (reversible — the token survives)
|
|
11134
|
+
--json
|
|
11135
|
+
|
|
11136
|
+
directory unshare <token> Delete a link for good → DELETE /api/v1/user/me/directory/shares/:token
|
|
11137
|
+
--json (to keep the record, prefer share-update --disable)
|
|
11138
|
+
|
|
10736
11139
|
Aliases: directory | dir (move|mv · copy|cp) · Auth: --api-key <key> or VIDFARM_API_KEY, --host <url>`;
|
|
10737
11140
|
async function runDirectoryCommand(argv) {
|
|
10738
11141
|
const sub = argv[0];
|
|
@@ -10756,6 +11159,14 @@ async function runDirectoryCommand(argv) {
|
|
|
10756
11159
|
case "from-url":
|
|
10757
11160
|
case "import-url":
|
|
10758
11161
|
return runDirectorySaveUrl(rest);
|
|
11162
|
+
case "shares":
|
|
11163
|
+
return runDirectorySharesList(rest);
|
|
11164
|
+
case "share":
|
|
11165
|
+
return runDirectoryShareCreate(rest);
|
|
11166
|
+
case "share-update":
|
|
11167
|
+
return runDirectoryShareUpdate(rest);
|
|
11168
|
+
case "unshare":
|
|
11169
|
+
return runDirectoryShareDelete(rest);
|
|
10759
11170
|
case undefined:
|
|
10760
11171
|
case "help":
|
|
10761
11172
|
case "--help":
|
|
@@ -10929,6 +11340,140 @@ async function runDirectoryCopy(argv) {
|
|
|
10929
11340
|
const suffix = typeof copied === "number" ? ` ${DIM}(${copied} file${copied === 1 ? "" : "s"})${RESET}` : "";
|
|
10930
11341
|
console.log(`${GREEN}✓${RESET} Copied to ${BOLD}${newPath}${RESET}${suffix} ${DIM}[${space}]${RESET}`);
|
|
10931
11342
|
}
|
|
11343
|
+
// ── directory share tokens ───────────────────────────────────────────────────
|
|
11344
|
+
// Public links onto ONE subfolder of any root. Three modes: read (browse +
|
|
11345
|
+
// vector search), upload (+ add files/folders, never delete), edit (+ rename &
|
|
11346
|
+
// delete). Minting a link is a PAID action; visiting one needs no account at
|
|
11347
|
+
// all. Shares live in the cloud directory, so these never fan out to --both.
|
|
11348
|
+
// A hoisted function, not a const: main() runs at module top-level ABOVE this
|
|
11349
|
+
// point, so a `const` declared down here is still in its temporal dead zone
|
|
11350
|
+
// when a command handler reads it.
|
|
11351
|
+
function shareModes() {
|
|
11352
|
+
return ["read", "upload", "edit"];
|
|
11353
|
+
}
|
|
11354
|
+
function shareLine(share) {
|
|
11355
|
+
const mode = String(share?.mode ?? "read");
|
|
11356
|
+
const state = share?.disabled ? `${DIM}(disabled)${RESET}` : "";
|
|
11357
|
+
const label = share?.label ? `${BOLD}${share.label}${RESET} ` : "";
|
|
11358
|
+
return [
|
|
11359
|
+
`${label}${DIM}${share?.path ?? ""}${RESET} [${mode}] ${state}`,
|
|
11360
|
+
` ${share?.url ?? ""}`,
|
|
11361
|
+
` ${DIM}${share?.token ?? ""}${RESET}`
|
|
11362
|
+
].join("\n");
|
|
11363
|
+
}
|
|
11364
|
+
async function runDirectorySharesList(argv) {
|
|
11365
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), path: { type: "string" } } });
|
|
11366
|
+
const ctx = commonContext(parsed.values);
|
|
11367
|
+
const pathFilter = parsed.values.path ?? parsed.positionals[0];
|
|
11368
|
+
const space = targetSpaces(ctx.target)[0];
|
|
11369
|
+
const result = await dispatch(ctx, { method: "GET", path: "/api/v1/user/me/directory/shares", query: pathFilter ? { path: pathFilter } : {} }, space);
|
|
11370
|
+
assertApiOk(result, "directory shares");
|
|
11371
|
+
if (ctx.json) {
|
|
11372
|
+
printJson(result.json ?? result.text);
|
|
11373
|
+
return;
|
|
11374
|
+
}
|
|
11375
|
+
const shares = (result.json?.shares ?? []);
|
|
11376
|
+
if (!shares.length) {
|
|
11377
|
+
console.log(`${DIM}No share links${pathFilter ? ` for ${pathFilter}` : ""}. Create one: vidfarm directory share <path> --mode read${RESET}`);
|
|
11378
|
+
return;
|
|
11379
|
+
}
|
|
11380
|
+
console.log(shares.map(shareLine).join("\n\n"));
|
|
11381
|
+
console.log(`\n${DIM}${shares.length} link${shares.length === 1 ? "" : "s"}${RESET}`);
|
|
11382
|
+
}
|
|
11383
|
+
async function runDirectoryShareCreate(argv) {
|
|
11384
|
+
const parsed = parseArgs({
|
|
11385
|
+
args: argv,
|
|
11386
|
+
allowPositionals: true,
|
|
11387
|
+
options: { ...commonOptions(), mode: { type: "string" }, label: { type: "string" } }
|
|
11388
|
+
});
|
|
11389
|
+
const ctx = commonContext(parsed.values);
|
|
11390
|
+
const targetPath = parsed.positionals[0];
|
|
11391
|
+
if (!targetPath) {
|
|
11392
|
+
throw new Error('directory share requires a folder path: vidfarm directory share <path> [--mode read|upload|edit] [--label <text>]\n e.g. vidfarm directory share /files/crowdsourced --mode upload --label "Contributor drop box"\n vidfarm directory share /raws/b-roll --mode read --label "Editor library"');
|
|
11393
|
+
}
|
|
11394
|
+
const mode = parsed.values.mode ?? "read";
|
|
11395
|
+
if (!shareModes().includes(mode)) {
|
|
11396
|
+
throw new Error(`--mode must be ${shareModes().join(" | ")} (got "${mode}")`);
|
|
11397
|
+
}
|
|
11398
|
+
const space = targetSpaces(ctx.target)[0];
|
|
11399
|
+
const result = await dispatch(ctx, {
|
|
11400
|
+
method: "POST",
|
|
11401
|
+
path: "/api/v1/user/me/directory/shares",
|
|
11402
|
+
body: { path: targetPath, mode, ...(parsed.values.label ? { label: String(parsed.values.label) } : {}) }
|
|
11403
|
+
}, space);
|
|
11404
|
+
assertApiOk(result, "directory share");
|
|
11405
|
+
if (ctx.json) {
|
|
11406
|
+
printJson(result.json ?? result.text);
|
|
11407
|
+
return;
|
|
11408
|
+
}
|
|
11409
|
+
const share = result.json?.share ?? {};
|
|
11410
|
+
console.log(`${GREEN}✓${RESET} Shared ${BOLD}${share.path ?? targetPath}${RESET} as ${BOLD}${mode}${RESET} ${DIM}[${space}]${RESET}`);
|
|
11411
|
+
console.log(` ${share.url ?? ""}`);
|
|
11412
|
+
console.log(` ${DIM}${share.token ?? ""} · disable it with: vidfarm directory share-update ${share.token ?? "<token>"} --disable${RESET}`);
|
|
11413
|
+
}
|
|
11414
|
+
async function runDirectoryShareUpdate(argv) {
|
|
11415
|
+
const parsed = parseArgs({
|
|
11416
|
+
args: argv,
|
|
11417
|
+
allowPositionals: true,
|
|
11418
|
+
options: {
|
|
11419
|
+
...commonOptions(),
|
|
11420
|
+
mode: { type: "string" },
|
|
11421
|
+
label: { type: "string" },
|
|
11422
|
+
disable: { type: "boolean" },
|
|
11423
|
+
enable: { type: "boolean" }
|
|
11424
|
+
}
|
|
11425
|
+
});
|
|
11426
|
+
const ctx = commonContext(parsed.values);
|
|
11427
|
+
const token = parsed.positionals[0];
|
|
11428
|
+
if (!token) {
|
|
11429
|
+
throw new Error('directory share-update requires a token: vidfarm directory share-update <token> [--mode read|upload|edit] [--label <text>] [--disable|--enable]');
|
|
11430
|
+
}
|
|
11431
|
+
const mode = parsed.values.mode;
|
|
11432
|
+
if (mode && !shareModes().includes(mode)) {
|
|
11433
|
+
throw new Error(`--mode must be ${shareModes().join(" | ")} (got "${mode}")`);
|
|
11434
|
+
}
|
|
11435
|
+
if (parsed.values.disable && parsed.values.enable) {
|
|
11436
|
+
throw new Error("Pass --disable or --enable, not both.");
|
|
11437
|
+
}
|
|
11438
|
+
const body = {};
|
|
11439
|
+
if (mode)
|
|
11440
|
+
body.mode = mode;
|
|
11441
|
+
if (parsed.values.label !== undefined)
|
|
11442
|
+
body.label = String(parsed.values.label);
|
|
11443
|
+
if (parsed.values.disable)
|
|
11444
|
+
body.disabled = true;
|
|
11445
|
+
if (parsed.values.enable)
|
|
11446
|
+
body.disabled = false;
|
|
11447
|
+
if (!Object.keys(body).length) {
|
|
11448
|
+
throw new Error("Nothing to change — pass --mode, --label, --disable or --enable.");
|
|
11449
|
+
}
|
|
11450
|
+
const space = targetSpaces(ctx.target)[0];
|
|
11451
|
+
const result = await dispatch(ctx, { method: "PATCH", path: `/api/v1/user/me/directory/shares/${encodeURIComponent(token)}`, body }, space);
|
|
11452
|
+
assertApiOk(result, "directory share-update");
|
|
11453
|
+
if (ctx.json) {
|
|
11454
|
+
printJson(result.json ?? result.text);
|
|
11455
|
+
return;
|
|
11456
|
+
}
|
|
11457
|
+
const share = result.json?.share ?? {};
|
|
11458
|
+
const state = share.disabled ? `${BOLD}disabled${RESET}` : `${BOLD}live${RESET}`;
|
|
11459
|
+
console.log(`${GREEN}✓${RESET} Share ${DIM}${token}${RESET} is ${state} as ${BOLD}${share.mode ?? "read"}${RESET} ${DIM}[${space}]${RESET}`);
|
|
11460
|
+
}
|
|
11461
|
+
async function runDirectoryShareDelete(argv) {
|
|
11462
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions() } });
|
|
11463
|
+
const ctx = commonContext(parsed.values);
|
|
11464
|
+
const token = parsed.positionals[0];
|
|
11465
|
+
if (!token) {
|
|
11466
|
+
throw new Error('directory unshare requires a token: vidfarm directory unshare <token>\n (to keep the record but cut off access, use: vidfarm directory share-update <token> --disable)');
|
|
11467
|
+
}
|
|
11468
|
+
const space = targetSpaces(ctx.target)[0];
|
|
11469
|
+
const result = await dispatch(ctx, { method: "DELETE", path: `/api/v1/user/me/directory/shares/${encodeURIComponent(token)}` }, space);
|
|
11470
|
+
assertApiOk(result, "directory unshare");
|
|
11471
|
+
if (ctx.json) {
|
|
11472
|
+
printJson(result.json ?? result.text);
|
|
11473
|
+
return;
|
|
11474
|
+
}
|
|
11475
|
+
console.log(`${GREEN}✓${RESET} Deleted share ${DIM}${token}${RESET} ${DIM}[${space}]${RESET}`);
|
|
11476
|
+
}
|
|
10932
11477
|
// Save a durable media URL (e.g. a finished job's output) INTO My Files at a
|
|
10933
11478
|
// folder — the CLI twin of the pop-panel "Save to Files" picker. Server fetches
|
|
10934
11479
|
// the URL (handles cross-origin S3), so no local download round-trip needed.
|
|
@@ -12092,7 +12637,12 @@ async function runIdeasCommand(argv) {
|
|
|
12092
12637
|
family: { type: "string" },
|
|
12093
12638
|
topic: { type: "string" },
|
|
12094
12639
|
count: { type: "string" },
|
|
12095
|
-
families: { type: "boolean", default: false }
|
|
12640
|
+
families: { type: "boolean", default: false },
|
|
12641
|
+
angles: { type: "boolean", default: false },
|
|
12642
|
+
angle: { type: "string" },
|
|
12643
|
+
stages: { type: "boolean", default: false },
|
|
12644
|
+
stage: { type: "string" },
|
|
12645
|
+
grid: { type: "boolean", default: false }
|
|
12096
12646
|
}
|
|
12097
12647
|
});
|
|
12098
12648
|
const json = Boolean(parsed.values.json);
|
|
@@ -12100,6 +12650,139 @@ async function runIdeasCommand(argv) {
|
|
|
12100
12650
|
const { frames, families } = loadIdeaBank();
|
|
12101
12651
|
if (!frames.length)
|
|
12102
12652
|
throw new Error("No bundled content-ideas reference found next to this devcli install. Fetch it with `vidfarm skills add vidfarm`.");
|
|
12653
|
+
// The three axes of an idea live in one reference, so they live behind one
|
|
12654
|
+
// command. A director who only ever sees the frames posts 30 videos at one
|
|
12655
|
+
// awareness stage from one angle, which is the exact failure this expansion
|
|
12656
|
+
// exists to prevent — so the stage and angle banks are flags here, not a
|
|
12657
|
+
// separate command nobody discovers.
|
|
12658
|
+
const ladder = loadAwarenessLadder();
|
|
12659
|
+
const angleBank = loadAngleBank();
|
|
12660
|
+
if (parsed.values.stages || parsed.values.stage) {
|
|
12661
|
+
const wanted = String(parsed.values.stage ?? "").trim().toLowerCase();
|
|
12662
|
+
const picked = wanted
|
|
12663
|
+
? ladder.filter((entry) => entry.stage.toLowerCase().includes(wanted) || String(entry.index) === wanted)
|
|
12664
|
+
: ladder;
|
|
12665
|
+
if (wanted && !picked.length) {
|
|
12666
|
+
throw new Error(`No awareness stage matches "${wanted}". Stages: ${ladder.map((entry) => `${entry.index} ${entry.stage}`).join(", ")}.`);
|
|
12667
|
+
}
|
|
12668
|
+
if (json)
|
|
12669
|
+
return printJson({ ok: true, offline: true, topic: topic || null, stages: picked });
|
|
12670
|
+
console.log(`${BOLD}The awareness ladder${RESET} ${DIM}(${picked.length} of ${ladder.length} stages · what the viewer already knows)${RESET}\n`);
|
|
12671
|
+
for (const entry of picked) {
|
|
12672
|
+
console.log(` ${GREEN}${entry.index} · ${entry.stage}${RESET} ${DIM}— ${entry.summary}${RESET}`);
|
|
12673
|
+
for (const field of entry.fields)
|
|
12674
|
+
console.log(` ${BOLD}${field.label}:${RESET} ${field.value}`);
|
|
12675
|
+
console.log("");
|
|
12676
|
+
}
|
|
12677
|
+
console.log(`${DIM}The stage decides the first ten seconds AND the ask. Do not sell at stage 1 or 2.${RESET}`);
|
|
12678
|
+
console.log(`${DIM}A month of posts should cover the ladder — default mix + the grid: vidfarm skill show content-ideas${RESET}`);
|
|
12679
|
+
console.log(`${DIM}Angles for a stage: vidfarm ideas --angles · pair them up: vidfarm ideas --grid --topic "<offer>"${RESET}`);
|
|
12680
|
+
return;
|
|
12681
|
+
}
|
|
12682
|
+
if (parsed.values.angles || parsed.values.angle) {
|
|
12683
|
+
const wanted = String(parsed.values.angle ?? "").trim().toLowerCase();
|
|
12684
|
+
const picked = wanted
|
|
12685
|
+
? angleBank.angles.filter((entry) => entry.family.toLowerCase().includes(wanted) || entry.frame.toLowerCase().includes(wanted))
|
|
12686
|
+
: angleBank.angles;
|
|
12687
|
+
if (wanted && !picked.length) {
|
|
12688
|
+
throw new Error(`No angle matches "${wanted}". Angle families: ${angleBank.families.join(", ")} (see \`vidfarm ideas --angles\`).`);
|
|
12689
|
+
}
|
|
12690
|
+
if (json) {
|
|
12691
|
+
return printJson({
|
|
12692
|
+
ok: true,
|
|
12693
|
+
offline: true,
|
|
12694
|
+
topic: topic || null,
|
|
12695
|
+
total_angles: angleBank.angles.length,
|
|
12696
|
+
families: angleBank.families,
|
|
12697
|
+
angles: picked,
|
|
12698
|
+
method: "references/content-ideas.md"
|
|
12699
|
+
});
|
|
12700
|
+
}
|
|
12701
|
+
console.log(`${BOLD}Problem angles${RESET} ${DIM}(${picked.length} of ${angleBank.angles.length} · which side of the problem the video comes from)${RESET}`);
|
|
12702
|
+
if (topic)
|
|
12703
|
+
console.log(`${DIM}Topic: ${RESET}${topic}`);
|
|
12704
|
+
console.log("");
|
|
12705
|
+
let angleGroup = "";
|
|
12706
|
+
for (const entry of picked) {
|
|
12707
|
+
if (entry.family !== angleGroup) {
|
|
12708
|
+
angleGroup = entry.family;
|
|
12709
|
+
console.log(` ${BOLD}${angleGroup}${RESET}`);
|
|
12710
|
+
}
|
|
12711
|
+
console.log(` ${GREEN}${entry.frame}${RESET}${entry.note ? ` ${DIM}— ${entry.note}${RESET}` : ""}`);
|
|
12712
|
+
}
|
|
12713
|
+
console.log("");
|
|
12714
|
+
console.log(`${DIM}"I already covered that topic" → hold the frame, change the angle. Same research, new video.${RESET}`);
|
|
12715
|
+
console.log(`${DIM}Pair angles with frames and stages: vidfarm ideas --grid --topic "<offer>"${RESET}`);
|
|
12716
|
+
return;
|
|
12717
|
+
}
|
|
12718
|
+
if (parsed.values.grid) {
|
|
12719
|
+
// The grid is the honest answer to "give me 30 videos": vary TWO axes.
|
|
12720
|
+
// Walking frames, angles and stages at different strides keeps every row a
|
|
12721
|
+
// different combination instead of 30 rows of one frame family.
|
|
12722
|
+
const rows = Number(parsed.values.count ?? 20);
|
|
12723
|
+
const total = Number.isFinite(rows) && rows > 0 ? Math.min(rows, 200) : 20;
|
|
12724
|
+
if (!angleBank.angles.length || !ladder.length) {
|
|
12725
|
+
throw new Error("The bundled content-ideas reference has no angle or awareness bank. Update it with `vidfarm skills add vidfarm`.");
|
|
12726
|
+
}
|
|
12727
|
+
// Stage order is not round-robin: this ten-row cycle reproduces the default
|
|
12728
|
+
// monthly mix from the reference (3 unaware, 3 problem-aware, 2 solution-
|
|
12729
|
+
// aware, 1 product-aware, 1 most-aware), so a 30-row grid is already a
|
|
12730
|
+
// balanced month instead of six videos of each.
|
|
12731
|
+
const mixCycle = [1, 2, 1, 3, 2, 4, 1, 2, 3, 5];
|
|
12732
|
+
// Each stage names the frames and angles it can actually carry. Pairing
|
|
12733
|
+
// "the price objection" with a stage-1 video would hand the director a row
|
|
12734
|
+
// the reference itself calls wrong, so draw from the stage's own lists and
|
|
12735
|
+
// fall back to the full banks only if a list is missing.
|
|
12736
|
+
const listed = (stage, label) => (stage.fields.find((field) => field.label.toLowerCase().startsWith(label))?.value ?? "")
|
|
12737
|
+
.split(" · ").map((item) => item.trim().toLowerCase()).filter(Boolean);
|
|
12738
|
+
const gridSeen = new Set();
|
|
12739
|
+
const grid = Array.from({ length: total }, (_, i) => {
|
|
12740
|
+
const stage = ladder.find((entry) => entry.index === mixCycle[i % mixCycle.length]) ?? ladder[i % ladder.length];
|
|
12741
|
+
const round = Math.floor(i / mixCycle.length);
|
|
12742
|
+
const fitFrames = listed(stage, "frames");
|
|
12743
|
+
const fitAngles = listed(stage, "angles");
|
|
12744
|
+
const framePool = frames.filter((entry) => fitFrames.includes(entry.frame.toLowerCase()));
|
|
12745
|
+
const anglePool = angleBank.angles.filter((entry) => fitAngles.includes(entry.frame.toLowerCase()));
|
|
12746
|
+
const usableFrames = framePool.length ? framePool : frames;
|
|
12747
|
+
const usableAngles = anglePool.length ? anglePool : angleBank.angles;
|
|
12748
|
+
// Strides alone collide once a pool size divides one of them, and a grid
|
|
12749
|
+
// that prints the same row twice is exactly the repetition this command
|
|
12750
|
+
// sells against — so walk forward until the triple is new.
|
|
12751
|
+
let frame = usableFrames[(i + round) % usableFrames.length];
|
|
12752
|
+
let angle = usableAngles[(i * 3 + round) % usableAngles.length];
|
|
12753
|
+
for (let bump = 0; bump < usableFrames.length * usableAngles.length; bump += 1) {
|
|
12754
|
+
if (!gridSeen.has(`${stage.index}|${frame.frame}|${angle.frame}`))
|
|
12755
|
+
break;
|
|
12756
|
+
angle = usableAngles[(i * 3 + round + bump + 1) % usableAngles.length];
|
|
12757
|
+
if ((bump + 1) % usableAngles.length === 0)
|
|
12758
|
+
frame = usableFrames[(i + round + bump + 1) % usableFrames.length];
|
|
12759
|
+
}
|
|
12760
|
+
gridSeen.add(`${stage.index}|${frame.frame}|${angle.frame}`);
|
|
12761
|
+
return {
|
|
12762
|
+
stage: `${stage.index} · ${stage.stage}`,
|
|
12763
|
+
angle: angle.frame,
|
|
12764
|
+
angle_family: angle.family,
|
|
12765
|
+
frame: frame.frame,
|
|
12766
|
+
frame_family: frame.family,
|
|
12767
|
+
starter: topic ? joinIdeaFrame(frame.frame, topic) : null
|
|
12768
|
+
};
|
|
12769
|
+
});
|
|
12770
|
+
if (json)
|
|
12771
|
+
return printJson({ ok: true, offline: true, topic: topic || null, rows: grid, method: "references/content-ideas.md" });
|
|
12772
|
+
console.log(`${BOLD}Idea grid${RESET} ${DIM}(${total} combinations · stage × angle × frame · offline, free, no AI call)${RESET}`);
|
|
12773
|
+
if (topic)
|
|
12774
|
+
console.log(`${DIM}Topic: ${RESET}${topic}`);
|
|
12775
|
+
console.log("");
|
|
12776
|
+
for (const row of grid) {
|
|
12777
|
+
console.log(` ${DIM}${row.stage.padEnd(18)}${RESET}${GREEN}${row.angle}${RESET}`);
|
|
12778
|
+
console.log(` ${row.starter ?? row.frame}`);
|
|
12779
|
+
}
|
|
12780
|
+
console.log("");
|
|
12781
|
+
console.log(`${DIM}Each row is a DIFFERENT video, not a rewrite — the stage and the angle both moved.${RESET}`);
|
|
12782
|
+
console.log(`${DIM}Sharpen each into a real title, keep the stage label on it, then write hook/loop/payoff/bait.${RESET}`);
|
|
12783
|
+
console.log(`${DIM}Method, the monthly stage mix, and a worked example: vidfarm skill show content-ideas${RESET}`);
|
|
12784
|
+
return;
|
|
12785
|
+
}
|
|
12103
12786
|
if (parsed.values.families) {
|
|
12104
12787
|
if (json)
|
|
12105
12788
|
return printJson({ ok: true, families, frames: frames.length });
|
|
@@ -12147,6 +12830,9 @@ async function runIdeasCommand(argv) {
|
|
|
12147
12830
|
total_frames: frames.length,
|
|
12148
12831
|
families,
|
|
12149
12832
|
frames: picked.map((entry) => ({ frame: entry.frame, family: entry.family, starter: topic ? joinIdeaFrame(entry.frame, topic) : null })),
|
|
12833
|
+
total_angles: angleBank.angles.length,
|
|
12834
|
+
angle_families: angleBank.families,
|
|
12835
|
+
awareness_stages: ladder.map((entry) => ({ index: entry.index, stage: entry.stage, summary: entry.summary })),
|
|
12150
12836
|
method: "references/content-ideas.md"
|
|
12151
12837
|
});
|
|
12152
12838
|
}
|
|
@@ -12170,9 +12856,18 @@ async function runIdeasCommand(argv) {
|
|
|
12170
12856
|
else {
|
|
12171
12857
|
console.log(`${DIM}Pour ONE topic into a frame to get a video: 'the rise of' + your niche. Add --topic "<offer>" to see it.${RESET}`);
|
|
12172
12858
|
}
|
|
12859
|
+
console.log(`${DIM}A frame is only ONE axis. The other two are here too, and a set that freezes them repeats:${RESET}`);
|
|
12860
|
+
console.log(`${DIM} vidfarm ideas --stages ${RESET}${ladder.length} awareness stages ${DIM}— what the viewer knows, and what you may ask for${RESET}`);
|
|
12861
|
+
console.log(`${DIM} vidfarm ideas --angles ${RESET}${angleBank.angles.length} problem angles ${DIM}— which side of the problem the video comes from${RESET}`);
|
|
12862
|
+
console.log(`${DIM} vidfarm ideas --grid --topic "<offer>"${RESET} ${DIM}— stage × angle × frame, one row per video${RESET}`);
|
|
12173
12863
|
console.log(`${DIM}Method + frame→format notes: vidfarm skill show content-ideas · families: vidfarm ideas --families${RESET}`);
|
|
12174
12864
|
console.log(`${DIM}Never ship a frame as a hook — the four charges still apply: vidfarm skill show hooks${RESET}`);
|
|
12175
12865
|
console.log(`${DIM}Producing the whole set is a batch: vidfarm skill show bulk (one frame per video).${RESET}`);
|
|
12866
|
+
// The natural upsell, in the right order: ideas are the easy win, and the
|
|
12867
|
+
// interview is what a director opts into once they want the strategy behind
|
|
12868
|
+
// the list. Offering it here means it never has to be asked for cold.
|
|
12869
|
+
console.log(`${DIM}Save the picks as content-ideas.md in your working folder. Want the strategy behind them${RESET}`);
|
|
12870
|
+
console.log(`${DIM}(who to sell to, which angle, which hook)? That is the interview: vidfarm consult coldstart --short${RESET}`);
|
|
12176
12871
|
}
|
|
12177
12872
|
async function runSkillDocsCommand(sub, argv) {
|
|
12178
12873
|
const parsed = parseArgs({
|
|
@@ -12340,4 +13035,25 @@ async function runUpdateSkillCommand(argv) {
|
|
|
12340
13035
|
...(ctx.json ? ["--json"] : [])
|
|
12341
13036
|
]);
|
|
12342
13037
|
}
|
|
13038
|
+
// ── entry point ──────────────────────────────────────────────────────────────
|
|
13039
|
+
// Last statement in the file ON PURPOSE: see the note where this used to live.
|
|
13040
|
+
// Every module-level declaration above is initialized by the time a command
|
|
13041
|
+
// handler reads it, so late-declared help texts and tables are safe.
|
|
13042
|
+
void main().catch(async (error) => {
|
|
13043
|
+
// Deliberate, expected user-facing conditions (e.g. local mode in the
|
|
13044
|
+
// published cloud-only CLI) print just the message — no stack, no crash report.
|
|
13045
|
+
if (error instanceof LocalModeUnavailableError
|
|
13046
|
+
|| error instanceof CostModeBlockedError
|
|
13047
|
+
// A missing OPTIONAL native module (better-sqlite3 with no C++ toolchain,
|
|
13048
|
+
// the common Windows case) is an environment fact, not a vidfarm bug.
|
|
13049
|
+
|| error instanceof NativeModuleUnavailableError) {
|
|
13050
|
+
console.error(error.message);
|
|
13051
|
+
process.exit(1);
|
|
13052
|
+
}
|
|
13053
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
13054
|
+
// Report only unexpected crashes (bugs), never the deliberate user-facing
|
|
13055
|
+
// `throw new Error(...)` above; no-op unless a devcli DSN is configured.
|
|
13056
|
+
await reportCliCrash(error);
|
|
13057
|
+
process.exit(1);
|
|
13058
|
+
});
|
|
12343
13059
|
//# sourceMappingURL=cli.js.map
|