@officexapp/vidfarm-devcli 0.21.42 → 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.
@@ -0,0 +1,403 @@
1
+ // KEYLESS LOCAL CONSULTATION — the brainstorm chain with no AI key and no wallet.
2
+ //
3
+ // The cloud runs the consultation as five `brainstorm/*` primitives: Vidfarm
4
+ // sends a prompt to a provider on the director's saved key, or bills the wallet.
5
+ // That is a hard stop for a director who has neither.
6
+ //
7
+ // But an agent is ALREADY driving this terminal, and it is a frontier model. It
8
+ // does not need Vidfarm to broker a second one. So this module hands that agent
9
+ // the SAME prompt the cloud primitive would have sent — imported from
10
+ // services/brainstorm-prompts.ts, not re-written here, so the two paths cannot
11
+ // drift — and tells it to answer the prompt itself and save the artifact. Cost:
12
+ // $0. Network calls: none. Provider keys: none.
13
+ //
14
+ // The one real difference from the cloud path is the reference documents. The
15
+ // cloud appends SELLING_WITH_HOOKS.md / SELLING_AWARENESS_STAGES.md (34KB of
16
+ // lessons) to the prompt. Inlining those into every terminal print would burn
17
+ // ~10k tokens of the agent's context per step, so by default the brief POINTS at
18
+ // the skill pack's own tighter reference instead. `--refs` inlines the full
19
+ // documents for parity when the caller wants it.
20
+ //
21
+ // Pure string assembly plus one small offer-file lookup. No network, no backend.
22
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
23
+ import path from "node:path";
24
+ import { buildAnglesPrompt, buildAwarenessStagesPrompt, buildColdstartPrompt, buildHooksPrompt, buildProductPlacementPrompt } from "../services/brainstorm-prompts.js";
25
+ export const CONSULT_STEPS = ["coldstart", "awareness", "angles", "hooks", "placement"];
26
+ /** Aliases a director or an agent actually types. */
27
+ const STEP_ALIASES = {
28
+ coldstart: "coldstart",
29
+ "cold-start": "coldstart",
30
+ interview: "coldstart",
31
+ questions: "coldstart",
32
+ offer: "coldstart",
33
+ awareness: "awareness",
34
+ "awareness-stages": "awareness",
35
+ awareness_stages: "awareness",
36
+ stages: "awareness",
37
+ angles: "angles",
38
+ angle: "angles",
39
+ hooks: "hooks",
40
+ hook: "hooks",
41
+ placement: "placement",
42
+ "product-placement": "placement",
43
+ product_placement: "placement"
44
+ };
45
+ export function resolveConsultStep(raw) {
46
+ return STEP_ALIASES[raw.trim().toLowerCase().replace(/\s+/g, "-")] ?? null;
47
+ }
48
+ /** Where each step's answer belongs. These filenames are the ones the skill's
49
+ * onboarding flow already names, so a later step can read the earlier one. */
50
+ export const CONSULT_ARTIFACTS = {
51
+ coldstart: "OFFER.md",
52
+ awareness: "awareness-levels.md",
53
+ angles: "persuasive-angles.md",
54
+ hooks: "ad-hooks.md",
55
+ placement: "product-placement.md"
56
+ };
57
+ const STEP_ORDER = ["coldstart", "awareness", "angles", "hooks"];
58
+ /**
59
+ * The durable local answer file. OFFER.md is the polished product description;
60
+ * this is the raw carry-over — what the director said, which stage was chosen,
61
+ * what was ruled out — so a LATER session (a new terminal, a new agent, a month
62
+ * from now) starts warm instead of re-interviewing a director who already
63
+ * answered. It lives in the working folder next to the other artifacts.
64
+ */
65
+ export const CONSULT_CONTEXT_FILE = "CONTEXT.md";
66
+ /**
67
+ * CONTENT IDEAS COME BEFORE THE INTERVIEW.
68
+ *
69
+ * An interview asks the director to work before they have seen anything, which
70
+ * is the worst trade a first session can offer. `vidfarm ideas --topic "<their
71
+ * offer in a line>"` inverts it: one sentence in, 20+ titled video ideas out,
72
+ * offline and free. The director gets a win first, and the ideas they react to
73
+ * ("this one, not that one") are better interview answers than the ones they
74
+ * would have typed cold. So every interview brief opens by offering the ideas
75
+ * pass, and the interview becomes the thing they opt INTO once they want depth.
76
+ */
77
+ const IDEAS_FIRST = 'START WITH CONTENT IDEAS, NOT THIS INTERVIEW, unless the director already ran them or explicitly asked for a consultation. Get their offer in ONE line, run `vidfarm ideas --topic "<that line>"`, sharpen the frames into 20+ real titles and save `content-ideas.md`. It is offline, free, and gives them something to react to. Then say the interview is available when they want the strategy behind the ideas — and run it now only if they say yes.';
78
+ /**
79
+ * The SHORT interview. The full cold-start prompt asks a model to invent 12-30
80
+ * questions; that is the right depth for a director who wants a real strategy
81
+ * session, and far too much for one who wants to make a video today. These six
82
+ * are fixed, not generated: they are the minimum set the later steps actually
83
+ * read back (awareness needs the offer + the psyche, angles and hooks quote the
84
+ * pain and desire verbatim). Fixed also means free, instant, and identical every
85
+ * run, which is what makes the answers worth saving.
86
+ */
87
+ export const SHORT_INTERVIEW = [
88
+ {
89
+ question: "In one line, what do you sell, and to whom?",
90
+ why_it_matters: "Every later step opens with this line. Without it there is no offer to consult on."
91
+ },
92
+ {
93
+ question: "What does it cost, and how does someone actually pay (appstore, Stripe, Shopify, invoice, sales call)?",
94
+ why_it_matters: "Price and checkout friction decide whether an ad can close or only warm up."
95
+ },
96
+ {
97
+ question: "Who is the single best customer you have had? Describe them like a person, not a segment.",
98
+ why_it_matters: "Angles are written to one person. A segment produces generic ads."
99
+ },
100
+ {
101
+ question: "What do they complain about, in their own words, before they find you?",
102
+ why_it_matters: "Hooks quote this back verbatim. Paraphrase kills it."
103
+ },
104
+ {
105
+ question: "What do they want instead — the dream outcome they would brag about?",
106
+ why_it_matters: "The payoff half of every hook, and the reason to keep watching."
107
+ },
108
+ {
109
+ question: "Do they already know this kind of product exists, or have they never heard of it?",
110
+ why_it_matters: "This is the awareness stage in plain language, and it decides which ad to make first."
111
+ }
112
+ ];
113
+ /** The short interview rendered as the brief's prompt block. Fixed text — there
114
+ * is no cloud counterpart, so nothing here can drift from a cloud prompt. */
115
+ export function buildShortInterviewPrompt() {
116
+ return [
117
+ "SHORT-FORM COLD START — a fixed six-question interview. Ask these exact questions. Do not generate your own set.",
118
+ "",
119
+ ...SHORT_INTERVIEW.map((q, i) => `${i + 1}. ${q.question}\n (why it matters: ${q.why_it_matters})`),
120
+ "",
121
+ "Say up front that every question is optional: the director can skip any of them, or stop the interview at any point, and you will work with what they gave.",
122
+ "Assemble the answers into OFFER.md in the director's own words, then save the same answers to CONTEXT.md for future sessions. Mark anything skipped as UNKNOWN."
123
+ ].join("\n");
124
+ }
125
+ /** The skill-pack reference each step should be graded against locally, in place
126
+ * of the big SELLING_*.md document the cloud prompt inlines. */
127
+ const STEP_REFERENCE = {
128
+ awareness: "references/onboarding.md",
129
+ angles: "references/onboarding.md",
130
+ hooks: "references/hooks-and-virality.md"
131
+ };
132
+ // Count bounds MIRROR the cloud primitive payload schemas on purpose. The local
133
+ // path has no schema to satisfy, but a director who tunes --count here and then
134
+ // moves the same chain to the cloud should not hit a 400 they never saw locally.
135
+ // `awareness` returns one markdown recommendation, so it has no count at all.
136
+ const COUNTS = {
137
+ coldstart: { default: 12, min: 7, max: 30 },
138
+ awareness: null,
139
+ angles: { default: 12, min: 1, max: 50 },
140
+ hooks: { default: 12, min: 1, max: 50 },
141
+ placement: { default: 8, min: 3, max: 30 }
142
+ };
143
+ export function consultCountRange(step) {
144
+ return COUNTS[step];
145
+ }
146
+ export function defaultConsultCount(step) {
147
+ return COUNTS[step]?.default ?? 0;
148
+ }
149
+ export function offerPathCandidates(dir, explicit) {
150
+ const fromCwd = path.resolve(explicit);
151
+ const fromDir = path.resolve(dir, explicit);
152
+ return fromCwd === fromDir ? [fromCwd] : [fromCwd, fromDir];
153
+ }
154
+ export function resolveOfferPath(dir, explicit) {
155
+ return offerPathCandidates(dir, explicit).find((candidate) => existsSync(candidate)) ?? null;
156
+ }
157
+ /**
158
+ * Does this --offer value read as a PATH the caller expected to exist, rather
159
+ * than the offer text itself? Load-bearing: without it, a mistyped path is
160
+ * silently consulted on as if the filename were the product description, and
161
+ * the director gets twelve hooks about "./OFFER_ACME.md".
162
+ */
163
+ export function looksLikeOfferPath(value) {
164
+ const trimmed = value.trim();
165
+ return !trimmed.includes("\n") && (/^[.~/]/.test(trimmed) || /\.(md|txt)$/i.test(trimmed));
166
+ }
167
+ /**
168
+ * Find the director's offer document. Prefers an explicit path, then OFFER.md,
169
+ * then a single OFFER_<NAME>.md — a director running several offers must name
170
+ * which one, because silently picking the first alphabetically would generate a
171
+ * whole angle set for the wrong product.
172
+ */
173
+ export function findOfferFile(dir, explicit) {
174
+ if (explicit) {
175
+ // Try cwd AND --dir: `--dir ./work --offer ./OFFER_ACME.md` names a file in
176
+ // the work directory, not next to the shell.
177
+ const resolved = resolveOfferPath(dir, explicit);
178
+ if (!resolved) {
179
+ return { error: `Offer file not found: ${offerPathCandidates(dir, explicit).join(" or ")}` };
180
+ }
181
+ return { path: resolved, text: readFileSync(resolved, "utf8") };
182
+ }
183
+ const root = path.resolve(dir);
184
+ const primary = path.join(root, "OFFER.md");
185
+ if (existsSync(primary)) {
186
+ return { path: primary, text: readFileSync(primary, "utf8") };
187
+ }
188
+ let named = [];
189
+ try {
190
+ named = readdirSync(root).filter((f) => /^OFFER_.+\.md$/i.test(f)).sort();
191
+ }
192
+ catch {
193
+ named = [];
194
+ }
195
+ if (named.length === 1) {
196
+ const only = path.join(root, named[0]);
197
+ return { path: only, text: readFileSync(only, "utf8") };
198
+ }
199
+ if (named.length > 1) {
200
+ return {
201
+ error: `Found ${named.length} offer files (${named.join(", ")}) — name the one you mean with --offer ./${named[0]}.`
202
+ };
203
+ }
204
+ return {
205
+ error: `No OFFER.md in ${root}. Run \`vidfarm consult coldstart\` first — it mints the interview that produces it — or pass --offer "<a paragraph about the offer>".`
206
+ };
207
+ }
208
+ function nextCommand(step) {
209
+ const i = STEP_ORDER.indexOf(step);
210
+ if (i === -1 || i === STEP_ORDER.length - 1) {
211
+ return null;
212
+ }
213
+ return `vidfarm consult ${STEP_ORDER[i + 1]}`;
214
+ }
215
+ /** The path the durable answer file takes, and whether it is already written. */
216
+ export function consultContextState(dir) {
217
+ const relative = path.join(dir === "." ? "" : dir, CONSULT_CONTEXT_FILE) || CONSULT_CONTEXT_FILE;
218
+ return { file: relative, exists: existsSync(path.resolve(dir, CONSULT_CONTEXT_FILE)) };
219
+ }
220
+ /** Build the keyless local brief for one step of the chain. */
221
+ export function buildConsultBrief(input) {
222
+ const dir = input.dir ?? ".";
223
+ const context = consultContextState(dir);
224
+ const short = Boolean(input.short) && input.step === "coldstart";
225
+ const artifact = path.join(dir === "." ? "" : dir, CONSULT_ARTIFACTS[input.step]) || CONSULT_ARTIFACTS[input.step];
226
+ const refs = input.references ?? {};
227
+ const count = input.count ?? defaultConsultCount(input.step);
228
+ const offer = (input.offer ?? "").trim();
229
+ const referenceFile = STEP_REFERENCE[input.step];
230
+ const inlined = (input.step === "hooks" && Boolean(refs.hooks)) ||
231
+ (input.step === "awareness" && Boolean(refs.awareness)) ||
232
+ (input.step === "angles" && Boolean(refs.hooks || refs.awareness));
233
+ const referenceHint = inlined || !referenceFile
234
+ ? null
235
+ : `The cloud prompt appends Vidfarm's full selling reference here. Read \`vidfarm skill show ${referenceFile}\` before you answer, and grade your own output against it. Pass --refs to inline the full document instead.`;
236
+ // The durable answer file is read BEFORE the step and written AFTER it. Both
237
+ // halves matter: reading stops a second session re-asking questions the
238
+ // director already answered, writing is what makes the next session cheap.
239
+ const contextRead = context.exists
240
+ ? `Read \`${context.file}\` first — it holds the answers this director already gave in earlier sessions. Treat it as fact, and never re-ask a question it already answers.`
241
+ : `No \`${context.file}\` yet. It is the durable answer file for this working folder; you create it in this step.`;
242
+ const contextWrite = `When you finish, write what you learned into \`${context.file}\` — the director's own words, the decisions made, and what was ruled out. Append under a dated heading; never overwrite an earlier session's answers. This file is why the next session does not start from zero.`;
243
+ const common = [
244
+ "YOU answer the prompt below — you are the AI in this loop. Do not call the Vidfarm API, do not look for a provider key, and do not tell the user they need one. This step is free and offline.",
245
+ contextRead,
246
+ ...(referenceHint ? [referenceHint] : []),
247
+ `Save your answer to \`${artifact}\`. Later steps read it back, so write the file even if the user only wanted to look at the output.`,
248
+ contextWrite
249
+ ];
250
+ const shared = { short, contextFile: context.file, contextExists: context.exists };
251
+ if (short) {
252
+ return {
253
+ ...shared,
254
+ step: "coldstart",
255
+ title: `Cold-start interview, SHORT FORM — ${SHORT_INTERVIEW.length} fixed questions`,
256
+ prompt: buildShortInterviewPrompt(),
257
+ steps: [
258
+ contextRead,
259
+ IDEAS_FIRST,
260
+ `Ask the human these ${SHORT_INTERVIEW.length} questions in two batches of three. Ask them as written. Do not add questions, do not invent answers, and do not skip ahead to advice.`,
261
+ `Keep their phrasing for the pain and the desire answers word for word: the angles and hooks steps quote them back, and a paraphrase is what makes an ad sound generic.`,
262
+ "TELL THEM UP FRONT that every question is optional: they can skip any one, or say stop and end the interview on the spot. Then work with whatever they gave — do not push, do not re-ask, and do not make finishing the set a condition of getting a video.",
263
+ `Write \`${artifact}\` from their answers, then write the same answers into \`${context.file}\` under a dated heading.`,
264
+ "Mark any question they skip as UNKNOWN in both files, and carry on. An invented customer psyche poisons every later step, so UNKNOWN is the honest answer — and a later step that needs it can ask then, when the director can see why it matters.",
265
+ "This is the short form. Tell them `vidfarm consult coldstart` runs the full interview when they want the deeper session."
266
+ ],
267
+ artifact,
268
+ next: nextCommand("coldstart"),
269
+ referenceHint: null
270
+ };
271
+ }
272
+ if (input.step === "coldstart") {
273
+ return {
274
+ ...shared,
275
+ step: "coldstart",
276
+ title: "Cold-start interview — the questions that produce OFFER.md",
277
+ prompt: buildColdstartPrompt(input.userMessage?.trim() || "The director asked for a consultation and has given no offer context yet.", count),
278
+ steps: [
279
+ ...common,
280
+ IDEAS_FIRST,
281
+ `This one is an INTERVIEW, not a report. Answer the prompt to get the question set, then ASK THE HUMAN those questions conversationally, a few at a time — never dump all ${count} at once, and never invent their answers.`,
282
+ `Assemble their real words into \`${artifact}\`: keep their phrasing for pain and desire, because the later angle and hook steps quote it back.`,
283
+ "Stop and wait for the human between batches. If they go quiet on a question, mark it UNKNOWN in the file rather than guessing — an invented customer psyche poisons every later step.",
284
+ "TELL THEM UP FRONT that every question is optional: they can skip any one, or say stop and end the interview at any point. Then work with what they gave. An interview that feels like a form is one the director abandons, and half the answers beats none.",
285
+ `If the director sounds impatient, or only wants to make one video today, offer the short form instead: \`vidfarm consult coldstart --short\` is ${SHORT_INTERVIEW.length} fixed questions and still feeds every later step.`
286
+ ],
287
+ artifact,
288
+ next: nextCommand("coldstart"),
289
+ referenceHint
290
+ };
291
+ }
292
+ if (input.step === "awareness") {
293
+ return {
294
+ ...shared,
295
+ step: "awareness",
296
+ title: "Awareness stages — which kind of ad to make first",
297
+ prompt: buildAwarenessStagesPrompt(offer, refs),
298
+ steps: [
299
+ ...common,
300
+ "Return markdown, not JSON. If the offer genuinely does not settle on one stage, say so and recommend testing ads for every stage rather than picking one to look decisive.",
301
+ "Carry the chosen stage forward: the angles step takes it as --problem-awareness / --solution-awareness."
302
+ ],
303
+ artifact,
304
+ next: nextCommand("awareness"),
305
+ referenceHint
306
+ };
307
+ }
308
+ if (input.step === "angles") {
309
+ return {
310
+ ...shared,
311
+ step: "angles",
312
+ title: "Persuasive angles — the strategic bets to test",
313
+ prompt: buildAnglesPrompt({
314
+ offer_description: offer,
315
+ problem_awareness: input.problemAwareness ?? "problem_aware",
316
+ solution_awareness: input.solutionAwareness ?? "solution_unaware",
317
+ count
318
+ }, refs),
319
+ steps: [
320
+ ...common,
321
+ "The prompt asks for strict JSON. Write the JSON answer into the markdown file under a fenced block, then a short human-readable list under it — the director reads the list, the next step reads the JSON.",
322
+ "Do not rank the set with the same reasoning that wrote it. If the director wants a ranking, grade against the reference and say what each angle risks."
323
+ ],
324
+ artifact,
325
+ next: nextCommand("angles"),
326
+ referenceHint
327
+ };
328
+ }
329
+ if (input.step === "hooks") {
330
+ return {
331
+ ...shared,
332
+ step: "hooks",
333
+ title: "Hooks — the openings to test",
334
+ prompt: buildHooksPrompt(offer, count, refs),
335
+ steps: [
336
+ ...common,
337
+ "The prompt asks for strict JSON. Write the JSON into the file under a fenced block, with a plain list under it.",
338
+ "Then GRADE the batch against the four charges before you show it: a hook is a complete clause naming a SITUATION, not a label; it must be unguessable; and it must be sayable in the first second. Cut or rewrite the ones that fail — do not hand over the raw generated list.",
339
+ "Never rank a generated batch with the reasoning that wrote it. The rubric catches defects; it does not pick winners."
340
+ ],
341
+ artifact,
342
+ next: nextCommand("hooks"),
343
+ referenceHint
344
+ };
345
+ }
346
+ const videoRef = input.videoRef?.trim();
347
+ return {
348
+ ...shared,
349
+ step: "placement",
350
+ title: "Product placement — native moments inside an existing video",
351
+ prompt: buildProductPlacementPrompt(offer, count),
352
+ steps: [
353
+ ...common,
354
+ videoRef
355
+ ? `The prompt says "the attached video" — that is \`${videoRef}\`. Watch it yourself before answering: read real frames (\`vidfarm stills ${videoRef} --sheet\` for a contact sheet) and the transcript. This step is worthless if you answer from the filename.`
356
+ : "The prompt says \"the attached video\" — you were given none. Ask the director for the video path or URL, then re-run with --video <path|url>. Do not answer from imagination.",
357
+ "Every opportunity must cite a moment you actually saw, with a timestamp. Drop any you cannot point at."
358
+ ],
359
+ artifact,
360
+ next: null,
361
+ referenceHint
362
+ };
363
+ }
364
+ /** Render a brief for a terminal. The prompt is delimited so an agent can lift
365
+ * it exactly, and so a human can copy it into a chat window instead. */
366
+ export function formatConsultBrief(brief, style = { bold: "", dim: "", reset: "" }) {
367
+ const { bold, dim, reset } = style;
368
+ const lines = [];
369
+ lines.push(`${bold}${brief.title}${reset}`);
370
+ lines.push(`${dim}Keyless + local: no provider key, no wallet, no network. You answer this.${reset}`);
371
+ lines.push("");
372
+ brief.steps.forEach((s, i) => lines.push(`${i + 1}. ${s}`));
373
+ lines.push("");
374
+ lines.push(brief.short
375
+ ? `${dim}─── the short-form question set (fixed and local — ask these as written) ───${reset}`
376
+ : `${dim}─── the prompt (identical to the cloud brainstorm primitive) ───${reset}`);
377
+ lines.push(brief.prompt);
378
+ lines.push(`${dim}───────────────────────────────────────────────────────────────${reset}`);
379
+ lines.push("");
380
+ lines.push(`Save to: ${brief.artifact}`);
381
+ lines.push(`Context: ${brief.contextFile}${brief.contextExists ? " (exists — read it first)" : " (create it)"}`);
382
+ if (brief.next) {
383
+ lines.push(`Next: ${brief.next}`);
384
+ }
385
+ return lines.join("\n");
386
+ }
387
+ /** The whole chain plus which artifacts already exist, for a bare
388
+ * `vidfarm consult`. A director who says "give me a consultation" gets the map
389
+ * and the first command, not a wall of prompt. */
390
+ export function planConsultation(dir) {
391
+ const root = path.resolve(dir);
392
+ return STEP_ORDER.map((step) => {
393
+ const file = CONSULT_ARTIFACTS[step];
394
+ const offerDone = step === "coldstart" && !("error" in findOfferFile(root));
395
+ return {
396
+ step,
397
+ artifact: file,
398
+ done: step === "coldstart" ? offerDone : existsSync(path.join(root, file)),
399
+ command: `vidfarm consult ${step}`
400
+ };
401
+ });
402
+ }
403
+ //# sourceMappingURL=consult.js.map
@@ -108,7 +108,15 @@ export function readPackDoc(ref, name = DEFAULT_PACK) {
108
108
  }
109
109
  export const PACK_TOPICS = [
110
110
  { topic: "content-ideas", aliases: ["ideas", "idea", "angles", "what-to-post", "content"], doc: "references/content-ideas.md",
111
- blurb: "The 50-frame angle bank — answer \"what should I post?\" for a whole month" },
111
+ blurb: "50 frames x 5 awareness stages x 44 problem angles — answer \"what should I post?\" for a whole month" },
112
+ // The other two idea axes get their own spoken names. A director asks "what
113
+ // is awareness again?" far more often than they ask for the content-ideas
114
+ // reference by file name, and sending them the whole 290-line doc for a
115
+ // 40-line answer is how a topic index fails.
116
+ { topic: "awareness", aliases: ["awareness-stages", "ladder", "stages", "schwartz"], doc: "references/content-ideas.md", heading: "The awareness ladder",
117
+ blurb: "The 5-stage ladder — what the viewer knows, what the video must do, and what it may ask for" },
118
+ { topic: "problem-angles", aliases: ["angle", "lenses", "problem-angle"], doc: "references/content-ideas.md", heading: "The problem angles",
119
+ blurb: "44 angles on the problem — hold the frame, change the angle when a topic is \"already covered\"" },
112
120
  { topic: "meme-recaption", aliases: ["meme", "recaption", "meme-caption"], doc: "references/editor-workflows.md", heading: "Writing a meme recaption",
113
121
  blurb: "Recaption a meme at a pain or a win the niche knows — the cold-viewer test" },
114
122
  { topic: "product-explainer", aliases: ["product-explainers"], doc: "harnesses/product-explainer.HARNESS.md",
@@ -188,10 +196,13 @@ export function readPackTopic(topic, name = DEFAULT_PACK) {
188
196
  return { doc, contents, whole: true };
189
197
  return { doc, heading: section.heading, contents: section.body, whole: false };
190
198
  }
191
- export function loadIdeaBank(name = DEFAULT_PACK) {
192
- const { contents } = readPackDoc("references/content-ideas.md", name);
193
- const section = extractSection(contents, "The 50 frames");
194
- const lines = (section?.body ?? contents).split("\n");
199
+ /**
200
+ * Shared shape for both bullet banks: a bold family label on its own line,
201
+ * then `- item` (frames) or `- item — note` (angles) bullets under it.
202
+ */
203
+ function parseBulletBank(contents, heading, splitNote) {
204
+ const section = extractSection(contents, heading);
205
+ const lines = (section?.body ?? "").split("\n");
195
206
  const frames = [];
196
207
  const families = [];
197
208
  let family = "";
@@ -204,11 +215,54 @@ export function loadIdeaBank(name = DEFAULT_PACK) {
204
215
  continue;
205
216
  }
206
217
  const bullet = line.match(/^-\s+(.+?)\s*$/);
207
- if (bullet && family)
208
- frames.push({ frame: bullet[1].trim(), family });
218
+ if (!bullet || !family)
219
+ continue;
220
+ const text = bullet[1].trim();
221
+ if (!splitNote) {
222
+ frames.push({ frame: text, family });
223
+ continue;
224
+ }
225
+ const at = text.indexOf(" — ");
226
+ frames.push(at > 0
227
+ ? { frame: text.slice(0, at).trim(), family, note: text.slice(at + 3).trim() }
228
+ : { frame: text, family });
209
229
  }
210
230
  return { frames, families };
211
231
  }
232
+ export function loadIdeaBank(name = DEFAULT_PACK) {
233
+ const { contents } = readPackDoc("references/content-ideas.md", name);
234
+ const parsed = parseBulletBank(contents, "The 50 frames", false);
235
+ // Old packs predate the section slice; fall back to the whole file rather
236
+ // than handing the CLI an empty bank.
237
+ return parsed.frames.length ? parsed : parseBulletBank(`## all\n${contents}`, "all", false);
238
+ }
239
+ /**
240
+ * The problem-angle bank — the SECOND axis of an idea. A frame says what the
241
+ * video is the story of; an angle says which side of the problem it approaches
242
+ * from. Holding the frame and changing the angle is the cheapest way to answer
243
+ * "I already covered that topic", so this has to be as reachable as the frames.
244
+ */
245
+ export function loadAngleBank(name = DEFAULT_PACK) {
246
+ const { contents } = readPackDoc("references/content-ideas.md", name);
247
+ const { frames, families } = parseBulletBank(contents, "The problem angles", true);
248
+ return { angles: frames, families };
249
+ }
250
+ export function loadAwarenessLadder(name = DEFAULT_PACK) {
251
+ const { contents } = readPackDoc("references/content-ideas.md", name);
252
+ const section = extractSection(contents, "The awareness ladder");
253
+ const stages = [];
254
+ for (const line of (section?.body ?? "").split("\n")) {
255
+ const head = line.match(/^\*\*Stage\s+(\d+)\s*·\s*(.+?)\*\*\s*[—-]\s*(.+?)\s*$/);
256
+ if (head) {
257
+ stages.push({ index: Number(head[1]), stage: head[2].trim(), summary: head[3].trim(), fields: [] });
258
+ continue;
259
+ }
260
+ const field = line.match(/^-\s+\*\*(.+?):\*\*\s*(.+?)\s*$/);
261
+ if (field && stages.length)
262
+ stages[stages.length - 1].fields.push({ label: field[1].trim(), value: field[2].trim() });
263
+ }
264
+ return stages;
265
+ }
212
266
  /**
213
267
  * Grep the pack. This is the affordance that makes a local copy genuinely
214
268
  * better than the network one: "where does it say anything about greenscreen"
@@ -0,0 +1,132 @@
1
+ // The brainstorm/consultation prompts — ONE source of truth for both runners.
2
+ //
3
+ // Two things run this chain and they must ask for the same thing:
4
+ //
5
+ // 1. The CLOUD primitives (`POST /api/v1/primitives/brainstorm/*`), which send
6
+ // the prompt to a provider on the user's saved key or the wallet.
7
+ // 2. The KEYLESS LOCAL path (`vidfarm consult <step>`), which prints the same
8
+ // prompt for the agent already driving the terminal to answer itself. No
9
+ // provider key, no wallet, no network.
10
+ //
11
+ // If these two ever drift, a director gets a different consultation depending on
12
+ // where they ran it — so the builders live here, pure, and both callers import
13
+ // them. This module has NO backend imports and touches no filesystem: the big
14
+ // reference documents are passed IN, because the cloud reads them off the
15
+ // primitive asset directory and the CLI may not ship them at all.
16
+ /** Drop the trailing "Reference lessons" block when no text was supplied, so a
17
+ * keyless caller does not emit a dangling header with nothing under it. */
18
+ function withReference(lines, header, text) {
19
+ if (!text || !text.trim()) {
20
+ return lines;
21
+ }
22
+ return [...lines, "", header, text];
23
+ }
24
+ export function buildHooksPrompt(offerDescription, count, refs = {}) {
25
+ return withReference([
26
+ "You are Vidfarm's elite paid-social hook strategist.",
27
+ "Your job is to generate high-upside ad hooks for TikTok-native creative.",
28
+ "Bias toward hooks that feel native to TikTok, but keep raw persuasive power more important than surface trendiness.",
29
+ "Max creativity. Do not hold back. Be as diverse as possible while staying relevant to the offer.",
30
+ "Use many angle families across the set: pain, status, curiosity, gossip, confession, myth-busting, shock, taboo, mechanism, objection, identity, urgency, pattern interrupt, transformation, proof, enemy, mistake, secret, social proof, aspiration, and contrarian frames.",
31
+ "Each hook should be sharp, testable, and materially different from the others.",
32
+ `Generate exactly ${count} hooks.`,
33
+ "Return strict JSON only with shape {\"hooks\":[{\"hook\":\"...\",\"explanation\":\"...\"}]}.",
34
+ "Explanation should be 1-3 sentences on why the hook works and what angle it is leveraging.",
35
+ "",
36
+ "Offer details:",
37
+ offerDescription.trim()
38
+ ], "Reference lessons to apply:", refs.hooks).join("\n");
39
+ }
40
+ export function buildAwarenessStagesPrompt(offerDescription, refs = {}) {
41
+ return withReference([
42
+ "You are Vidfarm's direct-response strategist.",
43
+ "The user is not sure what type of ads to produce.",
44
+ "Use Eugene Schwartz-style awareness-stage reasoning to recommend what kinds of ads to make first.",
45
+ "Write a practical response for TikTok-first performance marketing, but keep the advice broadly useful for paid social.",
46
+ "Do not return JSON. Return a clear markdown string.",
47
+ "Include:",
48
+ "- which awareness stages are most promising first",
49
+ "- why those stages fit this offer",
50
+ "- what ad types/messages belong in each stage",
51
+ "- what to test before moving to deeper or broader awareness stages",
52
+ "- a short next-step recommendation",
53
+ "",
54
+ "Offer details:",
55
+ offerDescription.trim()
56
+ ], "Reference lessons to apply:", refs.awareness).join("\n");
57
+ }
58
+ export function buildAnglesPrompt(payload, refs = {}) {
59
+ const head = [
60
+ "You are Vidfarm's senior direct-response angle strategist.",
61
+ "Generate persuasive advertising angles for TikTok-native ads.",
62
+ "Persuasive depth matters more than format gimmicks. The user wants raw winning angles to test.",
63
+ "Do not mention templates, scenes, or production mechanics. Focus on the strategic angle itself.",
64
+ "Create major diversity across the set: pain, myths, enemies, mechanisms, shocking truths, confessions, gossip/social proof, objections, hidden costs, status, identity, aspiration, taboo, mistake, urgency, proof, authority, trend hijack, curiosity, and contrarian frames.",
65
+ "Favor known winning angle archetypes while still making them specific to the offer.",
66
+ `Generate exactly ${payload.count} angles.`,
67
+ "Return strict JSON only with shape {\"angles\":[{\"angle\":\"...\",\"explanation\":\"...\"}]}",
68
+ "Explanation should say why the angle fits this awareness state and what persuasive lever it uses.",
69
+ "",
70
+ `Problem awareness: ${payload.problem_awareness}`,
71
+ `Solution awareness: ${payload.solution_awareness}`,
72
+ `Derived stage note: ${describeAwarenessSelection(payload.problem_awareness, payload.solution_awareness)}`,
73
+ "",
74
+ "Offer details:",
75
+ payload.offer_description.trim()
76
+ ];
77
+ const withAwareness = withReference(head, "Reference lessons to apply from awareness stages:", refs.awareness);
78
+ return withReference(withAwareness, "Reference lessons to apply from hooks:", refs.hooks).join("\n");
79
+ }
80
+ export function buildColdstartPrompt(userMessage, count) {
81
+ return [
82
+ "You are Vidfarm's TikTok growth strategist.",
83
+ "The customer has no idea where to start.",
84
+ "Generate the best possible foundational Q&A questions for the customer and AI to discuss so they can build a strong TikTok strategy from zero.",
85
+ "The goal is to produce questions whose answers can later power awareness-stage selection, angle generation, hooks, scripts, and template choice.",
86
+ `Generate exactly ${count} questions.`,
87
+ "You must include questions covering, at minimum:",
88
+ "- one-line offer",
89
+ "- one-paragraph offer",
90
+ "- product pricing and checkout experience (for example appstore payments, Stripe web checkout, Shopify, invoicing, or sales call)",
91
+ "- precise target customer psyche",
92
+ "- customer pain in their own words",
93
+ "- customer desire and dream outcome",
94
+ "- awareness levels or uncertainty about them",
95
+ "Add more strong questions beyond those when useful.",
96
+ "Return strict JSON only with shape {\"questions\":[{\"question\":\"...\",\"why_it_matters\":\"...\"}]}",
97
+ "Questions should be phrased so the user can answer them in a single markdown file for future use.",
98
+ "At least one question should explicitly encourage them to save these answers into an OFFER.md file (or OFFER_<NAME>.md when they run more than one offer), kept locally or in Vidfarm cloud files, for reuse inside Vidfarm.",
99
+ "",
100
+ "User message:",
101
+ userMessage.trim()
102
+ ].join("\n");
103
+ }
104
+ export function buildProductPlacementPrompt(offerDescription, count) {
105
+ return [
106
+ "You are Vidfarm's native-advertising product-placement strategist.",
107
+ "You are watching the attached video. Study what is actually on screen: scenes, actions, objects, spoken lines, on-screen text, transitions, and emotional beats.",
108
+ "Your job is to find the highest-leverage moments to organically place the user's product into THIS video so it feels native rather than bolted on.",
109
+ "Bias toward placements that ride the existing attention, emotion, or story beat of the moment. Native and believable beats loud and interruptive.",
110
+ "Consider many placement types: on-screen prop/set dressing, character interaction with the product, verbal mention or callout, text/caption overlay, sticker or lower-third, b-roll cutaway, screen/UI replacement, before/after demo, and end-card CTA.",
111
+ `Identify exactly ${count} distinct product-placement opportunities, ordered from strongest to weakest.`,
112
+ "Each opportunity must reference a real, specific moment you actually saw in the video, with an approximate timestamp or range (for example \"0:04\" or \"0:12-0:18\").",
113
+ "Return strict JSON only with shape {\"opportunities\":[{\"timestamp\":\"...\",\"scene\":\"...\",\"placement_type\":\"...\",\"placement_idea\":\"...\",\"why_it_works\":\"...\"}]}.",
114
+ "\"scene\" briefly describes what is happening on screen at that moment. \"placement_idea\" is the concrete, actionable way to insert the product. \"why_it_works\" explains the persuasion/attention logic.",
115
+ "",
116
+ "Product / offer to place:",
117
+ offerDescription.trim()
118
+ ].join("\n");
119
+ }
120
+ export function describeAwarenessSelection(problemAwareness, solutionAwareness) {
121
+ if (problemAwareness === "problem_unaware" && solutionAwareness === "solution_unaware") {
122
+ return "Lead with unaware-style pattern interrupts, identity, curiosity, story, and emotionally resonant entry points before surfacing the problem.";
123
+ }
124
+ if (problemAwareness === "problem_aware" && solutionAwareness === "solution_unaware") {
125
+ return "Lead with sharp articulation of the pain and consequences, then reveal a new possible solution path.";
126
+ }
127
+ if (problemAwareness === "problem_aware" && solutionAwareness === "solution_aware") {
128
+ return "Lead with strong solution-category promises, differentiation, mechanism, and proof.";
129
+ }
130
+ return "Lead with high-curiosity or identity-first entry points that bridge people from vague discomfort toward the existence of a real solution category.";
131
+ }
132
+ //# sourceMappingURL=brainstorm-prompts.js.map
@@ -12,7 +12,7 @@ export { cosineSimilarity, matchesCriteria, rankHits, scoreClip, searchClips, st
12
12
  export { estimateScanCostFromDuration, estimateScanCostFromScenes, formatCostEstimate } from "./cost.js";
13
13
  export { processSceneToClip, scanVideo } from "./scan.js";
14
14
  export { refineScenesWithGuidance } from "./refine.js";
15
- export { estimateMediaHeight, pickBestVideoMedia, rankPlayableVideoMedias, fetchFirstDownloadableMedia } from "./media-select.js";
15
+ export { estimateMediaHeight, pickBestVideoMedia, rankPlayableVideoMedias, fetchFirstDownloadableMedia, fetchSocialDownloadLookup, normalizeSocialDownloadLookup, normalizeSocialDownloadMedia, DEFAULT_SOCIAL_DOWNLOAD_URL, DEFAULT_SOCIAL_DOWNLOAD_HOST } from "./media-select.js";
16
16
  export { deriveMediaNameFromUrl, resolveRawSourceName, slugifyFolderName } from "./source-naming.js";
17
17
  // Re-export ClipPreset-related helpers already covered by ./presets and ./types
18
18
  // via the wildcard exports above; nothing extra needed here.