@aicopycoders/exodus 2026.9.2000-beta.2 → 2026.9.2000-beta.21

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.
@@ -34,14 +34,60 @@ export interface WorkflowEdge {
34
34
  targetHandle: string;
35
35
  }
36
36
  export type WorkflowTriggerEvent = "winner-promoted";
37
+ export interface ImageRigLineOverride {
38
+ count?: number;
39
+ aspects?: string[];
40
+ adType?: string;
41
+ style?: string;
42
+ memeFormat?: string;
43
+ steering?: string;
44
+ copyPin?: string;
45
+ }
46
+ export interface ImageRigPlanOverride {
47
+ lines?: unknown[];
48
+ steering?: string;
49
+ defaultAspects?: string[];
50
+ defaultStyle?: string;
51
+ model?: string;
52
+ confirmLargeRun?: boolean;
53
+ }
54
+ export interface ImageRigNodeOverride {
55
+ plan?: ImageRigPlanOverride;
56
+ lines?: Record<string, ImageRigLineOverride>;
57
+ confirmLargeRun?: boolean;
58
+ model?: string;
59
+ }
60
+ export type ImageRigOverrides = Record<string, ImageRigNodeOverride>;
61
+ export interface VoiceRequest {
62
+ who: string;
63
+ choice: {
64
+ kind: "pin";
65
+ voiceId: string;
66
+ label?: string;
67
+ } | {
68
+ kind: "clear";
69
+ };
70
+ }
71
+ export type TriggerVoiceMap = Record<string, string | {
72
+ voiceId: string;
73
+ label?: string;
74
+ } | null>;
37
75
  export type WorkflowTrigger = {
38
76
  type: "event";
39
77
  event: WorkflowTriggerEvent;
40
78
  enabled: boolean;
79
+ imageRigOverrides?: ImageRigOverrides;
80
+ voices?: VoiceRequest[];
41
81
  } | {
42
82
  type: "cron";
43
83
  cron: string;
44
84
  enabled: boolean;
85
+ imageRigOverrides?: ImageRigOverrides;
86
+ voices?: VoiceRequest[];
87
+ };
88
+ type WithoutVoices<T> = T extends unknown ? Omit<T, "voices"> : never;
89
+ export type WorkflowContractTrigger = WithoutVoices<WorkflowTrigger> & {
90
+ voices?: TriggerVoiceMap;
45
91
  };
46
92
  export interface WorkflowContractJson {
47
93
  contract: "exodus-workflow";
@@ -51,7 +97,7 @@ export interface WorkflowContractJson {
51
97
  name: string;
52
98
  description?: string;
53
99
  slots?: WorkflowSlot[];
54
- triggers?: WorkflowTrigger[];
100
+ triggers?: WorkflowContractTrigger[];
55
101
  nodes: WorkflowNode[];
56
102
  edges: WorkflowEdge[];
57
103
  }
@@ -363,7 +409,7 @@ export interface RunFormatProvenance {
363
409
  };
364
410
  specVersion: string | null;
365
411
  }
366
- export type VoicePath = "sts-revoice" | "lipsync-retarget" | "native-prompt" | "omni-audio-ids" | "gemini-direct";
412
+ export type VoicePath = "sts-revoice" | "lipsync-retarget" | "native-prompt" | "omni-audio-ids" | "gemini-direct" | "voice-first";
367
413
  export type RunVoiceProvenance = {
368
414
  source: "show";
369
415
  path: VoicePath;
@@ -414,6 +460,7 @@ export interface WorkflowRun {
414
460
  pauseAhead?: {
415
461
  nodeId: string;
416
462
  after: "frames" | "storyboard";
463
+ selfApproves?: true;
417
464
  };
418
465
  }
419
466
  export declare function pauseAheadLine(ahead: NonNullable<WorkflowRun["pauseAhead"]>): string;
@@ -530,12 +577,13 @@ export declare function parseVersionFlag(flags: Record<string, string | boolean>
530
577
  export declare function versionsFlow(workflowRef: string, opts: {
531
578
  json: boolean;
532
579
  }, deps: WorkflowRunDeps, channel?: Channel): Promise<FlowResult>;
533
- export declare function triggerExpect(t: WorkflowTrigger): {
580
+ type AnyTrigger = WorkflowTrigger | WorkflowContractTrigger;
581
+ export declare function triggerExpect(t: AnyTrigger): {
534
582
  type: string;
535
583
  event?: string;
536
584
  cron?: string;
537
585
  };
538
- export declare function formatTriggers(triggers: WorkflowTrigger[]): string;
586
+ export declare function formatTriggers(triggers: AnyTrigger[]): string;
539
587
  export declare function triggersListFlow(workflowRef: string, opts: {
540
588
  json: boolean;
541
589
  }, deps: WorkflowRunDeps): Promise<FlowResult>;
@@ -546,6 +594,7 @@ export declare function triggersFireFlow(workflowRef: string, opts: {
546
594
  n?: number;
547
595
  text?: string;
548
596
  imageRigOverrides?: Record<string, unknown>;
597
+ voices?: VoiceMap;
549
598
  wait: boolean;
550
599
  json: boolean;
551
600
  onProgressLine?: (line: string) => void;
@@ -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
 
@@ -32,7 +32,7 @@ Usage:
32
32
  exodus workflow triggers <workflowId|name> [--json]
33
33
  exodus workflow triggers <workflowId|name> enable <n> [--json]
34
34
  exodus workflow triggers <workflowId|name> disable <n> [--json]
35
- exodus workflow triggers <workflowId|name> fire [<n>] [--text "..."] [--rig-overrides <json|@file>] [--wait] [--json]
35
+ exodus workflow triggers <workflowId|name> fire [<n>] [--text "..."] [--rig-overrides <json|@file>] [--voices <json|@voices.json>] [--wait] [--json]
36
36
  exodus workflow inbox [--json]
37
37
  exodus workflow checkpoint <runId> [show] [--json]
38
38
  exodus workflow checkpoint <runId> edit <n> [--text "..." | --file <path> | (stdin)] [--json]
@@ -84,9 +84,9 @@ Flags:
84
84
  before anything runs, and the error names it. On
85
85
  "triggers fire" it REPLACES the schedule's own
86
86
  overrides for that one test fire.
87
- --voices <json> (run) Give the ad's characters their voices at the
88
- moment the run starts, instead of at the storyboard
89
- review. Keyed by the names the SCRIPT uses for its
87
+ --voices <json> (run, triggers fire) Give the ad's characters their
88
+ voices at the moment the run starts, instead of at the
89
+ storyboard review. Keyed by the names the SCRIPT uses for its
90
90
  speakers ("HOST 1") — the planner's character ids
91
91
  (C1, C2) do not exist yet when a run starts. Takes a
92
92
  JSON object, or @path to a .json file holding one:
@@ -103,6 +103,14 @@ Flags:
103
103
  <run>" at the storyboard review instead. Voice ids
104
104
  belong to the brand's own folder — never put them in a
105
105
  shared workflow or template.
106
+ A schedule can carry the same map in its YAML
107
+ ("voices:" beside the schedule), so a weekly ad arrives
108
+ already voiced. On "triggers fire" this flag REPLACES
109
+ the schedule's own voices for that one test fire. A
110
+ schedule whose voice has been deleted, or whose
111
+ ElevenLabs key is missing or refused, does not start at
112
+ all: it leaves a failed run saying which speaker and
113
+ what to fix, and spends nothing.
106
114
  --voice-treatment <v> (run) Say HOW this run makes its voices. Two kinds of
107
115
  answer. A name on its own picks a way of working that
108
116
  uses voices you already have:
@@ -279,7 +287,9 @@ Notes:
279
287
  panel in the app); anything you name that isn't there stops the launch and the
280
288
  message says which key was wrong. A trigger can carry the same payload in its
281
289
  YAML ("imageRigOverrides:" beside its schedule), so a Monday schedule and a
282
- Friday one can fire the same workflow at different sizes.
290
+ Friday one can fire the same workflow at different sizes. A trigger can carry
291
+ "voices:" the same way — the same map "workflow run --voices" takes — so a run
292
+ that starts on a schedule arrives at the storyboard already voiced.
283
293
  "workflow run --voices" hands a video run its voices at the start, so a brand
284
294
  that always uses the same hosts stops choosing them by hand on every run. The
285
295
  file is keyed by the names the script uses for its speakers, because that is
@@ -368,6 +378,9 @@ export function formatRulesLine(provenance) {
368
378
  return `Format rules: ${rigName}${version}${where}.${borrowed}`;
369
379
  }
370
380
  export function pauseAheadLine(ahead) {
381
+ if (ahead.after === "frames" && ahead.selfApproves) {
382
+ return "When the frames are ready, the app checks the storyboard and approves it itself if the checks pass, then goes on to the voices and clips. It only stops for your approval if a check finds a problem. The reference, cast and scene pictures are made first (a few cents each).";
383
+ }
371
384
  return ahead.after === "frames"
372
385
  ? "This run will pause for your approval once the frames are ready. The reference, cast and scene pictures are made first (a few cents each). Nothing bigger is spent until you approve."
373
386
  : "This run will pause for your approval once the storyboard is written. Nothing is spent on pictures, voices or clips until you approve.";
@@ -1372,6 +1385,43 @@ function planArtifactFile(artifact) {
1372
1385
  url,
1373
1386
  };
1374
1387
  }
1388
+ function artifactFileSuffixes(artifacts) {
1389
+ const sceneBearing = artifacts.some((a) => typeof a.sceneIndex === "number" || a.final === true);
1390
+ if (!sceneBearing) {
1391
+ return artifacts.map((_, i) => ({
1392
+ suffix: i === 0 ? "" : `-${i + 1}`,
1393
+ renamed: false,
1394
+ }));
1395
+ }
1396
+ const taken = new Set();
1397
+ let fullCount = 0;
1398
+ return artifacts.map((a, i) => {
1399
+ let suffix;
1400
+ if (typeof a.sceneIndex === "number") {
1401
+ suffix = `-${scenePrefix(a.sceneIndex)}`;
1402
+ }
1403
+ else if (a.final === true) {
1404
+ suffix = "-final";
1405
+ }
1406
+ else if (a.type === "audio") {
1407
+ fullCount += 1;
1408
+ suffix = fullCount === 1 ? "-full" : `-full-${fullCount}`;
1409
+ }
1410
+ else {
1411
+ suffix = i === 0 ? "" : `-${i + 1}`;
1412
+ }
1413
+ if (taken.has(suffix)) {
1414
+ let fallback = `${suffix}-${i + 1}`;
1415
+ while (taken.has(fallback))
1416
+ fallback = `${fallback}-${i + 1}`;
1417
+ suffix = fallback;
1418
+ taken.add(suffix);
1419
+ return { suffix, renamed: true };
1420
+ }
1421
+ taken.add(suffix);
1422
+ return { suffix, renamed: false };
1423
+ });
1424
+ }
1375
1425
  export async function saveDeliveries(run, dir, deps) {
1376
1426
  const paths = [];
1377
1427
  if (!run.isTerminal) {
@@ -1406,9 +1456,10 @@ export async function saveDeliveries(run, dir, deps) {
1406
1456
  lines.push(` skipped ${delivery.label} (${delivery.key}) — ${why}`);
1407
1457
  continue;
1408
1458
  }
1459
+ const suffixes = artifactFileSuffixes(delivery.artifacts);
1409
1460
  for (let i = 0; i < delivery.artifacts.length; i++) {
1410
1461
  const plan = planArtifactFile(delivery.artifacts[i]);
1411
- const suffix = i === 0 ? "" : `-${i + 1}`;
1462
+ const { suffix, renamed } = suffixes[i];
1412
1463
  if (plan.kind === "none") {
1413
1464
  lines.push(` skipped ${delivery.label} (${delivery.key})${suffix} — ${plan.reason}`);
1414
1465
  continue;
@@ -1419,7 +1470,7 @@ export async function saveDeliveries(run, dir, deps) {
1419
1470
  deps.writeFile(file, plan.body);
1420
1471
  else
1421
1472
  await downloadToFile(plan.url, file);
1422
- lines.push(` wrote ${file}`);
1473
+ lines.push(` wrote ${file}${renamed ? " — renamed to avoid a collision" : ""}`);
1423
1474
  paths.push(file);
1424
1475
  }
1425
1476
  catch (e) {
@@ -2040,6 +2091,7 @@ function videoParkStop(raw) {
2040
2091
  async function waitForRun(runId, opts, deps) {
2041
2092
  const seen = new Map();
2042
2093
  let pausedNotified = false;
2094
+ let autoRedoNotified;
2043
2095
  const landOnPark = opts.landOnPark;
2044
2096
  const isLandingPark = (raw) => (landOnPark !== undefined && raw["pauseReason"] === landOnPark.pauseReason) ||
2045
2097
  videoParkStop(raw) !== undefined;
@@ -2057,7 +2109,15 @@ async function waitForRun(runId, opts, deps) {
2057
2109
  return;
2058
2110
  const parked = isParkedSnapshot(raw);
2059
2111
  const isLanding = isLandingPark(raw);
2060
- if (parked && !pausedNotified && !isLanding) {
2112
+ const autoRedo = parked ? classifyRun(asVideoRun(raw)) : undefined;
2113
+ if (autoRedo?.at === "running" && autoRedo.autoRedoScenes) {
2114
+ const line = ` ${stopLines(autoRedo, runId, "")[0]}`;
2115
+ if (line !== autoRedoNotified) {
2116
+ autoRedoNotified = line;
2117
+ opts.onProgressLine(line);
2118
+ }
2119
+ }
2120
+ else if (parked && !pausedNotified && !isLanding) {
2061
2121
  pausedNotified = true;
2062
2122
  const dashboardUrl = deps.dashboardUrl ?? getDashboardUrl();
2063
2123
  const pauseReason = raw["pauseReason"];
@@ -2065,7 +2125,9 @@ async function waitForRun(runId, opts, deps) {
2065
2125
  opts.onProgressLine(line);
2066
2126
  }
2067
2127
  }
2068
- const parkedNodeId = gateParkedNodeId(raw["status"], raw["pauseReason"], raw["pausedNodeId"]);
2128
+ const parkedNodeId = autoRedo?.at === "running"
2129
+ ? undefined
2130
+ : gateParkedNodeId(raw["status"], raw["pauseReason"], raw["pausedNodeId"]);
2069
2131
  const nodes = Array.isArray(raw["nodes"]) ? raw["nodes"] : [];
2070
2132
  for (const node of nodes) {
2071
2133
  const isParked = parkedNodeId !== undefined && node.nodeId === parkedNodeId;
@@ -2450,6 +2512,7 @@ export async function triggersFireFlow(workflowRef, opts, deps) {
2450
2512
  ...(opts.imageRigOverrides
2451
2513
  ? { imageRigOverrides: opts.imageRigOverrides }
2452
2514
  : {}),
2515
+ ...(opts.voices ? { voices: opts.voices } : {}),
2453
2516
  });
2454
2517
  if (!res.ok)
2455
2518
  return triggerErrorResult(res, verb, opts.json);
@@ -3313,8 +3376,10 @@ export async function run(flags, occurrences) {
3313
3376
  }
3314
3377
  }
3315
3378
  let fireOverrides;
3379
+ let fireVoices;
3316
3380
  try {
3317
3381
  fireOverrides = parseRigOverridesFlag(occurrences, defaultDeps.readFile);
3382
+ fireVoices = parseVoicesFlag(occurrences, defaultDeps.readFile);
3318
3383
  }
3319
3384
  catch (e) {
3320
3385
  console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
@@ -3324,6 +3389,7 @@ export async function run(flags, occurrences) {
3324
3389
  n,
3325
3390
  text: flagString(flags, "text"),
3326
3391
  imageRigOverrides: fireOverrides,
3392
+ voices: fireVoices,
3327
3393
  wait: flags["wait"] === true,
3328
3394
  json,
3329
3395
  onProgressLine: (line) => console.log(line),
@@ -0,0 +1,3 @@
1
+ export declare const HELPER_LEDGER_BASE = 900000;
2
+ export declare const SET_OPTION_LEDGER_BASE = 920000;
3
+ export declare const CAST_OPTION_LEDGER_BASE = 960000;
@@ -0,0 +1,3 @@
1
+ export const HELPER_LEDGER_BASE = 900000;
2
+ export const SET_OPTION_LEDGER_BASE = 920000;
3
+ export const CAST_OPTION_LEDGER_BASE = 960000;
@@ -22,6 +22,7 @@ export interface AdWindowStats {
22
22
  spend?: number | null;
23
23
  spendDisplay?: string | null;
24
24
  impressions?: number | null;
25
+ reach?: number | null;
25
26
  clicks?: number | null;
26
27
  currency?: string | null;
27
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.9.2000-beta.2",
3
+ "version": "2026.9.2000-beta.21",
4
4
  "description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
5
5
  "repository": {
6
6
  "type": "git",