@officexapp/vidfarm-devcli 0.21.43 → 0.21.45
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 +2 -0
- package/.agents/skills/vidfarm/SKILL.md +37 -12
- package/.agents/skills/vidfarm/recipes/onboard-a-new-director.md +1 -1
- package/.agents/skills/vidfarm/references/assets-and-sourcing.md +97 -1
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +11 -5
- 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 +17 -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 +458 -31
- package/SKILL.md +117 -116
- package/crowdsourcing.md +44 -1
- package/dist/src/cli.js +544 -31
- package/dist/src/devcli/consult.js +14 -0
- package/dist/src/devcli/skill-docs.js +61 -7
- package/dist/src/services/clip-curation/index.js +1 -1
- package/dist/src/services/clip-curation/media-select.js +146 -3
- package/experimental/google-news-to-video.md +235 -0
- package/package.json +4 -1
- package/public/assets/file-directory-app.js +28 -28
- package/public/assets/homepage-client-app.js +15 -15
package/dist/src/cli.js
CHANGED
|
@@ -29,7 +29,7 @@ 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
35
|
import { startLocalFrontendServer, serveShellsPresent } from "./devcli/local-frontend-server.js";
|
|
@@ -131,6 +131,9 @@ Account (persisted login — points the CLI + \`serve\` at cloud prod):
|
|
|
131
131
|
Every step reads and updates CONTEXT.md in --dir, the
|
|
132
132
|
durable answer file, so a later session starts warm.
|
|
133
133
|
Run the whole chain from ONE working folder.
|
|
134
|
+
OFFER "vidfarm ideas" FIRST — 20+ video ideas for one
|
|
135
|
+
line of input is an easier first win than an interview,
|
|
136
|
+
and the picks make the interview answers better.
|
|
134
137
|
(aliases: consultation, brainstorm)
|
|
135
138
|
coldstart [--short] The interview → OFFER.md. Ask the human the questions a
|
|
136
139
|
few at a time; never invent their answers. --short is a
|
|
@@ -727,6 +730,31 @@ Speech (TTS/STT) — LOCAL-FIRST on your own AI key; --cloud is the explicit bac
|
|
|
727
730
|
key adds photos/vectors/stock video.
|
|
728
731
|
--provider <p> Force pixabay|openverse|iconify|iconscout
|
|
729
732
|
--limit <n> | --json
|
|
733
|
+
video-search "<query>" GOOGLE VIDEO SEARCH — find the actual SHOT anywhere on the web
|
|
734
|
+
(TikTok, YouTube, Pexels/Pixabay/Mixkit, news, archive.org).
|
|
735
|
+
This is how you SOURCE raws: take a result url straight into
|
|
736
|
+
'vidfarm raws scan <url>' (mine clips) or 'download-video <url>'
|
|
737
|
+
(collect the file). Search the SHOT, not the topic:
|
|
738
|
+
[subject]+[action]+[shot/style]+[quality/orientation], e.g.
|
|
739
|
+
"office worker typing laptop close up 4K". Google operators work:
|
|
740
|
+
site:pexels.com/videos · site:tiktok.com · "exact phrase" · -dance
|
|
741
|
+
PAID PLANS ONLY · flat $0.0003 per call whatever the result count,
|
|
742
|
+
so ask for a WIDE page instead of paging twice.
|
|
743
|
+
--limit <n> Results (default 25, max 50)
|
|
744
|
+
--fresh <d|w|m|y> Only the last day/week/month/year
|
|
745
|
+
--duration <d> short|medium|long --resolution high|standard
|
|
746
|
+
--region <r> wt-wt (default)|us-en|ph-en|… --json
|
|
747
|
+
image-search "<query>" GOOGLE IMAGE SEARCH — reference stills, textures, logos, product
|
|
748
|
+
shots. Same operators. PAID PLANS ONLY · flat $0.0003 per call.
|
|
749
|
+
For licence-checked, royalty-free assets prefer 'vidfarm media'
|
|
750
|
+
(free): image-search returns LINKS with no licence attached.
|
|
751
|
+
--limit <n> (default 25, max 100) --type-image photo|clipart|gif|transparent
|
|
752
|
+
--color <c> --size <s> --layout Square|Tall|Wide --region <r> --json
|
|
753
|
+
news-search "<query>" GOOGLE NEWS — recent real events to build TIMELY content on.
|
|
754
|
+
News finds the STORY; run video-search after for the VISUALS.
|
|
755
|
+
PAID PLANS ONLY · flat $0.0003 per call.
|
|
756
|
+
Method: https://vidfarm.cc/experimental/google-news-to-video.md
|
|
757
|
+
--limit <n> (default 25, max 100) --fresh <d|w|m|y> --region <r> --json
|
|
730
758
|
iconscout "<query>" DESIGNER icons, STICKERS, illustrations, 3D and Lottie —
|
|
731
759
|
THE CHEAP ALTERNATIVE TO AI IMAGE GENERATION. Reach for this
|
|
732
760
|
BEFORE 'generate' / 'image': an AI sticker costs cents per
|
|
@@ -921,6 +949,9 @@ Approve & schedule (publish a finished MP4 as a shareable post):
|
|
|
921
949
|
approve Approve media into a preview/share page → POST /api/v1/approved/posts
|
|
922
950
|
--video <url|file> Primary MP4 (URL, /files path, or local file → durable My Files)
|
|
923
951
|
--media <url|file> Extra media (repeatable; local files → durable My Files)
|
|
952
|
+
--thumbnail <url|file> Poster image for the share link's social card. Omit it and
|
|
953
|
+
the server extracts a frame (~1s in) from --video and files
|
|
954
|
+
it under /files/approved
|
|
924
955
|
--folder <path> Destination subfolder for uploaded local files (default: approved)
|
|
925
956
|
--temp Upload local files to the 30-day TEMP store instead of
|
|
926
957
|
durable My Files (throwaway preview; share video dies at day 30)
|
|
@@ -956,18 +987,28 @@ Account:
|
|
|
956
987
|
(iconscout takes BOTH values as one secret: <client_id>:<client_secret>)
|
|
957
988
|
|
|
958
989
|
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
|
-
|
|
990
|
+
ideas [topic] "What should I post?" — the THREE IDEA BANKS (local — offline, free, no AI)
|
|
991
|
+
50 frames (the rise of · then vs now · …), 5
|
|
992
|
+
AWARENESS STAGES (what the viewer already knows,
|
|
993
|
+
and what the video may ask for), and 44 PROBLEM
|
|
994
|
+
ANGLES (which side of the problem it comes from).
|
|
995
|
+
An idea is one point in that grid; vary TWO axes
|
|
996
|
+
and 30 videos never read as repeats. It hands over
|
|
997
|
+
the banks — you fill them with the director's
|
|
998
|
+
offer, then write hook/loop/payoff/bait per pick.
|
|
999
|
+
RUN THIS BEFORE "consult" — one line of input buys
|
|
1000
|
+
20+ ideas, and it is the easiest first win a new
|
|
1001
|
+
director can get. The interview comes after.
|
|
966
1002
|
--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
|
|
1003
|
+
--family <name> One frame family only (arc, contrast, pivot, …)
|
|
1004
|
+
--families List the frame families and their sizes
|
|
1005
|
+
--stages The awareness ladder — all 5 stages in full
|
|
1006
|
+
--stage <name|n> One stage (unaware · problem-aware · … · most-aware)
|
|
1007
|
+
--angles The 44 problem angles, by family
|
|
1008
|
+
--angle <name> One angle family or one named angle
|
|
1009
|
+
--grid Stage × angle × frame — one row per video
|
|
969
1010
|
--count <n> Sample N, spread across families (default 20 with --topic)
|
|
970
|
-
--json Machine-readable
|
|
1011
|
+
--json Machine-readable banks (+ starter lines)
|
|
971
1012
|
skill topics The craft this pack knows, by SPOKEN name — (local — offline, no account)
|
|
972
1013
|
meme-recaption, product-explainer, captions,
|
|
973
1014
|
first-frame, blurred-plate, density, avatar, …
|
|
@@ -1079,6 +1120,12 @@ Marketplace (paid, cloud-only — the bazaar never renders locally):
|
|
|
1079
1120
|
directory save-url <url> Save a durable media URL INTO My Files at a folder → POST /api/v1/user/me/attachments/from-url
|
|
1080
1121
|
--folder <path> Destination folder under /files (e.g. inpaints, promos)
|
|
1081
1122
|
--as <name> Name the saved file · --notes <text> vector-embedded notes
|
|
1123
|
+
directory share <path> Mint a PUBLIC link onto one subfolder (any root) → POST /api/v1/user/me/directory/shares
|
|
1124
|
+
--mode read|upload|edit read = browse + vector search · upload = + add files (never delete) · edit = + rename/delete
|
|
1125
|
+
--label <text> Name the link. PAID to mint; visiting one needs no account.
|
|
1126
|
+
directory shares [path] List your share links → GET /api/v1/user/me/directory/shares
|
|
1127
|
+
directory share-update <token> --mode/--label/--disable/--enable → PATCH /api/v1/user/me/directory/shares/:token
|
|
1128
|
+
directory unshare <token> Delete a share link for good → DELETE /api/v1/user/me/directory/shares/:token
|
|
1082
1129
|
(alias: dir · save-url|from-url|import-url — CLI twin of the "Save to Files" picker)
|
|
1083
1130
|
put-file / get-file / files / annotate-file are the My Files (persistent) set;
|
|
1084
1131
|
upload is the throwaway temp store for dropping media into a composition.
|
|
@@ -1147,9 +1194,8 @@ Publish-mode options:
|
|
|
1147
1194
|
--message <text> Message attached to the published version snapshot
|
|
1148
1195
|
--no-snapshot Only update the working copy; skip the version snapshot
|
|
1149
1196
|
`;
|
|
1150
|
-
// ANSI palette
|
|
1151
|
-
//
|
|
1152
|
-
// this module is still evaluating.
|
|
1197
|
+
// ANSI palette. (Its position no longer matters — main() now runs as the last
|
|
1198
|
+
// statement in this file — but it stays up here because everything below reads it.)
|
|
1153
1199
|
const FRONTEND = "\x1b[1m\x1b[36m"; // bold cyan — reserved for openable URLs
|
|
1154
1200
|
const BOLD = "\x1b[1m";
|
|
1155
1201
|
const DIM = "\x1b[2m";
|
|
@@ -1157,24 +1203,16 @@ const GREEN = "\x1b[32m";
|
|
|
1157
1203
|
const YELLOW = "\x1b[33m";
|
|
1158
1204
|
const RED = "\x1b[31m";
|
|
1159
1205
|
const RESET = "\x1b[0m";
|
|
1160
|
-
// Referenced by runMarketplaceCommand's SYNCHRONOUS browse path
|
|
1161
|
-
// initialized before `void main()` starts dispatching below.
|
|
1206
|
+
// Referenced by runMarketplaceCommand's SYNCHRONOUS browse path.
|
|
1162
1207
|
const DOLLARPLATOON_SKILL_HINT = `Gig inbox / proofs / payouts live on Dollar Platoon's own API — grab the skill:
|
|
1163
1208
|
npx skills add https://github.com/OfficeXApp/dollarplatoon-skill
|
|
1164
1209
|
(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
|
-
});
|
|
1210
|
+
// main() is INVOKED AT THE BOTTOM OF THIS FILE, not here. Command handlers read
|
|
1211
|
+
// module-level `const`s (help texts, tables, palettes) synchronously, and a
|
|
1212
|
+
// const declared below the invocation point is still in its temporal dead zone
|
|
1213
|
+
// when the handler runs — `vidfarm directory help` used to die with "Cannot
|
|
1214
|
+
// access 'DIRECTORY_HELP' before initialization". Starting main() last means
|
|
1215
|
+
// every declaration in this module is initialized before any command dispatches.
|
|
1178
1216
|
// AI agents habitually save credentials to a project-local .env instead of
|
|
1179
1217
|
// exporting them, so honor VIDFARM_API_KEY (and VIDFARM_TARGET/VIDFARM_HOST)
|
|
1180
1218
|
// from ./.env when the variable isn't already in the environment. Real env
|
|
@@ -1323,6 +1361,22 @@ async function main() {
|
|
|
1323
1361
|
case "media":
|
|
1324
1362
|
await runMediaCommand(rest);
|
|
1325
1363
|
return;
|
|
1364
|
+
case "video-search":
|
|
1365
|
+
case "videosearch":
|
|
1366
|
+
case "search-video":
|
|
1367
|
+
await runWebSearchCommand("video", rest);
|
|
1368
|
+
return;
|
|
1369
|
+
case "image-search":
|
|
1370
|
+
case "imagesearch":
|
|
1371
|
+
case "search-image":
|
|
1372
|
+
await runWebSearchCommand("image", rest);
|
|
1373
|
+
return;
|
|
1374
|
+
case "news-search":
|
|
1375
|
+
case "newssearch":
|
|
1376
|
+
case "news":
|
|
1377
|
+
case "search-news":
|
|
1378
|
+
await runWebSearchCommand("news", rest);
|
|
1379
|
+
return;
|
|
1326
1380
|
case "iconscout":
|
|
1327
1381
|
case "icons":
|
|
1328
1382
|
case "stickers":
|
|
@@ -2616,6 +2670,7 @@ Rules:
|
|
|
2616
2670
|
- 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
2671
|
- 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
2672
|
- 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\`.
|
|
2673
|
+
- 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
2674
|
- 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
2675
|
|
|
2621
2676
|
The three paintbrushes (Vidfarm is thrift-first — do NOT spend AI credits on every scene):
|
|
@@ -7820,6 +7875,121 @@ async function runMediaCommand(argv) {
|
|
|
7820
7875
|
}
|
|
7821
7876
|
console.log(`${DIM}Attribution shown for CC-BY-style items — include it when you use them. Pixabay/CC0 need none.${RESET}`);
|
|
7822
7877
|
}
|
|
7878
|
+
// Google search — video / image / news.
|
|
7879
|
+
//
|
|
7880
|
+
// The catalog (`vidfarm media`) answers "give me A clip of X". Google answers
|
|
7881
|
+
// "give me THE shot": it indexes TikTok, YouTube, every stock library, and the
|
|
7882
|
+
// news at once, so one query returns candidate SOURCE URLs that feed straight
|
|
7883
|
+
// into `raws scan` (mine clips out of them) or `download-video` (collect one).
|
|
7884
|
+
//
|
|
7885
|
+
// Flat $0.0003 per CALL — the same price for 1 or 100 links — so the CLI nudges
|
|
7886
|
+
// toward one wide page rather than repeated narrow ones. No cost-mode gate: at
|
|
7887
|
+
// this price a search is cheaper than the agent turn that decides to skip it.
|
|
7888
|
+
async function runWebSearchCommand(kind, argv) {
|
|
7889
|
+
const parsed = parseArgs({
|
|
7890
|
+
args: argv,
|
|
7891
|
+
allowPositionals: true,
|
|
7892
|
+
options: {
|
|
7893
|
+
...commonOptions(),
|
|
7894
|
+
limit: { type: "string" },
|
|
7895
|
+
fresh: { type: "string" },
|
|
7896
|
+
region: { type: "string" },
|
|
7897
|
+
duration: { type: "string" },
|
|
7898
|
+
resolution: { type: "string" },
|
|
7899
|
+
color: { type: "string" },
|
|
7900
|
+
size: { type: "string" },
|
|
7901
|
+
"type-image": { type: "string" },
|
|
7902
|
+
layout: { type: "string" },
|
|
7903
|
+
safesearch: { type: "string" }
|
|
7904
|
+
}
|
|
7905
|
+
});
|
|
7906
|
+
const positionals = parsed.positionals.slice();
|
|
7907
|
+
if (positionals[0] && ["search", "find"].includes(positionals[0].toLowerCase()))
|
|
7908
|
+
positionals.shift();
|
|
7909
|
+
const query = positionals.join(" ").trim();
|
|
7910
|
+
if (!query) {
|
|
7911
|
+
console.error(`${RED}${kind}-search: a search query is required${RESET}`);
|
|
7912
|
+
if (kind === "video") {
|
|
7913
|
+
console.error(`${DIM}Search the SHOT, not the topic: vidfarm video-search "office worker typing laptop close up 4K"${RESET}`);
|
|
7914
|
+
console.error(`${DIM}Operators work: site:pexels.com/videos · site:tiktok.com "packing orders" -dance${RESET}`);
|
|
7915
|
+
}
|
|
7916
|
+
else if (kind === "news") {
|
|
7917
|
+
console.error(`${DIM}e.g. vidfarm news-search "AI startup funding announced" --fresh w${RESET}`);
|
|
7918
|
+
}
|
|
7919
|
+
else {
|
|
7920
|
+
console.error(`${DIM}e.g. vidfarm image-search "warehouse interior wide shot" --limit 40${RESET}`);
|
|
7921
|
+
}
|
|
7922
|
+
process.exit(2);
|
|
7923
|
+
}
|
|
7924
|
+
const ctx = commonContext(parsed.values);
|
|
7925
|
+
const maxCap = kind === "video" ? 50 : 100;
|
|
7926
|
+
const limit = Math.min(Math.max(Number(parsed.values.limit ?? "25") || 25, 1), maxCap);
|
|
7927
|
+
const qs = new URLSearchParams({ q: query, max_results: String(limit) });
|
|
7928
|
+
if (parsed.values.region)
|
|
7929
|
+
qs.set("region", String(parsed.values.region));
|
|
7930
|
+
if (parsed.values.fresh)
|
|
7931
|
+
qs.set("timelimit", String(parsed.values.fresh));
|
|
7932
|
+
if (parsed.values.safesearch)
|
|
7933
|
+
qs.set("safesearch", String(parsed.values.safesearch));
|
|
7934
|
+
if (kind === "video") {
|
|
7935
|
+
if (parsed.values.duration)
|
|
7936
|
+
qs.set("duration", String(parsed.values.duration));
|
|
7937
|
+
if (parsed.values.resolution)
|
|
7938
|
+
qs.set("resolution", String(parsed.values.resolution));
|
|
7939
|
+
}
|
|
7940
|
+
if (kind === "image") {
|
|
7941
|
+
if (parsed.values.color)
|
|
7942
|
+
qs.set("color", String(parsed.values.color));
|
|
7943
|
+
if (parsed.values.size)
|
|
7944
|
+
qs.set("size", String(parsed.values.size));
|
|
7945
|
+
if (parsed.values["type-image"])
|
|
7946
|
+
qs.set("type_image", String(parsed.values["type-image"]));
|
|
7947
|
+
if (parsed.values.layout)
|
|
7948
|
+
qs.set("layout", String(parsed.values.layout));
|
|
7949
|
+
}
|
|
7950
|
+
const res = await apiRequest({
|
|
7951
|
+
method: "GET", host: ctx.host, path: `/api/v1/primitives/${kind}-search?${qs.toString()}`, auth: ctx.auth
|
|
7952
|
+
});
|
|
7953
|
+
assertApiOk(res, `${kind} search`);
|
|
7954
|
+
const body = (res.json ?? {});
|
|
7955
|
+
if (ctx.json) {
|
|
7956
|
+
printJson(body);
|
|
7957
|
+
return;
|
|
7958
|
+
}
|
|
7959
|
+
const results = Array.isArray(body.results) ? body.results : [];
|
|
7960
|
+
console.log(`${DIM}${results.length} ${kind} result(s) for "${query}" · $${body.charged_usd ?? 0} this call${RESET}`);
|
|
7961
|
+
if (!results.length) {
|
|
7962
|
+
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}`);
|
|
7963
|
+
return;
|
|
7964
|
+
}
|
|
7965
|
+
for (const item of results) {
|
|
7966
|
+
if (kind === "image") {
|
|
7967
|
+
console.log(`${GREEN}${item.image_url}${RESET}`);
|
|
7968
|
+
const dims = item.width && item.height ? ` · ${item.width}x${item.height}` : "";
|
|
7969
|
+
console.log(`${DIM} ${item.title}${dims}${item.source_page ? ` · ${item.source_page}` : ""}${RESET}`);
|
|
7970
|
+
continue;
|
|
7971
|
+
}
|
|
7972
|
+
if (kind === "news") {
|
|
7973
|
+
console.log(`${GREEN}${item.url}${RESET}`);
|
|
7974
|
+
console.log(`${DIM} ${item.title}${item.source ? ` · ${item.source}` : ""}${item.date ? ` · ${item.date}` : ""}${RESET}`);
|
|
7975
|
+
continue;
|
|
7976
|
+
}
|
|
7977
|
+
console.log(`${GREEN}${item.url}${RESET}`);
|
|
7978
|
+
const meta = [item.duration, item.publisher, item.uploader].filter(Boolean).join(" · ");
|
|
7979
|
+
console.log(`${DIM} ${item.title}${meta ? ` · ${meta}` : ""}${RESET}`);
|
|
7980
|
+
}
|
|
7981
|
+
if (kind === "video") {
|
|
7982
|
+
console.log(`${DIM}Next: 'vidfarm raws scan <url>' mines clips out of a source · 'vidfarm download-video <url>' collects the file.${RESET}`);
|
|
7983
|
+
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}`);
|
|
7984
|
+
}
|
|
7985
|
+
else if (kind === "news") {
|
|
7986
|
+
console.log(`${DIM}News finds the STORY. Run 'vidfarm video-search "<event> footage"' for the VISUALS — searching both at once finds neither.${RESET}`);
|
|
7987
|
+
console.log(`${DIM}Method: https://vidfarm.cc/experimental/google-news-to-video.md${RESET}`);
|
|
7988
|
+
}
|
|
7989
|
+
else {
|
|
7990
|
+
console.log(`${RED}These are LINKS with no licence attached. For royalty-free assets use 'vidfarm media search "<meaning>" --type image' instead.${RESET}`);
|
|
7991
|
+
}
|
|
7992
|
+
}
|
|
7823
7993
|
// IconScout — designer icons / STICKERS / illustrations / 3D / Lottie.
|
|
7824
7994
|
//
|
|
7825
7995
|
// This is the CHEAP path, and the CLI says so loudly: an AI image attempt costs
|
|
@@ -8896,6 +9066,7 @@ async function runApproveCommand(argv) {
|
|
|
8896
9066
|
...commonOptions(),
|
|
8897
9067
|
video: { type: "string" },
|
|
8898
9068
|
media: { type: "string", multiple: true },
|
|
9069
|
+
thumbnail: { type: "string" },
|
|
8899
9070
|
caption: { type: "string" },
|
|
8900
9071
|
title: { type: "string" },
|
|
8901
9072
|
pinned: { type: "string" },
|
|
@@ -8937,6 +9108,11 @@ async function runApproveCommand(argv) {
|
|
|
8937
9108
|
media.push({ url: await resolveApproveMedia(String(parsed.values.video)), kind: "video", role: "primary" });
|
|
8938
9109
|
for (const url of parsed.values.media ?? [])
|
|
8939
9110
|
media.push({ url: await resolveApproveMedia(String(url)) });
|
|
9111
|
+
// The social-card poster of the share page. Omit it and the server extracts a
|
|
9112
|
+
// frame (~1s in) from --video and files it under /files/approved.
|
|
9113
|
+
if (parsed.values.thumbnail) {
|
|
9114
|
+
media.push({ url: await resolveApproveMedia(String(parsed.values.thumbnail)), kind: "image", role: "thumbnail" });
|
|
9115
|
+
}
|
|
8940
9116
|
if (media.length === 0) {
|
|
8941
9117
|
console.warn(`${DIM}[vidfarm] no --video / --media given; approving a text-only post.${RESET}`);
|
|
8942
9118
|
}
|
|
@@ -9892,7 +10068,11 @@ async function runConsultCommand(argv) {
|
|
|
9892
10068
|
}
|
|
9893
10069
|
const first = plan.find((row) => !row.done) ?? plan[0];
|
|
9894
10070
|
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
|
-
|
|
10071
|
+
// The easier win comes first. A director who has seen 20 titled ideas
|
|
10072
|
+
// answers the interview better than one asked to think cold.
|
|
10073
|
+
console.log(`\nBefore the interview: ${BOLD}vidfarm ideas --topic "<your offer in one line>"${RESET}`);
|
|
10074
|
+
console.log(`${DIM}20+ video ideas, offline and free, for one sentence of input → content-ideas.md. Do that first.${RESET}`);
|
|
10075
|
+
console.log(`\nThen here: ${BOLD}${first.command}${RESET}`);
|
|
9896
10076
|
console.log(`${DIM}In a hurry: vidfarm consult coldstart --short (6 fixed questions instead of the full interview)${RESET}`);
|
|
9897
10077
|
console.log(`${DIM}Run every step from ONE working folder (--dir), so the artifacts and ${context.file} stay together.${RESET}`);
|
|
9898
10078
|
console.log(`${DIM}Also: vidfarm consult placement --video <path|url> (native product placement inside an existing video)${RESET}`);
|
|
@@ -10733,6 +10913,30 @@ const DIRECTORY_HELP = `vidfarm directory — browse the unified file tree (/fil
|
|
|
10733
10913
|
e.g. vidfarm directory save-url "https://…/inpaint-out.png" --folder inpaints --as hero.png
|
|
10734
10914
|
(server fetches the URL — handles cross-origin S3; the CLI twin of the pop-panel "Save to Files")
|
|
10735
10915
|
|
|
10916
|
+
directory share <path> Mint a PUBLIC link onto one subfolder → POST /api/v1/user/me/directory/shares
|
|
10917
|
+
--mode read|upload|edit read = browse + vector search (default) · upload = + add files/folders,
|
|
10918
|
+
never delete · edit = + rename & delete. Scoped to <path> and everything
|
|
10919
|
+
under it — never a sibling or a parent.
|
|
10920
|
+
--label <text> Name the link so you can tell your links apart
|
|
10921
|
+
--json Raw JSON response
|
|
10922
|
+
Works on ANY root (/files·/temp·/raws·/approved; /projects is read-only so it takes --mode read).
|
|
10923
|
+
Only /files and /temp accept uploads — the other roots have no plain-upload backend, so an upload
|
|
10924
|
+
link there only creates folders. PAID: minting a link needs a paid plan; VISITING one needs no account.
|
|
10925
|
+
e.g. vidfarm directory share /files/crowdsourced --mode upload --label "Contributor drop box"
|
|
10926
|
+
vidfarm directory share /raws/b-roll --mode read --label "Editor library"
|
|
10927
|
+
|
|
10928
|
+
directory shares [path] List your share links (optionally for one folder) → GET /api/v1/user/me/directory/shares
|
|
10929
|
+
--path <path> --json
|
|
10930
|
+
|
|
10931
|
+
directory share-update <token> Change or switch off a link → PATCH /api/v1/user/me/directory/shares/:token
|
|
10932
|
+
--mode read|upload|edit Re-scope what holders can do
|
|
10933
|
+
--label <text> Rename the link
|
|
10934
|
+
--disable | --enable Cut off access instantly (reversible — the token survives)
|
|
10935
|
+
--json
|
|
10936
|
+
|
|
10937
|
+
directory unshare <token> Delete a link for good → DELETE /api/v1/user/me/directory/shares/:token
|
|
10938
|
+
--json (to keep the record, prefer share-update --disable)
|
|
10939
|
+
|
|
10736
10940
|
Aliases: directory | dir (move|mv · copy|cp) · Auth: --api-key <key> or VIDFARM_API_KEY, --host <url>`;
|
|
10737
10941
|
async function runDirectoryCommand(argv) {
|
|
10738
10942
|
const sub = argv[0];
|
|
@@ -10756,6 +10960,14 @@ async function runDirectoryCommand(argv) {
|
|
|
10756
10960
|
case "from-url":
|
|
10757
10961
|
case "import-url":
|
|
10758
10962
|
return runDirectorySaveUrl(rest);
|
|
10963
|
+
case "shares":
|
|
10964
|
+
return runDirectorySharesList(rest);
|
|
10965
|
+
case "share":
|
|
10966
|
+
return runDirectoryShareCreate(rest);
|
|
10967
|
+
case "share-update":
|
|
10968
|
+
return runDirectoryShareUpdate(rest);
|
|
10969
|
+
case "unshare":
|
|
10970
|
+
return runDirectoryShareDelete(rest);
|
|
10759
10971
|
case undefined:
|
|
10760
10972
|
case "help":
|
|
10761
10973
|
case "--help":
|
|
@@ -10929,6 +11141,140 @@ async function runDirectoryCopy(argv) {
|
|
|
10929
11141
|
const suffix = typeof copied === "number" ? ` ${DIM}(${copied} file${copied === 1 ? "" : "s"})${RESET}` : "";
|
|
10930
11142
|
console.log(`${GREEN}✓${RESET} Copied to ${BOLD}${newPath}${RESET}${suffix} ${DIM}[${space}]${RESET}`);
|
|
10931
11143
|
}
|
|
11144
|
+
// ── directory share tokens ───────────────────────────────────────────────────
|
|
11145
|
+
// Public links onto ONE subfolder of any root. Three modes: read (browse +
|
|
11146
|
+
// vector search), upload (+ add files/folders, never delete), edit (+ rename &
|
|
11147
|
+
// delete). Minting a link is a PAID action; visiting one needs no account at
|
|
11148
|
+
// all. Shares live in the cloud directory, so these never fan out to --both.
|
|
11149
|
+
// A hoisted function, not a const: main() runs at module top-level ABOVE this
|
|
11150
|
+
// point, so a `const` declared down here is still in its temporal dead zone
|
|
11151
|
+
// when a command handler reads it.
|
|
11152
|
+
function shareModes() {
|
|
11153
|
+
return ["read", "upload", "edit"];
|
|
11154
|
+
}
|
|
11155
|
+
function shareLine(share) {
|
|
11156
|
+
const mode = String(share?.mode ?? "read");
|
|
11157
|
+
const state = share?.disabled ? `${DIM}(disabled)${RESET}` : "";
|
|
11158
|
+
const label = share?.label ? `${BOLD}${share.label}${RESET} ` : "";
|
|
11159
|
+
return [
|
|
11160
|
+
`${label}${DIM}${share?.path ?? ""}${RESET} [${mode}] ${state}`,
|
|
11161
|
+
` ${share?.url ?? ""}`,
|
|
11162
|
+
` ${DIM}${share?.token ?? ""}${RESET}`
|
|
11163
|
+
].join("\n");
|
|
11164
|
+
}
|
|
11165
|
+
async function runDirectorySharesList(argv) {
|
|
11166
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), path: { type: "string" } } });
|
|
11167
|
+
const ctx = commonContext(parsed.values);
|
|
11168
|
+
const pathFilter = parsed.values.path ?? parsed.positionals[0];
|
|
11169
|
+
const space = targetSpaces(ctx.target)[0];
|
|
11170
|
+
const result = await dispatch(ctx, { method: "GET", path: "/api/v1/user/me/directory/shares", query: pathFilter ? { path: pathFilter } : {} }, space);
|
|
11171
|
+
assertApiOk(result, "directory shares");
|
|
11172
|
+
if (ctx.json) {
|
|
11173
|
+
printJson(result.json ?? result.text);
|
|
11174
|
+
return;
|
|
11175
|
+
}
|
|
11176
|
+
const shares = (result.json?.shares ?? []);
|
|
11177
|
+
if (!shares.length) {
|
|
11178
|
+
console.log(`${DIM}No share links${pathFilter ? ` for ${pathFilter}` : ""}. Create one: vidfarm directory share <path> --mode read${RESET}`);
|
|
11179
|
+
return;
|
|
11180
|
+
}
|
|
11181
|
+
console.log(shares.map(shareLine).join("\n\n"));
|
|
11182
|
+
console.log(`\n${DIM}${shares.length} link${shares.length === 1 ? "" : "s"}${RESET}`);
|
|
11183
|
+
}
|
|
11184
|
+
async function runDirectoryShareCreate(argv) {
|
|
11185
|
+
const parsed = parseArgs({
|
|
11186
|
+
args: argv,
|
|
11187
|
+
allowPositionals: true,
|
|
11188
|
+
options: { ...commonOptions(), mode: { type: "string" }, label: { type: "string" } }
|
|
11189
|
+
});
|
|
11190
|
+
const ctx = commonContext(parsed.values);
|
|
11191
|
+
const targetPath = parsed.positionals[0];
|
|
11192
|
+
if (!targetPath) {
|
|
11193
|
+
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"');
|
|
11194
|
+
}
|
|
11195
|
+
const mode = parsed.values.mode ?? "read";
|
|
11196
|
+
if (!shareModes().includes(mode)) {
|
|
11197
|
+
throw new Error(`--mode must be ${shareModes().join(" | ")} (got "${mode}")`);
|
|
11198
|
+
}
|
|
11199
|
+
const space = targetSpaces(ctx.target)[0];
|
|
11200
|
+
const result = await dispatch(ctx, {
|
|
11201
|
+
method: "POST",
|
|
11202
|
+
path: "/api/v1/user/me/directory/shares",
|
|
11203
|
+
body: { path: targetPath, mode, ...(parsed.values.label ? { label: String(parsed.values.label) } : {}) }
|
|
11204
|
+
}, space);
|
|
11205
|
+
assertApiOk(result, "directory share");
|
|
11206
|
+
if (ctx.json) {
|
|
11207
|
+
printJson(result.json ?? result.text);
|
|
11208
|
+
return;
|
|
11209
|
+
}
|
|
11210
|
+
const share = result.json?.share ?? {};
|
|
11211
|
+
console.log(`${GREEN}✓${RESET} Shared ${BOLD}${share.path ?? targetPath}${RESET} as ${BOLD}${mode}${RESET} ${DIM}[${space}]${RESET}`);
|
|
11212
|
+
console.log(` ${share.url ?? ""}`);
|
|
11213
|
+
console.log(` ${DIM}${share.token ?? ""} · disable it with: vidfarm directory share-update ${share.token ?? "<token>"} --disable${RESET}`);
|
|
11214
|
+
}
|
|
11215
|
+
async function runDirectoryShareUpdate(argv) {
|
|
11216
|
+
const parsed = parseArgs({
|
|
11217
|
+
args: argv,
|
|
11218
|
+
allowPositionals: true,
|
|
11219
|
+
options: {
|
|
11220
|
+
...commonOptions(),
|
|
11221
|
+
mode: { type: "string" },
|
|
11222
|
+
label: { type: "string" },
|
|
11223
|
+
disable: { type: "boolean" },
|
|
11224
|
+
enable: { type: "boolean" }
|
|
11225
|
+
}
|
|
11226
|
+
});
|
|
11227
|
+
const ctx = commonContext(parsed.values);
|
|
11228
|
+
const token = parsed.positionals[0];
|
|
11229
|
+
if (!token) {
|
|
11230
|
+
throw new Error('directory share-update requires a token: vidfarm directory share-update <token> [--mode read|upload|edit] [--label <text>] [--disable|--enable]');
|
|
11231
|
+
}
|
|
11232
|
+
const mode = parsed.values.mode;
|
|
11233
|
+
if (mode && !shareModes().includes(mode)) {
|
|
11234
|
+
throw new Error(`--mode must be ${shareModes().join(" | ")} (got "${mode}")`);
|
|
11235
|
+
}
|
|
11236
|
+
if (parsed.values.disable && parsed.values.enable) {
|
|
11237
|
+
throw new Error("Pass --disable or --enable, not both.");
|
|
11238
|
+
}
|
|
11239
|
+
const body = {};
|
|
11240
|
+
if (mode)
|
|
11241
|
+
body.mode = mode;
|
|
11242
|
+
if (parsed.values.label !== undefined)
|
|
11243
|
+
body.label = String(parsed.values.label);
|
|
11244
|
+
if (parsed.values.disable)
|
|
11245
|
+
body.disabled = true;
|
|
11246
|
+
if (parsed.values.enable)
|
|
11247
|
+
body.disabled = false;
|
|
11248
|
+
if (!Object.keys(body).length) {
|
|
11249
|
+
throw new Error("Nothing to change — pass --mode, --label, --disable or --enable.");
|
|
11250
|
+
}
|
|
11251
|
+
const space = targetSpaces(ctx.target)[0];
|
|
11252
|
+
const result = await dispatch(ctx, { method: "PATCH", path: `/api/v1/user/me/directory/shares/${encodeURIComponent(token)}`, body }, space);
|
|
11253
|
+
assertApiOk(result, "directory share-update");
|
|
11254
|
+
if (ctx.json) {
|
|
11255
|
+
printJson(result.json ?? result.text);
|
|
11256
|
+
return;
|
|
11257
|
+
}
|
|
11258
|
+
const share = result.json?.share ?? {};
|
|
11259
|
+
const state = share.disabled ? `${BOLD}disabled${RESET}` : `${BOLD}live${RESET}`;
|
|
11260
|
+
console.log(`${GREEN}✓${RESET} Share ${DIM}${token}${RESET} is ${state} as ${BOLD}${share.mode ?? "read"}${RESET} ${DIM}[${space}]${RESET}`);
|
|
11261
|
+
}
|
|
11262
|
+
async function runDirectoryShareDelete(argv) {
|
|
11263
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions() } });
|
|
11264
|
+
const ctx = commonContext(parsed.values);
|
|
11265
|
+
const token = parsed.positionals[0];
|
|
11266
|
+
if (!token) {
|
|
11267
|
+
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)');
|
|
11268
|
+
}
|
|
11269
|
+
const space = targetSpaces(ctx.target)[0];
|
|
11270
|
+
const result = await dispatch(ctx, { method: "DELETE", path: `/api/v1/user/me/directory/shares/${encodeURIComponent(token)}` }, space);
|
|
11271
|
+
assertApiOk(result, "directory unshare");
|
|
11272
|
+
if (ctx.json) {
|
|
11273
|
+
printJson(result.json ?? result.text);
|
|
11274
|
+
return;
|
|
11275
|
+
}
|
|
11276
|
+
console.log(`${GREEN}✓${RESET} Deleted share ${DIM}${token}${RESET} ${DIM}[${space}]${RESET}`);
|
|
11277
|
+
}
|
|
10932
11278
|
// Save a durable media URL (e.g. a finished job's output) INTO My Files at a
|
|
10933
11279
|
// folder — the CLI twin of the pop-panel "Save to Files" picker. Server fetches
|
|
10934
11280
|
// the URL (handles cross-origin S3), so no local download round-trip needed.
|
|
@@ -12092,7 +12438,12 @@ async function runIdeasCommand(argv) {
|
|
|
12092
12438
|
family: { type: "string" },
|
|
12093
12439
|
topic: { type: "string" },
|
|
12094
12440
|
count: { type: "string" },
|
|
12095
|
-
families: { type: "boolean", default: false }
|
|
12441
|
+
families: { type: "boolean", default: false },
|
|
12442
|
+
angles: { type: "boolean", default: false },
|
|
12443
|
+
angle: { type: "string" },
|
|
12444
|
+
stages: { type: "boolean", default: false },
|
|
12445
|
+
stage: { type: "string" },
|
|
12446
|
+
grid: { type: "boolean", default: false }
|
|
12096
12447
|
}
|
|
12097
12448
|
});
|
|
12098
12449
|
const json = Boolean(parsed.values.json);
|
|
@@ -12100,6 +12451,139 @@ async function runIdeasCommand(argv) {
|
|
|
12100
12451
|
const { frames, families } = loadIdeaBank();
|
|
12101
12452
|
if (!frames.length)
|
|
12102
12453
|
throw new Error("No bundled content-ideas reference found next to this devcli install. Fetch it with `vidfarm skills add vidfarm`.");
|
|
12454
|
+
// The three axes of an idea live in one reference, so they live behind one
|
|
12455
|
+
// command. A director who only ever sees the frames posts 30 videos at one
|
|
12456
|
+
// awareness stage from one angle, which is the exact failure this expansion
|
|
12457
|
+
// exists to prevent — so the stage and angle banks are flags here, not a
|
|
12458
|
+
// separate command nobody discovers.
|
|
12459
|
+
const ladder = loadAwarenessLadder();
|
|
12460
|
+
const angleBank = loadAngleBank();
|
|
12461
|
+
if (parsed.values.stages || parsed.values.stage) {
|
|
12462
|
+
const wanted = String(parsed.values.stage ?? "").trim().toLowerCase();
|
|
12463
|
+
const picked = wanted
|
|
12464
|
+
? ladder.filter((entry) => entry.stage.toLowerCase().includes(wanted) || String(entry.index) === wanted)
|
|
12465
|
+
: ladder;
|
|
12466
|
+
if (wanted && !picked.length) {
|
|
12467
|
+
throw new Error(`No awareness stage matches "${wanted}". Stages: ${ladder.map((entry) => `${entry.index} ${entry.stage}`).join(", ")}.`);
|
|
12468
|
+
}
|
|
12469
|
+
if (json)
|
|
12470
|
+
return printJson({ ok: true, offline: true, topic: topic || null, stages: picked });
|
|
12471
|
+
console.log(`${BOLD}The awareness ladder${RESET} ${DIM}(${picked.length} of ${ladder.length} stages · what the viewer already knows)${RESET}\n`);
|
|
12472
|
+
for (const entry of picked) {
|
|
12473
|
+
console.log(` ${GREEN}${entry.index} · ${entry.stage}${RESET} ${DIM}— ${entry.summary}${RESET}`);
|
|
12474
|
+
for (const field of entry.fields)
|
|
12475
|
+
console.log(` ${BOLD}${field.label}:${RESET} ${field.value}`);
|
|
12476
|
+
console.log("");
|
|
12477
|
+
}
|
|
12478
|
+
console.log(`${DIM}The stage decides the first ten seconds AND the ask. Do not sell at stage 1 or 2.${RESET}`);
|
|
12479
|
+
console.log(`${DIM}A month of posts should cover the ladder — default mix + the grid: vidfarm skill show content-ideas${RESET}`);
|
|
12480
|
+
console.log(`${DIM}Angles for a stage: vidfarm ideas --angles · pair them up: vidfarm ideas --grid --topic "<offer>"${RESET}`);
|
|
12481
|
+
return;
|
|
12482
|
+
}
|
|
12483
|
+
if (parsed.values.angles || parsed.values.angle) {
|
|
12484
|
+
const wanted = String(parsed.values.angle ?? "").trim().toLowerCase();
|
|
12485
|
+
const picked = wanted
|
|
12486
|
+
? angleBank.angles.filter((entry) => entry.family.toLowerCase().includes(wanted) || entry.frame.toLowerCase().includes(wanted))
|
|
12487
|
+
: angleBank.angles;
|
|
12488
|
+
if (wanted && !picked.length) {
|
|
12489
|
+
throw new Error(`No angle matches "${wanted}". Angle families: ${angleBank.families.join(", ")} (see \`vidfarm ideas --angles\`).`);
|
|
12490
|
+
}
|
|
12491
|
+
if (json) {
|
|
12492
|
+
return printJson({
|
|
12493
|
+
ok: true,
|
|
12494
|
+
offline: true,
|
|
12495
|
+
topic: topic || null,
|
|
12496
|
+
total_angles: angleBank.angles.length,
|
|
12497
|
+
families: angleBank.families,
|
|
12498
|
+
angles: picked,
|
|
12499
|
+
method: "references/content-ideas.md"
|
|
12500
|
+
});
|
|
12501
|
+
}
|
|
12502
|
+
console.log(`${BOLD}Problem angles${RESET} ${DIM}(${picked.length} of ${angleBank.angles.length} · which side of the problem the video comes from)${RESET}`);
|
|
12503
|
+
if (topic)
|
|
12504
|
+
console.log(`${DIM}Topic: ${RESET}${topic}`);
|
|
12505
|
+
console.log("");
|
|
12506
|
+
let angleGroup = "";
|
|
12507
|
+
for (const entry of picked) {
|
|
12508
|
+
if (entry.family !== angleGroup) {
|
|
12509
|
+
angleGroup = entry.family;
|
|
12510
|
+
console.log(` ${BOLD}${angleGroup}${RESET}`);
|
|
12511
|
+
}
|
|
12512
|
+
console.log(` ${GREEN}${entry.frame}${RESET}${entry.note ? ` ${DIM}— ${entry.note}${RESET}` : ""}`);
|
|
12513
|
+
}
|
|
12514
|
+
console.log("");
|
|
12515
|
+
console.log(`${DIM}"I already covered that topic" → hold the frame, change the angle. Same research, new video.${RESET}`);
|
|
12516
|
+
console.log(`${DIM}Pair angles with frames and stages: vidfarm ideas --grid --topic "<offer>"${RESET}`);
|
|
12517
|
+
return;
|
|
12518
|
+
}
|
|
12519
|
+
if (parsed.values.grid) {
|
|
12520
|
+
// The grid is the honest answer to "give me 30 videos": vary TWO axes.
|
|
12521
|
+
// Walking frames, angles and stages at different strides keeps every row a
|
|
12522
|
+
// different combination instead of 30 rows of one frame family.
|
|
12523
|
+
const rows = Number(parsed.values.count ?? 20);
|
|
12524
|
+
const total = Number.isFinite(rows) && rows > 0 ? Math.min(rows, 200) : 20;
|
|
12525
|
+
if (!angleBank.angles.length || !ladder.length) {
|
|
12526
|
+
throw new Error("The bundled content-ideas reference has no angle or awareness bank. Update it with `vidfarm skills add vidfarm`.");
|
|
12527
|
+
}
|
|
12528
|
+
// Stage order is not round-robin: this ten-row cycle reproduces the default
|
|
12529
|
+
// monthly mix from the reference (3 unaware, 3 problem-aware, 2 solution-
|
|
12530
|
+
// aware, 1 product-aware, 1 most-aware), so a 30-row grid is already a
|
|
12531
|
+
// balanced month instead of six videos of each.
|
|
12532
|
+
const mixCycle = [1, 2, 1, 3, 2, 4, 1, 2, 3, 5];
|
|
12533
|
+
// Each stage names the frames and angles it can actually carry. Pairing
|
|
12534
|
+
// "the price objection" with a stage-1 video would hand the director a row
|
|
12535
|
+
// the reference itself calls wrong, so draw from the stage's own lists and
|
|
12536
|
+
// fall back to the full banks only if a list is missing.
|
|
12537
|
+
const listed = (stage, label) => (stage.fields.find((field) => field.label.toLowerCase().startsWith(label))?.value ?? "")
|
|
12538
|
+
.split(" · ").map((item) => item.trim().toLowerCase()).filter(Boolean);
|
|
12539
|
+
const gridSeen = new Set();
|
|
12540
|
+
const grid = Array.from({ length: total }, (_, i) => {
|
|
12541
|
+
const stage = ladder.find((entry) => entry.index === mixCycle[i % mixCycle.length]) ?? ladder[i % ladder.length];
|
|
12542
|
+
const round = Math.floor(i / mixCycle.length);
|
|
12543
|
+
const fitFrames = listed(stage, "frames");
|
|
12544
|
+
const fitAngles = listed(stage, "angles");
|
|
12545
|
+
const framePool = frames.filter((entry) => fitFrames.includes(entry.frame.toLowerCase()));
|
|
12546
|
+
const anglePool = angleBank.angles.filter((entry) => fitAngles.includes(entry.frame.toLowerCase()));
|
|
12547
|
+
const usableFrames = framePool.length ? framePool : frames;
|
|
12548
|
+
const usableAngles = anglePool.length ? anglePool : angleBank.angles;
|
|
12549
|
+
// Strides alone collide once a pool size divides one of them, and a grid
|
|
12550
|
+
// that prints the same row twice is exactly the repetition this command
|
|
12551
|
+
// sells against — so walk forward until the triple is new.
|
|
12552
|
+
let frame = usableFrames[(i + round) % usableFrames.length];
|
|
12553
|
+
let angle = usableAngles[(i * 3 + round) % usableAngles.length];
|
|
12554
|
+
for (let bump = 0; bump < usableFrames.length * usableAngles.length; bump += 1) {
|
|
12555
|
+
if (!gridSeen.has(`${stage.index}|${frame.frame}|${angle.frame}`))
|
|
12556
|
+
break;
|
|
12557
|
+
angle = usableAngles[(i * 3 + round + bump + 1) % usableAngles.length];
|
|
12558
|
+
if ((bump + 1) % usableAngles.length === 0)
|
|
12559
|
+
frame = usableFrames[(i + round + bump + 1) % usableFrames.length];
|
|
12560
|
+
}
|
|
12561
|
+
gridSeen.add(`${stage.index}|${frame.frame}|${angle.frame}`);
|
|
12562
|
+
return {
|
|
12563
|
+
stage: `${stage.index} · ${stage.stage}`,
|
|
12564
|
+
angle: angle.frame,
|
|
12565
|
+
angle_family: angle.family,
|
|
12566
|
+
frame: frame.frame,
|
|
12567
|
+
frame_family: frame.family,
|
|
12568
|
+
starter: topic ? joinIdeaFrame(frame.frame, topic) : null
|
|
12569
|
+
};
|
|
12570
|
+
});
|
|
12571
|
+
if (json)
|
|
12572
|
+
return printJson({ ok: true, offline: true, topic: topic || null, rows: grid, method: "references/content-ideas.md" });
|
|
12573
|
+
console.log(`${BOLD}Idea grid${RESET} ${DIM}(${total} combinations · stage × angle × frame · offline, free, no AI call)${RESET}`);
|
|
12574
|
+
if (topic)
|
|
12575
|
+
console.log(`${DIM}Topic: ${RESET}${topic}`);
|
|
12576
|
+
console.log("");
|
|
12577
|
+
for (const row of grid) {
|
|
12578
|
+
console.log(` ${DIM}${row.stage.padEnd(18)}${RESET}${GREEN}${row.angle}${RESET}`);
|
|
12579
|
+
console.log(` ${row.starter ?? row.frame}`);
|
|
12580
|
+
}
|
|
12581
|
+
console.log("");
|
|
12582
|
+
console.log(`${DIM}Each row is a DIFFERENT video, not a rewrite — the stage and the angle both moved.${RESET}`);
|
|
12583
|
+
console.log(`${DIM}Sharpen each into a real title, keep the stage label on it, then write hook/loop/payoff/bait.${RESET}`);
|
|
12584
|
+
console.log(`${DIM}Method, the monthly stage mix, and a worked example: vidfarm skill show content-ideas${RESET}`);
|
|
12585
|
+
return;
|
|
12586
|
+
}
|
|
12103
12587
|
if (parsed.values.families) {
|
|
12104
12588
|
if (json)
|
|
12105
12589
|
return printJson({ ok: true, families, frames: frames.length });
|
|
@@ -12147,6 +12631,9 @@ async function runIdeasCommand(argv) {
|
|
|
12147
12631
|
total_frames: frames.length,
|
|
12148
12632
|
families,
|
|
12149
12633
|
frames: picked.map((entry) => ({ frame: entry.frame, family: entry.family, starter: topic ? joinIdeaFrame(entry.frame, topic) : null })),
|
|
12634
|
+
total_angles: angleBank.angles.length,
|
|
12635
|
+
angle_families: angleBank.families,
|
|
12636
|
+
awareness_stages: ladder.map((entry) => ({ index: entry.index, stage: entry.stage, summary: entry.summary })),
|
|
12150
12637
|
method: "references/content-ideas.md"
|
|
12151
12638
|
});
|
|
12152
12639
|
}
|
|
@@ -12170,9 +12657,18 @@ async function runIdeasCommand(argv) {
|
|
|
12170
12657
|
else {
|
|
12171
12658
|
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
12659
|
}
|
|
12660
|
+
console.log(`${DIM}A frame is only ONE axis. The other two are here too, and a set that freezes them repeats:${RESET}`);
|
|
12661
|
+
console.log(`${DIM} vidfarm ideas --stages ${RESET}${ladder.length} awareness stages ${DIM}— what the viewer knows, and what you may ask for${RESET}`);
|
|
12662
|
+
console.log(`${DIM} vidfarm ideas --angles ${RESET}${angleBank.angles.length} problem angles ${DIM}— which side of the problem the video comes from${RESET}`);
|
|
12663
|
+
console.log(`${DIM} vidfarm ideas --grid --topic "<offer>"${RESET} ${DIM}— stage × angle × frame, one row per video${RESET}`);
|
|
12173
12664
|
console.log(`${DIM}Method + frame→format notes: vidfarm skill show content-ideas · families: vidfarm ideas --families${RESET}`);
|
|
12174
12665
|
console.log(`${DIM}Never ship a frame as a hook — the four charges still apply: vidfarm skill show hooks${RESET}`);
|
|
12175
12666
|
console.log(`${DIM}Producing the whole set is a batch: vidfarm skill show bulk (one frame per video).${RESET}`);
|
|
12667
|
+
// The natural upsell, in the right order: ideas are the easy win, and the
|
|
12668
|
+
// interview is what a director opts into once they want the strategy behind
|
|
12669
|
+
// the list. Offering it here means it never has to be asked for cold.
|
|
12670
|
+
console.log(`${DIM}Save the picks as content-ideas.md in your working folder. Want the strategy behind them${RESET}`);
|
|
12671
|
+
console.log(`${DIM}(who to sell to, which angle, which hook)? That is the interview: vidfarm consult coldstart --short${RESET}`);
|
|
12176
12672
|
}
|
|
12177
12673
|
async function runSkillDocsCommand(sub, argv) {
|
|
12178
12674
|
const parsed = parseArgs({
|
|
@@ -12340,4 +12836,21 @@ async function runUpdateSkillCommand(argv) {
|
|
|
12340
12836
|
...(ctx.json ? ["--json"] : [])
|
|
12341
12837
|
]);
|
|
12342
12838
|
}
|
|
12839
|
+
// ── entry point ──────────────────────────────────────────────────────────────
|
|
12840
|
+
// Last statement in the file ON PURPOSE: see the note where this used to live.
|
|
12841
|
+
// Every module-level declaration above is initialized by the time a command
|
|
12842
|
+
// handler reads it, so late-declared help texts and tables are safe.
|
|
12843
|
+
void main().catch(async (error) => {
|
|
12844
|
+
// Deliberate, expected user-facing conditions (e.g. local mode in the
|
|
12845
|
+
// published cloud-only CLI) print just the message — no stack, no crash report.
|
|
12846
|
+
if (error instanceof LocalModeUnavailableError || error instanceof CostModeBlockedError) {
|
|
12847
|
+
console.error(error.message);
|
|
12848
|
+
process.exit(1);
|
|
12849
|
+
}
|
|
12850
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
12851
|
+
// Report only unexpected crashes (bugs), never the deliberate user-facing
|
|
12852
|
+
// `throw new Error(...)` above; no-op unless a devcli DSN is configured.
|
|
12853
|
+
await reportCliCrash(error);
|
|
12854
|
+
process.exit(1);
|
|
12855
|
+
});
|
|
12343
12856
|
//# sourceMappingURL=cli.js.map
|