@aicopycoders/exodus 2026.7.200-beta.12 → 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.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @aicopycoders/exodus
2
+
3
+ The Exodus CLI + Claude Code skills for the Copycoders ad pipeline. It drives the
4
+ Genesis creative bots, workflows, banks, and image/meme pipelines against your
5
+ brand — from your terminal and from Claude Code.
6
+
7
+ You don't install it globally. Run it with `npx` from your workspace folder.
8
+
9
+ ## Quickstart
10
+
11
+ ```bash
12
+ # 1. Scaffold (or refresh) the workspace in the current folder
13
+ npx -y @aicopycoders/exodus@beta init
14
+ ```
15
+
16
+ `init` creates the workspace (the `.claude/` skills, docs, and your brand
17
+ subfolder) and prints the next steps. The `@beta` tag is the dev/preview channel;
18
+ drop it (`@aicopycoders/exodus`) for the stable release channel.
19
+
20
+ ### Connect your account
21
+
22
+ 1. Open the Exodus dashboard → **Settings → Claude Code**.
23
+ 2. Copy the `.env` snippet it shows you (your `EXODUS_API_KEY` + dashboard URL)
24
+ and paste it into a `.env` file in your workspace folder.
25
+ 3. Pull your provider keys down so the local tools have them:
26
+
27
+ ```bash
28
+ npx -y @aicopycoders/exodus@beta keys pull
29
+ ```
30
+
31
+ This writes your dashboard provider + image keys into `.env` (it only upserts
32
+ those keys and preserves the rest of the file). Values are never printed.
33
+
34
+ ### Check everything is wired up
35
+
36
+ ```bash
37
+ npx -y @aicopycoders/exodus@beta doctor
38
+ ```
39
+
40
+ `doctor` diagnoses local and dashboard-side issues in one shot — keys, workspace
41
+ health, CLI currency — and tells you exactly what to fix.
42
+
43
+ ## Where to go next
44
+
45
+ - `npx -y @aicopycoders/exodus@beta --help` — the full command surface.
46
+ - `npx -y @aicopycoders/exodus@beta workflow --help` — run, build, and automate
47
+ saved multi-node workflows; resolve gate/repair parks from the inbox; read
48
+ banks and promote winners.
49
+
50
+ Inside Claude Code, just say **"exodus"** plus what you want
51
+ ("exodus, write me some ads") and the skills route the rest.
@@ -52,6 +52,7 @@ export interface BankPromoteBody {
52
52
  export interface BankPromoteResponse {
53
53
  entryId: string;
54
54
  bankName: string;
55
+ event?: string;
55
56
  }
56
57
  export interface FlowResult {
57
58
  code: number;
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import { apiGet, apiPost } from "../lib/client.js";
3
- import { formatError } from "../lib/format.js";
3
+ import { formatApiError } from "../lib/format.js";
4
4
  import { getChannel } from "../lib/channel.js";
5
5
  import { missingRouteLine } from "../lib/route-support.js";
6
6
  export const helpText = `
@@ -37,7 +37,7 @@ Examples:
37
37
  exodus bank list
38
38
  exodus bank show hooks
39
39
  exodus bank promote hooks "Stop scrolling — your knees will thank you"
40
- exodus bank promote body --file winner.txt --awareness solutionProductAware
40
+ exodus bank promote body:solutionProductAware --file winner.txt --awareness solutionProductAware
41
41
  cat winner.txt | exodus bank promote hooks
42
42
  exodus bank promote hooks "..." --spend 4200 --roas 3.1 --ctr 1.8 --note "Q3 winner"
43
43
  exodus bank promote hooks "..." --run wr_123 --node bot-4
@@ -66,7 +66,7 @@ function asErrorResult(res, json) {
66
66
  code: 1,
67
67
  lines: json
68
68
  ? [JSON.stringify({ ok: false, status: res.status, data: res.data })]
69
- : [formatError(res)],
69
+ : [formatApiError(res)],
70
70
  };
71
71
  }
72
72
  function table(headers, rows) {
@@ -26,6 +26,7 @@ const PROVIDER_LABEL = {
26
26
  kie: "Kie.ai",
27
27
  imgflip: "Imgflip",
28
28
  scrapecreators: "ScrapeCreators",
29
+ apify: "Apify",
29
30
  };
30
31
  async function status() {
31
32
  let remote;
@@ -63,6 +63,7 @@ export interface WorkflowImportResult {
63
63
  workflowId?: string;
64
64
  nodeCount: number;
65
65
  edgeCount: number;
66
+ triggers?: WorkflowTrigger[];
66
67
  unresolved: UnresolvedWorkflowRef[];
67
68
  warnings: string[];
68
69
  }
@@ -240,6 +241,11 @@ export type WorkflowArtifact = {
240
241
  type: "image";
241
242
  storageId: string;
242
243
  imageUrl?: string;
244
+ } | {
245
+ type: "session";
246
+ sessionId: string;
247
+ label?: string;
248
+ port?: string;
243
249
  };
244
250
  export interface WorkflowRunOutput {
245
251
  nodeId: string;
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import { apiGet, apiGetText, apiPost, apiPostDashboard, getDashboardUrl, } from "../lib/client.js";
3
- import { formatError } from "../lib/format.js";
3
+ import { formatApiError } from "../lib/format.js";
4
4
  import { pollUntilDone } from "../lib/poll.js";
5
5
  import { workflowToYaml, parseWorkflowText } from "../lib/workflowText.js";
6
6
  import { missingRouteLine } from "../lib/route-support.js";
@@ -207,7 +207,24 @@ function asErrorResult(res, json) {
207
207
  code: 1,
208
208
  lines: json
209
209
  ? [JSON.stringify({ ok: false, status: res.status, data: res.data })]
210
- : [formatError(res)],
210
+ : [formatApiError(res)],
211
+ };
212
+ }
213
+ function resolveIdErrorResult(e, json) {
214
+ if (e instanceof WorkflowResolveError) {
215
+ return {
216
+ code: 1,
217
+ lines: json
218
+ ? [JSON.stringify({ ok: false, status: e.status, data: e.data })]
219
+ : [e.message],
220
+ };
221
+ }
222
+ const message = e instanceof Error ? e.message : String(e);
223
+ return {
224
+ code: 1,
225
+ lines: json
226
+ ? [JSON.stringify({ ok: false, status: 0, data: { error: { message } } })]
227
+ : [message],
211
228
  };
212
229
  }
213
230
  function isRecord(value) {
@@ -252,12 +269,26 @@ function outputLines(output) {
252
269
  if (output.type === "primer") {
253
270
  return [` primer:${output.primerKind}: ${truncateText(output.text)}`];
254
271
  }
255
- return [` image: ${output.imageUrl ?? output.storageId}`];
272
+ if (output.type === "image") {
273
+ const url = output.imageUrl ?? output.storageId;
274
+ return url ? [` image: ${url}`] : [];
275
+ }
276
+ return [];
256
277
  }
257
- function progressLine(node) {
278
+ function progressLine(node, parked = false) {
258
279
  const err = node.error ? ` — error: ${node.error}` : "";
280
+ if (parked) {
281
+ return ` ⏸ ${node.nodeId} (${node.kind}) awaiting review`;
282
+ }
259
283
  return ` ${statusIcon(node.status)} ${node.nodeId} (${node.kind}) ${node.status}${err}`;
260
284
  }
285
+ function gateParkedNodeId(status, pauseReason, pausedNodeId) {
286
+ if (status !== "awaiting-review")
287
+ return undefined;
288
+ if (pauseReason === "taste" || pauseReason === undefined)
289
+ return pausedNodeId;
290
+ return undefined;
291
+ }
261
292
  export function formatPauseNotice(pauseReason, runId, dashboardUrl) {
262
293
  if (!pauseReason) {
263
294
  return [
@@ -398,6 +429,28 @@ export function formatImportSummary(result, mode = {}) {
398
429
  lines.push(`workflowId: ${result.workflowId}`);
399
430
  lines.push(`nodes: ${result.nodeCount}`);
400
431
  lines.push(`edges: ${result.edgeCount}`);
432
+ if (result.triggers && result.triggers.length > 0) {
433
+ lines.push("");
434
+ const enabledCount = result.triggers.filter((t) => t.enabled).length;
435
+ lines.push(`Triggers (${result.triggers.length}):`);
436
+ for (const t of result.triggers) {
437
+ const detail = t.type === "event" ? `event (${t.event})` : `cron (${t.cron})`;
438
+ if (t.enabled) {
439
+ const fires = t.type === "event"
440
+ ? t.event === "winner-promoted"
441
+ ? "fires on every promote"
442
+ : `fires on ${t.event}`
443
+ : `fires on schedule ${t.cron}`;
444
+ lines.push(` ⚠ ${detail} — ENABLED, ${fires}`);
445
+ }
446
+ else {
447
+ lines.push(` ${detail} — disabled`);
448
+ }
449
+ }
450
+ if (enabledCount > 0) {
451
+ lines.push(` ⚠ ${enabledCount} enabled trigger${enabledCount === 1 ? "" : "s"} armed — a background run may start on the owner's keys. Disable with: exodus workflow triggers <id> disable <n>`);
452
+ }
453
+ }
401
454
  if (result.unresolved.length > 0) {
402
455
  lines.push("");
403
456
  lines.push(`Unresolved references (${result.unresolved.length}):`);
@@ -430,11 +483,12 @@ export function formatWorkflowRun(run) {
430
483
  const inputs = Object.entries(run.inputs).map(([k, v]) => `${k}=${v}`).join(", ");
431
484
  lines.push(`inputs: ${inputs}`);
432
485
  }
486
+ const parkedNodeId = gateParkedNodeId(run.status, run.pauseReason, run.pausedNodeId);
433
487
  if (run.nodes.length > 0) {
434
488
  lines.push("");
435
489
  lines.push(`Nodes (${run.nodes.length}):`);
436
490
  for (const node of run.nodes) {
437
- lines.push(progressLine(node));
491
+ lines.push(progressLine(node, parkedNodeId !== undefined && node.nodeId === parkedNodeId));
438
492
  for (const output of node.outputs)
439
493
  lines.push(...outputLines(output));
440
494
  }
@@ -442,8 +496,9 @@ export function formatWorkflowRun(run) {
442
496
  if (run.outputs && run.outputs.length > 0) {
443
497
  lines.push("");
444
498
  lines.push(`Outputs (${run.outputs.length}):`);
445
- for (const output of run.outputs)
446
- lines.push(...runOutputLines(output));
499
+ for (const output of run.outputs) {
500
+ lines.push(...runOutputLines(output, parkedNodeId !== undefined && output.nodeId === parkedNodeId));
501
+ }
447
502
  }
448
503
  if (run.sessions && run.sessions.length > 0) {
449
504
  lines.push("");
@@ -454,30 +509,31 @@ export function formatWorkflowRun(run) {
454
509
  }
455
510
  return lines.join("\n");
456
511
  }
457
- function runOutputLines(output) {
512
+ function runOutputLines(output, awaitingReview = false) {
458
513
  const slug = output.botSlug ? ` (${output.botSlug})` : "";
514
+ const review = awaitingReview ? " — awaiting review, not yet approved" : "";
459
515
  if (output.type === "image") {
460
- return [` ${output.label} [image]${slug}: ${output.imageUrl ?? output.imageId ?? "(no url)"}`];
516
+ return [` ${output.label} [image]${slug}: ${output.imageUrl ?? output.imageId ?? "(no url)"}${review}`];
461
517
  }
462
518
  if (output.type === "video") {
463
519
  const tag = output.final === true ? "final video" : "video";
464
- return [` ${output.label} [${tag}]${slug}: ${output.videoUrl ?? "(no url)"}`];
520
+ return [` ${output.label} [${tag}]${slug}: ${output.videoUrl ?? "(no url)"}${review}`];
465
521
  }
466
522
  if (output.type === "audio") {
467
- return [` ${output.label} [audio]${slug}: ${output.audioUrl ?? "(no url)"}`];
523
+ return [` ${output.label} [audio]${slug}: ${output.audioUrl ?? "(no url)"}${review}`];
468
524
  }
469
525
  if (output.type === "frames") {
470
526
  const n = output.frames?.length ?? 0;
471
- return [` ${output.label} [frames]${slug}: ${n} scene${n === 1 ? "" : "s"}`];
527
+ return [` ${output.label} [frames]${slug}: ${n} scene${n === 1 ? "" : "s"}${review}`];
472
528
  }
473
529
  if (output.type === "storyboard") {
474
- return [` ${output.label} [storyboard]${slug}: use --json for the scene plan`];
530
+ return [` ${output.label} [storyboard]${slug}: use --json for the scene plan${review}`];
475
531
  }
476
532
  const raw = output.text ?? "";
477
533
  const normalized = raw.replace(/\s+/g, " ").trim();
478
534
  const body = truncateText(raw, 400);
479
535
  const note = normalized.length > 400 ? "\n (truncated — use --json for the full text)" : "";
480
- return [` ${output.label} [text]${slug}:`, ` ${body}${note}`];
536
+ return [` ${output.label} [text]${slug}${review}:`, ` ${body}${note}`];
481
537
  }
482
538
  export function formatDescribe(res) {
483
539
  const lines = [];
@@ -812,15 +868,31 @@ function formatImportError(res) {
812
868
  lines.push(`fix: ${remedy}`);
813
869
  return lines.join("\n");
814
870
  }
815
- return formatError(res);
871
+ return formatApiError(res);
872
+ }
873
+ class WorkflowResolveError extends Error {
874
+ status;
875
+ data;
876
+ constructor(status, data, message) {
877
+ super(message);
878
+ this.status = status;
879
+ this.data = data;
880
+ this.name = "WorkflowResolveError";
881
+ }
816
882
  }
817
883
  export async function resolveWorkflowId(ref, deps) {
818
884
  const res = await deps.get(LIST_PATH);
819
885
  if (!res.ok)
820
- throw new Error(formatError(res));
886
+ throw new WorkflowResolveError(res.status, res.data, formatApiError(res));
821
887
  const workflows = res.data.workflows ?? [];
822
- const match = workflows.find((w) => w.name.toLowerCase() === ref.toLowerCase());
823
- return match?._id ?? ref;
888
+ const byName = workflows.find((w) => w.name.toLowerCase() === ref.toLowerCase());
889
+ if (byName)
890
+ return byName._id;
891
+ const byId = workflows.find((w) => w._id === ref);
892
+ if (byId)
893
+ return byId._id;
894
+ const message = `No workflow named "${ref}" on this brand — run: exodus workflow list`;
895
+ throw new WorkflowResolveError(404, { error: { code: "NOT_FOUND", message } }, message);
824
896
  }
825
897
  export async function listFlow(json, deps) {
826
898
  const res = await deps.get(LIST_PATH);
@@ -838,7 +910,7 @@ export async function describeFlow(workflowRef, opts, deps) {
838
910
  workflowId = await resolveWorkflowId(workflowRef, deps);
839
911
  }
840
912
  catch (e) {
841
- return { code: 1, lines: [e instanceof Error ? e.message : String(e)] };
913
+ return resolveIdErrorResult(e, opts.json ?? false);
842
914
  }
843
915
  const res = await deps.get(`${DESCRIBE_PATH}?id=${encodeURIComponent(workflowId)}`);
844
916
  if (!res.ok)
@@ -872,7 +944,7 @@ export async function runFlow(workflowRef, opts, deps) {
872
944
  workflowId = await resolveWorkflowId(workflowRef, deps);
873
945
  }
874
946
  catch (e) {
875
- return { code: 1, lines: [e instanceof Error ? e.message : String(e)] };
947
+ return resolveIdErrorResult(e, opts.json ?? false);
876
948
  }
877
949
  const body = {
878
950
  workflowId,
@@ -927,12 +999,15 @@ async function waitForRun(runId, opts, deps) {
927
999
  opts.onProgressLine(line);
928
1000
  }
929
1001
  }
1002
+ const parkedNodeId = gateParkedNodeId(raw["status"], raw["pauseReason"], raw["pausedNodeId"]);
930
1003
  const nodes = Array.isArray(raw["nodes"]) ? raw["nodes"] : [];
931
1004
  for (const node of nodes) {
932
- if (seen.get(node.nodeId) === node.status)
1005
+ const isParked = parkedNodeId !== undefined && node.nodeId === parkedNodeId;
1006
+ const renderKey = `${node.status}:${isParked}`;
1007
+ if (seen.get(node.nodeId) === renderKey)
933
1008
  continue;
934
- seen.set(node.nodeId, node.status);
935
- opts.onProgressLine(progressLine(node));
1009
+ seen.set(node.nodeId, renderKey);
1010
+ opts.onProgressLine(progressLine(node, isParked));
936
1011
  }
937
1012
  },
938
1013
  });
@@ -977,7 +1052,7 @@ export async function exportFlow(workflowRef, opts, deps) {
977
1052
  workflowId = await resolveWorkflowId(workflowRef, deps);
978
1053
  }
979
1054
  catch (e) {
980
- return { code: 1, lines: [e instanceof Error ? e.message : String(e)] };
1055
+ return resolveIdErrorResult(e, opts.json ?? false);
981
1056
  }
982
1057
  const versionParam = opts.version !== undefined ? `&version=${encodeURIComponent(String(opts.version))}` : "";
983
1058
  const res = await deps.get(`${EXPORT_PATH}?id=${encodeURIComponent(workflowId)}${versionParam}`);
@@ -1011,7 +1086,7 @@ export async function versionsFlow(workflowRef, opts, deps, channel = getChannel
1011
1086
  workflowId = await resolveWorkflowId(workflowRef, deps);
1012
1087
  }
1013
1088
  catch (e) {
1014
- return { code: 1, lines: [e instanceof Error ? e.message : String(e)] };
1089
+ return resolveIdErrorResult(e, opts.json ?? false);
1015
1090
  }
1016
1091
  const res = await deps.get(`${VERSIONS_PATH}?id=${encodeURIComponent(workflowId)}`);
1017
1092
  const unsupported = missingRouteLine(res, "workflow versions", channel);
@@ -1148,7 +1223,7 @@ export async function triggersListFlow(workflowRef, opts, deps) {
1148
1223
  workflowId = await resolveWorkflowId(workflowRef, deps);
1149
1224
  }
1150
1225
  catch (e) {
1151
- return { code: 1, lines: [e instanceof Error ? e.message : String(e)] };
1226
+ return resolveIdErrorResult(e, opts.json ?? false);
1152
1227
  }
1153
1228
  const fetched = await fetchTriggers(workflowId, deps);
1154
1229
  if (!fetched.ok)
@@ -1177,7 +1252,7 @@ export async function triggersSetEnabledFlow(workflowRef, n, enabled, opts, deps
1177
1252
  workflowId = await resolveWorkflowId(workflowRef, deps);
1178
1253
  }
1179
1254
  catch (e) {
1180
- return { code: 1, lines: [e instanceof Error ? e.message : String(e)] };
1255
+ return resolveIdErrorResult(e, opts.json ?? false);
1181
1256
  }
1182
1257
  const fetched = await fetchTriggers(workflowId, deps);
1183
1258
  if (!fetched.ok)
@@ -1206,7 +1281,7 @@ export async function triggersFireFlow(workflowRef, opts, deps) {
1206
1281
  workflowId = await resolveWorkflowId(workflowRef, deps);
1207
1282
  }
1208
1283
  catch (e) {
1209
- return { code: 1, lines: [e instanceof Error ? e.message : String(e)] };
1284
+ return resolveIdErrorResult(e, opts.json ?? false);
1210
1285
  }
1211
1286
  const fetched = await fetchTriggers(workflowId, deps);
1212
1287
  if (!fetched.ok)
@@ -1335,9 +1410,9 @@ function formatTextError(res) {
1335
1410
  }
1336
1411
  catch {
1337
1412
  const snippet = body.replace(/\s+/g, " ").trim().slice(0, 300);
1338
- return formatError({ ok: false, status: res.status, data: snippet || "(empty response)" });
1413
+ return formatApiError({ ok: false, status: res.status, data: snippet || "(empty response)" });
1339
1414
  }
1340
- return formatError({ ok: false, status: res.status, data: parsed });
1415
+ return formatApiError({ ok: false, status: res.status, data: parsed });
1341
1416
  }
1342
1417
  export async function schemaFlow(opts, deps) {
1343
1418
  const res = await deps.get(SCHEMA_PATH);
@@ -1358,9 +1433,6 @@ export async function schemaFlow(opts, deps) {
1358
1433
  }
1359
1434
  return { code: 0, lines: [formatSchema(payload)] };
1360
1435
  }
1361
- function shortId(id) {
1362
- return id.length <= 12 ? id : `${id.slice(0, 11)}…`;
1363
- }
1364
1436
  export function formatAge(value, now = Date.now()) {
1365
1437
  if (value === undefined || value === null)
1366
1438
  return "-";
@@ -1399,7 +1471,7 @@ export function formatInbox(rows, now = Date.now()) {
1399
1471
  if (rows.length === 0)
1400
1472
  return NO_INBOX;
1401
1473
  return table(["run", "workflow", "kind", "node", "via", "age"], rows.map((r) => [
1402
- shortId(r._id),
1474
+ r._id,
1403
1475
  r.workflowName || "(unnamed)",
1404
1476
  parkBadge(r.pauseReason),
1405
1477
  r.pausedNodeId ?? "-",
@@ -1607,17 +1679,28 @@ export async function gateEditFlow(runId, n, sources, opts, deps) {
1607
1679
  return triggerErrorResult(res, "workflow gate edit", opts.json);
1608
1680
  return okLine(`Edited candidate ${n} at ${pf.run.pausedNodeId}.`, { ok: true, runId, nodeId: pf.run.pausedNodeId, outputIndex: cand.outputIndex }, opts.json);
1609
1681
  }
1682
+ function resolveGateSessionId(run) {
1683
+ const gateNode = (run.nodes ?? []).find((x) => x.nodeId === run.pausedNodeId);
1684
+ const artifact = (gateNode?.outputs ?? []).find((a) => a.type === "session" && a.port === "session" && !!a.sessionId);
1685
+ if (artifact)
1686
+ return artifact.sessionId;
1687
+ const sessions = run.sessions ?? [];
1688
+ const byNode = sessions.find((s) => s.nodeId === run.pausedNodeId);
1689
+ if (byNode)
1690
+ return byNode.sessionId;
1691
+ return undefined;
1692
+ }
1610
1693
  export async function gatePushFlow(runId, message, opts, deps) {
1611
1694
  const pf = await preflightPark(runId, "taste", "workflow gate push", opts.json, deps);
1612
1695
  if (!pf.ok)
1613
1696
  return pf.result;
1614
- const session = (pf.run.sessions ?? []).find((s) => s.nodeId === pf.run.pausedNodeId);
1615
- if (!session)
1697
+ const sessionId = resolveGateSessionId(pf.run);
1698
+ if (!sessionId)
1616
1699
  return errLine("This gate has no live session — nothing to push to.", opts.json);
1617
1700
  if (!deps.postDashboard) {
1618
1701
  return errLine("gate push is unavailable here (no dashboard client).", opts.json);
1619
1702
  }
1620
- const chat = await deps.postDashboard(CHAT_PATH, { sessionId: session.sessionId, text: message }, { timeoutMs: CHAT_TIMEOUT_MS });
1703
+ const chat = await deps.postDashboard(CHAT_PATH, { sessionId, text: message }, { timeoutMs: CHAT_TIMEOUT_MS });
1621
1704
  if (!chat.ok) {
1622
1705
  const err = routeErrorText(chat.data, chat.status);
1623
1706
  return {
@@ -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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.7.200-beta.12",
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",