@aicopycoders/exodus 2026.7.200-beta.11 → 2026.7.200-beta.13

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.
@@ -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,11 @@
1
1
  export declare function formatGeneration(data: Record<string, unknown>): string;
2
2
  export declare function formatGenesisRun(data: Record<string, unknown>): string;
3
3
  export declare function formatBrowse(generations: unknown[]): string;
4
+ export declare function formatApiError(res: {
5
+ ok: boolean;
6
+ status: number;
7
+ data: unknown;
8
+ }): string;
4
9
  export declare function formatError(res: {
5
10
  ok: boolean;
6
11
  status: number;
@@ -140,6 +140,37 @@ export function formatBrowse(generations) {
140
140
  }
141
141
  return lines.join("\n");
142
142
  }
143
+ export function formatApiError(res) {
144
+ let message;
145
+ let remedy;
146
+ const d = res.data;
147
+ if (d && typeof d === "object" && !Array.isArray(d)) {
148
+ const rec = d;
149
+ const err = rec["error"];
150
+ if (typeof err === "string" && err) {
151
+ message = err;
152
+ }
153
+ else if (err && typeof err === "object") {
154
+ const e = err;
155
+ if (typeof e["message"] === "string" && e["message"])
156
+ message = e["message"];
157
+ if (typeof e["remedy"] === "string" && e["remedy"])
158
+ remedy = e["remedy"];
159
+ }
160
+ if (!message && typeof rec["message"] === "string" && rec["message"]) {
161
+ message = rec["message"];
162
+ }
163
+ if (!remedy && typeof rec["remedy"] === "string" && rec["remedy"]) {
164
+ remedy = rec["remedy"];
165
+ }
166
+ }
167
+ else if (typeof d === "string" && d.trim()) {
168
+ message = d.trim();
169
+ }
170
+ if (!message)
171
+ message = `HTTP ${res.status}`;
172
+ return remedy ? `${message}\nfix: ${remedy}` : message;
173
+ }
143
174
  export function formatError(res) {
144
175
  const lines = [];
145
176
  lines.push(`## Error`);
@@ -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");
@@ -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,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.11",
3
+ "version": "2026.7.200-beta.13",
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",