@officexapp/vidfarm-devcli 0.21.42 → 0.21.43
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/vidfarm/SKILL.md +3 -2
- package/.agents/skills/vidfarm/recipes/onboard-a-new-director.md +9 -8
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +2 -1
- package/.agents/skills/vidfarm/references/content-ideas.md +2 -0
- package/.agents/skills/vidfarm/references/onboarding.md +63 -2
- package/SKILL.director.md +79 -13
- package/SKILL.md +5 -2
- package/dist/src/cli.js +171 -0
- package/dist/src/devcli/consult.js +389 -0
- package/dist/src/services/brainstorm-prompts.js +132 -0
- package/experimental/unique-product-explainers.md +855 -0
- package/package.json +7 -1
- package/src/assets/SELLING_AWARENESS_STAGES.md +579 -0
- package/src/assets/SELLING_WITH_HOOKS.md +377 -0
package/dist/src/cli.js
CHANGED
|
@@ -37,6 +37,8 @@ import { readStoredAuth, writeStoredAuth, clearStoredAuth, hostsMatch } from "./
|
|
|
37
37
|
import { CostModeBlockedError, assertBilledAllowed, clearStoredCostMode, costModeExplainer, costModeSummaryLine, normalizeCostMode, resolveCostMode, writeStoredCostMode, COST_MODE_BLURB, COST_MODE_DISPLAY_LIST, costModeDisplayName } from "./devcli/cost-mode.js";
|
|
38
38
|
import { INTERACTION_MODES, INTERACTION_MODE_BLURB, clearStoredInteractionMode, interactionModeExplainer, interactionModeSummaryLine, interactiveOfferLine, normalizeInteractionMode, resolveInteractionMode, writeStoredInteractionMode } from "./devcli/interaction-mode.js";
|
|
39
39
|
import { buildImageHandoff, buildRawsHandoff, formatHandoff } from "./devcli/handoff.js";
|
|
40
|
+
import { buildConsultBrief, CONSULT_ARTIFACTS, consultContextState, consultCountRange, defaultConsultCount, findOfferFile, formatConsultBrief, looksLikeOfferPath, planConsultation, resolveConsultStep, resolveOfferPath } from "./devcli/consult.js";
|
|
41
|
+
import { resolveRootFileCandidates } from "./lib/package-root.js";
|
|
40
42
|
import { STORYBOARD_FILENAME, readStoryboard, renderStoryboardScaffold, storyboardStage } from "./devcli/storyboard.js";
|
|
41
43
|
import { CORE_METRICS, DEFAULT_FORMAT, DEFAULT_METRIC, EASY_FORMATS, EXPERIMENTS_FILENAME, EXPERIMENT_MODES, analyzeRound, appendResult, appendRound, capacityOf, epochsNeeded, findRoundIndex, lintDiary, markPosted, readDiary, renderDiaryScaffold, writeDiary } from "./devcli/experiments.js";
|
|
42
44
|
// vidfarm-devcli — command-line bridge for the Vidfarm video studio. The
|
|
@@ -120,6 +122,31 @@ Account (persisted login — points the CLI + \`serve\` at cloud prod):
|
|
|
120
122
|
download (a downloader SEARCH, not a rotting link), and
|
|
121
123
|
the vidfarm clipper / raws scan follow-up. Try browser control
|
|
122
124
|
or "vidfarm raws scan" first.
|
|
125
|
+
consult [step] THE CONSULTATION / BRAINSTORM CHAIN, KEYLESS AND LOCAL.
|
|
126
|
+
The cloud runs these as brainstorm/* primitives that need
|
|
127
|
+
a provider key or wallet credits. This prints the IDENTICAL
|
|
128
|
+
prompt for YOU (the agent already in this terminal) to
|
|
129
|
+
answer — $0, no key, no network. Bare "consult" = the map
|
|
130
|
+
of the four steps + which artifacts already exist.
|
|
131
|
+
Every step reads and updates CONTEXT.md in --dir, the
|
|
132
|
+
durable answer file, so a later session starts warm.
|
|
133
|
+
Run the whole chain from ONE working folder.
|
|
134
|
+
(aliases: consultation, brainstorm)
|
|
135
|
+
coldstart [--short] The interview → OFFER.md. Ask the human the questions a
|
|
136
|
+
few at a time; never invent their answers. --short is a
|
|
137
|
+
fixed 6-question form for a director in a hurry, and it
|
|
138
|
+
still feeds every later step. (alias: interview)
|
|
139
|
+
awareness Which awareness stage to sell to → awareness-levels.md
|
|
140
|
+
angles Persuasive angles → persuasive-angles.md
|
|
141
|
+
[--problem-awareness problem_unaware|problem_aware]
|
|
142
|
+
[--solution-awareness solution_unaware|solution_aware]
|
|
143
|
+
hooks Openings to test → ad-hooks.md (grade them before showing)
|
|
144
|
+
placement --video <p> Native product-placement moments in an existing video
|
|
145
|
+
--offer <text|path> The offer. Auto-reads OFFER.md / OFFER_<NAME>.md from --dir.
|
|
146
|
+
--count <n> How many questions/angles/hooks (default 12)
|
|
147
|
+
--refs Inline the full SELLING_*.md references, exactly as the
|
|
148
|
+
cloud prompt does. Off by default: ~34KB of your context.
|
|
149
|
+
--json The brief as data (prompt, steps, artifact, next)
|
|
123
150
|
storyboard [dir] The PLAN pass: read the project's STORYBOARD.md (ordered
|
|
124
151
|
frames + duration/status/src/scene/voiceover, plus SCRIPT.md).
|
|
125
152
|
The editor renders this file in its Storyboard view — a
|
|
@@ -1396,6 +1423,13 @@ async function main() {
|
|
|
1396
1423
|
case "hand-off":
|
|
1397
1424
|
await runHandoffCommand(rest);
|
|
1398
1425
|
return;
|
|
1426
|
+
// The brainstorm chain with no AI key: the prompt goes to the agent that is
|
|
1427
|
+
// already here, not to a provider. "consultation" is the director's word.
|
|
1428
|
+
case "consult":
|
|
1429
|
+
case "consultation":
|
|
1430
|
+
case "brainstorm":
|
|
1431
|
+
await runConsultCommand(rest);
|
|
1432
|
+
return;
|
|
1399
1433
|
case "storyboard":
|
|
1400
1434
|
case "plan":
|
|
1401
1435
|
await runStoryboardCommand(rest);
|
|
@@ -9819,6 +9853,143 @@ async function runHandoffCommand(argv) {
|
|
|
9819
9853
|
}
|
|
9820
9854
|
throw new Error('handoff needs a kind: `vidfarm handoff image --theme "…" --items "a,b,c"` (free web image gen → sticker pack) or `vidfarm handoff raws --keywords "…"` (manual clip sourcing).');
|
|
9821
9855
|
}
|
|
9856
|
+
// `vidfarm consult [step]` — the consultation / brainstorm chain, run KEYLESS
|
|
9857
|
+
// and LOCAL. The cloud version of each step is a `brainstorm/*` primitive that
|
|
9858
|
+
// needs a provider key or wallet credits; this prints the identical prompt for
|
|
9859
|
+
// the agent in this terminal to answer itself, for $0.
|
|
9860
|
+
async function runConsultCommand(argv) {
|
|
9861
|
+
const parsed = parseArgs({
|
|
9862
|
+
args: argv,
|
|
9863
|
+
allowPositionals: true,
|
|
9864
|
+
options: {
|
|
9865
|
+
...commonOptions(),
|
|
9866
|
+
offer: { type: "string" },
|
|
9867
|
+
message: { type: "string" },
|
|
9868
|
+
video: { type: "string" },
|
|
9869
|
+
count: { type: "string" },
|
|
9870
|
+
dir: { type: "string" },
|
|
9871
|
+
"problem-awareness": { type: "string" },
|
|
9872
|
+
"solution-awareness": { type: "string" },
|
|
9873
|
+
refs: { type: "boolean", default: false },
|
|
9874
|
+
short: { type: "boolean", default: false }
|
|
9875
|
+
}
|
|
9876
|
+
});
|
|
9877
|
+
const json = Boolean(parsed.values.json);
|
|
9878
|
+
const dir = parsed.values.dir ?? ".";
|
|
9879
|
+
const rawStep = (parsed.positionals[0] ?? "").trim();
|
|
9880
|
+
// Bare `vidfarm consult` = the map, not a wall of prompt. A director who says
|
|
9881
|
+
// "give me a consultation" needs to see the four steps and where they land.
|
|
9882
|
+
if (!rawStep) {
|
|
9883
|
+
const plan = planConsultation(dir);
|
|
9884
|
+
const context = consultContextState(dir);
|
|
9885
|
+
if (json)
|
|
9886
|
+
return printJson({ ok: true, dir: path.resolve(dir), plan, context });
|
|
9887
|
+
console.log(`${BOLD}Vidfarm consultation${RESET} ${DIM}— keyless and local. No provider key, no wallet, no network.${RESET}`);
|
|
9888
|
+
console.log(`${DIM}Each step prints the same prompt the cloud brainstorm primitive would send. YOU answer it.${RESET}\n`);
|
|
9889
|
+
for (const row of plan) {
|
|
9890
|
+
const mark = row.done ? `${GREEN}✓${RESET}` : `${DIM}·${RESET}`;
|
|
9891
|
+
console.log(` ${mark} ${row.command.padEnd(28)} ${DIM}→ ${row.artifact}${row.done ? " (exists)" : ""}${RESET}`);
|
|
9892
|
+
}
|
|
9893
|
+
const first = plan.find((row) => !row.done) ?? plan[0];
|
|
9894
|
+
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
|
+
console.log(`\nStart here: ${BOLD}${first.command}${RESET}`);
|
|
9896
|
+
console.log(`${DIM}In a hurry: vidfarm consult coldstart --short (6 fixed questions instead of the full interview)${RESET}`);
|
|
9897
|
+
console.log(`${DIM}Run every step from ONE working folder (--dir), so the artifacts and ${context.file} stay together.${RESET}`);
|
|
9898
|
+
console.log(`${DIM}Also: vidfarm consult placement --video <path|url> (native product placement inside an existing video)${RESET}`);
|
|
9899
|
+
return;
|
|
9900
|
+
}
|
|
9901
|
+
const step = resolveConsultStep(rawStep);
|
|
9902
|
+
if (!step) {
|
|
9903
|
+
throw new Error(`Unknown consult step "${rawStep}". Use one of: coldstart, awareness, angles, hooks, placement — or bare \`vidfarm consult\` for the map.`);
|
|
9904
|
+
}
|
|
9905
|
+
// coldstart is the step that PRODUCES the offer document, so it is the only
|
|
9906
|
+
// one that runs without one.
|
|
9907
|
+
let offer = "";
|
|
9908
|
+
if (step !== "coldstart") {
|
|
9909
|
+
const inline = parsed.values.offer?.trim();
|
|
9910
|
+
// --offer takes either a path or the offer text itself. A real file wins; a
|
|
9911
|
+
// string that READS like a path but resolves to nothing is an error, never
|
|
9912
|
+
// offer text — otherwise a typo silently consults on the filename.
|
|
9913
|
+
if (inline && !resolveOfferPath(dir, inline) && !looksLikeOfferPath(inline)) {
|
|
9914
|
+
offer = inline;
|
|
9915
|
+
}
|
|
9916
|
+
else {
|
|
9917
|
+
const found = findOfferFile(dir, inline);
|
|
9918
|
+
if ("error" in found) {
|
|
9919
|
+
throw new Error(found.error);
|
|
9920
|
+
}
|
|
9921
|
+
offer = found.text;
|
|
9922
|
+
}
|
|
9923
|
+
if (offer.trim().length < 10) {
|
|
9924
|
+
throw new Error("The offer text is too thin to consult on (under 10 characters). Fill in OFFER.md, or pass --offer \"<a paragraph about the offer>\".");
|
|
9925
|
+
}
|
|
9926
|
+
}
|
|
9927
|
+
const awarenessValue = (raw, allowed, flag) => {
|
|
9928
|
+
if (raw === undefined)
|
|
9929
|
+
return undefined;
|
|
9930
|
+
const value = raw.trim().toLowerCase();
|
|
9931
|
+
if (!allowed.includes(value)) {
|
|
9932
|
+
throw new Error(`${flag} must be one of: ${allowed.join(", ")}.`);
|
|
9933
|
+
}
|
|
9934
|
+
return value;
|
|
9935
|
+
};
|
|
9936
|
+
// --short is a fixed six-question set, so it belongs to the interview only.
|
|
9937
|
+
// On the other steps the equivalent knob is --count, and silently ignoring the
|
|
9938
|
+
// flag would let a director believe they ran a shorter angles pass.
|
|
9939
|
+
const short = Boolean(parsed.values.short);
|
|
9940
|
+
if (short && step !== "coldstart") {
|
|
9941
|
+
throw new Error(`--short is the short-form interview, so it only applies to \`vidfarm consult coldstart\`. For a shorter ${step} run, use --count.`);
|
|
9942
|
+
}
|
|
9943
|
+
const countRaw = parsed.values.count;
|
|
9944
|
+
const range = consultCountRange(step);
|
|
9945
|
+
if (!range && countRaw !== undefined) {
|
|
9946
|
+
throw new Error("consult awareness takes no --count — it returns one markdown recommendation, not a list.");
|
|
9947
|
+
}
|
|
9948
|
+
if (short && countRaw !== undefined) {
|
|
9949
|
+
throw new Error("--short is a fixed six-question set, so it takes no --count. Drop one of the two.");
|
|
9950
|
+
}
|
|
9951
|
+
const count = countRaw === undefined ? defaultConsultCount(step) : Number(countRaw);
|
|
9952
|
+
if (range && (!Number.isInteger(count) || count < range.min || count > range.max)) {
|
|
9953
|
+
throw new Error(`--count for ${step} must be a whole number between ${range.min} and ${range.max} (the cloud primitive enforces the same range).`);
|
|
9954
|
+
}
|
|
9955
|
+
const brief = buildConsultBrief({
|
|
9956
|
+
step,
|
|
9957
|
+
offer,
|
|
9958
|
+
userMessage: parsed.values.message,
|
|
9959
|
+
videoRef: parsed.values.video,
|
|
9960
|
+
count,
|
|
9961
|
+
problemAwareness: awarenessValue(parsed.values["problem-awareness"], ["problem_unaware", "problem_aware"], "--problem-awareness"),
|
|
9962
|
+
solutionAwareness: awarenessValue(parsed.values["solution-awareness"], ["solution_unaware", "solution_aware"], "--solution-awareness"),
|
|
9963
|
+
references: parsed.values.refs ? readSellingReferences() : undefined,
|
|
9964
|
+
dir,
|
|
9965
|
+
short
|
|
9966
|
+
});
|
|
9967
|
+
if (json) {
|
|
9968
|
+
return printJson({ ok: true, keyless: true, cost_usd: 0, artifact_name: CONSULT_ARTIFACTS[step], ...brief });
|
|
9969
|
+
}
|
|
9970
|
+
console.log(formatConsultBrief(brief, { bold: BOLD, dim: DIM, reset: RESET }));
|
|
9971
|
+
}
|
|
9972
|
+
// The two selling documents the CLOUD prompt inlines. Only read on --refs: they
|
|
9973
|
+
// are ~34KB together, which is real context spend for the local agent.
|
|
9974
|
+
function readSellingReferences() {
|
|
9975
|
+
const read = (name) => {
|
|
9976
|
+
for (const candidate of resolveRootFileCandidates(path.join("src", "assets", name))) {
|
|
9977
|
+
try {
|
|
9978
|
+
return readFileSync(candidate, "utf8");
|
|
9979
|
+
}
|
|
9980
|
+
catch {
|
|
9981
|
+
continue;
|
|
9982
|
+
}
|
|
9983
|
+
}
|
|
9984
|
+
return undefined;
|
|
9985
|
+
};
|
|
9986
|
+
const hooks = read("SELLING_WITH_HOOKS.md");
|
|
9987
|
+
const awareness = read("SELLING_AWARENESS_STAGES.md");
|
|
9988
|
+
if (!hooks && !awareness) {
|
|
9989
|
+
throw new Error("--refs needs the bundled selling references (src/assets/SELLING_*.md) and neither was found in this install. Drop --refs to use the skill-pack reference instead.");
|
|
9990
|
+
}
|
|
9991
|
+
return { hooks, awareness };
|
|
9992
|
+
}
|
|
9822
9993
|
async function runProviderKeysCommand(argv) {
|
|
9823
9994
|
const parsed = parseArgs({ args: argv, allowPositionals: false, options: commonOptions() });
|
|
9824
9995
|
const ctx = commonContext(parsed.values);
|
|
@@ -0,0 +1,389 @@
|
|
|
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
|
+
* The SHORT interview. The full cold-start prompt asks a model to invent 12-30
|
|
68
|
+
* questions; that is the right depth for a director who wants a real strategy
|
|
69
|
+
* session, and far too much for one who wants to make a video today. These six
|
|
70
|
+
* are fixed, not generated: they are the minimum set the later steps actually
|
|
71
|
+
* read back (awareness needs the offer + the psyche, angles and hooks quote the
|
|
72
|
+
* pain and desire verbatim). Fixed also means free, instant, and identical every
|
|
73
|
+
* run, which is what makes the answers worth saving.
|
|
74
|
+
*/
|
|
75
|
+
export const SHORT_INTERVIEW = [
|
|
76
|
+
{
|
|
77
|
+
question: "In one line, what do you sell, and to whom?",
|
|
78
|
+
why_it_matters: "Every later step opens with this line. Without it there is no offer to consult on."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
question: "What does it cost, and how does someone actually pay (appstore, Stripe, Shopify, invoice, sales call)?",
|
|
82
|
+
why_it_matters: "Price and checkout friction decide whether an ad can close or only warm up."
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
question: "Who is the single best customer you have had? Describe them like a person, not a segment.",
|
|
86
|
+
why_it_matters: "Angles are written to one person. A segment produces generic ads."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
question: "What do they complain about, in their own words, before they find you?",
|
|
90
|
+
why_it_matters: "Hooks quote this back verbatim. Paraphrase kills it."
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
question: "What do they want instead — the dream outcome they would brag about?",
|
|
94
|
+
why_it_matters: "The payoff half of every hook, and the reason to keep watching."
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
question: "Do they already know this kind of product exists, or have they never heard of it?",
|
|
98
|
+
why_it_matters: "This is the awareness stage in plain language, and it decides which ad to make first."
|
|
99
|
+
}
|
|
100
|
+
];
|
|
101
|
+
/** The short interview rendered as the brief's prompt block. Fixed text — there
|
|
102
|
+
* is no cloud counterpart, so nothing here can drift from a cloud prompt. */
|
|
103
|
+
export function buildShortInterviewPrompt() {
|
|
104
|
+
return [
|
|
105
|
+
"SHORT-FORM COLD START — a fixed six-question interview. Ask these exact questions. Do not generate your own set.",
|
|
106
|
+
"",
|
|
107
|
+
...SHORT_INTERVIEW.map((q, i) => `${i + 1}. ${q.question}\n (why it matters: ${q.why_it_matters})`),
|
|
108
|
+
"",
|
|
109
|
+
"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.",
|
|
110
|
+
"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."
|
|
111
|
+
].join("\n");
|
|
112
|
+
}
|
|
113
|
+
/** The skill-pack reference each step should be graded against locally, in place
|
|
114
|
+
* of the big SELLING_*.md document the cloud prompt inlines. */
|
|
115
|
+
const STEP_REFERENCE = {
|
|
116
|
+
awareness: "references/onboarding.md",
|
|
117
|
+
angles: "references/onboarding.md",
|
|
118
|
+
hooks: "references/hooks-and-virality.md"
|
|
119
|
+
};
|
|
120
|
+
// Count bounds MIRROR the cloud primitive payload schemas on purpose. The local
|
|
121
|
+
// path has no schema to satisfy, but a director who tunes --count here and then
|
|
122
|
+
// moves the same chain to the cloud should not hit a 400 they never saw locally.
|
|
123
|
+
// `awareness` returns one markdown recommendation, so it has no count at all.
|
|
124
|
+
const COUNTS = {
|
|
125
|
+
coldstart: { default: 12, min: 7, max: 30 },
|
|
126
|
+
awareness: null,
|
|
127
|
+
angles: { default: 12, min: 1, max: 50 },
|
|
128
|
+
hooks: { default: 12, min: 1, max: 50 },
|
|
129
|
+
placement: { default: 8, min: 3, max: 30 }
|
|
130
|
+
};
|
|
131
|
+
export function consultCountRange(step) {
|
|
132
|
+
return COUNTS[step];
|
|
133
|
+
}
|
|
134
|
+
export function defaultConsultCount(step) {
|
|
135
|
+
return COUNTS[step]?.default ?? 0;
|
|
136
|
+
}
|
|
137
|
+
export function offerPathCandidates(dir, explicit) {
|
|
138
|
+
const fromCwd = path.resolve(explicit);
|
|
139
|
+
const fromDir = path.resolve(dir, explicit);
|
|
140
|
+
return fromCwd === fromDir ? [fromCwd] : [fromCwd, fromDir];
|
|
141
|
+
}
|
|
142
|
+
export function resolveOfferPath(dir, explicit) {
|
|
143
|
+
return offerPathCandidates(dir, explicit).find((candidate) => existsSync(candidate)) ?? null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Does this --offer value read as a PATH the caller expected to exist, rather
|
|
147
|
+
* than the offer text itself? Load-bearing: without it, a mistyped path is
|
|
148
|
+
* silently consulted on as if the filename were the product description, and
|
|
149
|
+
* the director gets twelve hooks about "./OFFER_ACME.md".
|
|
150
|
+
*/
|
|
151
|
+
export function looksLikeOfferPath(value) {
|
|
152
|
+
const trimmed = value.trim();
|
|
153
|
+
return !trimmed.includes("\n") && (/^[.~/]/.test(trimmed) || /\.(md|txt)$/i.test(trimmed));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Find the director's offer document. Prefers an explicit path, then OFFER.md,
|
|
157
|
+
* then a single OFFER_<NAME>.md — a director running several offers must name
|
|
158
|
+
* which one, because silently picking the first alphabetically would generate a
|
|
159
|
+
* whole angle set for the wrong product.
|
|
160
|
+
*/
|
|
161
|
+
export function findOfferFile(dir, explicit) {
|
|
162
|
+
if (explicit) {
|
|
163
|
+
// Try cwd AND --dir: `--dir ./work --offer ./OFFER_ACME.md` names a file in
|
|
164
|
+
// the work directory, not next to the shell.
|
|
165
|
+
const resolved = resolveOfferPath(dir, explicit);
|
|
166
|
+
if (!resolved) {
|
|
167
|
+
return { error: `Offer file not found: ${offerPathCandidates(dir, explicit).join(" or ")}` };
|
|
168
|
+
}
|
|
169
|
+
return { path: resolved, text: readFileSync(resolved, "utf8") };
|
|
170
|
+
}
|
|
171
|
+
const root = path.resolve(dir);
|
|
172
|
+
const primary = path.join(root, "OFFER.md");
|
|
173
|
+
if (existsSync(primary)) {
|
|
174
|
+
return { path: primary, text: readFileSync(primary, "utf8") };
|
|
175
|
+
}
|
|
176
|
+
let named = [];
|
|
177
|
+
try {
|
|
178
|
+
named = readdirSync(root).filter((f) => /^OFFER_.+\.md$/i.test(f)).sort();
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
named = [];
|
|
182
|
+
}
|
|
183
|
+
if (named.length === 1) {
|
|
184
|
+
const only = path.join(root, named[0]);
|
|
185
|
+
return { path: only, text: readFileSync(only, "utf8") };
|
|
186
|
+
}
|
|
187
|
+
if (named.length > 1) {
|
|
188
|
+
return {
|
|
189
|
+
error: `Found ${named.length} offer files (${named.join(", ")}) — name the one you mean with --offer ./${named[0]}.`
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
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>".`
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function nextCommand(step) {
|
|
197
|
+
const i = STEP_ORDER.indexOf(step);
|
|
198
|
+
if (i === -1 || i === STEP_ORDER.length - 1) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
return `vidfarm consult ${STEP_ORDER[i + 1]}`;
|
|
202
|
+
}
|
|
203
|
+
/** The path the durable answer file takes, and whether it is already written. */
|
|
204
|
+
export function consultContextState(dir) {
|
|
205
|
+
const relative = path.join(dir === "." ? "" : dir, CONSULT_CONTEXT_FILE) || CONSULT_CONTEXT_FILE;
|
|
206
|
+
return { file: relative, exists: existsSync(path.resolve(dir, CONSULT_CONTEXT_FILE)) };
|
|
207
|
+
}
|
|
208
|
+
/** Build the keyless local brief for one step of the chain. */
|
|
209
|
+
export function buildConsultBrief(input) {
|
|
210
|
+
const dir = input.dir ?? ".";
|
|
211
|
+
const context = consultContextState(dir);
|
|
212
|
+
const short = Boolean(input.short) && input.step === "coldstart";
|
|
213
|
+
const artifact = path.join(dir === "." ? "" : dir, CONSULT_ARTIFACTS[input.step]) || CONSULT_ARTIFACTS[input.step];
|
|
214
|
+
const refs = input.references ?? {};
|
|
215
|
+
const count = input.count ?? defaultConsultCount(input.step);
|
|
216
|
+
const offer = (input.offer ?? "").trim();
|
|
217
|
+
const referenceFile = STEP_REFERENCE[input.step];
|
|
218
|
+
const inlined = (input.step === "hooks" && Boolean(refs.hooks)) ||
|
|
219
|
+
(input.step === "awareness" && Boolean(refs.awareness)) ||
|
|
220
|
+
(input.step === "angles" && Boolean(refs.hooks || refs.awareness));
|
|
221
|
+
const referenceHint = inlined || !referenceFile
|
|
222
|
+
? null
|
|
223
|
+
: `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.`;
|
|
224
|
+
// The durable answer file is read BEFORE the step and written AFTER it. Both
|
|
225
|
+
// halves matter: reading stops a second session re-asking questions the
|
|
226
|
+
// director already answered, writing is what makes the next session cheap.
|
|
227
|
+
const contextRead = context.exists
|
|
228
|
+
? `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.`
|
|
229
|
+
: `No \`${context.file}\` yet. It is the durable answer file for this working folder; you create it in this step.`;
|
|
230
|
+
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.`;
|
|
231
|
+
const common = [
|
|
232
|
+
"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.",
|
|
233
|
+
contextRead,
|
|
234
|
+
...(referenceHint ? [referenceHint] : []),
|
|
235
|
+
`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.`,
|
|
236
|
+
contextWrite
|
|
237
|
+
];
|
|
238
|
+
const shared = { short, contextFile: context.file, contextExists: context.exists };
|
|
239
|
+
if (short) {
|
|
240
|
+
return {
|
|
241
|
+
...shared,
|
|
242
|
+
step: "coldstart",
|
|
243
|
+
title: `Cold-start interview, SHORT FORM — ${SHORT_INTERVIEW.length} fixed questions`,
|
|
244
|
+
prompt: buildShortInterviewPrompt(),
|
|
245
|
+
steps: [
|
|
246
|
+
contextRead,
|
|
247
|
+
`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.`,
|
|
248
|
+
`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.`,
|
|
249
|
+
"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.",
|
|
250
|
+
`Write \`${artifact}\` from their answers, then write the same answers into \`${context.file}\` under a dated heading.`,
|
|
251
|
+
"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.",
|
|
252
|
+
"This is the short form. Tell them `vidfarm consult coldstart` runs the full interview when they want the deeper session."
|
|
253
|
+
],
|
|
254
|
+
artifact,
|
|
255
|
+
next: nextCommand("coldstart"),
|
|
256
|
+
referenceHint: null
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (input.step === "coldstart") {
|
|
260
|
+
return {
|
|
261
|
+
...shared,
|
|
262
|
+
step: "coldstart",
|
|
263
|
+
title: "Cold-start interview — the questions that produce OFFER.md",
|
|
264
|
+
prompt: buildColdstartPrompt(input.userMessage?.trim() || "The director asked for a consultation and has given no offer context yet.", count),
|
|
265
|
+
steps: [
|
|
266
|
+
...common,
|
|
267
|
+
`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.`,
|
|
268
|
+
`Assemble their real words into \`${artifact}\`: keep their phrasing for pain and desire, because the later angle and hook steps quote it back.`,
|
|
269
|
+
"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.",
|
|
270
|
+
"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.",
|
|
271
|
+
`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.`
|
|
272
|
+
],
|
|
273
|
+
artifact,
|
|
274
|
+
next: nextCommand("coldstart"),
|
|
275
|
+
referenceHint
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (input.step === "awareness") {
|
|
279
|
+
return {
|
|
280
|
+
...shared,
|
|
281
|
+
step: "awareness",
|
|
282
|
+
title: "Awareness stages — which kind of ad to make first",
|
|
283
|
+
prompt: buildAwarenessStagesPrompt(offer, refs),
|
|
284
|
+
steps: [
|
|
285
|
+
...common,
|
|
286
|
+
"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.",
|
|
287
|
+
"Carry the chosen stage forward: the angles step takes it as --problem-awareness / --solution-awareness."
|
|
288
|
+
],
|
|
289
|
+
artifact,
|
|
290
|
+
next: nextCommand("awareness"),
|
|
291
|
+
referenceHint
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (input.step === "angles") {
|
|
295
|
+
return {
|
|
296
|
+
...shared,
|
|
297
|
+
step: "angles",
|
|
298
|
+
title: "Persuasive angles — the strategic bets to test",
|
|
299
|
+
prompt: buildAnglesPrompt({
|
|
300
|
+
offer_description: offer,
|
|
301
|
+
problem_awareness: input.problemAwareness ?? "problem_aware",
|
|
302
|
+
solution_awareness: input.solutionAwareness ?? "solution_unaware",
|
|
303
|
+
count
|
|
304
|
+
}, refs),
|
|
305
|
+
steps: [
|
|
306
|
+
...common,
|
|
307
|
+
"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.",
|
|
308
|
+
"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."
|
|
309
|
+
],
|
|
310
|
+
artifact,
|
|
311
|
+
next: nextCommand("angles"),
|
|
312
|
+
referenceHint
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
if (input.step === "hooks") {
|
|
316
|
+
return {
|
|
317
|
+
...shared,
|
|
318
|
+
step: "hooks",
|
|
319
|
+
title: "Hooks — the openings to test",
|
|
320
|
+
prompt: buildHooksPrompt(offer, count, refs),
|
|
321
|
+
steps: [
|
|
322
|
+
...common,
|
|
323
|
+
"The prompt asks for strict JSON. Write the JSON into the file under a fenced block, with a plain list under it.",
|
|
324
|
+
"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.",
|
|
325
|
+
"Never rank a generated batch with the reasoning that wrote it. The rubric catches defects; it does not pick winners."
|
|
326
|
+
],
|
|
327
|
+
artifact,
|
|
328
|
+
next: nextCommand("hooks"),
|
|
329
|
+
referenceHint
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const videoRef = input.videoRef?.trim();
|
|
333
|
+
return {
|
|
334
|
+
...shared,
|
|
335
|
+
step: "placement",
|
|
336
|
+
title: "Product placement — native moments inside an existing video",
|
|
337
|
+
prompt: buildProductPlacementPrompt(offer, count),
|
|
338
|
+
steps: [
|
|
339
|
+
...common,
|
|
340
|
+
videoRef
|
|
341
|
+
? `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.`
|
|
342
|
+
: "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.",
|
|
343
|
+
"Every opportunity must cite a moment you actually saw, with a timestamp. Drop any you cannot point at."
|
|
344
|
+
],
|
|
345
|
+
artifact,
|
|
346
|
+
next: null,
|
|
347
|
+
referenceHint
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/** Render a brief for a terminal. The prompt is delimited so an agent can lift
|
|
351
|
+
* it exactly, and so a human can copy it into a chat window instead. */
|
|
352
|
+
export function formatConsultBrief(brief, style = { bold: "", dim: "", reset: "" }) {
|
|
353
|
+
const { bold, dim, reset } = style;
|
|
354
|
+
const lines = [];
|
|
355
|
+
lines.push(`${bold}${brief.title}${reset}`);
|
|
356
|
+
lines.push(`${dim}Keyless + local: no provider key, no wallet, no network. You answer this.${reset}`);
|
|
357
|
+
lines.push("");
|
|
358
|
+
brief.steps.forEach((s, i) => lines.push(`${i + 1}. ${s}`));
|
|
359
|
+
lines.push("");
|
|
360
|
+
lines.push(brief.short
|
|
361
|
+
? `${dim}─── the short-form question set (fixed and local — ask these as written) ───${reset}`
|
|
362
|
+
: `${dim}─── the prompt (identical to the cloud brainstorm primitive) ───${reset}`);
|
|
363
|
+
lines.push(brief.prompt);
|
|
364
|
+
lines.push(`${dim}───────────────────────────────────────────────────────────────${reset}`);
|
|
365
|
+
lines.push("");
|
|
366
|
+
lines.push(`Save to: ${brief.artifact}`);
|
|
367
|
+
lines.push(`Context: ${brief.contextFile}${brief.contextExists ? " (exists — read it first)" : " (create it)"}`);
|
|
368
|
+
if (brief.next) {
|
|
369
|
+
lines.push(`Next: ${brief.next}`);
|
|
370
|
+
}
|
|
371
|
+
return lines.join("\n");
|
|
372
|
+
}
|
|
373
|
+
/** The whole chain plus which artifacts already exist, for a bare
|
|
374
|
+
* `vidfarm consult`. A director who says "give me a consultation" gets the map
|
|
375
|
+
* and the first command, not a wall of prompt. */
|
|
376
|
+
export function planConsultation(dir) {
|
|
377
|
+
const root = path.resolve(dir);
|
|
378
|
+
return STEP_ORDER.map((step) => {
|
|
379
|
+
const file = CONSULT_ARTIFACTS[step];
|
|
380
|
+
const offerDone = step === "coldstart" && !("error" in findOfferFile(root));
|
|
381
|
+
return {
|
|
382
|
+
step,
|
|
383
|
+
artifact: file,
|
|
384
|
+
done: step === "coldstart" ? offerDone : existsSync(path.join(root, file)),
|
|
385
|
+
command: `vidfarm consult ${step}`
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
//# sourceMappingURL=consult.js.map
|