@aicopycoders/exodus 2026.9.2000-beta.3 → 2026.9.2000-beta.4

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.
@@ -152,7 +152,8 @@ run options:
152
152
  --mode auto|manual|hybrid Generation mode (default: auto)
153
153
  --render-mode images|prompts prompts = skip render, return LLM prompts only (default: images)
154
154
  --aspect 1:1|9:16 Image aspect ratio (default: 1:1)
155
- --model gpt-image-2|nano-banana-pro Kie.ai model (default: gpt-image-2)
155
+ --model gpt-image-2.5|gpt-image-2.5-sunburst|gpt-image-2|nano-banana-pro
156
+ Kie.ai model (default: gpt-image-2.5)
156
157
  --realism off|realistic Realism enforcement (default: off)
157
158
  --quantities "slug:N,slug:N" manual-mode per-type counts (e.g. "testimonial:3,hero:2")
158
159
  --pairings '<JSON>' hybrid-mode host/modifier pairs: '[{"host":"testimonial","modifier":"hero"}]'
@@ -244,6 +244,7 @@ internals into chat.
244
244
  - The rig's child image runs are still `creativeSuiteRuns` with engine `meme`,
245
245
  so finished memes appear in the creative-suite library alongside other engines
246
246
  and `exodus browse --agent meme` still covers them.
247
- - `run` accepts only the two models the rig renders (`gpt-image-2`,
248
- `nano-banana-pro`, or their Kie slugs); anything else is a 400 listing the
249
- accepted values. It used to accept any value and silently ignore it.
247
+ - `run` accepts only the models the rig renders (`gpt-image-2.5`, the house
248
+ default, plus `gpt-image-2.5-sunburst`, `gpt-image-2` and `nano-banana-pro`,
249
+ or their Kie slugs); anything else is a 400 listing the accepted values. It
250
+ used to accept any value and silently ignore it.
@@ -17,7 +17,8 @@ run options:
17
17
  --mode auto|manual|hybrid Generation mode (default: auto)
18
18
  --render-mode images|prompts prompts = skip render, return LLM prompts only (default: images)
19
19
  --aspect 1:1|9:16 Image aspect ratio (default: 1:1)
20
- --model gpt-image-2|nano-banana-pro Kie.ai model (default: gpt-image-2)
20
+ --model gpt-image-2.5|gpt-image-2.5-sunburst|gpt-image-2|nano-banana-pro
21
+ Kie.ai model (default: gpt-image-2.5)
21
22
  --realism off|realistic Realism enforcement (default: off)
22
23
  --quantities "slug:N,slug:N" manual-mode per-type counts (e.g. "testimonial:3,hero:2")
23
24
  --pairings '<JSON>' hybrid-mode host/modifier pairs: '[{"host":"testimonial","modifier":"hero"}]'
@@ -184,7 +184,7 @@ async function runTemplate(text, flags, who) {
184
184
  renderMode: "images",
185
185
  inputAds: text,
186
186
  aspectRatio: aspect,
187
- model: "gpt-image-2",
187
+ model: "gpt-image-2.5",
188
188
  realismMode: realisticFlag ? "realistic" : "off",
189
189
  ...(steering ? { steering } : {}),
190
190
  ...(requestedImageCount !== undefined ? { requestedImageCount } : {}),
@@ -17,7 +17,8 @@ Run options:
17
17
  --mode auto|manual Generation mode (default: auto)
18
18
  --render-mode images|prompts Skip render and return prompts only (default: images)
19
19
  --aspect 1:1|9:16 Image aspect ratio (default: 1:1)
20
- --model gpt-image-2|nano-banana-pro Kie.ai model (default: gpt-image-2)
20
+ --model gpt-image-2.5|gpt-image-2.5-sunburst|gpt-image-2|nano-banana-pro
21
+ Kie.ai model (default: gpt-image-2.5)
21
22
  --realism off|realistic Realism enforcement (default: off)
22
23
  --quantities <slug:N,slug:N> Manual-mode per-type counts (e.g. "testimonial:3,hero:2")
23
24
  --requested-count N Auto-mode total render target (optional)
@@ -103,9 +104,15 @@ async function runTemplate(flags) {
103
104
  process.exit(1);
104
105
  return;
105
106
  }
106
- const model = typeof flags["model"] === "string" ? flags["model"] : "gpt-image-2";
107
- if (model !== "gpt-image-2" && model !== "nano-banana-pro") {
108
- console.error(`Error: --model must be gpt-image-2 | nano-banana-pro (got "${model}").`);
107
+ const TEMPLATE_MODELS = [
108
+ "gpt-image-2.5",
109
+ "gpt-image-2.5-sunburst",
110
+ "gpt-image-2",
111
+ "nano-banana-pro",
112
+ ];
113
+ const model = typeof flags["model"] === "string" ? flags["model"] : "gpt-image-2.5";
114
+ if (!TEMPLATE_MODELS.includes(model)) {
115
+ console.error(`Error: --model must be one of ${TEMPLATE_MODELS.join(" | ")} (got "${model}").`);
109
116
  process.exit(1);
110
117
  return;
111
118
  }
@@ -173,6 +173,7 @@ export type RunStop = {
173
173
  at: "storyboard-gate";
174
174
  nodeId?: string;
175
175
  framesNodeId?: string;
176
+ showAd?: true;
176
177
  } | {
177
178
  at: "final-watch";
178
179
  } | {
@@ -201,6 +202,7 @@ export declare function resolveStop(stop: RunStop, cutAttached: boolean | null):
201
202
  export declare function resolveStopAtPark(stop: RunStop, runId: string, deps: Pick<VideoDeps, "get">): Promise<ResolvedStop>;
202
203
  export declare function stageWord(stage: string): string;
203
204
  export declare function stepName(kind: string | undefined): string;
205
+ export declare function isShowAd(run: Pick<VideoRun, "moduleOwned">): boolean;
204
206
  export declare function reviewUrl(dashboardUrl: string, run: Pick<VideoRun, "_id" | "workflowId" | "moduleOwned">): string;
205
207
  export declare function stopLines(stop: ResolvedStop, runId: string, runUrl: string): string[];
206
208
  export interface PullDownload {
@@ -250,6 +250,7 @@ export function classifyRun(run) {
250
250
  ...(pausedNode.kind === "scene-frames"
251
251
  ? { framesNodeId: pausedNode.nodeId }
252
252
  : {}),
253
+ ...(isShowAd(run) ? { showAd: true } : {}),
253
254
  };
254
255
  }
255
256
  if (parkedAtFinalWatch(run))
@@ -300,8 +301,11 @@ export function stepName(kind) {
300
301
  return "A step in this run";
301
302
  return STEP_NAMES[kind] ?? `The ${kind} step`;
302
303
  }
304
+ export function isShowAd(run) {
305
+ return run.moduleOwned === true;
306
+ }
303
307
  export function reviewUrl(dashboardUrl, run) {
304
- if (run.moduleOwned === true)
308
+ if (isShowAd(run))
305
309
  return `${dashboardUrl}/video?ad=${run._id}`;
306
310
  if (run.workflowId)
307
311
  return `${dashboardUrl}/workflows/${run.workflowId}/runs/${run._id}`;
@@ -313,8 +317,10 @@ export function stopLines(stop, runId, runUrl) {
313
317
  "Parked: the storyboard is waiting for your yes.",
314
318
  `Read it: exodus video storyboard ${runId}`,
315
319
  `Approve it: exodus video approve ${runId}`,
316
- `Send back: exodus video flag ${runId} --note "what's wrong"`,
317
320
  ];
321
+ if (stop.showAd) {
322
+ lines.push(`Send back: exodus video flag ${runId} --note "what's wrong"`);
323
+ }
318
324
  if (stop.framesNodeId) {
319
325
  lines.push(`Redo one frame: exodus video retry-frame ${runId} --node ${stop.framesNodeId} --scene <n>`);
320
326
  }
@@ -1119,7 +1125,14 @@ export async function storyboardFlow(runId, json, deps) {
1119
1125
  }
1120
1126
  }
1121
1127
  }
1122
- lines.push("", `approve with: exodus video approve ${runId}`, `send it back: exodus video flag ${runId} --note "what's wrong"`);
1128
+ const runRes = await deps
1129
+ .get(`${RUN_PATH}?runId=${encodeURIComponent(runId)}`)
1130
+ .catch(() => null);
1131
+ const showAd = runRes?.ok === true && isShowAd(asVideoRun(runRes.data));
1132
+ lines.push("", `approve with: exodus video approve ${runId}`);
1133
+ if (showAd) {
1134
+ lines.push(`send it back: exodus video flag ${runId} --note "what's wrong"`);
1135
+ }
1123
1136
  if (cards.framesNodeId) {
1124
1137
  lines.push(`redo one frame: exodus video retry-frame ${runId} --node ${cards.framesNodeId} --scene <n>`);
1125
1138
  }
@@ -10,7 +10,7 @@ import { runDisplayName, runHeadingWord, runTypeWord } from "../lib/runNames.js"
10
10
  import { workflowToYaml, parseWorkflowText } from "../lib/workflowText.js";
11
11
  import { missingRouteLine } from "../lib/route-support.js";
12
12
  import { getChannel } from "../lib/channel.js";
13
- import { asVideoRun, classifyRun, resolveStopAtPark, reviewUrl, stopLines, } from "./video.js";
13
+ import { asVideoRun, classifyRun, resolveStopAtPark, reviewUrl, scenePrefix, stopLines, } from "./video.js";
14
14
  export const helpText = `
15
15
  exodus workflow — List, describe, run, inspect, import, and export saved workflows
16
16
 
@@ -1382,6 +1382,43 @@ function planArtifactFile(artifact) {
1382
1382
  url,
1383
1383
  };
1384
1384
  }
1385
+ function artifactFileSuffixes(artifacts) {
1386
+ const sceneBearing = artifacts.some((a) => typeof a.sceneIndex === "number" || a.final === true);
1387
+ if (!sceneBearing) {
1388
+ return artifacts.map((_, i) => ({
1389
+ suffix: i === 0 ? "" : `-${i + 1}`,
1390
+ renamed: false,
1391
+ }));
1392
+ }
1393
+ const taken = new Set();
1394
+ let fullCount = 0;
1395
+ return artifacts.map((a, i) => {
1396
+ let suffix;
1397
+ if (typeof a.sceneIndex === "number") {
1398
+ suffix = `-${scenePrefix(a.sceneIndex)}`;
1399
+ }
1400
+ else if (a.final === true) {
1401
+ suffix = "-final";
1402
+ }
1403
+ else if (a.type === "audio") {
1404
+ fullCount += 1;
1405
+ suffix = fullCount === 1 ? "-full" : `-full-${fullCount}`;
1406
+ }
1407
+ else {
1408
+ suffix = i === 0 ? "" : `-${i + 1}`;
1409
+ }
1410
+ if (taken.has(suffix)) {
1411
+ let fallback = `${suffix}-${i + 1}`;
1412
+ while (taken.has(fallback))
1413
+ fallback = `${fallback}-${i + 1}`;
1414
+ suffix = fallback;
1415
+ taken.add(suffix);
1416
+ return { suffix, renamed: true };
1417
+ }
1418
+ taken.add(suffix);
1419
+ return { suffix, renamed: false };
1420
+ });
1421
+ }
1385
1422
  export async function saveDeliveries(run, dir, deps) {
1386
1423
  const paths = [];
1387
1424
  if (!run.isTerminal) {
@@ -1416,9 +1453,10 @@ export async function saveDeliveries(run, dir, deps) {
1416
1453
  lines.push(` skipped ${delivery.label} (${delivery.key}) — ${why}`);
1417
1454
  continue;
1418
1455
  }
1456
+ const suffixes = artifactFileSuffixes(delivery.artifacts);
1419
1457
  for (let i = 0; i < delivery.artifacts.length; i++) {
1420
1458
  const plan = planArtifactFile(delivery.artifacts[i]);
1421
- const suffix = i === 0 ? "" : `-${i + 1}`;
1459
+ const { suffix, renamed } = suffixes[i];
1422
1460
  if (plan.kind === "none") {
1423
1461
  lines.push(` skipped ${delivery.label} (${delivery.key})${suffix} — ${plan.reason}`);
1424
1462
  continue;
@@ -1429,7 +1467,7 @@ export async function saveDeliveries(run, dir, deps) {
1429
1467
  deps.writeFile(file, plan.body);
1430
1468
  else
1431
1469
  await downloadToFile(plan.url, file);
1432
- lines.push(` wrote ${file}`);
1470
+ lines.push(` wrote ${file}${renamed ? " — renamed to avoid a collision" : ""}`);
1433
1471
  paths.push(file);
1434
1472
  }
1435
1473
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.9.2000-beta.3",
3
+ "version": "2026.9.2000-beta.4",
4
4
  "description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
5
5
  "repository": {
6
6
  "type": "git",