@aicopycoders/exodus 2026.6.2400
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/assets/references/awareness-framework.md +156 -0
- package/assets/references/cast-video.md +165 -0
- package/assets/references/copy-blocks.md +138 -0
- package/assets/references/creative-strategy.md +990 -0
- package/assets/references/editing-rules.md +81 -0
- package/assets/references/hook-quality-checklist.md +153 -0
- package/assets/references/iterations.md +73 -0
- package/assets/references/scrawls.md +483 -0
- package/assets/references/segments.md +79 -0
- package/assets/skills/exodus/SKILL.md +50 -0
- package/assets/skills/exodus-brand/SKILL.md +117 -0
- package/assets/skills/exodus-browse/SKILL.md +96 -0
- package/assets/skills/exodus-creative/SKILL.md +94 -0
- package/assets/skills/exodus-drive/SKILL.md +118 -0
- package/assets/skills/exodus-foundation/SKILL.md +170 -0
- package/assets/skills/exodus-genesis/SKILL.md +159 -0
- package/assets/skills/exodus-idea/SKILL.md +78 -0
- package/assets/skills/exodus-image/SKILL.md +224 -0
- package/assets/skills/exodus-meme/SKILL.md +183 -0
- package/assets/skills/exodus-primer/SKILL.md +145 -0
- package/assets/skills/exodus-strategist/SKILL.md +217 -0
- package/assets/skills/exodus-swipe/SKILL.md +85 -0
- package/assets/skills/exodus-template/SKILL.md +106 -0
- package/assets/skills/exodus-write/SKILL.md +157 -0
- package/assets/skills/genesis-bots/.env.example +12 -0
- package/assets/skills/genesis-bots/README.txt +29 -0
- package/assets/skills/genesis-bots/SKILL.md +219 -0
- package/assets/skills/genesis-bots/scripts/genesis-stream.mjs +307 -0
- package/assets/skills/genesis-bots/scripts/genesis-stream.py +220 -0
- package/dist/bin/exodus.d.ts +2 -0
- package/dist/bin/exodus.js +177 -0
- package/dist/commands/__tests__/genesis.hookgate.test.d.ts +1 -0
- package/dist/commands/__tests__/genesis.hookgate.test.js +12 -0
- package/dist/commands/brand.d.ts +24 -0
- package/dist/commands/brand.js +292 -0
- package/dist/commands/browse.d.ts +9 -0
- package/dist/commands/browse.js +132 -0
- package/dist/commands/creative.d.ts +2 -0
- package/dist/commands/creative.js +223 -0
- package/dist/commands/doctor.d.ts +21 -0
- package/dist/commands/doctor.js +590 -0
- package/dist/commands/drive.d.ts +2 -0
- package/dist/commands/drive.js +154 -0
- package/dist/commands/foundation.d.ts +2 -0
- package/dist/commands/foundation.js +468 -0
- package/dist/commands/genesis.d.ts +64 -0
- package/dist/commands/genesis.js +564 -0
- package/dist/commands/idea.d.ts +53 -0
- package/dist/commands/idea.js +333 -0
- package/dist/commands/image.d.ts +2 -0
- package/dist/commands/image.js +249 -0
- package/dist/commands/init.d.ts +13 -0
- package/dist/commands/init.js +70 -0
- package/dist/commands/keys.d.ts +2 -0
- package/dist/commands/keys.js +101 -0
- package/dist/commands/meme.d.ts +12 -0
- package/dist/commands/meme.js +272 -0
- package/dist/commands/migrate.d.ts +3 -0
- package/dist/commands/migrate.js +158 -0
- package/dist/commands/primer.d.ts +2 -0
- package/dist/commands/primer.js +540 -0
- package/dist/commands/read-doc.d.ts +3 -0
- package/dist/commands/read-doc.js +62 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +205 -0
- package/dist/commands/swipe.d.ts +2 -0
- package/dist/commands/swipe.js +398 -0
- package/dist/commands/template.d.ts +2 -0
- package/dist/commands/template.js +225 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +51 -0
- package/dist/lib/ad-library-write.d.ts +15 -0
- package/dist/lib/ad-library-write.js +67 -0
- package/dist/lib/assets.d.ts +3 -0
- package/dist/lib/assets.js +23 -0
- package/dist/lib/backend-hint.d.ts +2 -0
- package/dist/lib/backend-hint.js +22 -0
- package/dist/lib/cc-command.d.ts +1 -0
- package/dist/lib/cc-command.js +13 -0
- package/dist/lib/client.d.ts +23 -0
- package/dist/lib/client.js +213 -0
- package/dist/lib/format.d.ts +8 -0
- package/dist/lib/format.js +172 -0
- package/dist/lib/intel-format.d.ts +1 -0
- package/dist/lib/intel-format.js +38 -0
- package/dist/lib/keys-sync.d.ts +16 -0
- package/dist/lib/keys-sync.js +88 -0
- package/dist/lib/layout.d.ts +38 -0
- package/dist/lib/layout.js +153 -0
- package/dist/lib/load-env.d.ts +1 -0
- package/dist/lib/load-env.js +47 -0
- package/dist/lib/pixar-orchestrator-types.d.ts +54 -0
- package/dist/lib/pixar-orchestrator-types.js +1 -0
- package/dist/lib/poll.d.ts +14 -0
- package/dist/lib/poll.js +32 -0
- package/dist/lib/preflight.d.ts +18 -0
- package/dist/lib/preflight.js +56 -0
- package/dist/lib/prompts.d.ts +9 -0
- package/dist/lib/prompts.js +99 -0
- package/dist/lib/reel-write.d.ts +14 -0
- package/dist/lib/reel-write.js +59 -0
- package/dist/lib/release.d.ts +4 -0
- package/dist/lib/release.js +81 -0
- package/dist/lib/scaffold.d.ts +8 -0
- package/dist/lib/scaffold.js +67 -0
- package/dist/lib/scout-hydrate.d.ts +1 -0
- package/dist/lib/scout-hydrate.js +10 -0
- package/dist/lib/state.d.ts +6 -0
- package/dist/lib/state.js +49 -0
- package/dist/lib/template-constants.d.ts +4 -0
- package/dist/lib/template-constants.js +85 -0
- package/dist/lib/version.d.ts +1 -0
- package/dist/lib/version.js +19 -0
- package/package.json +30 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { apiGet, apiPost, getDashboardUrl } from "../lib/client.js";
|
|
5
|
+
import { pollUntilDone } from "../lib/poll.js";
|
|
6
|
+
import { formatGenesisRun, formatError } from "../lib/format.js";
|
|
7
|
+
import { formatCcCommand } from "../lib/cc-command.js";
|
|
8
|
+
import { resolveActiveBrand } from "../lib/layout.js";
|
|
9
|
+
import { captureReelAndWrite } from "../lib/reel-write.js";
|
|
10
|
+
import { captureSwipeAndWrite } from "../lib/ad-library-write.js";
|
|
11
|
+
export const helpText = `
|
|
12
|
+
exodus genesis — write ads with the Genesis writer (Mario + Infeed, 1 pass each by default)
|
|
13
|
+
|
|
14
|
+
Genesis is the one writing pipeline (Mario + Infeed voices → body copy → QA →
|
|
15
|
+
Google Doc). Every mode below feeds that same writer; they differ only in where
|
|
16
|
+
the inspiration comes from.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
exodus genesis run --brief <file|"text"> Write from a typed brief
|
|
20
|
+
exodus genesis --reel "<url>" Mode 1: transcribe a reel → idea bank → write ads
|
|
21
|
+
exodus genesis --swipe-url "<url>" Swipe: scrape a Facebook Ad Library link → idea bank → write ads
|
|
22
|
+
exodus genesis --from-bank Mode 2: list banked reels to write from
|
|
23
|
+
exodus genesis --from-bank --idea <id> Mode 2: write from a specific banked reel
|
|
24
|
+
exodus genesis --list-swipes Swipe: list saved competitor ads to write from
|
|
25
|
+
exodus genesis --swipe <id> Swipe: write from a saved competitor ad (model after it)
|
|
26
|
+
exodus genesis scrape [--organic] Mode 3: fill the idea bank for the active brand
|
|
27
|
+
exodus genesis connect-instagram Mode 3: connect this brand's Instagram account
|
|
28
|
+
|
|
29
|
+
Source (choose one):
|
|
30
|
+
--brief <file|"text"> Typed brief (file path or inline string)
|
|
31
|
+
--reel "<url>" Instagram or TikTok reel URL to model
|
|
32
|
+
--swipe-url "<url>" Facebook Ad Library link to swipe (scrape → hook → write)
|
|
33
|
+
--from-bank [--idea <id>] Idea bank: list with no id, or write from <id>
|
|
34
|
+
--swipe <id> Saved swipe (competitor ad): write modeled after <id>
|
|
35
|
+
--list-swipes List saved swipes for your tracked competitors
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--steering "<text>" With --swipe-url: what to model from the ad (angle,
|
|
39
|
+
tone, structure) — folded into the brief as direction.
|
|
40
|
+
--seeds <file|"text"> Per-run creative seeds (brief mode). File: one per
|
|
41
|
+
line OR a JSON array. Inline: a single seed.
|
|
42
|
+
--awareness <level> unaware | problem-aware (default) | solution-aware | product-aware
|
|
43
|
+
--passes <n> Writing passes per bot — Mario + Infeed (1-5, default 1 = 2 variants)
|
|
44
|
+
--variants <n> Advanced: raw total variant count (1-10); overrides --passes
|
|
45
|
+
--ad-account <id> Meta ad account ID for the top-ads-biased track
|
|
46
|
+
--limit <n> Rows when listing the bank (default 20)
|
|
47
|
+
--no-wait Return immediately with the run ID
|
|
48
|
+
--wait Poll until the run completes (default)
|
|
49
|
+
|
|
50
|
+
Scrape (Mode 3) options:
|
|
51
|
+
--organic Authenticated FYP walk via the connected IG account
|
|
52
|
+
--term "<search>" Fresh ScrapeCreators discovery scoped to a term (no IG login needed)
|
|
53
|
+
--smoke With --organic: shorter verification walk
|
|
54
|
+
--username <handle> With connect-instagram: the IG handle being connected
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
exodus genesis run --brief brief.txt --seeds seeds.txt
|
|
58
|
+
exodus genesis --reel "https://www.instagram.com/reel/Cabc123/"
|
|
59
|
+
exodus genesis --swipe-url "https://www.facebook.com/ads/library/?id=2119680725509450"
|
|
60
|
+
exodus genesis --from-bank
|
|
61
|
+
exodus genesis --from-bank --idea kn7abc... --awareness solution-aware
|
|
62
|
+
exodus genesis --list-swipes
|
|
63
|
+
exodus genesis --swipe kn7abc... --awareness solution-aware
|
|
64
|
+
exodus genesis connect-instagram --username mybrandhandle
|
|
65
|
+
exodus genesis scrape --organic
|
|
66
|
+
`.trim();
|
|
67
|
+
function resolveTextFlag(raw) {
|
|
68
|
+
if (typeof raw !== "string" || raw.trim() === "")
|
|
69
|
+
return undefined;
|
|
70
|
+
const candidate = resolve(process.cwd(), raw);
|
|
71
|
+
if (existsSync(candidate)) {
|
|
72
|
+
return readFileSync(candidate, "utf8").trim();
|
|
73
|
+
}
|
|
74
|
+
return raw.trim();
|
|
75
|
+
}
|
|
76
|
+
function resolveSeedsFlag(raw) {
|
|
77
|
+
const text = resolveTextFlag(raw);
|
|
78
|
+
if (!text)
|
|
79
|
+
return undefined;
|
|
80
|
+
if (text.startsWith("[")) {
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(text);
|
|
83
|
+
if (Array.isArray(parsed)) {
|
|
84
|
+
return parsed.filter((s) => typeof s === "string" && s.trim().length > 0);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return text
|
|
91
|
+
.split("\n")
|
|
92
|
+
.map((line) => line.trim())
|
|
93
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
94
|
+
}
|
|
95
|
+
export function buildBriefBody(brief, opts) {
|
|
96
|
+
const body = {
|
|
97
|
+
brief,
|
|
98
|
+
awarenessLevel: opts.awarenessLevel,
|
|
99
|
+
inputMethod: "brief",
|
|
100
|
+
};
|
|
101
|
+
if (opts.seeds && opts.seeds.length > 0)
|
|
102
|
+
body.seeds = opts.seeds;
|
|
103
|
+
if (typeof opts.variantCount === "number")
|
|
104
|
+
body.variantCount = opts.variantCount;
|
|
105
|
+
if (opts.adAccountId)
|
|
106
|
+
body.adAccountId = opts.adAccountId;
|
|
107
|
+
if (typeof opts.stopAtHooks === "boolean")
|
|
108
|
+
body.stopAtHooks = opts.stopAtHooks;
|
|
109
|
+
return body;
|
|
110
|
+
}
|
|
111
|
+
export function buildPasteBody(text, opts) {
|
|
112
|
+
const body = {
|
|
113
|
+
brief: text,
|
|
114
|
+
awarenessLevel: opts.awarenessLevel,
|
|
115
|
+
inputMethod: "paste",
|
|
116
|
+
sourcePayload: { winningAd: text },
|
|
117
|
+
};
|
|
118
|
+
if (opts.seeds && opts.seeds.length > 0)
|
|
119
|
+
body.seeds = opts.seeds;
|
|
120
|
+
if (typeof opts.variantCount === "number")
|
|
121
|
+
body.variantCount = opts.variantCount;
|
|
122
|
+
if (opts.adAccountId)
|
|
123
|
+
body.adAccountId = opts.adAccountId;
|
|
124
|
+
return body;
|
|
125
|
+
}
|
|
126
|
+
export function resolveSwipeText(swipe) {
|
|
127
|
+
const transcript = typeof swipe.transcript === "string" ? swipe.transcript.trim() : "";
|
|
128
|
+
if (transcript)
|
|
129
|
+
return transcript;
|
|
130
|
+
return [swipe.headline, swipe.bodyText, swipe.ctaText]
|
|
131
|
+
.map((s) => (typeof s === "string" ? s.trim() : ""))
|
|
132
|
+
.filter((s) => s.length > 0)
|
|
133
|
+
.join("\n\n");
|
|
134
|
+
}
|
|
135
|
+
function parseOpts(flags) {
|
|
136
|
+
const awarenessLevel = flags["awareness"] ?? "problem-aware";
|
|
137
|
+
const seeds = resolveSeedsFlag(flags["seeds"]);
|
|
138
|
+
let variantCount;
|
|
139
|
+
if (typeof flags["variants"] === "string") {
|
|
140
|
+
const n = parseInt(flags["variants"], 10);
|
|
141
|
+
variantCount = Number.isNaN(n) ? 2 : Math.max(1, Math.min(10, n));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
let passes = 1;
|
|
145
|
+
if (typeof flags["passes"] === "string") {
|
|
146
|
+
const p = parseInt(flags["passes"], 10);
|
|
147
|
+
passes = Number.isNaN(p) ? 1 : Math.max(1, Math.min(5, p));
|
|
148
|
+
}
|
|
149
|
+
variantCount = passes * 2;
|
|
150
|
+
}
|
|
151
|
+
const adAccountId = typeof flags["ad-account"] === "string" ? flags["ad-account"] : undefined;
|
|
152
|
+
let stopAtHooks;
|
|
153
|
+
if (flags["stop-at-hooks"] === true)
|
|
154
|
+
stopAtHooks = true;
|
|
155
|
+
else if (flags["auto-hooks"] === true)
|
|
156
|
+
stopAtHooks = false;
|
|
157
|
+
return { awarenessLevel, seeds, variantCount, adAccountId, stopAtHooks };
|
|
158
|
+
}
|
|
159
|
+
export function resolveGenesisAction(flags) {
|
|
160
|
+
const opts = parseOpts(flags);
|
|
161
|
+
const reel = typeof flags["reel"] === "string" && flags["reel"].trim() ? flags["reel"].trim() : undefined;
|
|
162
|
+
const swipeUrl = typeof flags["swipe-url"] === "string" && flags["swipe-url"].trim()
|
|
163
|
+
? flags["swipe-url"].trim()
|
|
164
|
+
: undefined;
|
|
165
|
+
const steering = typeof flags["steering"] === "string" && flags["steering"].trim()
|
|
166
|
+
? flags["steering"].trim()
|
|
167
|
+
: undefined;
|
|
168
|
+
const brief = resolveTextFlag(flags["brief"]);
|
|
169
|
+
const idea = typeof flags["idea"] === "string" && flags["idea"].trim() ? flags["idea"].trim() : undefined;
|
|
170
|
+
const wantsBank = flags["from-bank"] === true || !!idea;
|
|
171
|
+
const swipeId = typeof flags["swipe"] === "string" && flags["swipe"].trim() ? flags["swipe"].trim() : undefined;
|
|
172
|
+
const wantsSwipe = flags["list-swipes"] === true || flags["swipe"] === true || !!swipeId;
|
|
173
|
+
const sources = [
|
|
174
|
+
reel ? "reel" : null,
|
|
175
|
+
swipeUrl ? "swipe-url" : null,
|
|
176
|
+
brief ? "brief" : null,
|
|
177
|
+
wantsBank ? "bank" : null,
|
|
178
|
+
wantsSwipe ? "swipe" : null,
|
|
179
|
+
].filter(Boolean);
|
|
180
|
+
if (sources.length === 0) {
|
|
181
|
+
return {
|
|
182
|
+
kind: "error",
|
|
183
|
+
message: 'No source. Use one of: --brief <file|"text">, --reel "<url>", --swipe-url "<fb-url>", --from-bank [--idea <id>], or --swipe <id> / --list-swipes.',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (sources.length > 1) {
|
|
187
|
+
return {
|
|
188
|
+
kind: "error",
|
|
189
|
+
message: "Choose one source: --brief, --reel, --swipe-url, --from-bank/--idea, or --swipe/--list-swipes.",
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const limit = typeof flags["limit"] === "string" ? Math.max(1, parseInt(flags["limit"], 10) || 20) : 20;
|
|
193
|
+
if (reel)
|
|
194
|
+
return { kind: "reel", urls: [reel], opts };
|
|
195
|
+
if (swipeUrl)
|
|
196
|
+
return { kind: "swipe-url", urls: [swipeUrl], opts, steering };
|
|
197
|
+
if (brief)
|
|
198
|
+
return { kind: "submit", body: buildBriefBody(brief, opts) };
|
|
199
|
+
if (swipeId)
|
|
200
|
+
return { kind: "from-swipe", swipeId, opts };
|
|
201
|
+
if (wantsSwipe)
|
|
202
|
+
return { kind: "list-swipes", limit };
|
|
203
|
+
if (idea)
|
|
204
|
+
return { kind: "from-bank", ideaId: idea, opts };
|
|
205
|
+
return { kind: "list-bank", limit };
|
|
206
|
+
}
|
|
207
|
+
function parsePositional() {
|
|
208
|
+
const args = process.argv.slice(3);
|
|
209
|
+
const out = [];
|
|
210
|
+
let i = 0;
|
|
211
|
+
while (i < args.length) {
|
|
212
|
+
const a = args[i];
|
|
213
|
+
if (a.startsWith("--")) {
|
|
214
|
+
const next = args[i + 1];
|
|
215
|
+
i += next !== undefined && !next.startsWith("--") ? 2 : 1;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
out.push(a);
|
|
219
|
+
i++;
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
export async function run(flags) {
|
|
224
|
+
const cc = formatCcCommand(process.argv.slice(2));
|
|
225
|
+
const sub = parsePositional()[0];
|
|
226
|
+
if (sub === "connect-instagram")
|
|
227
|
+
return runConnectInstagram(flags, cc);
|
|
228
|
+
if (sub === "scrape")
|
|
229
|
+
return runScrape(flags, cc);
|
|
230
|
+
if (sub === "hook-pref")
|
|
231
|
+
return runHookPref();
|
|
232
|
+
const action = resolveGenesisAction(flags);
|
|
233
|
+
const noWait = flags["wait"] === false || flags["no-wait"] === true;
|
|
234
|
+
switch (action.kind) {
|
|
235
|
+
case "error":
|
|
236
|
+
console.error(`Error: ${action.message}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
return;
|
|
239
|
+
case "reel":
|
|
240
|
+
return runReel(action.urls, action.opts, { noWait, cc });
|
|
241
|
+
case "swipe-url":
|
|
242
|
+
return runSwipeUrl(action.urls, action.opts, action.steering, { noWait, cc });
|
|
243
|
+
case "list-bank":
|
|
244
|
+
return listBank(action.limit);
|
|
245
|
+
case "from-bank":
|
|
246
|
+
return runFromBank(action.ideaId, action.opts, { noWait, cc });
|
|
247
|
+
case "list-swipes":
|
|
248
|
+
return listSwipes(action.limit);
|
|
249
|
+
case "from-swipe":
|
|
250
|
+
return runFromSwipe(action.swipeId, action.opts, { noWait, cc });
|
|
251
|
+
case "submit":
|
|
252
|
+
return submitGenesis(action.body, { noWait, cc });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function listBank(limit) {
|
|
256
|
+
const res = await apiGet(`/api/v2/scout/bank?limit=${limit}`);
|
|
257
|
+
if (!res.ok) {
|
|
258
|
+
console.log(formatError(res));
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
const ideas = Array.isArray(res.data.ideas) ? res.data.ideas : [];
|
|
262
|
+
if (ideas.length === 0) {
|
|
263
|
+
console.log("Bank is empty.");
|
|
264
|
+
console.log('Fill it with: exodus genesis scrape (or paste a reel: exodus genesis --reel "<url>")');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
console.log(`## Idea Bank (${ideas.length} reel${ideas.length === 1 ? "" : "s"})`);
|
|
268
|
+
console.log("");
|
|
269
|
+
for (const i of ideas) {
|
|
270
|
+
const hook = i.hook ?? "(no hook)";
|
|
271
|
+
const user = typeof i.sourceUsername === "string" && i.sourceUsername.length > 0
|
|
272
|
+
? ` @${i.sourceUsername.replace(/^@+/, "")}`
|
|
273
|
+
: "";
|
|
274
|
+
const score = typeof i.relevanceScore === "number" ? ` score=${i.relevanceScore.toFixed(2)}` : "";
|
|
275
|
+
const used = typeof i.useCount === "number" && i.useCount > 0 ? " · used" : "";
|
|
276
|
+
console.log(` • ${hook}${user}${score}`);
|
|
277
|
+
console.log(` id: ${i._id ?? "?"} [${i.status ?? "new"}${used}]`);
|
|
278
|
+
if (i.sourceUrl)
|
|
279
|
+
console.log(` ${i.sourceUrl}`);
|
|
280
|
+
}
|
|
281
|
+
console.log("");
|
|
282
|
+
console.log("Write from one: exodus genesis --from-bank --idea <id>");
|
|
283
|
+
}
|
|
284
|
+
async function runFromBank(ideaId, opts, rt) {
|
|
285
|
+
const res = await apiPost("/api/v2/scout/bank/ideas", { ideaIds: [ideaId] }, { ccCommand: rt.cc });
|
|
286
|
+
if (!res.ok) {
|
|
287
|
+
console.log(formatError(res));
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
const idea = (res.data.ideas ?? [])[0];
|
|
291
|
+
if (!idea) {
|
|
292
|
+
console.error(`No banked idea found for id "${ideaId}". List the bank: exodus genesis --from-bank`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
if (!idea.sourceUrl) {
|
|
296
|
+
console.error(`Banked idea "${ideaId}" has no source reel URL to write from.`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
console.log(`Writing from banked reel: ${idea.sourceUrl}`);
|
|
300
|
+
return runReel([idea.sourceUrl], opts, rt);
|
|
301
|
+
}
|
|
302
|
+
async function runReel(urls, opts, rt) {
|
|
303
|
+
console.log(`Transcribing ${urls.length === 1 ? "the reel" : `${urls.length} reels`} → idea → Genesis…`);
|
|
304
|
+
const result = await captureReelAndWrite(urls, { awarenessLevel: opts.awarenessLevel, variantCount: opts.variantCount }, { cc: rt.cc });
|
|
305
|
+
for (const f of result.bankedFailed) {
|
|
306
|
+
console.error(` ✗ couldn't pull an idea from ${f} (private, region-locked, or no transcript?)`);
|
|
307
|
+
}
|
|
308
|
+
if (result.dispatched.length === 0) {
|
|
309
|
+
console.error("No reel produced a usable idea — nothing to write.");
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
for (const d of result.dispatched) {
|
|
313
|
+
console.log(` ✓ banked ${d.key} from the reel → Genesis run ${d.runId}`);
|
|
314
|
+
}
|
|
315
|
+
if (rt.noWait || result.dispatched.length !== 1) {
|
|
316
|
+
console.log("");
|
|
317
|
+
console.log("Track them: exodus idea list (status flips to 'written' with a doc link)");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
console.log("");
|
|
321
|
+
await waitForGenesisRun(result.dispatched[0].runId);
|
|
322
|
+
}
|
|
323
|
+
async function runSwipeUrl(urls, opts, steering, rt) {
|
|
324
|
+
console.log(`Swiping ${urls.length === 1 ? "the ad" : `${urls.length} ads`} → idea → Genesis…`);
|
|
325
|
+
const result = await captureSwipeAndWrite(urls, { awarenessLevel: opts.awarenessLevel, variantCount: opts.variantCount, steering }, { cc: rt.cc });
|
|
326
|
+
for (const f of result.bankedFailed) {
|
|
327
|
+
console.error(` ✗ couldn't pull an idea from ${f} (inactive, image-only, or not in the Ad Library index?)`);
|
|
328
|
+
}
|
|
329
|
+
if (result.dispatched.length === 0) {
|
|
330
|
+
console.error("No ad produced a usable idea — nothing to write.");
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
for (const d of result.dispatched) {
|
|
334
|
+
console.log(` ✓ swiped ${d.key} from the ad → Genesis run ${d.runId}`);
|
|
335
|
+
}
|
|
336
|
+
if (rt.noWait || result.dispatched.length !== 1) {
|
|
337
|
+
console.log("");
|
|
338
|
+
console.log("Track them: exodus idea list (status flips to 'written' with a doc link)");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
console.log("");
|
|
342
|
+
await waitForGenesisRun(result.dispatched[0].runId);
|
|
343
|
+
}
|
|
344
|
+
function snippet(text, max = 80) {
|
|
345
|
+
if (typeof text !== "string")
|
|
346
|
+
return "";
|
|
347
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
348
|
+
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
|
|
349
|
+
}
|
|
350
|
+
async function listSwipes(limit) {
|
|
351
|
+
const res = await apiGet(`/api/v2/swipe-library?limit=${limit}`);
|
|
352
|
+
if (!res.ok) {
|
|
353
|
+
console.log(formatError(res));
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
const swipes = Array.isArray(res.data.swipes) ? res.data.swipes : [];
|
|
357
|
+
if (swipes.length === 0) {
|
|
358
|
+
console.log("No saved swipes for your tracked competitors.");
|
|
359
|
+
console.log("Collect competitor ads first from the swipe library in the dashboard.");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
console.log(`## Swipe Library (${swipes.length} ad${swipes.length === 1 ? "" : "s"})`);
|
|
363
|
+
console.log("");
|
|
364
|
+
for (const s of swipes) {
|
|
365
|
+
const brand = s.brandName ?? "(unknown brand)";
|
|
366
|
+
const fmt = s.format ? ` [${s.format}]` : "";
|
|
367
|
+
const preview = snippet(resolveSwipeText(s));
|
|
368
|
+
console.log(` • ${brand}${fmt}`);
|
|
369
|
+
if (preview)
|
|
370
|
+
console.log(` ${preview}`);
|
|
371
|
+
console.log(` id: ${s._id ?? "?"}`);
|
|
372
|
+
}
|
|
373
|
+
console.log("");
|
|
374
|
+
console.log("Write from one: exodus genesis --swipe <id>");
|
|
375
|
+
}
|
|
376
|
+
async function runFromSwipe(swipeId, opts, rt) {
|
|
377
|
+
const res = await apiPost("/api/v2/swipe-library/get", { ids: [swipeId] }, { ccCommand: rt.cc });
|
|
378
|
+
if (!res.ok) {
|
|
379
|
+
console.log(formatError(res));
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
const swipe = (res.data.swipes ?? [])[0];
|
|
383
|
+
if (!swipe) {
|
|
384
|
+
console.error(`No saved swipe found for id "${swipeId}". List them: exodus genesis --list-swipes`);
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
const text = resolveSwipeText(swipe);
|
|
388
|
+
if (!text) {
|
|
389
|
+
console.error(`Swipe "${swipeId}" has no usable text (no transcript, headline, or body) to write from.`);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
console.log(`Writing from swipe: ${swipe.brandName ?? "competitor ad"}${swipe.format ? ` [${swipe.format}]` : ""}`);
|
|
393
|
+
return submitGenesis(buildPasteBody(text, opts), rt);
|
|
394
|
+
}
|
|
395
|
+
async function submitGenesis(body, rt) {
|
|
396
|
+
const res = await apiPost("/api/v2/genesis", body, {
|
|
397
|
+
ccCommand: rt.cc,
|
|
398
|
+
});
|
|
399
|
+
if (!res.ok) {
|
|
400
|
+
console.log(formatError(res));
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
const runId = res.data.runId;
|
|
404
|
+
if (!runId) {
|
|
405
|
+
console.error("No runId returned from /api/v2/genesis");
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
console.log(`Genesis run started: ${runId}`);
|
|
409
|
+
if (body.seeds && body.seeds.length > 0)
|
|
410
|
+
console.log(`Seeds supplied: ${body.seeds.length}`);
|
|
411
|
+
if (typeof body.variantCount === "number")
|
|
412
|
+
console.log(`Variant count: ${body.variantCount}`);
|
|
413
|
+
if (rt.noWait) {
|
|
414
|
+
console.log(`Run ID: ${runId}`);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
await waitForGenesisRun(runId);
|
|
418
|
+
}
|
|
419
|
+
async function waitForGenesisRun(runId) {
|
|
420
|
+
console.log("Polling for completion (variants run in parallel and finish around the same time; more passes just means more variants)...");
|
|
421
|
+
const result = await pollUntilDone({
|
|
422
|
+
path: `/api/v2/genesis?id=${runId}`,
|
|
423
|
+
timeoutMs: 60 * 60 * 1000,
|
|
424
|
+
intervalMs: 5_000,
|
|
425
|
+
terminalStatuses: ["awaiting_hook_selection"],
|
|
426
|
+
onProgress: (data) => {
|
|
427
|
+
const status = data["status"];
|
|
428
|
+
const currentStep = data["currentStep"];
|
|
429
|
+
if (status) {
|
|
430
|
+
process.stdout.write(`\r ${status}${currentStep ? ` — ${currentStep}` : ""} `);
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
console.log();
|
|
435
|
+
if (result.data["status"] === "awaiting_hook_selection") {
|
|
436
|
+
const dashboardUrl = getDashboardUrl();
|
|
437
|
+
console.log(`\n⏸ Paused for hook selection.`);
|
|
438
|
+
console.log(`Pick your hooks here: ${dashboardUrl}/runs/${runId}`);
|
|
439
|
+
console.log(`(The pipeline resumes automatically once you choose.)`);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
console.log(formatGenesisRun(result.data));
|
|
443
|
+
if (!result.ok)
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
async function runHookPref() {
|
|
447
|
+
const positionals = parsePositional();
|
|
448
|
+
const pref = positionals[1];
|
|
449
|
+
if (pref !== "manual" && pref !== "auto") {
|
|
450
|
+
console.error('Usage: exodus genesis hook-pref <manual|auto>');
|
|
451
|
+
console.error(' manual Pause each run at hook selection so you choose hooks.');
|
|
452
|
+
console.error(' auto Skip hook selection and let the pipeline pick automatically.');
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
const res = await apiPost("/api/v2/genesis/hook-pref", { preference: pref });
|
|
456
|
+
if (!res.ok) {
|
|
457
|
+
console.log(`Error: ${res.data.error ?? "Unknown error"}`);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
console.log(`Hook preference set to "${pref}".`);
|
|
461
|
+
if (pref === "manual") {
|
|
462
|
+
console.log("Future runs will pause at hook selection for your input.");
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
console.log("Future runs will select hooks automatically without pausing.");
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function requireActiveBrand() {
|
|
469
|
+
const slug = resolveActiveBrand().slug;
|
|
470
|
+
if (!slug) {
|
|
471
|
+
console.error("No active brand. Pick one first: exodus brand use <slug>");
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
return slug;
|
|
475
|
+
}
|
|
476
|
+
async function runConnectInstagram(flags, cc) {
|
|
477
|
+
const client = requireActiveBrand();
|
|
478
|
+
const start = await apiPost("/api/v2/genesis/instagram/connect/start", { client }, { ccCommand: cc });
|
|
479
|
+
if (!start.ok || !start.data.liveViewUrl) {
|
|
480
|
+
console.log(formatError(start));
|
|
481
|
+
process.exit(1);
|
|
482
|
+
}
|
|
483
|
+
console.log(`\nConnecting an Instagram account to brand "${client}".`);
|
|
484
|
+
console.log("\n1. Open this link in your browser:\n");
|
|
485
|
+
console.log(` ${start.data.liveViewUrl}\n`);
|
|
486
|
+
console.log('2. Go to instagram.com and log into the account for THIS brand.');
|
|
487
|
+
console.log("3. Scroll your feed for a bit so the algorithm learns the niche.");
|
|
488
|
+
console.log("4. Come back here and press Enter.\n");
|
|
489
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
490
|
+
try {
|
|
491
|
+
await rl.question("Press Enter once you're logged in and done grooming... ");
|
|
492
|
+
let username = typeof flags["username"] === "string" ? flags["username"].trim() : "";
|
|
493
|
+
while (!username) {
|
|
494
|
+
username = (await rl.question("Instagram handle you just logged in as (e.g. mybrand): ")).trim();
|
|
495
|
+
}
|
|
496
|
+
const finish = await apiPost("/api/v2/genesis/instagram/connect/finish", { client, igUsername: username }, { ccCommand: cc });
|
|
497
|
+
if (!finish.ok) {
|
|
498
|
+
console.log(formatError(finish));
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
console.log(`\n✓ Instagram @${username.replace(/^@+/, "")} connected to "${client}".`);
|
|
502
|
+
console.log("Fill the idea bank now: exodus genesis scrape --organic");
|
|
503
|
+
}
|
|
504
|
+
finally {
|
|
505
|
+
rl.close();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async function runScrape(flags, cc) {
|
|
509
|
+
const client = requireActiveBrand();
|
|
510
|
+
const noWait = flags["wait"] === false || flags["no-wait"] === true;
|
|
511
|
+
if (flags["organic"] === true) {
|
|
512
|
+
const smoke = flags["smoke"] === true;
|
|
513
|
+
const res = await apiPost("/api/v2/scout/organic", { client, ...(smoke ? { smoke: true } : {}) }, { ccCommand: cc });
|
|
514
|
+
if (!res.ok || !res.data.runId) {
|
|
515
|
+
console.log(formatError(res));
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
console.log(`Organic scrape started for "${client}": ${res.data.runId}${smoke ? " (smoke)" : ""}`);
|
|
519
|
+
return pollScrape(res.data.runId, noWait, 1_800_000);
|
|
520
|
+
}
|
|
521
|
+
const term = typeof flags["term"] === "string" ? flags["term"] : undefined;
|
|
522
|
+
const body = { sourceMode: "fresh", client };
|
|
523
|
+
if (term)
|
|
524
|
+
body.term = term;
|
|
525
|
+
const res = await apiPost("/api/v2/scout", body, { ccCommand: cc });
|
|
526
|
+
if (!res.ok || !res.data.runId) {
|
|
527
|
+
console.log(formatError(res));
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
console.log(`Discovery scrape started for "${client}": ${res.data.runId}`);
|
|
531
|
+
return pollScrape(res.data.runId, noWait, 1_200_000);
|
|
532
|
+
}
|
|
533
|
+
async function pollScrape(runId, noWait, timeoutMs) {
|
|
534
|
+
if (noWait) {
|
|
535
|
+
console.log(`Run ID: ${runId}`);
|
|
536
|
+
console.log(`Check status: exodus status --type scout --id ${runId}`);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
console.log("Polling for completion...");
|
|
540
|
+
const result = await pollUntilDone({
|
|
541
|
+
path: `/api/v2/scout?runId=${runId}`,
|
|
542
|
+
intervalMs: 15_000,
|
|
543
|
+
timeoutMs,
|
|
544
|
+
onProgress: (data) => {
|
|
545
|
+
const status = data["status"];
|
|
546
|
+
const captured = data["organicCaptured"];
|
|
547
|
+
const qualified = data["organicQualified"];
|
|
548
|
+
const counts = captured !== undefined || qualified !== undefined
|
|
549
|
+
? ` captured=${captured ?? 0} qualified=${qualified ?? 0}`
|
|
550
|
+
: "";
|
|
551
|
+
if (status)
|
|
552
|
+
process.stdout.write(`\r status: ${status}${counts} `);
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
console.log();
|
|
556
|
+
if (result.timedOut) {
|
|
557
|
+
console.log(`Timed out waiting. Check later: exodus status --type scout --id ${runId}`);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
console.log(`Done. Banked reels are ready to write: exodus genesis --from-bank`);
|
|
561
|
+
}
|
|
562
|
+
if (!result.ok && !result.timedOut)
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export declare const helpText: string;
|
|
2
|
+
declare const SOURCES: readonly ["gambit", "organic", "swipe"];
|
|
3
|
+
type Source = (typeof SOURCES)[number];
|
|
4
|
+
export declare function parseKeys(raw: string): string[] | null;
|
|
5
|
+
export type IdeaAction = {
|
|
6
|
+
kind: "add";
|
|
7
|
+
hook: string;
|
|
8
|
+
description: string;
|
|
9
|
+
source: Source;
|
|
10
|
+
sourceRef?: string;
|
|
11
|
+
notes?: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "list";
|
|
14
|
+
source?: Source;
|
|
15
|
+
since?: string;
|
|
16
|
+
status?: string;
|
|
17
|
+
limit?: number;
|
|
18
|
+
} | {
|
|
19
|
+
kind: "note";
|
|
20
|
+
key: string;
|
|
21
|
+
notes: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: "edit";
|
|
24
|
+
key: string;
|
|
25
|
+
description: string;
|
|
26
|
+
} | {
|
|
27
|
+
kind: "write";
|
|
28
|
+
keys: string[];
|
|
29
|
+
awarenessLevel: string;
|
|
30
|
+
variantCount?: number;
|
|
31
|
+
} | {
|
|
32
|
+
kind: "rm";
|
|
33
|
+
key: string;
|
|
34
|
+
hard: boolean;
|
|
35
|
+
} | {
|
|
36
|
+
kind: "gambit";
|
|
37
|
+
dump: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: "organic";
|
|
40
|
+
urls: string[];
|
|
41
|
+
write: boolean;
|
|
42
|
+
awarenessLevel: string;
|
|
43
|
+
variantCount?: number;
|
|
44
|
+
} | {
|
|
45
|
+
kind: "swipe";
|
|
46
|
+
limit?: number;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "error";
|
|
49
|
+
message: string;
|
|
50
|
+
};
|
|
51
|
+
export declare function resolveIdeaAction(positionals: string[], flags: Record<string, string | boolean>): IdeaAction;
|
|
52
|
+
export declare function run(flags: Record<string, string | boolean>): Promise<void>;
|
|
53
|
+
export {};
|