@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
@@ -8,6 +8,10 @@ export declare function apiGet<T>(path: string, opts?: {
8
8
  skipActiveBrand?: boolean;
9
9
  activeBrandOverride?: string;
10
10
  }): Promise<ApiResponse<T>>;
11
+ export declare function apiGetText(path: string, opts?: {
12
+ skipActiveBrand?: boolean;
13
+ activeBrandOverride?: string;
14
+ }): Promise<ApiResponse<string>>;
11
15
  export declare function apiPost<T>(path: string, body: unknown, opts?: {
12
16
  ccCommand?: string;
13
17
  }): Promise<ApiResponse<T>>;
@@ -20,4 +24,7 @@ export declare function apiGetDashboard<T>(path: string, opts?: {
20
24
  timeoutMs?: number;
21
25
  activeBrandOverride?: string;
22
26
  }): Promise<ApiResponse<T>>;
23
- export declare function apiPostDashboard<T>(path: string, body: unknown): Promise<ApiResponse<T>>;
27
+ export declare function apiPostDashboard<T>(path: string, body: unknown, opts?: {
28
+ timeoutMs?: number;
29
+ activeBrandOverride?: string;
30
+ }): Promise<ApiResponse<T>>;
@@ -108,6 +108,16 @@ export async function apiGet(path, opts) {
108
108
  }
109
109
  return { ok: res.ok, status: res.status, data: data };
110
110
  }
111
+ export async function apiGetText(path, opts) {
112
+ const { apiUrl, apiKey } = getConfig();
113
+ const url = `${apiUrl}${path}`;
114
+ const res = await fetch(url, {
115
+ method: "GET",
116
+ headers: buildHeaders(apiKey, undefined, opts),
117
+ });
118
+ const text = await res.text();
119
+ return { ok: res.ok, status: res.status, data: text };
120
+ }
111
121
  export async function apiPost(path, body, opts) {
112
122
  const { apiUrl, apiKey } = getConfig();
113
123
  const url = `${apiUrl}${path}`;
@@ -187,15 +197,29 @@ export async function apiGetDashboard(path, opts) {
187
197
  }
188
198
  return { ok: res.ok, status: res.status, data: data };
189
199
  }
190
- export async function apiPostDashboard(path, body) {
200
+ export async function apiPostDashboard(path, body, opts) {
191
201
  const { apiKey } = getConfig();
192
202
  const url = `${getDashboardUrl()}${path}`;
193
- const headers = buildHeaders(apiKey);
194
- const res = await fetch(url, {
195
- method: "POST",
196
- headers,
197
- body: JSON.stringify(body),
203
+ const headers = buildHeaders(apiKey, undefined, {
204
+ activeBrandOverride: opts?.activeBrandOverride,
198
205
  });
206
+ const controller = opts?.timeoutMs ? new AbortController() : undefined;
207
+ const timer = controller
208
+ ? setTimeout(() => controller.abort(), opts.timeoutMs)
209
+ : undefined;
210
+ let res;
211
+ try {
212
+ res = await fetch(url, {
213
+ method: "POST",
214
+ headers,
215
+ body: JSON.stringify(body),
216
+ signal: controller?.signal,
217
+ });
218
+ }
219
+ finally {
220
+ if (timer)
221
+ clearTimeout(timer);
222
+ }
199
223
  const text = await res.text();
200
224
  let data;
201
225
  try {
@@ -1,6 +1,13 @@
1
+ export declare function displayRunStatus(raw: unknown, missing?: string): string;
2
+ export declare function tickerRunStatus(raw: unknown): string;
1
3
  export declare function formatGeneration(data: Record<string, unknown>): string;
2
4
  export declare function formatGenesisRun(data: Record<string, unknown>): string;
3
5
  export declare function formatBrowse(generations: unknown[]): string;
6
+ export declare function formatApiError(res: {
7
+ ok: boolean;
8
+ status: number;
9
+ data: unknown;
10
+ }): string;
4
11
  export declare function formatError(res: {
5
12
  ok: boolean;
6
13
  status: number;
@@ -1,3 +1,21 @@
1
+ import { normalizeRunStatus, runStatusLabel } from "./runStatus.js";
2
+ export function displayRunStatus(raw, missing = "unknown") {
3
+ return typeof raw === "string" && raw.trim() ? runStatusLabel(raw) : missing;
4
+ }
5
+ const GENERIC_ACTIVE_STATUSES = new Set([
6
+ "queued",
7
+ "pending",
8
+ "running",
9
+ "processing",
10
+ "in-progress",
11
+ ]);
12
+ export function tickerRunStatus(raw) {
13
+ if (typeof raw !== "string" || !raw.trim())
14
+ return "";
15
+ const canonical = normalizeRunStatus(raw);
16
+ const isPhaseWord = (canonical === "running" || canonical === "queued") && !GENERIC_ACTIVE_STATUSES.has(raw);
17
+ return isPhaseWord ? raw : runStatusLabel(raw);
18
+ }
1
19
  function stepIcon(status) {
2
20
  if (status === "completed" || status === "done")
3
21
  return "✓";
@@ -29,9 +47,8 @@ function formatSteps(steps) {
29
47
  }
30
48
  export function formatGeneration(data) {
31
49
  const lines = [];
32
- const status = data["status"] ?? "unknown";
33
50
  lines.push(`## Generation Result`);
34
- lines.push(`**Status:** ${status}`);
51
+ lines.push(`**Status:** ${displayRunStatus(data["status"])}`);
35
52
  if (data["agentName"] || data["agent"]) {
36
53
  lines.push(`**Agent:** ${data["agentName"] ?? data["agent"]}`);
37
54
  }
@@ -69,9 +86,8 @@ export function formatGeneration(data) {
69
86
  }
70
87
  export function formatGenesisRun(data) {
71
88
  const lines = [];
72
- const status = data["status"] ?? "unknown";
73
89
  lines.push(`## Genesis Run`);
74
- lines.push(`**Status:** ${status}`);
90
+ lines.push(`**Status:** ${displayRunStatus(data["status"])}`);
75
91
  if (data["awarenessLevel"] || data["awareness"]) {
76
92
  lines.push(`**Awareness Level:** ${data["awarenessLevel"] ?? data["awareness"]}`);
77
93
  }
@@ -134,12 +150,43 @@ export function formatBrowse(generations) {
134
150
  date = isNaN(parsed.getTime()) ? "—" : parsed.toISOString().slice(0, 10);
135
151
  }
136
152
  const agent = gen["agentName"] ?? gen["agentId"] ?? gen["agent"] ?? gen["pipeline"] ?? "—";
137
- const status = gen["status"] ?? "—";
153
+ const status = displayRunStatus(gen["status"], "—");
138
154
  const doc = gen["googleDocUrl"] ?? gen["docUrl"] ?? "—";
139
155
  lines.push(`${date} | ${agent} | ${status} | ${doc}`);
140
156
  }
141
157
  return lines.join("\n");
142
158
  }
159
+ export function formatApiError(res) {
160
+ let message;
161
+ let remedy;
162
+ const d = res.data;
163
+ if (d && typeof d === "object" && !Array.isArray(d)) {
164
+ const rec = d;
165
+ const err = rec["error"];
166
+ if (typeof err === "string" && err) {
167
+ message = err;
168
+ }
169
+ else if (err && typeof err === "object") {
170
+ const e = err;
171
+ if (typeof e["message"] === "string" && e["message"])
172
+ message = e["message"];
173
+ if (typeof e["remedy"] === "string" && e["remedy"])
174
+ remedy = e["remedy"];
175
+ }
176
+ if (!message && typeof rec["message"] === "string" && rec["message"]) {
177
+ message = rec["message"];
178
+ }
179
+ if (!remedy && typeof rec["remedy"] === "string" && rec["remedy"]) {
180
+ remedy = rec["remedy"];
181
+ }
182
+ }
183
+ else if (typeof d === "string" && d.trim()) {
184
+ message = d.trim();
185
+ }
186
+ if (!message)
187
+ message = `HTTP ${res.status}`;
188
+ return remedy ? `${message}\nfix: ${remedy}` : message;
189
+ }
143
190
  export function formatError(res) {
144
191
  const lines = [];
145
192
  lines.push(`## Error`);
@@ -1,7 +1,8 @@
1
+ import { displayRunStatus } from "./format.js";
1
2
  export function formatIntelResult(data) {
2
3
  const lines = [];
3
4
  lines.push("## Intel Analysis Result");
4
- lines.push(`**Status:** ${data["status"] ?? "unknown"}`);
5
+ lines.push(`**Status:** ${displayRunStatus(data["status"])}`);
5
6
  if (data["mode"])
6
7
  lines.push(`**Mode:** ${data["mode"]}`);
7
8
  if (data["phase"] !== undefined)
@@ -11,6 +11,7 @@ export const PROVIDER_ENV_MAP = {
11
11
  export const SERVER_RESOLVED_PROVIDERS = new Set([
12
12
  "imgflip",
13
13
  "scrapecreators",
14
+ "apify",
14
15
  ]);
15
16
  export async function fetchRemoteKeys() {
16
17
  const res = await apiGetDashboard("/api/settings/keys");
package/dist/lib/poll.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { apiGet } from "./client.js";
2
- const DEFAULT_TERMINAL_STATUSES = ["completed", "failed"];
2
+ import { isTerminalRunStatus, normalizeRunStatus } from "./runStatus.js";
3
3
  function delay(ms) {
4
4
  return new Promise((resolve) => setTimeout(resolve, ms));
5
5
  }
6
6
  export async function pollUntilDone(opts) {
7
7
  const { path, intervalMs = 3000, timeoutMs = 600_000, terminalStatuses, onProgress, isDone, } = opts;
8
- const terminal = new Set([...DEFAULT_TERMINAL_STATUSES, ...(terminalStatuses ?? [])]);
8
+ const extraTerminal = new Set(terminalStatuses ?? []);
9
9
  const deadline = Date.now() + timeoutMs;
10
10
  while (true) {
11
11
  const res = await apiGet(path);
@@ -16,10 +16,13 @@ export async function pollUntilDone(opts) {
16
16
  if (!res.ok) {
17
17
  return { ok: false, data: res.data, timedOut: false };
18
18
  }
19
- if (status === "failed") {
19
+ const canonical = status ? normalizeRunStatus(status) : undefined;
20
+ if (canonical === "failed" || canonical === "cancelled") {
20
21
  return { ok: false, data: res.data, timedOut: false };
21
22
  }
22
- const statusTerminal = status ? terminal.has(status) : false;
23
+ const statusTerminal = status
24
+ ? isTerminalRunStatus(status) || extraTerminal.has(status)
25
+ : false;
23
26
  const customDone = isDone ? isDone(res.data) : true;
24
27
  if (statusTerminal && customDone) {
25
28
  return { ok: true, data: res.data, timedOut: false };
@@ -0,0 +1,4 @@
1
+ import type { ApiResponse } from "./client.js";
2
+ import { type Channel } from "./channel.js";
3
+ export declare function isSemanticApiError(data: unknown): boolean;
4
+ export declare function missingRouteLine(res: ApiResponse<unknown>, verb: string, channel?: Channel): string | undefined;
@@ -0,0 +1,22 @@
1
+ import { getChannel } from "./channel.js";
2
+ export function isSemanticApiError(data) {
3
+ if (typeof data !== "object" || data === null)
4
+ return false;
5
+ const record = data;
6
+ const topCode = record.code;
7
+ if (typeof topCode === "string" && topCode.length > 0)
8
+ return true;
9
+ const err = record.error;
10
+ if (typeof err !== "object" || err === null)
11
+ return false;
12
+ const nestedCode = err.code;
13
+ return typeof nestedCode === "string" && nestedCode.length > 0;
14
+ }
15
+ export function missingRouteLine(res, verb, channel = getChannel()) {
16
+ if (res.status !== 404)
17
+ return undefined;
18
+ if (isSemanticApiError(res.data))
19
+ return undefined;
20
+ return (`this server does not support ${verb} yet — ` +
21
+ `your backend has not been updated (channel: ${channel})`);
22
+ }
@@ -0,0 +1,22 @@
1
+ export declare const RUN_STATUSES: readonly ["queued", "running", "awaiting-approval", "succeeded", "succeeded-with-warnings", "failed", "cancelled"];
2
+ export type RunStatus = (typeof RUN_STATUSES)[number];
3
+ export declare const RUN_STATUS_LABELS: Record<RunStatus, string>;
4
+ export type RunStatusTone = "pending" | "active" | "attention" | "success" | "warning" | "danger" | "muted";
5
+ export declare const RUN_STATUS_TONES: Record<RunStatus, RunStatusTone>;
6
+ export declare const LEGACY_RUN_STATUS_ALIASES: Record<string, RunStatus>;
7
+ export declare function normalizeRunStatus(raw: string | null | undefined, fallback?: RunStatus): RunStatus;
8
+ export declare function runStatusLabel(raw: string | null | undefined): string;
9
+ export declare function runStatusTone(raw: string | null | undefined): RunStatusTone;
10
+ export declare const TERMINAL_RUN_STATUSES: readonly ["succeeded", "succeeded-with-warnings", "failed", "cancelled"];
11
+ export declare const ACTIVE_RUN_STATUSES: readonly ["queued", "running"];
12
+ export declare function isTerminalRunStatus(raw: string | null | undefined): boolean;
13
+ export declare function isActiveRunStatus(raw: string | null | undefined): boolean;
14
+ export declare const WORKFLOW_RUN_STATUS_VALUES: readonly ["queued", "running", "awaiting-approval", "succeeded", "succeeded-with-warnings", "failed", "cancelled"];
15
+ export declare const LEGACY_WORKFLOW_RUN_STATUS_VALUES: readonly ["awaiting-review", "completed", "partial", "canceled"];
16
+ export declare function storedWorkflowStatusForms(status: RunStatus): string[];
17
+ export declare function workflowRunPresentation(rawStatus: string | null | undefined, pauseReason?: string | null): {
18
+ status: RunStatus;
19
+ label: string;
20
+ tone: RunStatusTone;
21
+ detail?: string;
22
+ };
@@ -0,0 +1,139 @@
1
+ export const RUN_STATUSES = [
2
+ "queued",
3
+ "running",
4
+ "awaiting-approval",
5
+ "succeeded",
6
+ "succeeded-with-warnings",
7
+ "failed",
8
+ "cancelled",
9
+ ];
10
+ export const RUN_STATUS_LABELS = {
11
+ queued: "Queued",
12
+ running: "Running",
13
+ "awaiting-approval": "Awaiting approval",
14
+ succeeded: "Succeeded",
15
+ "succeeded-with-warnings": "Succeeded with warnings",
16
+ failed: "Failed",
17
+ cancelled: "Cancelled",
18
+ };
19
+ export const RUN_STATUS_TONES = {
20
+ queued: "pending",
21
+ running: "active",
22
+ "awaiting-approval": "attention",
23
+ succeeded: "success",
24
+ "succeeded-with-warnings": "warning",
25
+ failed: "danger",
26
+ cancelled: "muted",
27
+ };
28
+ export const LEGACY_RUN_STATUS_ALIASES = {
29
+ pending: "queued",
30
+ processing: "running",
31
+ "in-progress": "running",
32
+ walking: "running",
33
+ mining: "running",
34
+ scoring: "running",
35
+ selecting: "running",
36
+ writing: "running",
37
+ assembling: "running",
38
+ discovery: "running",
39
+ analyzing: "running",
40
+ combining: "running",
41
+ phase1_complete: "running",
42
+ phase2_complete: "running",
43
+ "phase1-running": "running",
44
+ "phase1-completed": "running",
45
+ "phase2-running": "running",
46
+ "script-prep-running": "running",
47
+ "parser-running": "running",
48
+ "storyboard-running": "running",
49
+ "cast-sheet-running": "running",
50
+ "storyboard-images-running": "running",
51
+ "voice-and-clips-running": "running",
52
+ stitching: "running",
53
+ extracting: "running",
54
+ ranking: "running",
55
+ conceptualizing: "running",
56
+ rendering: "running",
57
+ "awaiting-review": "awaiting-approval",
58
+ awaiting_hook_selection: "awaiting-approval",
59
+ paused: "awaiting-approval",
60
+ "script-review": "awaiting-approval",
61
+ "storyboard-review": "awaiting-approval",
62
+ "cast-sheet-review": "awaiting-approval",
63
+ completed: "succeeded",
64
+ complete: "succeeded",
65
+ done: "succeeded",
66
+ partial: "succeeded-with-warnings",
67
+ "partial-success": "succeeded-with-warnings",
68
+ "partial-error": "succeeded-with-warnings",
69
+ error: "failed",
70
+ stalled: "failed",
71
+ "verification-failed": "failed",
72
+ canceled: "cancelled",
73
+ superseded: "cancelled",
74
+ };
75
+ export function normalizeRunStatus(raw, fallback = "running") {
76
+ if (!raw)
77
+ return fallback;
78
+ if (RUN_STATUSES.includes(raw))
79
+ return raw;
80
+ return LEGACY_RUN_STATUS_ALIASES[raw] ?? fallback;
81
+ }
82
+ export function runStatusLabel(raw) {
83
+ return RUN_STATUS_LABELS[normalizeRunStatus(raw)];
84
+ }
85
+ export function runStatusTone(raw) {
86
+ return RUN_STATUS_TONES[normalizeRunStatus(raw)];
87
+ }
88
+ export const TERMINAL_RUN_STATUSES = [
89
+ "succeeded",
90
+ "succeeded-with-warnings",
91
+ "failed",
92
+ "cancelled",
93
+ ];
94
+ export const ACTIVE_RUN_STATUSES = ["queued", "running"];
95
+ export function isTerminalRunStatus(raw) {
96
+ return TERMINAL_RUN_STATUSES.includes(normalizeRunStatus(raw));
97
+ }
98
+ export function isActiveRunStatus(raw) {
99
+ return ACTIVE_RUN_STATUSES.includes(normalizeRunStatus(raw));
100
+ }
101
+ export const WORKFLOW_RUN_STATUS_VALUES = RUN_STATUSES;
102
+ export const LEGACY_WORKFLOW_RUN_STATUS_VALUES = [
103
+ "awaiting-review",
104
+ "completed",
105
+ "partial",
106
+ "canceled",
107
+ ];
108
+ const LEGACY_WORKFLOW_TWIN = {
109
+ "awaiting-approval": "awaiting-review",
110
+ succeeded: "completed",
111
+ "succeeded-with-warnings": "partial",
112
+ cancelled: "canceled",
113
+ };
114
+ export function storedWorkflowStatusForms(status) {
115
+ const twin = LEGACY_WORKFLOW_TWIN[status];
116
+ return twin ? [status, twin] : [status];
117
+ }
118
+ export function workflowRunPresentation(rawStatus, pauseReason) {
119
+ let status = normalizeRunStatus(rawStatus);
120
+ let detail;
121
+ if (status === "awaiting-approval") {
122
+ if (pauseReason === "call") {
123
+ status = "running";
124
+ detail = "sub-workflow";
125
+ }
126
+ else if (pauseReason === "slots") {
127
+ detail = "needs inputs";
128
+ }
129
+ else if (pauseReason === "repair") {
130
+ detail = "needs repair";
131
+ }
132
+ }
133
+ return {
134
+ status,
135
+ label: RUN_STATUS_LABELS[status],
136
+ tone: RUN_STATUS_TONES[status],
137
+ detail,
138
+ };
139
+ }
@@ -0,0 +1,3 @@
1
+ import type { WorkflowContractJson } from "../commands/workflow.js";
2
+ export declare function workflowToYaml(contract: WorkflowContractJson): string;
3
+ export declare function parseWorkflowText(text: string): unknown;
@@ -0,0 +1,113 @@
1
+ import yaml from "js-yaml";
2
+ function canonicalizeValue(value) {
3
+ if (Array.isArray(value))
4
+ return value.map(canonicalizeValue);
5
+ if (value !== null && typeof value === "object") {
6
+ const src = value;
7
+ const out = {};
8
+ for (const key of Object.keys(src).sort()) {
9
+ out[key] = canonicalizeValue(src[key]);
10
+ }
11
+ return out;
12
+ }
13
+ return value;
14
+ }
15
+ function orderKeys(record, fixedOrder) {
16
+ const out = {};
17
+ for (const key of fixedOrder) {
18
+ if (record[key] !== undefined)
19
+ out[key] = record[key];
20
+ }
21
+ const fixed = new Set(fixedOrder);
22
+ for (const key of Object.keys(record).sort()) {
23
+ if (!fixed.has(key) && record[key] !== undefined)
24
+ out[key] = record[key];
25
+ }
26
+ return out;
27
+ }
28
+ function canonicalizeContract(contract) {
29
+ const root = contract;
30
+ const nodes = Array.isArray(root.nodes) ? root.nodes : [];
31
+ const edges = Array.isArray(root.edges) ? root.edges : [];
32
+ const canonicalNodes = nodes.map((n) => {
33
+ const rec = { ...n };
34
+ if (rec.position !== null && typeof rec.position === "object") {
35
+ rec.position = orderKeys(rec.position, ["x", "y"]);
36
+ }
37
+ if (rec.config !== null && typeof rec.config === "object") {
38
+ rec.config = canonicalizeValue(rec.config);
39
+ }
40
+ return orderKeys(rec, ["id", "kind", "position", "config"]);
41
+ });
42
+ const canonicalEdges = edges.map((e) => orderKeys({ ...e }, [
43
+ "id",
44
+ "source",
45
+ "sourceHandle",
46
+ "target",
47
+ "targetHandle",
48
+ ]));
49
+ const rootRecord = {
50
+ ...root,
51
+ nodes: canonicalNodes,
52
+ edges: canonicalEdges,
53
+ };
54
+ if (Array.isArray(root.slots)) {
55
+ rootRecord.slots = root.slots.map((s) => orderKeys({ ...s }, [
56
+ "id",
57
+ "label",
58
+ "state",
59
+ "nodeId",
60
+ "configKey",
61
+ "value",
62
+ "hint",
63
+ ]));
64
+ }
65
+ if (Array.isArray(root.triggers)) {
66
+ rootRecord.triggers = root.triggers.map((t) => orderKeys({ ...t }, [
67
+ "type",
68
+ "event",
69
+ "cron",
70
+ "enabled",
71
+ ]));
72
+ }
73
+ return orderKeys(rootRecord, [
74
+ "contract",
75
+ "version",
76
+ "workflowId",
77
+ "updatedAt",
78
+ "name",
79
+ "description",
80
+ "slots",
81
+ "triggers",
82
+ "nodes",
83
+ "edges",
84
+ ]);
85
+ }
86
+ export function workflowToYaml(contract) {
87
+ return yaml.dump(canonicalizeContract(contract), {
88
+ schema: yaml.CORE_SCHEMA,
89
+ lineWidth: -1,
90
+ noRefs: true,
91
+ sortKeys: false,
92
+ indent: 2,
93
+ });
94
+ }
95
+ export function parseWorkflowText(text) {
96
+ try {
97
+ return JSON.parse(text);
98
+ }
99
+ catch {
100
+ }
101
+ let loaded;
102
+ try {
103
+ loaded = yaml.load(text, { schema: yaml.CORE_SCHEMA });
104
+ }
105
+ catch (error) {
106
+ const detail = error instanceof Error ? error.message : String(error);
107
+ throw new Error(`workflow file is not valid YAML or JSON: ${detail}`);
108
+ }
109
+ if (typeof loaded !== "object" || loaded === null || Array.isArray(loaded)) {
110
+ throw new Error("workflow file is not valid YAML or JSON");
111
+ }
112
+ return loaded;
113
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.7.200-beta.8",
3
+ "version": "2026.8.1100",
4
4
  "description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,8 +28,11 @@
28
28
  "prepack": "npm run build && npm run bundle-assets",
29
29
  "dev": "tsx bin/exodus.ts"
30
30
  },
31
- "dependencies": {},
31
+ "dependencies": {
32
+ "js-yaml": "^4.1.1"
33
+ },
32
34
  "devDependencies": {
35
+ "@types/js-yaml": "^4.0.9",
33
36
  "@types/node": "^20.0.0",
34
37
  "esbuild": "^0.25.0",
35
38
  "tsx": "^4.7.0",