@aicopycoders/exodus 2026.6.2902 → 2026.7.200-beta.10
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/docs/CLAUDE.md +3 -1
- package/assets/docs/PIPELINES.md +24 -0
- package/assets/references/cast-video.md +9 -0
- package/assets/skills/exodus/SKILL.md +3 -1
- package/assets/skills/exodus-brand/SKILL.md +2 -2
- package/assets/skills/exodus-genesis/SKILL.md +9 -0
- package/assets/skills/exodus-winners/SKILL.md +230 -0
- package/assets/skills/exodus-workflow/SKILL.md +173 -0
- package/dist/bin/exodus.js +4 -1
- package/dist/commands/brand.js +161 -1
- package/dist/commands/creative.js +5 -22
- package/dist/commands/doctor.js +22 -16
- package/dist/commands/genesis.js +19 -1
- package/dist/commands/init.js +3 -2
- package/dist/commands/keys.js +1 -0
- package/dist/commands/meme.js +5 -4
- package/dist/commands/migrate.js +7 -6
- package/dist/commands/primer.js +4 -2
- package/dist/commands/segment.d.ts +57 -0
- package/dist/commands/segment.js +274 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +20 -0
- package/dist/commands/winners.d.ts +10 -0
- package/dist/commands/winners.js +401 -0
- package/dist/commands/workflow.d.ts +298 -0
- package/dist/commands/workflow.js +763 -0
- package/dist/lib/channel.d.ts +5 -0
- package/dist/lib/channel.js +15 -0
- package/dist/lib/image-sniff.d.ts +6 -0
- package/dist/lib/image-sniff.js +18 -0
- package/dist/lib/keys-sync.js +4 -1
- package/dist/lib/scaffold.d.ts +5 -4
- package/dist/lib/scaffold.js +27 -6
- package/dist/lib/template-constants.d.ts +1 -1
- package/dist/lib/template-constants.js +102 -0
- package/package.json +1 -1
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { apiGet, apiPost } from "../lib/client.js";
|
|
4
|
+
import { formatError } from "../lib/format.js";
|
|
5
|
+
import { pollUntilDone } from "../lib/poll.js";
|
|
6
|
+
export const helpText = `
|
|
7
|
+
exodus winners — Import your own brand's winning ads as generative fuel
|
|
8
|
+
|
|
9
|
+
Your agent (holding the Meta Ads MCP) assembles a winner-package JSON of
|
|
10
|
+
designated ad-account winners; this command pushes it into Exodus. Each
|
|
11
|
+
winner lands as a swipe row under an auto-created own-brand entry, enriched
|
|
12
|
+
server-side (Ad Library match → scrape, or your pushed media files), and
|
|
13
|
+
surfaces in generation selection with its verdict.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
exodus winners import <file.json | -> Push a winner package (- reads stdin)
|
|
17
|
+
exodus winners status <importId> Re-poll an import later
|
|
18
|
+
exodus winners list Winners Exodus already holds
|
|
19
|
+
|
|
20
|
+
Import flags:
|
|
21
|
+
--dry-run Local schema check + server dry-run: reports would-create vs
|
|
22
|
+
would-update per winner. Zero writes, no media upload.
|
|
23
|
+
--no-wait Return the importId immediately instead of polling
|
|
24
|
+
--json Machine-readable JSON output
|
|
25
|
+
|
|
26
|
+
Notes:
|
|
27
|
+
• Scopes to your active brand's workspace (exodus brand current).
|
|
28
|
+
• Requires your Scrape Creators API key (Settings → Keys) — the own-page
|
|
29
|
+
match scrape bills your account.
|
|
30
|
+
• Re-pushing the same file is safe: no duplicate rows, the verdict snapshot
|
|
31
|
+
is replaced wholesale, and previously gap-filled winners that now match
|
|
32
|
+
upgrade in place. Winners absent from a re-push are untouched.
|
|
33
|
+
• assets paths in the package resolve relative to the JSON file's folder.
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
exodus winners import winners.json
|
|
37
|
+
exodus winners import winners.json --dry-run
|
|
38
|
+
cat winners.json | exodus winners import -
|
|
39
|
+
exodus winners status k97abc...
|
|
40
|
+
exodus winners list
|
|
41
|
+
`.trim();
|
|
42
|
+
export function validatePackageLocally(pkg) {
|
|
43
|
+
const errors = [];
|
|
44
|
+
const warnings = [];
|
|
45
|
+
let winnerCount = 0;
|
|
46
|
+
if (pkg === null || typeof pkg !== "object") {
|
|
47
|
+
return { ok: false, errors: ["package is not a JSON object"], warnings, winnerCount };
|
|
48
|
+
}
|
|
49
|
+
const p = pkg;
|
|
50
|
+
if (p.version !== 1)
|
|
51
|
+
errors.push("version must be 1");
|
|
52
|
+
const source = (p.source ?? {});
|
|
53
|
+
if (typeof source.pageId !== "string" || !source.pageId.trim())
|
|
54
|
+
errors.push("source.pageId is required (the scrape target + own-brand key)");
|
|
55
|
+
if (typeof source.pageName !== "string" || !source.pageName.trim())
|
|
56
|
+
errors.push("source.pageName is required (names the own-brand entry)");
|
|
57
|
+
if (typeof source.adAccountId !== "string" || !source.adAccountId.trim())
|
|
58
|
+
errors.push("source.adAccountId is required");
|
|
59
|
+
if (!Array.isArray(p.winners) || p.winners.length === 0) {
|
|
60
|
+
errors.push("winners array is required and must be non-empty");
|
|
61
|
+
return { ok: errors.length === 0, errors, warnings, winnerCount };
|
|
62
|
+
}
|
|
63
|
+
winnerCount = p.winners.length;
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
p.winners.forEach((raw, i) => {
|
|
66
|
+
const label = `winner #${i + 1}`;
|
|
67
|
+
if (raw === null || typeof raw !== "object") {
|
|
68
|
+
warnings.push(`${label}: not an object — the server will reject it`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const w = raw;
|
|
72
|
+
const id = typeof w.accountAdId === "string" ? w.accountAdId : undefined;
|
|
73
|
+
if (!id)
|
|
74
|
+
warnings.push(`${label}: missing accountAdId`);
|
|
75
|
+
else if (seen.has(id))
|
|
76
|
+
warnings.push(`${label} (${id}): duplicate accountAdId`);
|
|
77
|
+
else
|
|
78
|
+
seen.add(id);
|
|
79
|
+
if (w.format !== "video" && w.format !== "image")
|
|
80
|
+
warnings.push(`${label}${id ? ` (${id})` : ""}: format must be "video" or "image"`);
|
|
81
|
+
const verdict = (w.verdict ?? {});
|
|
82
|
+
if (typeof verdict.sentence !== "string" || !verdict.sentence.trim())
|
|
83
|
+
warnings.push(`${label}${id ? ` (${id})` : ""}: missing verdict.sentence`);
|
|
84
|
+
if ((typeof w.bodyText !== "string" || !w.bodyText.trim()) &&
|
|
85
|
+
(typeof w.headline !== "string" || !w.headline.trim()))
|
|
86
|
+
warnings.push(`${label}${id ? ` (${id})` : ""}: needs bodyText or headline`);
|
|
87
|
+
});
|
|
88
|
+
return { ok: errors.length === 0, errors, warnings, winnerCount };
|
|
89
|
+
}
|
|
90
|
+
export function contentTypeFor(filePath) {
|
|
91
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
92
|
+
const map = {
|
|
93
|
+
".jpg": "image/jpeg",
|
|
94
|
+
".jpeg": "image/jpeg",
|
|
95
|
+
".png": "image/png",
|
|
96
|
+
".webp": "image/webp",
|
|
97
|
+
".gif": "image/gif",
|
|
98
|
+
".mp4": "video/mp4",
|
|
99
|
+
".m4v": "video/mp4",
|
|
100
|
+
".mov": "video/quicktime",
|
|
101
|
+
".webm": "video/webm",
|
|
102
|
+
};
|
|
103
|
+
return map[ext] ?? "application/octet-stream";
|
|
104
|
+
}
|
|
105
|
+
export async function run(flags) {
|
|
106
|
+
const positional = parsePositional();
|
|
107
|
+
const [sub, ...rest] = positional;
|
|
108
|
+
if (sub === "import")
|
|
109
|
+
return runImport(rest, flags);
|
|
110
|
+
if (sub === "status")
|
|
111
|
+
return runStatus(rest, flags);
|
|
112
|
+
if (sub === "list")
|
|
113
|
+
return runList(flags);
|
|
114
|
+
if (!sub) {
|
|
115
|
+
console.log(helpText);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
console.error(`Unknown subcommand: "${sub}"\n`);
|
|
119
|
+
console.log(helpText);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
function parsePositional() {
|
|
123
|
+
return process.argv.slice(3).filter((a) => !a.startsWith("--"));
|
|
124
|
+
}
|
|
125
|
+
async function runImport(positional, flags) {
|
|
126
|
+
const json = !!flags["json"];
|
|
127
|
+
const dryRun = !!flags["dry-run"] || !!flags["dryRun"];
|
|
128
|
+
const noWait = flags["wait"] === false || flags["no-wait"] === true;
|
|
129
|
+
const fileArg = positional[0];
|
|
130
|
+
if (!fileArg) {
|
|
131
|
+
console.error("Error: a package file is required (or - for stdin)");
|
|
132
|
+
console.log("Usage: exodus winners import <file.json | ->");
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
let rawText;
|
|
136
|
+
let baseDir;
|
|
137
|
+
if (fileArg === "-") {
|
|
138
|
+
rawText = fs.readFileSync(0, "utf-8");
|
|
139
|
+
baseDir = process.cwd();
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
if (!fs.existsSync(fileArg)) {
|
|
143
|
+
console.error(`Error: file not found: ${fileArg}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
rawText = fs.readFileSync(fileArg, "utf-8");
|
|
147
|
+
baseDir = path.dirname(path.resolve(fileArg));
|
|
148
|
+
}
|
|
149
|
+
let pkg;
|
|
150
|
+
try {
|
|
151
|
+
pkg = JSON.parse(rawText);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
console.error(`Error: package is not valid JSON: ${err instanceof Error ? err.message : err}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
const validation = validatePackageLocally(pkg);
|
|
158
|
+
if (!validation.ok) {
|
|
159
|
+
if (json) {
|
|
160
|
+
console.log(JSON.stringify({ ok: false, errors: validation.errors }));
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.error("Package failed local validation:");
|
|
164
|
+
for (const e of validation.errors)
|
|
165
|
+
console.error(` • ${e}`);
|
|
166
|
+
}
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
if (!json) {
|
|
170
|
+
for (const w of validation.warnings)
|
|
171
|
+
console.error(`Warning: ${w}`);
|
|
172
|
+
}
|
|
173
|
+
const winners = (pkg.winners ?? []).filter((w) => w !== null && typeof w === "object");
|
|
174
|
+
const missingFiles = [];
|
|
175
|
+
for (const w of winners) {
|
|
176
|
+
const assets = (w.assets ?? {});
|
|
177
|
+
for (const key of ["imagePath", "videoPath", "posterPath"]) {
|
|
178
|
+
const p = assets[key];
|
|
179
|
+
if (typeof p === "string" && p.trim() && !fs.existsSync(path.resolve(baseDir, p))) {
|
|
180
|
+
missingFiles.push(p);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (!dryRun && missingFiles.length > 0) {
|
|
185
|
+
console.error("Error: referenced asset file(s) not found:");
|
|
186
|
+
for (const f of missingFiles)
|
|
187
|
+
console.error(` • ${f}`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
if (dryRun) {
|
|
191
|
+
const res = await apiPost("/api/v2/winners", {
|
|
192
|
+
...pkg,
|
|
193
|
+
dryRun: true,
|
|
194
|
+
});
|
|
195
|
+
if (!res.ok) {
|
|
196
|
+
if (json)
|
|
197
|
+
console.log(JSON.stringify({ ok: false, status: res.status, data: res.data }));
|
|
198
|
+
else
|
|
199
|
+
console.log(formatError(res));
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
if (json) {
|
|
203
|
+
console.log(JSON.stringify({ ok: true, ...res.data, localWarnings: validation.warnings }));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const per = res.data.perWinner ?? [];
|
|
207
|
+
const creates = per.filter((p) => p.action === "create").length;
|
|
208
|
+
const updates = per.filter((p) => p.action === "update").length;
|
|
209
|
+
console.log("Dry run — no writes, no Trigger fire, no media upload.");
|
|
210
|
+
console.log(res.data.wouldCreateBrand
|
|
211
|
+
? " own-brand entry: would be created"
|
|
212
|
+
: ` own-brand entry: exists (${res.data.brandName ?? "?"})`);
|
|
213
|
+
console.log(` winners: ${creates} would-create, ${updates} would-update`);
|
|
214
|
+
for (const p of per)
|
|
215
|
+
console.log(` ${p.accountAdId} ${p.action}`);
|
|
216
|
+
for (const r of res.data.rejected ?? []) {
|
|
217
|
+
console.log(` ${r.accountAdId} would-reject (${r.reason})`);
|
|
218
|
+
}
|
|
219
|
+
if (res.data.scrapecreatorsKey === "missing") {
|
|
220
|
+
console.log("\nHeads up: no Scrape Creators key on your account — a real import will fail.");
|
|
221
|
+
console.log("Add it in Settings → Keys (get one at scrapecreators.com).");
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
for (const w of winners) {
|
|
226
|
+
const assets = (w.assets ?? {});
|
|
227
|
+
const swapped = {};
|
|
228
|
+
const pairs = [
|
|
229
|
+
["imagePath", "imageStorageId"],
|
|
230
|
+
["videoPath", "videoStorageId"],
|
|
231
|
+
["posterPath", "posterStorageId"],
|
|
232
|
+
];
|
|
233
|
+
for (const [pathKey, idKey] of pairs) {
|
|
234
|
+
const rel = assets[pathKey];
|
|
235
|
+
if (typeof rel !== "string" || !rel.trim())
|
|
236
|
+
continue;
|
|
237
|
+
const abs = path.resolve(baseDir, rel);
|
|
238
|
+
const storageId = await uploadAsset(abs, json);
|
|
239
|
+
if (!storageId)
|
|
240
|
+
process.exit(1);
|
|
241
|
+
swapped[idKey] = storageId;
|
|
242
|
+
}
|
|
243
|
+
if (Object.keys(swapped).length > 0) {
|
|
244
|
+
w.assets = swapped;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
delete w.assets;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const res = await apiPost("/api/v2/winners", pkg);
|
|
251
|
+
if (!res.ok || !res.data.importId) {
|
|
252
|
+
if (json)
|
|
253
|
+
console.log(JSON.stringify({ ok: false, status: res.status, data: res.data }));
|
|
254
|
+
else
|
|
255
|
+
console.log(formatError(res));
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
const importId = res.data.importId;
|
|
259
|
+
if (noWait) {
|
|
260
|
+
if (json) {
|
|
261
|
+
console.log(JSON.stringify({ ok: true, importId, triggerRunId: res.data.triggerRunId, accepted: res.data.accepted, rejected: res.data.rejected }));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
console.log(`Import started: ${importId}`);
|
|
265
|
+
console.log(` accepted: ${res.data.accepted ?? "?"} winner(s)`);
|
|
266
|
+
for (const r of res.data.rejected ?? []) {
|
|
267
|
+
console.log(` rejected: ${r.accountAdId} (${r.reason})`);
|
|
268
|
+
}
|
|
269
|
+
console.log(`\nPoll: exodus winners status ${importId}`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (!json) {
|
|
273
|
+
console.log(`Import started: ${importId} — waiting for outcomes…`);
|
|
274
|
+
}
|
|
275
|
+
let lastStatus = "";
|
|
276
|
+
const poll = await pollUntilDone({
|
|
277
|
+
path: `/api/v2/winners/imports/${importId}`,
|
|
278
|
+
terminalStatuses: ["done"],
|
|
279
|
+
onProgress: (data) => {
|
|
280
|
+
const status = typeof data.status === "string" ? data.status : "";
|
|
281
|
+
if (!json && status && status !== lastStatus) {
|
|
282
|
+
lastStatus = status;
|
|
283
|
+
console.log(` status: ${status}`);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
const outcome = poll.data;
|
|
288
|
+
if (json) {
|
|
289
|
+
console.log(JSON.stringify({ ok: poll.ok, timedOut: poll.timedOut, ...outcome }));
|
|
290
|
+
if (!poll.ok)
|
|
291
|
+
process.exit(1);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (poll.timedOut) {
|
|
295
|
+
console.log(`Timed out waiting. Re-poll: exodus winners status ${importId}`);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
printOutcomeTable(outcome);
|
|
299
|
+
if (!poll.ok)
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
async function uploadAsset(absPath, json) {
|
|
303
|
+
const upRes = await apiPost("/api/v2/winners/upload-url", {});
|
|
304
|
+
if (!upRes.ok || !upRes.data.uploadUrl) {
|
|
305
|
+
if (json)
|
|
306
|
+
console.log(JSON.stringify({ ok: false, step: "upload-url", data: upRes.data }));
|
|
307
|
+
else
|
|
308
|
+
console.log(formatError(upRes));
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
const bytes = fs.readFileSync(absPath);
|
|
312
|
+
const put = await fetch(upRes.data.uploadUrl, {
|
|
313
|
+
method: "POST",
|
|
314
|
+
headers: { "Content-Type": contentTypeFor(absPath) },
|
|
315
|
+
body: new Blob([bytes]),
|
|
316
|
+
});
|
|
317
|
+
if (!put.ok) {
|
|
318
|
+
const text = await put.text().catch(() => "(unreadable)");
|
|
319
|
+
if (json)
|
|
320
|
+
console.log(JSON.stringify({ ok: false, step: "upload", status: put.status }));
|
|
321
|
+
else
|
|
322
|
+
console.error(`Error uploading ${path.basename(absPath)}: ${put.status} ${text.slice(0, 200)}`);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
const { storageId } = (await put.json());
|
|
326
|
+
if (!storageId) {
|
|
327
|
+
console.error(`Error: no storageId returned for ${path.basename(absPath)}`);
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
if (!json)
|
|
331
|
+
console.log(` uploaded ${path.basename(absPath)}`);
|
|
332
|
+
return storageId;
|
|
333
|
+
}
|
|
334
|
+
function printOutcomeTable(data) {
|
|
335
|
+
const winners = data.winners ?? [];
|
|
336
|
+
console.log(`\nImport ${data.importId ?? "?"}: ${data.status ?? "?"}`);
|
|
337
|
+
if (data.error)
|
|
338
|
+
console.log(` error: ${data.error}`);
|
|
339
|
+
const created = winners.filter((w) => w.created === true).length;
|
|
340
|
+
const updated = winners.filter((w) => w.created === false).length;
|
|
341
|
+
console.log(` ${winners.length} winner(s): ${created} created, ${updated} updated`);
|
|
342
|
+
for (const w of winners) {
|
|
343
|
+
let detail = "";
|
|
344
|
+
if (w.outcome === "partial" && w.missing?.length)
|
|
345
|
+
detail = ` (missing: ${w.missing.join(", ")})`;
|
|
346
|
+
if (w.outcome === "rejected" && w.reason)
|
|
347
|
+
detail = ` (${w.reason})`;
|
|
348
|
+
console.log(` ${w.accountAdId} ${w.outcome}${detail}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async function runStatus(positional, flags) {
|
|
352
|
+
const importId = positional[0];
|
|
353
|
+
if (!importId) {
|
|
354
|
+
console.error("Error: importId is required");
|
|
355
|
+
console.log("Usage: exodus winners status <importId>");
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
const json = !!flags["json"];
|
|
359
|
+
const res = await apiGet(`/api/v2/winners/imports/${encodeURIComponent(importId)}`);
|
|
360
|
+
if (!res.ok) {
|
|
361
|
+
if (json)
|
|
362
|
+
console.log(JSON.stringify({ ok: false, status: res.status, data: res.data }));
|
|
363
|
+
else
|
|
364
|
+
console.log(formatError(res));
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
if (json) {
|
|
368
|
+
console.log(JSON.stringify({ ok: true, ...res.data }));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
printOutcomeTable(res.data);
|
|
372
|
+
}
|
|
373
|
+
async function runList(flags) {
|
|
374
|
+
const json = !!flags["json"];
|
|
375
|
+
const res = await apiGet("/api/v2/winners");
|
|
376
|
+
if (!res.ok) {
|
|
377
|
+
if (json)
|
|
378
|
+
console.log(JSON.stringify({ ok: false, status: res.status, data: res.data }));
|
|
379
|
+
else
|
|
380
|
+
console.log(formatError(res));
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
const winners = res.data.winners ?? [];
|
|
384
|
+
if (json) {
|
|
385
|
+
console.log(JSON.stringify({ ok: true, winners }));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (winners.length === 0) {
|
|
389
|
+
console.log("No imported winners yet. Push some: exodus winners import <file.json>");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
console.log(`Winners (${winners.length}):`);
|
|
393
|
+
for (const w of winners) {
|
|
394
|
+
const status = w.enrichmentStatus === "partial"
|
|
395
|
+
? `partial${w.enrichmentMissing?.length ? ` (missing: ${w.enrichmentMissing.join(", ")})` : ""}`
|
|
396
|
+
: (w.enrichmentStatus ?? "?");
|
|
397
|
+
const when = w.designatedAt ? new Date(w.designatedAt).toLocaleDateString() : "?";
|
|
398
|
+
console.log(` ${w.sourceAdId ?? w.id} [${w.format}] ${status} designated=${when}`);
|
|
399
|
+
console.log(` ${w.verdictSentence}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { type ApiResponse } from "../lib/client.js";
|
|
2
|
+
import { type PollOptions, type PollResult } from "../lib/poll.js";
|
|
3
|
+
export declare const helpText: string;
|
|
4
|
+
export type WorkflowNodeKind = "brief" | "bot" | "primer" | "image" | "rig" | "storyboard" | "reference" | "scene-frames" | "video" | "voiceover" | "output";
|
|
5
|
+
export interface WorkflowNode {
|
|
6
|
+
id: string;
|
|
7
|
+
kind: WorkflowNodeKind;
|
|
8
|
+
position: {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
};
|
|
12
|
+
config: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
export interface WorkflowEdge {
|
|
15
|
+
id: string;
|
|
16
|
+
source: string;
|
|
17
|
+
sourceHandle: string;
|
|
18
|
+
target: string;
|
|
19
|
+
targetHandle: string;
|
|
20
|
+
}
|
|
21
|
+
export interface WorkflowContractJson {
|
|
22
|
+
contract: "exodus-workflow";
|
|
23
|
+
version: number;
|
|
24
|
+
workflowId?: string;
|
|
25
|
+
updatedAt?: string;
|
|
26
|
+
name: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
nodes: WorkflowNode[];
|
|
29
|
+
edges: WorkflowEdge[];
|
|
30
|
+
}
|
|
31
|
+
export interface UnresolvedWorkflowRef {
|
|
32
|
+
nodeId: string;
|
|
33
|
+
ref: "persona" | "primer";
|
|
34
|
+
value: string;
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
export interface WorkflowImportResult {
|
|
38
|
+
name: string;
|
|
39
|
+
created: boolean;
|
|
40
|
+
workflowId?: string;
|
|
41
|
+
nodeCount: number;
|
|
42
|
+
edgeCount: number;
|
|
43
|
+
unresolved: UnresolvedWorkflowRef[];
|
|
44
|
+
warnings: string[];
|
|
45
|
+
}
|
|
46
|
+
export type GraphIssueCode = "bad-shape" | "unknown-kind" | "duplicate-node-id" | "dangling-edge" | "unknown-port" | "type-mismatch" | "duplicate-input" | "cycle" | "missing-required-input" | "bad-config";
|
|
47
|
+
export interface GraphIssue {
|
|
48
|
+
code: GraphIssueCode;
|
|
49
|
+
message: string;
|
|
50
|
+
nodeId?: string;
|
|
51
|
+
edgeId?: string;
|
|
52
|
+
portId?: string;
|
|
53
|
+
remedy?: string;
|
|
54
|
+
}
|
|
55
|
+
export type WorkflowImportErrorCode = "invalid-graph" | "conflict" | "forbidden" | "not-found";
|
|
56
|
+
export interface WorkflowImportError {
|
|
57
|
+
code: WorkflowImportErrorCode;
|
|
58
|
+
message: string;
|
|
59
|
+
issues?: GraphIssue[];
|
|
60
|
+
currentUpdatedAt?: string;
|
|
61
|
+
}
|
|
62
|
+
export type WorkflowPortType = "text" | "primer" | "image" | "rig" | "storyboard" | "frames" | "video" | "audio";
|
|
63
|
+
export type WorkflowPrimerKind = "body" | "hook" | "headline" | "summary";
|
|
64
|
+
export type WorkflowDurationSpec = {
|
|
65
|
+
kind: "fixed";
|
|
66
|
+
values: number[];
|
|
67
|
+
} | {
|
|
68
|
+
kind: "range";
|
|
69
|
+
min: number;
|
|
70
|
+
max: number;
|
|
71
|
+
};
|
|
72
|
+
export interface CatalogVideoModel {
|
|
73
|
+
id: string;
|
|
74
|
+
label: string;
|
|
75
|
+
durations: WorkflowDurationSpec;
|
|
76
|
+
audioTogglable: boolean;
|
|
77
|
+
}
|
|
78
|
+
export type WorkflowParamKind = "select" | "text" | "textarea" | "toggle" | "number" | "multiselect";
|
|
79
|
+
export interface CatalogInput {
|
|
80
|
+
id: string;
|
|
81
|
+
label: string;
|
|
82
|
+
accepts: WorkflowPortType[];
|
|
83
|
+
required: boolean;
|
|
84
|
+
multi?: boolean;
|
|
85
|
+
primerKinds?: WorkflowPrimerKind[];
|
|
86
|
+
}
|
|
87
|
+
export interface CatalogParam {
|
|
88
|
+
key: string;
|
|
89
|
+
label: string;
|
|
90
|
+
kind: WorkflowParamKind;
|
|
91
|
+
options?: string[];
|
|
92
|
+
min?: number;
|
|
93
|
+
max?: number;
|
|
94
|
+
required?: boolean;
|
|
95
|
+
help?: string;
|
|
96
|
+
default?: string;
|
|
97
|
+
}
|
|
98
|
+
export interface CatalogBot {
|
|
99
|
+
slug: string;
|
|
100
|
+
name: string;
|
|
101
|
+
blurb: string;
|
|
102
|
+
category: string;
|
|
103
|
+
categoryLabel: string;
|
|
104
|
+
inputs: CatalogInput[];
|
|
105
|
+
params: CatalogParam[];
|
|
106
|
+
outputType: string;
|
|
107
|
+
splitsOutput: boolean;
|
|
108
|
+
}
|
|
109
|
+
export interface WorkflowCatalog {
|
|
110
|
+
catalog: "exodus-workflow-catalog";
|
|
111
|
+
version: 1;
|
|
112
|
+
bots: CatalogBot[];
|
|
113
|
+
vocabulary: {
|
|
114
|
+
nodeKinds: string[];
|
|
115
|
+
briefSources: string[];
|
|
116
|
+
primerKinds: string[];
|
|
117
|
+
imageModels: string[];
|
|
118
|
+
aspectRatios: string[];
|
|
119
|
+
imageQuantityModes: string[];
|
|
120
|
+
videoModels: CatalogVideoModel[];
|
|
121
|
+
categories: Array<{
|
|
122
|
+
id: string;
|
|
123
|
+
label: string;
|
|
124
|
+
}>;
|
|
125
|
+
};
|
|
126
|
+
customBot: {
|
|
127
|
+
slug: "custom";
|
|
128
|
+
configKey: "customSlug";
|
|
129
|
+
inputs: CatalogInput[];
|
|
130
|
+
summaryOnlySlugs: string[];
|
|
131
|
+
notes: string;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export type WorkflowBriefSource = "text" | "swipe-ad" | "swipe-bundle" | "organic-url" | "ad-url";
|
|
135
|
+
export interface WorkflowInputDescriptor {
|
|
136
|
+
fieldName: string;
|
|
137
|
+
nodeId: string;
|
|
138
|
+
source: WorkflowBriefSource;
|
|
139
|
+
required: boolean;
|
|
140
|
+
description?: string;
|
|
141
|
+
bundleSize?: number;
|
|
142
|
+
}
|
|
143
|
+
export interface WorkflowPrerequisiteDescriptor {
|
|
144
|
+
primerKind: WorkflowPrimerKind;
|
|
145
|
+
nodeIds: string[];
|
|
146
|
+
}
|
|
147
|
+
export interface WorkflowOutputDescriptor {
|
|
148
|
+
type: "text" | "image" | "video" | "audio" | "frames" | "storyboard";
|
|
149
|
+
label: string;
|
|
150
|
+
nodeId: string;
|
|
151
|
+
botSlug?: string;
|
|
152
|
+
}
|
|
153
|
+
export interface WorkflowDescribeResponse {
|
|
154
|
+
workflowId: string;
|
|
155
|
+
name: string;
|
|
156
|
+
description?: string;
|
|
157
|
+
updatedAt: string;
|
|
158
|
+
inputs: WorkflowInputDescriptor[];
|
|
159
|
+
prerequisites: Array<WorkflowPrerequisiteDescriptor & {
|
|
160
|
+
stored: boolean;
|
|
161
|
+
}>;
|
|
162
|
+
outputs: WorkflowOutputDescriptor[];
|
|
163
|
+
}
|
|
164
|
+
export interface WorkflowListItem {
|
|
165
|
+
_id: string;
|
|
166
|
+
name: string;
|
|
167
|
+
description?: string;
|
|
168
|
+
nodeCount: number;
|
|
169
|
+
edgeCount: number;
|
|
170
|
+
createdAt: string;
|
|
171
|
+
updatedAt: string;
|
|
172
|
+
isCrossBrand?: boolean;
|
|
173
|
+
homeBrandName?: string | null;
|
|
174
|
+
}
|
|
175
|
+
export interface WorkflowListResponse {
|
|
176
|
+
workflows: WorkflowListItem[];
|
|
177
|
+
}
|
|
178
|
+
export type WorkflowRunStatus = "queued" | "running" | "awaiting-review" | "completed" | "partial" | "failed" | "canceled";
|
|
179
|
+
export type WorkflowNodeRunStatus = "idle" | "running" | "done" | "failed" | "skipped";
|
|
180
|
+
export type WorkflowArtifact = {
|
|
181
|
+
type: "text";
|
|
182
|
+
text: string;
|
|
183
|
+
label?: string;
|
|
184
|
+
} | {
|
|
185
|
+
type: "primer";
|
|
186
|
+
text: string;
|
|
187
|
+
primerKind: string;
|
|
188
|
+
} | {
|
|
189
|
+
type: "image";
|
|
190
|
+
storageId: string;
|
|
191
|
+
imageUrl?: string;
|
|
192
|
+
};
|
|
193
|
+
export interface WorkflowRunOutput {
|
|
194
|
+
nodeId: string;
|
|
195
|
+
botSlug?: string;
|
|
196
|
+
type: "text" | "image" | "video" | "audio" | "frames" | "storyboard";
|
|
197
|
+
label: string;
|
|
198
|
+
text?: string;
|
|
199
|
+
imageUrl?: string;
|
|
200
|
+
imageId?: string;
|
|
201
|
+
videoUrl?: string;
|
|
202
|
+
audioUrl?: string;
|
|
203
|
+
durationSec?: number;
|
|
204
|
+
sceneIndex?: number;
|
|
205
|
+
final?: boolean;
|
|
206
|
+
frames?: Array<{
|
|
207
|
+
sceneIndex: number;
|
|
208
|
+
imageUrl?: string;
|
|
209
|
+
}>;
|
|
210
|
+
storyboardJson?: string;
|
|
211
|
+
}
|
|
212
|
+
export interface WorkflowRunNode {
|
|
213
|
+
nodeId: string;
|
|
214
|
+
kind: string;
|
|
215
|
+
status: WorkflowNodeRunStatus;
|
|
216
|
+
error?: string;
|
|
217
|
+
startedAt?: number;
|
|
218
|
+
completedAt?: number;
|
|
219
|
+
outputs: WorkflowArtifact[];
|
|
220
|
+
}
|
|
221
|
+
export interface WorkflowCounts {
|
|
222
|
+
done: number;
|
|
223
|
+
failed: number;
|
|
224
|
+
skipped: number;
|
|
225
|
+
total: number;
|
|
226
|
+
}
|
|
227
|
+
export interface WorkflowRun {
|
|
228
|
+
_id: string;
|
|
229
|
+
workflowId: string;
|
|
230
|
+
workflowName: string;
|
|
231
|
+
status: WorkflowRunStatus;
|
|
232
|
+
error?: string;
|
|
233
|
+
counts?: WorkflowCounts;
|
|
234
|
+
inputs: Record<string, string>;
|
|
235
|
+
triggerRunId?: string;
|
|
236
|
+
createdAt: number;
|
|
237
|
+
startedAt?: number;
|
|
238
|
+
completedAt?: number;
|
|
239
|
+
isTerminal: boolean;
|
|
240
|
+
nodes: WorkflowRunNode[];
|
|
241
|
+
outputs?: WorkflowRunOutput[];
|
|
242
|
+
}
|
|
243
|
+
export type WorkflowRunProjection = Omit<WorkflowRun, "nodes"> & {
|
|
244
|
+
nodes?: never;
|
|
245
|
+
};
|
|
246
|
+
export interface WorkflowRunDeps {
|
|
247
|
+
get: (path: string) => Promise<ApiResponse<unknown>>;
|
|
248
|
+
post: (path: string, body: unknown) => Promise<ApiResponse<unknown>>;
|
|
249
|
+
readFile: (path: string) => string;
|
|
250
|
+
writeFile: (path: string, text: string) => void;
|
|
251
|
+
poll: (opts: PollOptions) => Promise<PollResult>;
|
|
252
|
+
}
|
|
253
|
+
export interface FlowResult {
|
|
254
|
+
code: number;
|
|
255
|
+
lines: string[];
|
|
256
|
+
}
|
|
257
|
+
interface RunFlowOptions {
|
|
258
|
+
inputs: Record<string, string>;
|
|
259
|
+
wait: boolean;
|
|
260
|
+
json: boolean;
|
|
261
|
+
onProgressLine?: (line: string) => void;
|
|
262
|
+
}
|
|
263
|
+
export declare function parseInputFlags(args: string[], readFile?: (path: string) => string): Record<string, string>;
|
|
264
|
+
export declare function formatWorkflowList(workflows: WorkflowListItem[]): string;
|
|
265
|
+
export declare function formatRecentRuns(runs: WorkflowRunProjection[]): string;
|
|
266
|
+
export declare function formatImportSummary(result: WorkflowImportResult, mode?: {
|
|
267
|
+
dryRun?: boolean;
|
|
268
|
+
update?: boolean;
|
|
269
|
+
}): string;
|
|
270
|
+
export declare function formatWorkflowRun(run: WorkflowRun): string;
|
|
271
|
+
export declare function formatDescribe(res: WorkflowDescribeResponse): string;
|
|
272
|
+
export declare function formatBotsList(catalog: WorkflowCatalog, category?: string): string;
|
|
273
|
+
export declare function formatBotDetail(bot: CatalogBot): string;
|
|
274
|
+
export declare function resolveWorkflowId(ref: string, deps: WorkflowRunDeps): Promise<string>;
|
|
275
|
+
export declare function listFlow(json: boolean, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
276
|
+
export declare function describeFlow(workflowRef: string, opts: {
|
|
277
|
+
json: boolean;
|
|
278
|
+
}, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
279
|
+
export declare function botsFlow(opts: {
|
|
280
|
+
category?: string;
|
|
281
|
+
slug?: string;
|
|
282
|
+
json: boolean;
|
|
283
|
+
}, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
284
|
+
export declare function runFlow(workflowRef: string, opts: RunFlowOptions, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
285
|
+
export declare function statusFlow(opts: {
|
|
286
|
+
id?: string;
|
|
287
|
+
json: boolean;
|
|
288
|
+
}, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
289
|
+
export declare function exportFlow(workflowRef: string, opts: {
|
|
290
|
+
out?: string;
|
|
291
|
+
}, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
292
|
+
export declare function importFlow(file: string, opts: {
|
|
293
|
+
dryRun: boolean;
|
|
294
|
+
json: boolean;
|
|
295
|
+
update?: string;
|
|
296
|
+
}, deps: WorkflowRunDeps): Promise<FlowResult>;
|
|
297
|
+
export declare function run(flags: Record<string, string | boolean>): Promise<void>;
|
|
298
|
+
export {};
|