@aicopycoders/exodus 2026.7.200-beta.8 → 2026.8.1100

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.
Files changed (40) hide show
  1. package/README.md +51 -0
  2. package/assets/docs/CLAUDE.md +3 -0
  3. package/assets/docs/PIPELINES.md +50 -0
  4. package/assets/skills/exodus/SKILL.md +6 -2
  5. package/assets/skills/exodus-meme/SKILL.md +44 -28
  6. package/assets/skills/exodus-winners/SKILL.md +239 -0
  7. package/assets/skills/exodus-workflow/SKILL.md +464 -63
  8. package/dist/bin/exodus.js +3 -0
  9. package/dist/commands/bank.d.ts +88 -0
  10. package/dist/commands/bank.js +346 -0
  11. package/dist/commands/browse.js +2 -1
  12. package/dist/commands/creative.js +2 -2
  13. package/dist/commands/genesis.js +16 -4
  14. package/dist/commands/keys.js +1 -0
  15. package/dist/commands/meme.js +9 -6
  16. package/dist/commands/primer.js +2 -1
  17. package/dist/commands/session.d.ts +55 -0
  18. package/dist/commands/session.js +202 -0
  19. package/dist/commands/status.js +4 -4
  20. package/dist/commands/swipe.js +4 -4
  21. package/dist/commands/template.js +2 -2
  22. package/dist/commands/update.d.ts +2 -0
  23. package/dist/commands/update.js +20 -0
  24. package/dist/commands/winners.js +13 -3
  25. package/dist/commands/workflow.d.ts +293 -8
  26. package/dist/commands/workflow.js +2396 -90
  27. package/dist/lib/client.d.ts +8 -1
  28. package/dist/lib/client.js +30 -6
  29. package/dist/lib/format.d.ts +7 -0
  30. package/dist/lib/format.js +52 -5
  31. package/dist/lib/intel-format.js +2 -1
  32. package/dist/lib/keys-sync.js +1 -0
  33. package/dist/lib/poll.js +7 -4
  34. package/dist/lib/route-support.d.ts +4 -0
  35. package/dist/lib/route-support.js +22 -0
  36. package/dist/lib/runStatus.d.ts +22 -0
  37. package/dist/lib/runStatus.js +139 -0
  38. package/dist/lib/workflowText.d.ts +3 -0
  39. package/dist/lib/workflowText.js +113 -0
  40. package/package.json +5 -2
@@ -16,9 +16,12 @@ const OTHER_COMMANDS = new Set([
16
16
  "drive",
17
17
  "doctor",
18
18
  "init",
19
+ "update",
19
20
  "migrate",
20
21
  "segment",
21
22
  "workflow",
23
+ "session",
24
+ "bank",
22
25
  ]);
23
26
  const HIDDEN_COMMANDS = new Set([
24
27
  "creative",
@@ -0,0 +1,88 @@
1
+ import { type ApiResponse } from "../lib/client.js";
2
+ import { type Channel } from "../lib/channel.js";
3
+ export declare const helpText: string;
4
+ export declare const AWARENESS_PAIRS: readonly ["unawareProblemAware", "solutionProductAware", "mostAware"];
5
+ export type AwarenessPair = (typeof AWARENESS_PAIRS)[number];
6
+ export interface BankSummary {
7
+ key: string;
8
+ name: string;
9
+ type: string;
10
+ awarenessPair?: AwarenessPair;
11
+ entryCount: number;
12
+ }
13
+ export interface BankWinMetrics {
14
+ spend?: number;
15
+ roas?: number;
16
+ ctr?: number;
17
+ note?: string;
18
+ }
19
+ export interface BankProvenance {
20
+ workflowRunId?: string;
21
+ genesisRunId?: string;
22
+ nodeId?: string;
23
+ variantIndex?: number;
24
+ artifactKind?: string;
25
+ }
26
+ export interface BankEntry {
27
+ _id: string;
28
+ text: string;
29
+ source: string;
30
+ awarenessPair?: AwarenessPair;
31
+ winMetrics?: BankWinMetrics;
32
+ humanEdited?: boolean;
33
+ provenance?: BankProvenance;
34
+ createdAt: number | string;
35
+ }
36
+ export interface BankShowResponse {
37
+ bank: {
38
+ key: string;
39
+ name: string;
40
+ type: string;
41
+ awarenessPair?: AwarenessPair;
42
+ };
43
+ entries: BankEntry[];
44
+ }
45
+ export interface BankPromoteBody {
46
+ bankKey: string;
47
+ text: string;
48
+ awarenessPair?: AwarenessPair;
49
+ winMetrics?: BankWinMetrics;
50
+ provenance?: BankProvenance;
51
+ }
52
+ export interface BankPromoteResponse {
53
+ entryId: string;
54
+ bankName: string;
55
+ event?: string;
56
+ }
57
+ export interface FlowResult {
58
+ code: number;
59
+ lines: string[];
60
+ }
61
+ export interface BankDeps {
62
+ get: (path: string) => Promise<ApiResponse<unknown>>;
63
+ post: (path: string, body: unknown) => Promise<ApiResponse<unknown>>;
64
+ readFile: (path: string) => string;
65
+ readStdin: () => string;
66
+ stdinIsTTY: () => boolean;
67
+ channel: Channel;
68
+ }
69
+ export declare function relativeAge(value: number | string, now?: number): string;
70
+ export declare function formatBankList(banks: BankSummary[]): string;
71
+ export declare function formatBankShow(res: BankShowResponse): string;
72
+ interface PromoteTextSources {
73
+ arg?: string;
74
+ file?: string;
75
+ readFile: (path: string) => string;
76
+ readStdin: () => string;
77
+ stdinIsTTY: () => boolean;
78
+ }
79
+ export declare function resolvePromoteText(opts: PromoteTextSources): string;
80
+ export declare function parseWinMetrics(flags: Record<string, string | boolean>): BankWinMetrics | undefined;
81
+ export declare function parseProvenance(flags: Record<string, string | boolean>): BankProvenance | undefined;
82
+ export declare function parseAwareness(flags: Record<string, string | boolean>): AwarenessPair | undefined;
83
+ export declare function buildPromoteBody(bankKey: string, text: string, flags: Record<string, string | boolean>): BankPromoteBody;
84
+ export declare function listFlow(json: boolean, deps: BankDeps): Promise<FlowResult>;
85
+ export declare function showFlow(key: string, json: boolean, deps: BankDeps): Promise<FlowResult>;
86
+ export declare function promoteFlow(key: string, arg: string | undefined, flags: Record<string, string | boolean>, json: boolean, deps: BankDeps): Promise<FlowResult>;
87
+ export declare function run(flags: Record<string, string | boolean>): Promise<void>;
88
+ export {};
@@ -0,0 +1,346 @@
1
+ import fs from "node:fs";
2
+ import { apiGet, apiPost } from "../lib/client.js";
3
+ import { formatApiError } from "../lib/format.js";
4
+ import { getChannel } from "../lib/channel.js";
5
+ import { missingRouteLine } from "../lib/route-support.js";
6
+ export const helpText = `
7
+ exodus bank — List, inspect, and promote entries into this brand's copy banks
8
+
9
+ Usage:
10
+ exodus bank list [--json]
11
+ exodus bank show <key> [--json]
12
+ exodus bank promote <key> [text] [--file <path>] [options] [--json]
13
+
14
+ Flags:
15
+ --json Machine-readable JSON output (includes ids + provenance)
16
+ --file <path> (promote) Read the entry text from a file instead of an
17
+ argument; piping text on stdin is a third option. A text
18
+ argument and --file conflict (provide one, not both);
19
+ stdin is read only when neither is given.
20
+ --awareness <pair> (promote) Tag a body-bank entry with an awareness pair:
21
+ unawareProblemAware | solutionProductAware | mostAware
22
+ --spend <n> (promote) Win metric: ad spend (number)
23
+ --roas <n> (promote) Win metric: return on ad spend (number)
24
+ --ctr <n> (promote) Win metric: click-through rate (number)
25
+ --note <s> (promote) Win metric: freeform note
26
+ --run <runId> (promote) Provenance: the run this copy came from
27
+ --node <nodeId> (promote) Provenance: the node this copy came from
28
+
29
+ Banks are addressed by their well-known KEY (shown by \`bank list\`), never by an
30
+ internal id. A fresh brand starts with six banks, all empty. \`bank show\` numbers
31
+ entries newest-first with their source, win metrics, and age. \`bank promote\`
32
+ appends a winning line to a bank; the server rejects an unknown key (listing the
33
+ valid ones), empty/over-long text, and a bank that has hit its 2000-entry cap —
34
+ those messages print verbatim.
35
+
36
+ Examples:
37
+ exodus bank list
38
+ exodus bank show hooks
39
+ exodus bank promote hooks "Stop scrolling — your knees will thank you"
40
+ exodus bank promote body:solutionProductAware --file winner.txt --awareness solutionProductAware
41
+ cat winner.txt | exodus bank promote hooks
42
+ exodus bank promote hooks "..." --spend 4200 --roas 3.1 --ctr 1.8 --note "Q3 winner"
43
+ exodus bank promote hooks "..." --run wr_123 --node bot-4
44
+ `.trim();
45
+ export const AWARENESS_PAIRS = [
46
+ "unawareProblemAware",
47
+ "solutionProductAware",
48
+ "mostAware",
49
+ ];
50
+ const LIST_PATH = "/api/v2/banks";
51
+ const SHOW_PATH = "/api/v2/banks/show";
52
+ const PROMOTE_PATH = "/api/v2/banks/promote";
53
+ const FLYWHEEL_LINE = 'Winner Flywheel: "winner-promoted" dispatched — any workflow with an enabled ' +
54
+ "trigger starts a background run (watch: exodus workflow inbox)";
55
+ const VALUE_FLAGS = new Set(["file", "awareness", "spend", "roas", "ctr", "note", "run", "node"]);
56
+ const defaultDeps = {
57
+ get: (path) => apiGet(path),
58
+ post: (path, body) => apiPost(path, body),
59
+ readFile: (path) => fs.readFileSync(path, "utf-8"),
60
+ readStdin: () => fs.readFileSync(0, "utf-8"),
61
+ stdinIsTTY: () => !!process.stdin.isTTY,
62
+ channel: getChannel(),
63
+ };
64
+ function asErrorResult(res, json) {
65
+ return {
66
+ code: 1,
67
+ lines: json
68
+ ? [JSON.stringify({ ok: false, status: res.status, data: res.data })]
69
+ : [formatApiError(res)],
70
+ };
71
+ }
72
+ function table(headers, rows) {
73
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)));
74
+ const fmt = (row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
75
+ return [fmt(headers), fmt(headers.map((h) => "-".repeat(h.length))), ...rows.map(fmt)].join("\n");
76
+ }
77
+ export function relativeAge(value, now = Date.now()) {
78
+ const t = typeof value === "number" ? value : Date.parse(value);
79
+ if (!t || Number.isNaN(t))
80
+ return "unknown";
81
+ const diff = now - t;
82
+ if (diff < 0)
83
+ return "just now";
84
+ const sec = Math.floor(diff / 1000);
85
+ if (sec < 45)
86
+ return "just now";
87
+ const min = Math.floor(sec / 60);
88
+ if (min < 60)
89
+ return `${min}m ago`;
90
+ const hr = Math.floor(min / 60);
91
+ if (hr < 24)
92
+ return `${hr}h ago`;
93
+ const day = Math.floor(hr / 24);
94
+ if (day < 7)
95
+ return `${day}d ago`;
96
+ const wk = Math.floor(day / 7);
97
+ if (wk < 5)
98
+ return `${wk}w ago`;
99
+ const mo = Math.floor(day / 30);
100
+ if (mo < 12)
101
+ return `${mo}mo ago`;
102
+ return `${Math.floor(day / 365)}y ago`;
103
+ }
104
+ function winMetricsLine(wm) {
105
+ if (!wm)
106
+ return null;
107
+ const parts = [];
108
+ if (typeof wm.spend === "number")
109
+ parts.push(`spend=${wm.spend}`);
110
+ if (typeof wm.roas === "number")
111
+ parts.push(`roas=${wm.roas}`);
112
+ if (typeof wm.ctr === "number")
113
+ parts.push(`ctr=${wm.ctr}`);
114
+ if (wm.note)
115
+ parts.push(`note="${wm.note}"`);
116
+ return parts.length > 0 ? ` win: ${parts.join(" ")}` : null;
117
+ }
118
+ function textBlock(text) {
119
+ const lines = text.split("\n");
120
+ return lines.map((l) => ` ${l}`);
121
+ }
122
+ export function formatBankList(banks) {
123
+ if (banks.length === 0)
124
+ return "No banks found for the active brand.";
125
+ return table(["key", "name", "type", "entries"], banks.map((b) => [b.key, b.name, b.type, String(b.entryCount)]));
126
+ }
127
+ export function formatBankShow(res) {
128
+ const lines = [];
129
+ const { bank, entries } = res;
130
+ lines.push(`Bank — ${bank.name}`);
131
+ lines.push(`key: ${bank.key}`);
132
+ lines.push(`type: ${bank.type}`);
133
+ if (bank.awarenessPair)
134
+ lines.push(`awareness: ${bank.awarenessPair}`);
135
+ lines.push(`entries: ${entries.length}`);
136
+ if (entries.length === 0) {
137
+ lines.push("");
138
+ lines.push(" (no entries yet — promote a winner with `exodus bank promote`)");
139
+ return lines.join("\n");
140
+ }
141
+ entries.forEach((entry, i) => {
142
+ lines.push("");
143
+ const badges = [`[${entry.source}]`];
144
+ if (entry.awarenessPair)
145
+ badges.push(entry.awarenessPair);
146
+ if (entry.humanEdited)
147
+ badges.push("edited");
148
+ badges.push(relativeAge(entry.createdAt));
149
+ lines.push(`${i + 1}. ${badges.join(" · ")}`);
150
+ lines.push(...textBlock(entry.text));
151
+ const win = winMetricsLine(entry.winMetrics);
152
+ if (win)
153
+ lines.push(win);
154
+ });
155
+ return lines.join("\n");
156
+ }
157
+ export function resolvePromoteText(opts) {
158
+ if (opts.arg !== undefined && opts.file !== undefined) {
159
+ throw new Error("provide exactly ONE entry text source, got 2: text argument + --file.");
160
+ }
161
+ if (opts.arg !== undefined)
162
+ return opts.arg;
163
+ if (opts.file !== undefined) {
164
+ try {
165
+ return opts.readFile(opts.file);
166
+ }
167
+ catch (e) {
168
+ const msg = e instanceof Error ? e.message : String(e);
169
+ throw new Error(`could not read --file "${opts.file}": ${msg}`);
170
+ }
171
+ }
172
+ if (!opts.stdinIsTTY()) {
173
+ const text = opts.readStdin();
174
+ if (text.trim() !== "")
175
+ return text;
176
+ }
177
+ throw new Error("no entry text — pass it as an argument, with --file <path>, or pipe it on stdin.");
178
+ }
179
+ export function parseWinMetrics(flags) {
180
+ const wm = {};
181
+ const num = (name) => {
182
+ const raw = flags[name];
183
+ if (raw === undefined)
184
+ return undefined;
185
+ if (typeof raw !== "string" || raw.trim() === "") {
186
+ throw new Error(`--${name} must be a number (got "${String(raw)}")`);
187
+ }
188
+ const n = Number(raw);
189
+ if (Number.isNaN(n))
190
+ throw new Error(`--${name} must be a number (got "${raw}")`);
191
+ return n;
192
+ };
193
+ const spend = num("spend");
194
+ const roas = num("roas");
195
+ const ctr = num("ctr");
196
+ if (spend !== undefined)
197
+ wm.spend = spend;
198
+ if (roas !== undefined)
199
+ wm.roas = roas;
200
+ if (ctr !== undefined)
201
+ wm.ctr = ctr;
202
+ if (typeof flags["note"] === "string")
203
+ wm.note = flags["note"];
204
+ return Object.keys(wm).length > 0 ? wm : undefined;
205
+ }
206
+ export function parseProvenance(flags) {
207
+ const prov = {};
208
+ if (typeof flags["run"] === "string")
209
+ prov.workflowRunId = flags["run"];
210
+ if (typeof flags["node"] === "string")
211
+ prov.nodeId = flags["node"];
212
+ return Object.keys(prov).length > 0 ? prov : undefined;
213
+ }
214
+ export function parseAwareness(flags) {
215
+ const raw = flags["awareness"];
216
+ if (raw === undefined)
217
+ return undefined;
218
+ if (typeof raw !== "string" || !AWARENESS_PAIRS.includes(raw)) {
219
+ throw new Error(`--awareness must be one of: ${AWARENESS_PAIRS.join(", ")} (got "${String(raw)}").`);
220
+ }
221
+ return raw;
222
+ }
223
+ export function buildPromoteBody(bankKey, text, flags) {
224
+ const body = { bankKey, text };
225
+ const awarenessPair = parseAwareness(flags);
226
+ if (awarenessPair)
227
+ body.awarenessPair = awarenessPair;
228
+ const winMetrics = parseWinMetrics(flags);
229
+ if (winMetrics)
230
+ body.winMetrics = winMetrics;
231
+ const provenance = parseProvenance(flags);
232
+ if (provenance)
233
+ body.provenance = provenance;
234
+ return body;
235
+ }
236
+ export async function listFlow(json, deps) {
237
+ const res = await deps.get(LIST_PATH);
238
+ const unsupported = missingRouteLine(res, "bank list", deps.channel);
239
+ if (unsupported)
240
+ return { code: 1, lines: [unsupported] };
241
+ if (!res.ok)
242
+ return asErrorResult(res, json);
243
+ const banks = res.data ?? [];
244
+ return { code: 0, lines: json ? [JSON.stringify(res.data)] : [formatBankList(banks)] };
245
+ }
246
+ export async function showFlow(key, json, deps) {
247
+ const res = await deps.get(`${SHOW_PATH}?key=${encodeURIComponent(key)}`);
248
+ const unsupported = missingRouteLine(res, "bank show", deps.channel);
249
+ if (unsupported)
250
+ return { code: 1, lines: [unsupported] };
251
+ if (!res.ok)
252
+ return asErrorResult(res, json);
253
+ return {
254
+ code: 0,
255
+ lines: json ? [JSON.stringify(res.data)] : [formatBankShow(res.data)],
256
+ };
257
+ }
258
+ export async function promoteFlow(key, arg, flags, json, deps) {
259
+ let body;
260
+ try {
261
+ const text = resolvePromoteText({
262
+ arg,
263
+ file: typeof flags["file"] === "string" ? flags["file"] : undefined,
264
+ readFile: deps.readFile,
265
+ readStdin: deps.readStdin,
266
+ stdinIsTTY: deps.stdinIsTTY,
267
+ });
268
+ body = buildPromoteBody(key, text, flags);
269
+ }
270
+ catch (e) {
271
+ return { code: 1, lines: [`Error: ${e instanceof Error ? e.message : String(e)}`] };
272
+ }
273
+ const res = await deps.post(PROMOTE_PATH, body);
274
+ const unsupported = missingRouteLine(res, "bank promote", deps.channel);
275
+ if (unsupported)
276
+ return { code: 1, lines: [unsupported] };
277
+ if (!res.ok)
278
+ return asErrorResult(res, json);
279
+ const data = res.data;
280
+ if (json)
281
+ return { code: 0, lines: [JSON.stringify(res.data)] };
282
+ return {
283
+ code: 0,
284
+ lines: [
285
+ `✓ Promoted to ${data.bankName}.`,
286
+ `entryId: ${data.entryId}`,
287
+ FLYWHEEL_LINE,
288
+ ],
289
+ };
290
+ }
291
+ function parsePositional(args = process.argv.slice(3)) {
292
+ const out = [];
293
+ let i = 0;
294
+ while (i < args.length) {
295
+ const arg = args[i];
296
+ if (arg.startsWith("--")) {
297
+ const key = arg.slice(2).split("=", 1)[0] ?? "";
298
+ if (!arg.includes("=") && VALUE_FLAGS.has(key))
299
+ i += 2;
300
+ else
301
+ i++;
302
+ continue;
303
+ }
304
+ out.push(arg);
305
+ i++;
306
+ }
307
+ return out;
308
+ }
309
+ async function printResult(result) {
310
+ for (const line of result.lines)
311
+ console.log(line);
312
+ if (result.code !== 0)
313
+ process.exit(result.code);
314
+ }
315
+ export async function run(flags) {
316
+ const positional = parsePositional();
317
+ const [sub, ...rest] = positional;
318
+ const json = !!flags["json"];
319
+ if (!sub) {
320
+ console.log(helpText);
321
+ return;
322
+ }
323
+ if (sub === "list")
324
+ return printResult(await listFlow(json, defaultDeps));
325
+ if (sub === "show") {
326
+ const key = rest[0];
327
+ if (!key) {
328
+ console.error("Error: bank show requires <key>.");
329
+ console.log("Usage: exodus bank show <key> [--json]");
330
+ process.exit(1);
331
+ }
332
+ return printResult(await showFlow(key, json, defaultDeps));
333
+ }
334
+ if (sub === "promote") {
335
+ const key = rest[0];
336
+ if (!key) {
337
+ console.error("Error: bank promote requires <key>.");
338
+ console.log("Usage: exodus bank promote <key> [text] [--file <path>] [--json]");
339
+ process.exit(1);
340
+ }
341
+ return printResult(await promoteFlow(key, rest[1], flags, json, defaultDeps));
342
+ }
343
+ console.error(`Unknown subcommand: "${sub}"\n`);
344
+ console.log(helpText);
345
+ process.exit(1);
346
+ }
@@ -1,5 +1,6 @@
1
1
  import { apiGet } from "../lib/client.js";
2
2
  import { formatBrowse, formatError } from "../lib/format.js";
3
+ import { isActiveRunStatus } from "../lib/runStatus.js";
3
4
  export const helpText = `
4
5
  exodus browse — List recent pipeline runs (hooks, ads, image concepts, etc.)
5
6
 
@@ -119,7 +120,7 @@ export async function run(flags) {
119
120
  ? all
120
121
  : all.filter((g) => {
121
122
  const status = g["status"];
122
- if (status !== "running" && status !== "pending")
123
+ if (!status || !isActiveRunStatus(status))
123
124
  return true;
124
125
  const created = normalizeCreatedAt(g);
125
126
  return created > 0 && now - created < STUCK_THRESHOLD_MS;
@@ -1,5 +1,5 @@
1
1
  import { apiGet, apiGetDashboard, apiPost, getDashboardUrl, } from "../lib/client.js";
2
- import { formatError } from "../lib/format.js";
2
+ import { displayRunStatus, formatError } from "../lib/format.js";
3
3
  export const helpText = `
4
4
  exodus creative — Creative-suite engines (native, copy-derived, ref-match)
5
5
 
@@ -165,7 +165,7 @@ async function runStatus(flags) {
165
165
  console.log(`name: ${d.name}`);
166
166
  if (d.engine)
167
167
  console.log(`engine: ${d.engine}`);
168
- console.log(`status: ${d.status ?? "—"}${d.isTerminal ? " (terminal)" : ""}`);
168
+ console.log(`status: ${displayRunStatus(d.status, "—")}${d.isTerminal ? " (terminal)" : ""}`);
169
169
  if (d.completedImages !== undefined || d.totalImages !== undefined) {
170
170
  console.log(`progress: ${d.completedImages ?? 0} / ${d.totalImages ?? "?"} completed${d.failedImages ? `, ${d.failedImages} failed` : ""}`);
171
171
  }
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { apiGet, apiPost, getDashboardUrl } from "../lib/client.js";
5
5
  import { pollUntilDone } from "../lib/poll.js";
6
- import { formatGenesisRun, formatError } from "../lib/format.js";
6
+ import { displayRunStatus, formatGenesisRun, formatError, tickerRunStatus } from "../lib/format.js";
7
7
  import { formatCcCommand } from "../lib/cc-command.js";
8
8
  import { resolveActiveBrand } from "../lib/layout.js";
9
9
  import { captureReelAndWrite } from "../lib/reel-write.js";
@@ -16,9 +16,21 @@ Google Doc). Write from a typed brief.
16
16
 
17
17
  Usage:
18
18
  exodus genesis run --brief <file|"text"> Write from a typed brief
19
+ exodus genesis hook-pref [manual|auto] Read (no arg) or save your default
20
+ hook-gate choice
21
+ exodus genesis hooks --id <runId> Show the hook pool of a run paused
22
+ at the hook gate
23
+ exodus genesis continue --id <runId> --hooks 1,3,5 Pick hooks and resume
24
+ exodus genesis regenerate --id <runId> [--steering "…"] Re-roll the hook pool
25
+
26
+ Every run must know whether you review hooks: pass --stop-at-hooks or
27
+ --auto-hooks, or save a default once with hook-pref. A flagless run with no
28
+ saved preference is refused.
19
29
 
20
30
  Options:
21
31
  --brief <file|"text"> Typed brief (file path or inline string)
32
+ --stop-at-hooks Pause at the hook gate to review/pick hooks
33
+ --auto-hooks Auto-pick hooks and write straight through
22
34
  --seeds <file|"text"> Per-run creative seeds. File: one per line OR a
23
35
  JSON array. Inline: a single seed.
24
36
  --steering "<text>" With regenerate: re-roll guidance for the hook pool.
@@ -441,7 +453,7 @@ async function waitForGenesisRun(runId) {
441
453
  const status = data["status"];
442
454
  const currentStep = data["currentStep"];
443
455
  if (status) {
444
- process.stdout.write(`\r ${status}${currentStep ? ` — ${currentStep}` : ""} `);
456
+ process.stdout.write(`\r ${tickerRunStatus(status)}${currentStep ? ` — ${currentStep}` : ""} `);
445
457
  }
446
458
  },
447
459
  });
@@ -515,7 +527,7 @@ async function runHooks(flags) {
515
527
  process.exit(1);
516
528
  }
517
529
  if (res.data.status !== "awaiting_hook_selection") {
518
- console.log(`Run ${runId} is not awaiting hook selection (status: ${res.data.status ?? "unknown"}).`);
530
+ console.log(`Run ${runId} is not awaiting hook selection (status: ${displayRunStatus(res.data.status)}).`);
519
531
  return;
520
532
  }
521
533
  const pool = Array.isArray(res.data.hookPool) ? res.data.hookPool : [];
@@ -677,7 +689,7 @@ async function pollScrape(runId, noWait, timeoutMs) {
677
689
  ? ` captured=${captured ?? 0} qualified=${qualified ?? 0}`
678
690
  : "";
679
691
  if (status)
680
- process.stdout.write(`\r status: ${status}${counts} `);
692
+ process.stdout.write(`\r status: ${tickerRunStatus(status)}${counts} `);
681
693
  },
682
694
  });
683
695
  console.log();
@@ -26,6 +26,7 @@ const PROVIDER_LABEL = {
26
26
  kie: "Kie.ai",
27
27
  imgflip: "Imgflip",
28
28
  scrapecreators: "ScrapeCreators",
29
+ apify: "Apify",
29
30
  };
30
31
  async function status() {
31
32
  let remote;
@@ -19,11 +19,14 @@ Flow:
19
19
  2. run → pass the picked recommendation objects straight into --formats
20
20
  (a JSON array; both recommendation-shape and run-shape entries
21
21
  are accepted, 1–50). Returns { runId } — the whole batch renders
22
- server-side via Trigger.dev; closing your session won't kill it.
23
- 3. poll → exodus status --id <runId> --type creative
24
- (terminal: complete | partial-error | error)
22
+ server-side; closing your session won't kill it.
23
+ 3. poll → exodus workflow status --id <runId>
24
+ Memes render through the Image Rig now, so a meme run IS a
25
+ workflow run — the workflow verb is the one that can read it.
25
26
  4. regenerate → re-render a single miss using the format fields from the
26
27
  recommend output. Synchronous; returns the new image URL.
28
+ Its optional --id takes a CREATIVE-SUITE run id, which the id
29
+ from step 2 is not — omit --id and use the returned URL.
27
30
 
28
31
  Keys (strict BYOK — checked server-side before anything starts):
29
32
  classic (layer 1) memes need your Imgflip login; AI (layer 2/3) memes need
@@ -32,10 +35,10 @@ Keys (strict BYOK — checked server-side before anything starts):
32
35
  Examples:
33
36
  exodus meme recommend --brief "grounding sheets reduce inflammation"
34
37
  exodus meme run --brief "grounding sheets reduce inflammation" --formats '[{"layer":2,"name":"Group Chat","format_id":"group-chat"}]'
35
- exodus meme regenerate --brief "grounding sheets reduce inflammation" --layer 2 --format group-chat --hint "make the last message land the product" --id <runId>
38
+ exodus meme regenerate --brief "grounding sheets reduce inflammation" --layer 2 --format group-chat --hint "make the last message land the product"
36
39
  `.trim();
37
40
  function dashboardUrlForRun(runId) {
38
- return `${getDashboardUrl()}/creative-suite/runs/${runId}`;
41
+ return `${getDashboardUrl()}/runs/${runId}`;
39
42
  }
40
43
  function flagString(flags, name) {
41
44
  const v = flags[name];
@@ -185,7 +188,7 @@ async function runRun(flags) {
185
188
  console.log(` memes: ${formats.length} queued`);
186
189
  console.log(` dashboard: ${dashboardUrlForRun(res.data.runId)}`);
187
190
  console.log("");
188
- console.log(`Poll: exodus status --id ${res.data.runId} --type creative`);
191
+ console.log(`Poll: exodus workflow status --id ${res.data.runId}`);
189
192
  console.log("(The batch renders server-side — closing this session won't kill it.)");
190
193
  }
191
194
  async function runRegenerate(flags) {
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { apiGetDashboard, apiPostDashboard } from "../lib/client.js";
4
4
  import { promptChoice, promptMultiline, openInEditor } from "../lib/prompts.js";
5
+ import { normalizeRunStatus } from "../lib/runStatus.js";
5
6
  import { refreshBrandProfileMd } from "./brand.js";
6
7
  export const helpText = `
7
8
  exodus primer — Build a brand's primer (the single foundation step)
@@ -152,7 +153,7 @@ async function buildPrimer(submission) {
152
153
  process.stdout.write("·");
153
154
  continue;
154
155
  }
155
- if (data.status === "failed") {
156
+ if (normalizeRunStatus(data.status) === "failed") {
156
157
  console.log("");
157
158
  fail(`primer build failed: ${data.error ?? "unknown error"}`);
158
159
  }
@@ -0,0 +1,55 @@
1
+ import { type ApiResponse } from "../lib/client.js";
2
+ import { type Channel } from "../lib/channel.js";
3
+ export declare const helpText: string;
4
+ export interface SessionSummary {
5
+ _id: string;
6
+ title: string;
7
+ botSlug: string;
8
+ runId?: string;
9
+ nodeId?: string;
10
+ archived: boolean;
11
+ lastTouchedAt: number | string;
12
+ createdAt: number | string;
13
+ }
14
+ export interface SessionListResponse {
15
+ sessions: SessionSummary[];
16
+ }
17
+ export type SessionMessageRole = "system" | "user" | "assistant";
18
+ export interface SessionMessage {
19
+ role: SessionMessageRole;
20
+ body: string;
21
+ createdAt: number | string;
22
+ }
23
+ export interface SessionShowResponse {
24
+ session: SessionSummary & {
25
+ promptText?: string;
26
+ model?: string;
27
+ };
28
+ messages: SessionMessage[];
29
+ }
30
+ export interface SessionChatResponse {
31
+ reply: string;
32
+ }
33
+ export interface SessionDeps {
34
+ get: (path: string) => Promise<ApiResponse<unknown>>;
35
+ postDashboard: (path: string, body: unknown, opts?: {
36
+ timeoutMs?: number;
37
+ }) => Promise<ApiResponse<unknown>>;
38
+ channel: Channel;
39
+ }
40
+ export interface FlowResult {
41
+ code: number;
42
+ lines: string[];
43
+ }
44
+ export declare function shortId(id: string): string;
45
+ export declare function formatAge(value: number | string | undefined, now?: number): string;
46
+ export declare function formatSessionList(sessions: SessionSummary[], now?: number): string;
47
+ export declare function formatSessionShow(data: SessionShowResponse): string;
48
+ export declare function listFlow(json: boolean, deps: SessionDeps): Promise<FlowResult>;
49
+ export declare function showFlow(sessionId: string, opts: {
50
+ json: boolean;
51
+ }, deps: SessionDeps): Promise<FlowResult>;
52
+ export declare function chatFlow(sessionId: string, message: string, opts: {
53
+ json: boolean;
54
+ }, deps: SessionDeps): Promise<FlowResult>;
55
+ export declare function run(flags: Record<string, string | boolean>): Promise<void>;