@aicopycoders/exodus 2026.9.1900-beta.0 → 2026.9.1900-beta.2

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.
@@ -20,7 +20,7 @@ Usage:
20
20
  exodus workflow templates [list] [--json]
21
21
  exodus workflow templates export <key> [--out <file>] [--json]
22
22
  exodus workflow schema [--kind <kind>] [--face <face>] [--json]
23
- exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=<path> ...] [--rig-overrides <json|@file>] [--voices <json|@voices.json>] [--auto-approve] [--wait] [--out <dir>] [--json]
23
+ exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=<path> ...] [--rig-overrides <json|@file>] [--voices <json|@voices.json>] [--voice-treatment <name|json|@file>] [--auto-approve] [--wait] [--out <dir>] [--json]
24
24
  exodus workflow status [--json]
25
25
  exodus workflow status --id <runId> [--out <dir>] [--json]
26
26
  exodus workflow cancel <runId> [--reason "..."] [--json]
@@ -102,6 +102,29 @@ Flags:
102
102
  <run>" at the storyboard review instead. Voice ids
103
103
  belong to the brand's own folder — never put them in a
104
104
  shared workflow or template.
105
+ --voice-treatment <v> (run) Say HOW this run makes its voices. Two kinds of
106
+ answer. A name on its own picks a way of working that
107
+ uses voices you already have:
108
+ --voice-treatment sts-revoice
109
+ --voice-treatment lipsync-retarget --voices @voices.json
110
+ The other kind has the video model speak the lines
111
+ itself, in a voice you WRITE instead of one you pick.
112
+ Those need a description for every speaker in your
113
+ script, so they take a JSON object, or @path to a .json
114
+ file holding one:
115
+ --voice-treatment @voice-treatment.json
116
+ --voice-treatment '{"path":"native-prompt","describe":{"HOST 1":"Dry, tired baritone."}}'
117
+ Written voices and --voices are never used together:
118
+ the video model owns the voice on those runs, so
119
+ ElevenLabs voices would be paid for and never heard,
120
+ and asking for both stops the launch. Anything the run
121
+ could not actually do — a treatment this stack has not
122
+ switched on, a Video step on a model that cannot do it,
123
+ a missing key, a rig that shows every line as on-screen
124
+ text — stops the launch before the run is created, and
125
+ the message says which. Descriptions are chosen HERE,
126
+ at the start: "exodus video voices <run>" shows them at
127
+ the review but cannot change them.
105
128
  --auto-approve (run) Deliberately unattended launch: every Checkpoint
106
129
  box this run stops at is approved automatically, with
107
130
  whatever it was holding left exactly as it is, and the
@@ -194,6 +217,7 @@ Examples:
194
217
  exodus workflow run "Product Shots" --rig-overrides '{"rig_1":{"lines":{"line_1":{"count":3}}}}'
195
218
  exodus workflow run "Product Shots" --rig-overrides @rig.json --wait
196
219
  exodus workflow run "Podcast Ad" --input script=@script.txt --voices @voices.json --wait
220
+ exodus workflow run "Podcast Ad" --input script=@script.txt --voice-treatment @voice-treatment.json --wait
197
221
  exodus workflow run "Launch Flow" --wait --out ./deliverables
198
222
  exodus workflow status # recent runs + their ids
199
223
  exodus workflow status --id wr_123
@@ -268,6 +292,20 @@ Notes:
268
292
  no speaker names to key on at the start, so it is refused with a pointer at
269
293
  the review-time command. Keep voice ids in the brand's own folder, never in a
270
294
  shared workflow or template. (#1846)
295
+ "workflow run --voice-treatment" says HOW a run makes its voices. Some ways of
296
+ working play a voice you picked (that is today's behaviour, and it still takes
297
+ --voices); others have the video model speak the CAST's lines itself, in voices
298
+ you describe in words. A described voice needs one description per speaker,
299
+ because a speaker nobody described gets a voice the model invents afresh on
300
+ every clip, and the drift only shows up once the clips are paid for. Written
301
+ voices and --voices are refused together, since the ElevenLabs voices would
302
+ never be heard. Everything the run could not do — a way of working this stack
303
+ has not switched on, a Video step whose model cannot do it, a missing key, a
304
+ rig that shows every line as on-screen text — stops the launch before the run
305
+ is created. The ways of working beyond the two shipped ones are switched on
306
+ only on the test stack while they are being tried out, so a live run refuses
307
+ them by name. Both the launch receipt and "workflow status" say which way a
308
+ run took, and "exodus video voices <run>" shows each written voice. (#1869)
271
309
  workflow triggers are addressed by 1-based position — a trigger carries no id,
272
310
  so the CLI reads the live list from the export contract and sends a fingerprint
273
311
  of the trigger's fields as the guard; a concurrent edit fails loud rather than
@@ -310,6 +348,24 @@ Notes:
310
348
  ids it wants). The "gate" verbs retired in 2.0 along with the Gate node — the
311
349
  command now prints a pointer at "checkpoint" and exits 1.
312
350
  `.trim();
351
+ export function formatRulesLine(provenance) {
352
+ const format = provenance?.format;
353
+ if (!format || typeof format !== "object")
354
+ return undefined;
355
+ const { rigName, specVersion, nodeId, rulesFrom } = format;
356
+ if (typeof rigName !== "string" || rigName === "")
357
+ return undefined;
358
+ const version = typeof specVersion === "string" && specVersion !== ""
359
+ ? `, version ${specVersion}`
360
+ : "";
361
+ const where = typeof nodeId === "string" && nodeId !== ""
362
+ ? ` (from the "${nodeId}" Rig box)`
363
+ : " (from the Show's rig)";
364
+ const borrowed = rulesFrom && typeof rulesFrom.rigName === "string"
365
+ ? ` The rules come from "${rulesFrom.rigName}".`
366
+ : "";
367
+ return `Format rules: ${rigName}${version}${where}.${borrowed}`;
368
+ }
313
369
  const LIST_PATH = "/api/v2/workflows";
314
370
  const RUN_PATH = "/api/v2/workflows/run";
315
371
  const STATUS_PATH = "/api/v2/workflow";
@@ -354,6 +410,7 @@ const VALUE_FLAGS = new Set([
354
410
  "reject",
355
411
  "rig-overrides",
356
412
  "voices",
413
+ "voice-treatment",
357
414
  "note",
358
415
  ]);
359
416
  function defaultMkdirp(dir) {
@@ -552,35 +609,25 @@ function normalizeMultiValue(value) {
552
609
  .filter((p) => p.length > 0)
553
610
  .join(", ");
554
611
  }
555
- export function parseRawInputFlags(args) {
612
+ export function parseRawInputFlags(occurrences) {
556
613
  const inputs = {};
557
- for (let i = 0; i < args.length; i++) {
558
- const arg = args[i];
559
- let raw;
560
- if (arg === "--input") {
561
- raw = args[i + 1];
562
- i++;
563
- }
564
- else if (arg.startsWith("--input=")) {
565
- raw = arg.slice("--input=".length);
566
- }
567
- else {
614
+ for (const { flag, value } of occurrences) {
615
+ if (flag !== "input")
568
616
  continue;
569
- }
570
- if (!raw)
617
+ if (!value)
571
618
  throw new Error("--input requires key=value");
572
- const eq = raw.indexOf("=");
619
+ const eq = value.indexOf("=");
573
620
  if (eq <= 0)
574
- throw new Error(`--input must be key=value (got "${raw}")`);
575
- const key = raw.slice(0, eq).trim();
621
+ throw new Error(`--input must be key=value (got "${value}")`);
622
+ const key = value.slice(0, eq).trim();
576
623
  if (!key)
577
- throw new Error(`--input must include a key (got "${raw}")`);
578
- inputs[key] = raw.slice(eq + 1);
624
+ throw new Error(`--input must include a key (got "${value}")`);
625
+ inputs[key] = value.slice(eq + 1);
579
626
  }
580
627
  return inputs;
581
628
  }
582
- export function parseInputFlags(args, readFile) {
583
- const inputs = parseRawInputFlags(args);
629
+ export function parseInputFlags(occurrences, readFile) {
630
+ const inputs = parseRawInputFlags(occurrences);
584
631
  for (const [key, value] of Object.entries(inputs)) {
585
632
  inputs[key] = expandInputValue(key, value, readFile);
586
633
  }
@@ -876,12 +923,10 @@ async function prepareRunInputs(workflowId, raw, deps, note) {
876
923
  }
877
924
  return { inputs: prepared, warnings };
878
925
  }
879
- export function rejectTerminalFlag(args) {
880
- for (const arg of args) {
881
- if (arg === "--terminal" || arg.startsWith("--terminal=")) {
882
- throw new Error("--terminal is no longer supported: a run now executes the whole " +
883
- "workflow or it doesn't start. Re-run without --terminal.");
884
- }
926
+ export function rejectTerminalFlag(occurrences) {
927
+ if (occurrences.some((o) => o.flag === "terminal")) {
928
+ throw new Error("--terminal is no longer supported: a run now executes the whole " +
929
+ "workflow or it doesn't start. Re-run without --terminal.");
885
930
  }
886
931
  }
887
932
  export function parseAutoApproveFlag(args) {
@@ -894,21 +939,11 @@ export function parseAutoApproveFlag(args) {
894
939
  }
895
940
  return false;
896
941
  }
897
- export function parseRigOverridesFlag(args, readFile) {
942
+ export function parseRigOverridesFlag(occurrences, readFile) {
898
943
  let raw;
899
- for (let i = 0; i < args.length; i++) {
900
- const arg = args[i];
901
- let value;
902
- if (arg === "--rig-overrides") {
903
- value = args[i + 1];
904
- i++;
905
- }
906
- else if (arg.startsWith("--rig-overrides=")) {
907
- value = arg.slice("--rig-overrides=".length);
908
- }
909
- else {
944
+ for (const { flag, value } of occurrences) {
945
+ if (flag !== "rig-overrides")
910
946
  continue;
911
- }
912
947
  if (value === undefined || value.startsWith("--")) {
913
948
  throw new Error("--rig-overrides requires JSON or @path/to/file.json");
914
949
  }
@@ -951,21 +986,11 @@ export function parseRigOverridesFlag(args, readFile) {
951
986
  }
952
987
  return parsed;
953
988
  }
954
- export function parseVoicesFlag(args, readFile) {
989
+ export function parseVoicesFlag(occurrences, readFile) {
955
990
  let raw;
956
- for (let i = 0; i < args.length; i++) {
957
- const arg = args[i];
958
- let value;
959
- if (arg === "--voices") {
960
- value = args[i + 1];
961
- i++;
962
- }
963
- else if (arg.startsWith("--voices=")) {
964
- value = arg.slice("--voices=".length);
965
- }
966
- else {
991
+ for (const { flag, value } of occurrences) {
992
+ if (flag !== "voices")
967
993
  continue;
968
- }
969
994
  if (value === undefined || value.startsWith("--")) {
970
995
  throw new Error("--voices requires JSON or @path/to/voices.json");
971
996
  }
@@ -1008,25 +1033,66 @@ export function parseVoicesFlag(args, readFile) {
1008
1033
  }
1009
1034
  return parsed;
1010
1035
  }
1011
- export function parseFillFlag(args) {
1012
- let name;
1013
- for (let i = 0; i < args.length; i++) {
1014
- const arg = args[i];
1015
- let raw;
1016
- if (arg === "--fill") {
1017
- raw = args[i + 1];
1018
- i++;
1036
+ export function parseVoiceTreatmentFlag(occurrences, readFile) {
1037
+ const usage = "--voice-treatment requires a treatment name, JSON, or @path/to/voice-treatment.json";
1038
+ let raw;
1039
+ for (const { flag, value } of occurrences) {
1040
+ if (flag !== "voice-treatment")
1041
+ continue;
1042
+ if (value === undefined || value.startsWith("--"))
1043
+ throw new Error(usage);
1044
+ raw = value;
1045
+ }
1046
+ if (raw === undefined)
1047
+ return undefined;
1048
+ let text = raw.trim();
1049
+ if (!text)
1050
+ throw new Error(usage);
1051
+ let fromFile = false;
1052
+ if (text.startsWith("@@")) {
1053
+ text = text.slice(1);
1054
+ }
1055
+ else if (text.startsWith("@")) {
1056
+ const filePath = text.slice(1);
1057
+ if (!filePath) {
1058
+ throw new Error("--voice-treatment @<file> needs a file path after \"@\"");
1019
1059
  }
1020
- else if (arg.startsWith("--fill=")) {
1021
- raw = arg.slice("--fill=".length);
1060
+ if (!readFile) {
1061
+ throw new Error(`--voice-treatment: cannot load @${filePath} here (no file access)`);
1022
1062
  }
1023
- else {
1024
- continue;
1063
+ try {
1064
+ text = readFile(filePath);
1065
+ }
1066
+ catch (e) {
1067
+ const msg = e instanceof Error ? e.message : String(e);
1068
+ throw new Error(`--voice-treatment: could not read file "${filePath}": ${msg}`);
1025
1069
  }
1026
- if (raw === undefined || raw.startsWith("--")) {
1070
+ fromFile = true;
1071
+ }
1072
+ if (!fromFile && !text.startsWith("{"))
1073
+ return { path: text };
1074
+ let parsed;
1075
+ try {
1076
+ parsed = JSON.parse(text);
1077
+ }
1078
+ catch (e) {
1079
+ const msg = e instanceof Error ? e.message : String(e);
1080
+ throw new Error(`--voice-treatment is not valid JSON: ${msg}`);
1081
+ }
1082
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1083
+ throw new Error('--voice-treatment must be a treatment name, or a JSON object like {"path":"native-prompt","describe":{"HOST 1":"..."}}');
1084
+ }
1085
+ return parsed;
1086
+ }
1087
+ export function parseFillFlag(occurrences) {
1088
+ let name;
1089
+ for (const { flag, value } of occurrences) {
1090
+ if (flag !== "fill")
1091
+ continue;
1092
+ if (value === undefined || value.startsWith("--")) {
1027
1093
  throw new Error("--fill requires a saved fill's name");
1028
1094
  }
1029
- const trimmed = raw.trim();
1095
+ const trimmed = value.trim();
1030
1096
  if (!trimmed)
1031
1097
  throw new Error("--fill requires a saved fill's name");
1032
1098
  name = trimmed;
@@ -1137,6 +1203,12 @@ export function formatWorkflowRun(run) {
1137
1203
  lines.push(`status: ${runVerdict(run)}${counts ? ` (${counts})` : ""}`);
1138
1204
  if (run.isTerminal)
1139
1205
  lines.push("terminal: yes");
1206
+ const rulesLine = formatRulesLine(run.provenance);
1207
+ if (rulesLine)
1208
+ lines.push(rulesLine);
1209
+ const treatmentLine = voiceTreatmentLine(run.provenance);
1210
+ if (treatmentLine)
1211
+ lines.push(treatmentLine);
1140
1212
  if (run.error)
1141
1213
  lines.push(`error: ${run.error}`);
1142
1214
  if (Object.keys(run.inputs ?? {}).length > 0) {
@@ -1828,6 +1900,23 @@ function launchVoicesLine(attached) {
1828
1900
  }
1829
1901
  return said.length > 0 ? `Voices: ${said.join(", ")}` : undefined;
1830
1902
  }
1903
+ export function voiceTreatmentLine(provenance) {
1904
+ const voice = provenance?.voice;
1905
+ if (!voice || typeof voice !== "object")
1906
+ return undefined;
1907
+ if (voice.source === "unreadable") {
1908
+ return ("Voice treatment: the setting saved when this run started cannot be read, so no " +
1909
+ "clip will be made. Start the run again.");
1910
+ }
1911
+ const path = voice.path;
1912
+ if (typeof path !== "string" || path === "")
1913
+ return undefined;
1914
+ const where = voice.source === "show" ? "from its Show" : "chosen at launch";
1915
+ const described = voice.source === "launch" && Array.isArray(voice.descriptions) && voice.descriptions.length > 0
1916
+ ? `. Written voices: ${voice.descriptions.map((d) => d.speaker).join(", ")}`
1917
+ : "";
1918
+ return `Voice treatment: ${path} (${where})${described}`;
1919
+ }
1831
1920
  export async function runFlow(workflowRef, opts, deps) {
1832
1921
  let workflowId;
1833
1922
  try {
@@ -1871,6 +1960,7 @@ export async function runFlow(workflowRef, opts, deps) {
1871
1960
  ...(opts.autoApprove === true ? { autoApprove: true } : {}),
1872
1961
  ...(opts.imageRigOverrides ? { imageRigOverrides: opts.imageRigOverrides } : {}),
1873
1962
  ...(opts.voices ? { voices: opts.voices } : {}),
1963
+ ...(opts.voiceTreatment ? { voiceTreatment: opts.voiceTreatment } : {}),
1874
1964
  launchedVia: "cli",
1875
1965
  };
1876
1966
  const start = await deps.post(RUN_PATH, body);
@@ -1885,6 +1975,12 @@ export async function runFlow(workflowRef, opts, deps) {
1885
1975
  if (opts.json && !opts.wait)
1886
1976
  return { code: 0, lines: [JSON.stringify(base)] };
1887
1977
  const voicesLine = launchVoicesLine(data?.launchVoices);
1978
+ const provenance = { format: data?.format ?? null, voice: data?.voice ?? null };
1979
+ const rulesLine = formatRulesLine(provenance);
1980
+ const treatmentLine = voiceTreatmentLine(provenance);
1981
+ const advisories = Array.isArray(data?.advisories)
1982
+ ? data.advisories.filter((line) => typeof line === "string")
1983
+ : [];
1888
1984
  const lines = opts.json
1889
1985
  ? []
1890
1986
  : [
@@ -1892,7 +1988,10 @@ export async function runFlow(workflowRef, opts, deps) {
1892
1988
  "Workflow run started.",
1893
1989
  `runId: ${data.runId}`,
1894
1990
  `triggerRunId: ${data.triggerRunId}`,
1991
+ ...(rulesLine ? [rulesLine] : []),
1992
+ ...advisories,
1895
1993
  ...(voicesLine ? [voicesLine] : []),
1994
+ ...(treatmentLine ? [treatmentLine] : []),
1896
1995
  `Poll: exodus workflow status --id ${data.runId}`,
1897
1996
  ];
1898
1997
  if (!opts.wait) {
@@ -2858,30 +2957,20 @@ async function resumeAndMaybeWait(runId, triggerRunId, headline, opts, deps, lan
2858
2957
  const prefix = opts.onProgressLine ? [] : startLines;
2859
2958
  return { code: waited.code, lines: [...prefix, ...waited.lines] };
2860
2959
  }
2861
- export function parseSlotFlags(args) {
2960
+ export function parseSlotFlags(occurrences) {
2862
2961
  const values = {};
2863
- for (let i = 0; i < args.length; i++) {
2864
- const arg = args[i];
2865
- let raw;
2866
- if (arg === "--slot") {
2867
- raw = args[i + 1];
2868
- i++;
2869
- }
2870
- else if (arg.startsWith("--slot=")) {
2871
- raw = arg.slice("--slot=".length);
2872
- }
2873
- else {
2962
+ for (const { flag, value } of occurrences) {
2963
+ if (flag !== "slot")
2874
2964
  continue;
2875
- }
2876
- if (!raw)
2965
+ if (!value)
2877
2966
  throw new Error("--slot requires key=value");
2878
- const eq = raw.indexOf("=");
2967
+ const eq = value.indexOf("=");
2879
2968
  if (eq <= 0)
2880
- throw new Error(`--slot must be key=value (got "${raw}")`);
2881
- const key = raw.slice(0, eq).trim();
2969
+ throw new Error(`--slot must be key=value (got "${value}")`);
2970
+ const key = value.slice(0, eq).trim();
2882
2971
  if (!key)
2883
- throw new Error(`--slot must include a key (got "${raw}")`);
2884
- values[key] = raw.slice(eq + 1);
2972
+ throw new Error(`--slot must include a key (got "${value}")`);
2973
+ values[key] = value.slice(eq + 1);
2885
2974
  }
2886
2975
  return values;
2887
2976
  }
@@ -2953,7 +3042,7 @@ async function maybeReadStdin(flags) {
2953
3042
  return undefined;
2954
3043
  return raw.endsWith("\n") ? raw.slice(0, -1) : raw;
2955
3044
  }
2956
- export async function run(flags) {
3045
+ export async function run(flags, occurrences) {
2957
3046
  const positional = parsePositional();
2958
3047
  const [sub, ...rest] = positional;
2959
3048
  const json = !!flags["json"];
@@ -3009,7 +3098,7 @@ export async function run(flags) {
3009
3098
  const workflowRef = rest[0];
3010
3099
  if (!workflowRef) {
3011
3100
  console.error("Error: workflow run requires <workflowId|name>.");
3012
- console.log("Usage: exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=./path/to/file ...] [--rig-overrides <json|@file>] [--voices <json|@voices.json>] [--auto-approve] [--wait] [--json]");
3101
+ console.log("Usage: exodus workflow run <workflowId|name> [--fill <name>] [--input key=value ...] [--input <fileField>=./path/to/file ...] [--rig-overrides <json|@file>] [--voices <json|@voices.json>] [--voice-treatment <name|json|@file>] [--auto-approve] [--wait] [--json]");
3013
3102
  process.exit(1);
3014
3103
  }
3015
3104
  let inputs;
@@ -3017,13 +3106,15 @@ export async function run(flags) {
3017
3106
  let autoApprove;
3018
3107
  let imageRigOverrides;
3019
3108
  let voices;
3109
+ let voiceTreatment;
3020
3110
  try {
3021
- inputs = parseRawInputFlags(process.argv.slice(3));
3022
- rejectTerminalFlag(process.argv.slice(3));
3023
- fill = parseFillFlag(process.argv.slice(3));
3111
+ inputs = parseRawInputFlags(occurrences);
3112
+ rejectTerminalFlag(occurrences);
3113
+ fill = parseFillFlag(occurrences);
3024
3114
  autoApprove = parseAutoApproveFlag(process.argv.slice(3));
3025
- imageRigOverrides = parseRigOverridesFlag(process.argv.slice(3), defaultDeps.readFile);
3026
- voices = parseVoicesFlag(process.argv.slice(3), defaultDeps.readFile);
3115
+ imageRigOverrides = parseRigOverridesFlag(occurrences, defaultDeps.readFile);
3116
+ voices = parseVoicesFlag(occurrences, defaultDeps.readFile);
3117
+ voiceTreatment = parseVoiceTreatmentFlag(occurrences, defaultDeps.readFile);
3027
3118
  }
3028
3119
  catch (e) {
3029
3120
  console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
@@ -3035,6 +3126,7 @@ export async function run(flags) {
3035
3126
  autoApprove,
3036
3127
  imageRigOverrides,
3037
3128
  voices,
3129
+ voiceTreatment,
3038
3130
  wait: flags["wait"] === true,
3039
3131
  json,
3040
3132
  out: flagString(flags, "out"),
@@ -3167,7 +3259,7 @@ export async function run(flags) {
3167
3259
  }
3168
3260
  let values;
3169
3261
  try {
3170
- values = parseSlotFlags(process.argv.slice(3));
3262
+ values = parseSlotFlags(occurrences);
3171
3263
  }
3172
3264
  catch (e) {
3173
3265
  console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
@@ -3205,7 +3297,7 @@ export async function run(flags) {
3205
3297
  }
3206
3298
  let fireOverrides;
3207
3299
  try {
3208
- fireOverrides = parseRigOverridesFlag(process.argv.slice(3), defaultDeps.readFile);
3300
+ fireOverrides = parseRigOverridesFlag(occurrences, defaultDeps.readFile);
3209
3301
  }
3210
3302
  catch (e) {
3211
3303
  console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
@@ -0,0 +1,10 @@
1
+ export interface FlagOccurrence {
2
+ flag: string;
3
+ value: string | undefined;
4
+ }
5
+ export interface ParsedArgs {
6
+ command: string;
7
+ flags: Record<string, string | boolean>;
8
+ occurrences: FlagOccurrence[];
9
+ }
10
+ export declare function parseArgs(argv: string[]): ParsedArgs;
@@ -0,0 +1,39 @@
1
+ export function parseArgs(argv) {
2
+ const [, , rawCommand = "help", ...rest] = argv;
3
+ const command = ["--help", "-h", "-help"].includes(rawCommand) ? "help" : rawCommand;
4
+ const flags = {};
5
+ const occurrences = [];
6
+ let i = 0;
7
+ while (i < rest.length) {
8
+ const arg = rest[i];
9
+ if (arg === "--help" || arg === "-h") {
10
+ flags["help"] = true;
11
+ i++;
12
+ continue;
13
+ }
14
+ if (arg.startsWith("--")) {
15
+ const key = arg.slice(2);
16
+ const next = rest[i + 1];
17
+ const eq = key.indexOf("=");
18
+ occurrences.push(eq === -1
19
+ ? { flag: key, value: next }
20
+ : { flag: key.slice(0, eq), value: key.slice(eq + 1) });
21
+ if (key.startsWith("no-")) {
22
+ flags[key.slice(3)] = false;
23
+ i++;
24
+ }
25
+ else if (next !== undefined && !next.startsWith("--")) {
26
+ flags[key] = next;
27
+ i += 2;
28
+ }
29
+ else {
30
+ flags[key] = true;
31
+ i++;
32
+ }
33
+ }
34
+ else {
35
+ i++;
36
+ }
37
+ }
38
+ return { command, flags, occurrences };
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.9.1900-beta.0",
3
+ "version": "2026.9.1900-beta.2",
4
4
  "description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
5
5
  "repository": {
6
6
  "type": "git",