@aicopycoders/exodus 2026.9.200 → 2026.9.1100-beta.1

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.
@@ -1,3 +1,4 @@
1
+ import { type AdAccountRef } from "../lib/meta-ads.js";
1
2
  export declare const helpText: string;
2
3
  export interface LocalValidation {
3
4
  ok: boolean;
@@ -8,3 +9,52 @@ export interface LocalValidation {
8
9
  export declare function validatePackageLocally(pkg: unknown): LocalValidation;
9
10
  export declare function contentTypeFor(filePath: string): string;
10
11
  export declare function run(flags: Record<string, string | boolean>): Promise<void>;
12
+ export declare const VALUE_FLAGS: Set<string>;
13
+ export declare function parsePositional(args?: string[]): string[];
14
+ export interface WinnerDefinitionView {
15
+ accountId?: string | null;
16
+ campaignRoleMap?: Record<string, string> | null;
17
+ ruleVariant?: string | null;
18
+ otherDefinition?: string | null;
19
+ ignoredCampaignIds?: string[] | null;
20
+ defaults?: {
21
+ window?: string | null;
22
+ resultsFloor?: number | null;
23
+ contributionLine?: number | null;
24
+ } | null;
25
+ lastAppliedAt?: number | null;
26
+ setupCompletedAt?: number | null;
27
+ summary?: WinnerDefinitionSummary | null;
28
+ }
29
+ export interface WinnerDefinitionSummaryGroup {
30
+ resultLabel?: string | null;
31
+ objective?: string | null;
32
+ instanceCount?: number | null;
33
+ creativeCount?: number | null;
34
+ totalResults?: number | null;
35
+ winnerCount?: number | null;
36
+ winnerShare?: number | null;
37
+ videoWinners?: number | null;
38
+ imageWinners?: number | null;
39
+ flatCurve?: boolean | null;
40
+ }
41
+ export interface WinnerDefinitionSummary {
42
+ instanceCount?: number | null;
43
+ creativeCount?: number | null;
44
+ winnerCount?: number | null;
45
+ groups?: WinnerDefinitionSummaryGroup[] | null;
46
+ ignoredCampaignCount?: number | null;
47
+ computedAt?: number | null;
48
+ }
49
+ export interface WinnerDefinitionResponse {
50
+ account?: AdAccountRef | null;
51
+ definition?: WinnerDefinitionView | null;
52
+ accounts?: AdAccountRef[];
53
+ }
54
+ export declare function roleTally(map: Record<string, string> | null | undefined): {
55
+ total: number;
56
+ line: string;
57
+ };
58
+ export declare function sharePercent(value: unknown): string;
59
+ export declare function summaryLines(summary: WinnerDefinitionSummary | null | undefined): string[];
60
+ export declare function formatDefinition(data: WinnerDefinitionResponse): string[];
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { apiGet, apiPost } from "../lib/client.js";
4
4
  import { displayRunStatus, formatError, tickerRunStatus } from "../lib/format.js";
5
5
  import { pollUntilDone } from "../lib/poll.js";
6
+ import { accountLine, accountRequiredLines, count as formatCount, dateOrDash, errorCode, } from "../lib/meta-ads.js";
6
7
  export const helpText = `
7
8
  exodus winners — Import your own brand's winning ads as generative fuel
8
9
 
@@ -16,6 +17,14 @@ Usage:
16
17
  exodus winners import <file.json | -> Push a winner package (- reads stdin)
17
18
  exodus winners status <importId> Re-poll an import later
18
19
  exodus winners list Winners Exodus already holds
20
+ exodus winners definition [--account act_…] What THIS brand means by "a winner"
21
+
22
+ Definition flags:
23
+ --account <id> Which connected ad account, e.g. act_1234567890. Required
24
+ only when the brand has more than one connected — a
25
+ definition read off the wrong account is a wrong answer that
26
+ looks right.
27
+ --json Machine-readable JSON output
19
28
 
20
29
  Import flags:
21
30
  --dry-run Local schema check + server dry-run: reports would-create vs
@@ -25,6 +34,10 @@ Import flags:
25
34
 
26
35
  Notes:
27
36
  • Scopes to your active brand's workspace (exodus brand current).
37
+ • \`definition\` reads the answers a human already gave on the dashboard — it
38
+ never writes, and it never shows a machine guess nobody has confirmed. A
39
+ brand with a Meta integration keeps its definition there, not in a local
40
+ file. Its ads read back with \`exodus ads list\`.
28
41
  • Requires your Scrape Creators API key (Settings → Keys) — the own-page
29
42
  match scrape bills your account.
30
43
  • Re-pushing the same file is safe: no duplicate rows, the verdict snapshot
@@ -38,6 +51,8 @@ Examples:
38
51
  cat winners.json | exodus winners import -
39
52
  exodus winners status k97abc...
40
53
  exodus winners list
54
+ exodus winners definition
55
+ exodus winners definition --account act_1234567890 --json
41
56
  `.trim();
42
57
  export function validatePackageLocally(pkg) {
43
58
  const errors = [];
@@ -116,6 +131,8 @@ export async function run(flags) {
116
131
  return runStatus(rest, flags);
117
132
  if (sub === "list")
118
133
  return runList(flags);
134
+ if (sub === "definition")
135
+ return runDefinition(flags);
119
136
  if (!sub) {
120
137
  console.log(helpText);
121
138
  return;
@@ -124,8 +141,24 @@ export async function run(flags) {
124
141
  console.log(helpText);
125
142
  process.exit(1);
126
143
  }
127
- function parsePositional() {
128
- return process.argv.slice(3).filter((a) => !a.startsWith("--"));
144
+ export const VALUE_FLAGS = new Set(["account"]);
145
+ export function parsePositional(args = process.argv.slice(3)) {
146
+ const out = [];
147
+ let i = 0;
148
+ while (i < args.length) {
149
+ const arg = args[i];
150
+ if (arg.startsWith("--")) {
151
+ const key = arg.slice(2).split("=", 1)[0] ?? "";
152
+ if (!arg.includes("=") && VALUE_FLAGS.has(key))
153
+ i += 2;
154
+ else
155
+ i++;
156
+ continue;
157
+ }
158
+ out.push(arg);
159
+ i++;
160
+ }
161
+ return out;
129
162
  }
130
163
  async function runImport(positional, flags) {
131
164
  const json = !!flags["json"];
@@ -409,3 +442,143 @@ async function runList(flags) {
409
442
  console.log(` ${w.verdictSentence}`);
410
443
  }
411
444
  }
445
+ const RULE_VARIANTS = {
446
+ standard: "standard — the smallest set of creatives that together carry most of a result group's results",
447
+ efficiency: "efficiency — the creatives with the best cost per result in each group",
448
+ "ignore-campaigns": "standard, with some campaigns deliberately left out of the count",
449
+ other: "written out in the brand's own words (below)",
450
+ };
451
+ export function roleTally(map) {
452
+ const entries = map && typeof map === "object" ? Object.values(map) : [];
453
+ const counts = new Map();
454
+ for (const role of entries) {
455
+ if (typeof role !== "string" || !role.trim())
456
+ continue;
457
+ const key = role.trim();
458
+ counts.set(key, (counts.get(key) ?? 0) + 1);
459
+ }
460
+ const order = ["testing", "scaling", "other"];
461
+ const parts = [...counts.entries()]
462
+ .sort((a, b) => {
463
+ const ai = order.indexOf(a[0]);
464
+ const bi = order.indexOf(b[0]);
465
+ return (ai === -1 ? order.length : ai) - (bi === -1 ? order.length : bi);
466
+ })
467
+ .map(([role, n]) => `${n} ${role}`);
468
+ return { total: entries.length, line: parts.join(" · ") || "none confirmed yet" };
469
+ }
470
+ export function sharePercent(value) {
471
+ if (typeof value !== "number" || !Number.isFinite(value))
472
+ return "—";
473
+ const pct = value <= 1 ? value * 100 : value;
474
+ return `${Math.round(pct)}%`;
475
+ }
476
+ export function summaryLines(summary) {
477
+ if (!summary) {
478
+ return ["Last run", " (the rule has not been run over this account yet)"];
479
+ }
480
+ const lines = [`Last run — ${dateOrDash(summary.computedAt)}`];
481
+ lines.push(` ${formatCount(summary.instanceCount)} ad instances → ${formatCount(summary.creativeCount)} distinct creatives · ${formatCount(summary.winnerCount)} winners`);
482
+ const groups = Array.isArray(summary.groups) ? summary.groups : [];
483
+ for (const group of groups) {
484
+ const label = group.resultLabel?.trim() || group.objective?.trim() || "(unlabelled group)";
485
+ lines.push(` ${label}: ${formatCount(group.winnerCount)} of ${formatCount(group.creativeCount)} creatives carry ${sharePercent(group.winnerShare)} of ${formatCount(group.totalResults)} results (${formatCount(group.videoWinners)} video / ${formatCount(group.imageWinners)} image)`);
486
+ if (group.flatCurve) {
487
+ lines.push(" flat curve — results spread evenly, so there is no clean winner set here; treat these as top contributors, not outliers.");
488
+ }
489
+ }
490
+ if (typeof summary.ignoredCampaignCount === "number" && summary.ignoredCampaignCount > 0) {
491
+ lines.push(` ${summary.ignoredCampaignCount} campaign(s) were left out of the count.`);
492
+ }
493
+ return lines;
494
+ }
495
+ export function formatDefinition(data) {
496
+ const accounts = Array.isArray(data.accounts) ? data.accounts : [];
497
+ const account = data.account ?? null;
498
+ const definition = data.definition ?? null;
499
+ if (!account) {
500
+ if (accounts.length === 0) {
501
+ return [
502
+ "No Meta ad account is connected to this brand.",
503
+ "Connect one on the dashboard (Settings → Meta) — then the daily sync fills in the ads and this definition.",
504
+ ];
505
+ }
506
+ const lines = ["Pick which ad account you mean:", ...accounts.map(accountLine)];
507
+ const first = accounts[0]?.accountId;
508
+ if (first) {
509
+ lines.push("");
510
+ lines.push(` exodus winners definition --account ${first}`);
511
+ }
512
+ return lines;
513
+ }
514
+ const label = account.name?.trim()
515
+ ? `${account.name.trim()} (${account.accountId ?? "?"})`
516
+ : String(account.accountId ?? "?");
517
+ if (!definition) {
518
+ return [
519
+ `No winner definition for ${label} yet.`,
520
+ "Someone has to say what a winner means for this account before the rule can run — that happens on the dashboard (Settings → Meta → winner setup).",
521
+ ];
522
+ }
523
+ const lines = [];
524
+ lines.push(`Winner definition — ${label}`);
525
+ lines.push(` setup: ${definition.setupCompletedAt ? `confirmed ${dateOrDash(definition.setupCompletedAt)}` : "not finished yet"}`);
526
+ const variant = definition.ruleVariant?.trim() ?? "";
527
+ lines.push(` rule: ${RULE_VARIANTS[variant] ?? (variant || "—")}`);
528
+ const defaults = definition.defaults ?? {};
529
+ lines.push(` dials: window ${defaults.window ?? "—"} · results floor ${formatCount(defaults.resultsFloor)} · contribution line ${sharePercent(defaults.contributionLine)}`);
530
+ const roles = roleTally(definition.campaignRoleMap);
531
+ lines.push(` campaigns: ${roles.total} with a confirmed role — ${roles.line}`);
532
+ const ignored = Array.isArray(definition.ignoredCampaignIds) ? definition.ignoredCampaignIds : [];
533
+ if (ignored.length > 0) {
534
+ lines.push(` ignored: ${ignored.length} campaign(s) left out of the rule`);
535
+ }
536
+ if (definition.lastAppliedAt) {
537
+ lines.push(` last run: ${dateOrDash(definition.lastAppliedAt)}`);
538
+ }
539
+ if (definition.otherDefinition?.trim()) {
540
+ lines.push("");
541
+ lines.push("In the brand's own words");
542
+ for (const line of definition.otherDefinition.trim().split("\n"))
543
+ lines.push(` ${line}`);
544
+ }
545
+ lines.push("");
546
+ lines.push(...summaryLines(definition.summary));
547
+ if (accounts.length > 1) {
548
+ lines.push("");
549
+ lines.push(`This brand has ${accounts.length} connected accounts — each keeps its own definition (--account).`);
550
+ }
551
+ return lines;
552
+ }
553
+ async function runDefinition(flags) {
554
+ const json = !!flags["json"];
555
+ const accountRaw = flags["account"];
556
+ if (accountRaw !== undefined && (typeof accountRaw !== "string" || !accountRaw.trim())) {
557
+ console.error("Error: --account needs an ad account id, e.g. act_1234567890");
558
+ console.log("Usage: exodus winners definition [--account act_…] [--json]");
559
+ process.exit(1);
560
+ }
561
+ const account = typeof accountRaw === "string" ? accountRaw.trim() : undefined;
562
+ const query = account ? `?account=${encodeURIComponent(account)}` : "";
563
+ const res = await apiGet(`/api/v2/winners/definition${query}`);
564
+ if (!res.ok) {
565
+ if (json) {
566
+ console.log(JSON.stringify({ ok: false, status: res.status, data: res.data }));
567
+ process.exit(1);
568
+ }
569
+ if (res.status === 400 && errorCode(res.data) === "ACCOUNT_REQUIRED") {
570
+ for (const line of accountRequiredLines(res, (id) => `exodus winners definition --account ${id}`)) {
571
+ console.log(line);
572
+ }
573
+ process.exit(1);
574
+ }
575
+ console.log(formatError(res));
576
+ process.exit(1);
577
+ }
578
+ if (json) {
579
+ console.log(JSON.stringify({ ok: true, ...res.data }));
580
+ return;
581
+ }
582
+ for (const line of formatDefinition(res.data))
583
+ console.log(line);
584
+ }
@@ -0,0 +1,108 @@
1
+ import { type ApiResponse } from "./client.js";
2
+ import { type Channel } from "./channel.js";
3
+ export interface AdAccountRef {
4
+ accountId?: string | null;
5
+ name?: string | null;
6
+ currency?: string | null;
7
+ }
8
+ export interface AdMedia {
9
+ image?: string | null;
10
+ video?: string | null;
11
+ poster?: string | null;
12
+ }
13
+ export interface AdWindowStats {
14
+ resultType?: string | null;
15
+ results?: number | null;
16
+ costPerResult?: number | null;
17
+ costPerResultDisplay?: string | null;
18
+ ctr?: number | null;
19
+ ctrDisplay?: string | null;
20
+ cpc?: number | null;
21
+ cpcDisplay?: string | null;
22
+ spend?: number | null;
23
+ spendDisplay?: string | null;
24
+ impressions?: number | null;
25
+ clicks?: number | null;
26
+ currency?: string | null;
27
+ }
28
+ export interface AdSummary extends AdWindowStats {
29
+ adId?: string | null;
30
+ accountId?: string | null;
31
+ name?: string | null;
32
+ status?: string | null;
33
+ format?: string | null;
34
+ campaign?: {
35
+ id?: string | null;
36
+ name?: string | null;
37
+ objective?: string | null;
38
+ } | null;
39
+ adset?: {
40
+ id?: string | null;
41
+ name?: string | null;
42
+ optimizationGoal?: string | null;
43
+ } | null;
44
+ createdTime?: string | number | null;
45
+ headline?: string | null;
46
+ bodyText?: string | null;
47
+ winnerStatus?: string | null;
48
+ media?: AdMedia | null;
49
+ syncedAt?: string | number | null;
50
+ }
51
+ export interface AdsListResponse {
52
+ ads?: AdSummary[];
53
+ count?: number;
54
+ window?: string;
55
+ sort?: string;
56
+ accounts?: AdAccountRef[];
57
+ }
58
+ export interface AdShowResponse extends AdSummary {
59
+ lifetime?: AdWindowStats | null;
60
+ last90d?: AdWindowStats | null;
61
+ commentCounts?: {
62
+ facebook?: number | null;
63
+ instagram?: number | null;
64
+ } | null;
65
+ }
66
+ export interface AdComment {
67
+ id?: string | null;
68
+ adId?: string | null;
69
+ platform?: string | null;
70
+ text?: string | null;
71
+ createdAt?: string | number | null;
72
+ likeCount?: number | null;
73
+ parentId?: string | null;
74
+ syncedAt?: string | number | null;
75
+ }
76
+ export interface CommentsResponse {
77
+ adId?: string | null;
78
+ comments?: AdComment[];
79
+ count?: number;
80
+ }
81
+ export interface FlowResult {
82
+ code: number;
83
+ lines: string[];
84
+ warnings?: string[];
85
+ }
86
+ export interface MetaAdsDeps {
87
+ get: (path: string) => Promise<ApiResponse<unknown>>;
88
+ channel: Channel;
89
+ apiUrl: () => string;
90
+ now: () => number;
91
+ }
92
+ export declare const defaultMetaAdsDeps: MetaAdsDeps;
93
+ export declare function displayOr(display: unknown, value: unknown, format: (n: number) => string): string;
94
+ export declare function money(n: number, currency?: string | null): string;
95
+ export declare function percent(n: number): string;
96
+ export declare function count(n: unknown): string;
97
+ export declare function sanitizeForTerminal(text: unknown): string;
98
+ export declare function safeText(text: unknown): string;
99
+ export declare function safeTextOr(text: unknown, fallback: string): string;
100
+ export declare function truncate(text: unknown, max: number): string;
101
+ export declare function dateOrDash(value: unknown): string;
102
+ export declare function table(headers: string[], rows: string[][]): string;
103
+ export declare function accountLine(account: AdAccountRef): string;
104
+ export declare function mediaUrl(apiBase: string, relative: unknown): string | null;
105
+ export declare function errorCode(data: unknown): string | undefined;
106
+ export declare function accountsOf(data: unknown): AdAccountRef[];
107
+ export declare function accountRequiredLines(res: ApiResponse<unknown>, example: (accountId: string) => string): string[];
108
+ export declare function errorFor(res: ApiResponse<unknown>, verb: string, json: boolean, deps: MetaAdsDeps, example: (accountId: string) => string): FlowResult | undefined;
@@ -0,0 +1,156 @@
1
+ import { apiGet, getApiUrl } from "./client.js";
2
+ import { formatApiError } from "./format.js";
3
+ import { auth401Hint } from "./backend-hint.js";
4
+ import { getChannel } from "./channel.js";
5
+ import { missingRouteLine } from "./route-support.js";
6
+ export const defaultMetaAdsDeps = {
7
+ get: (path) => apiGet(path),
8
+ channel: getChannel(),
9
+ apiUrl: () => getApiUrl(),
10
+ now: () => Date.now(),
11
+ };
12
+ export function displayOr(display, value, format) {
13
+ if (typeof display === "string" && display.trim())
14
+ return safeTextOr(display, "—");
15
+ if (typeof value === "number" && Number.isFinite(value))
16
+ return format(value);
17
+ return "—";
18
+ }
19
+ export function money(n, currency) {
20
+ const body = n.toLocaleString("en-US", {
21
+ minimumFractionDigits: n < 100 ? 2 : 0,
22
+ maximumFractionDigits: 2,
23
+ });
24
+ return currency ? `${body} ${currency}` : body;
25
+ }
26
+ export function percent(n) {
27
+ return `${n.toFixed(2)}%`;
28
+ }
29
+ export function count(n) {
30
+ return typeof n === "number" && Number.isFinite(n) ? n.toLocaleString("en-US") : "—";
31
+ }
32
+ const ESC_STRING_SEQUENCE = /\x1b[\]PX^_][\s\S]*?(?:\x07|\x1b\\|$)/g;
33
+ const ESC_CSI_SEQUENCE = /\x1b\[[0-?]*[ -\/]*[@-~]?/g;
34
+ const ESC_SIMPLE_SEQUENCE = /\x1b[ -\/]*[0-~]?/g;
35
+ const C0_AND_DEL = /[\x00-\x08\x0b-\x1f\x7f]/g;
36
+ const C1_CONTROLS = /[\u0080-\u009f]/g;
37
+ export function sanitizeForTerminal(text) {
38
+ if (typeof text !== "string" || text === "")
39
+ return "";
40
+ return text
41
+ .replace(ESC_STRING_SEQUENCE, "")
42
+ .replace(ESC_CSI_SEQUENCE, "")
43
+ .replace(ESC_SIMPLE_SEQUENCE, "")
44
+ .replace(C0_AND_DEL, "")
45
+ .replace(C1_CONTROLS, "");
46
+ }
47
+ export function safeText(text) {
48
+ return sanitizeForTerminal(text).trim();
49
+ }
50
+ export function safeTextOr(text, fallback) {
51
+ return safeText(text) || fallback;
52
+ }
53
+ export function truncate(text, max) {
54
+ if (typeof text !== "string")
55
+ return "";
56
+ const flat = sanitizeForTerminal(text).replace(/\s+/g, " ").trim();
57
+ if (flat.length <= max)
58
+ return flat;
59
+ const cut = flat.slice(0, max - 1);
60
+ const lastSpace = cut.lastIndexOf(" ");
61
+ const body = lastSpace > max * 0.6 ? cut.slice(0, lastSpace) : cut;
62
+ return `${body}…`;
63
+ }
64
+ export function dateOrDash(value) {
65
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
66
+ const d = new Date(value);
67
+ return Number.isNaN(d.getTime()) ? "—" : d.toISOString().slice(0, 10);
68
+ }
69
+ if (typeof value === "string" && value.trim()) {
70
+ const d = new Date(value);
71
+ return Number.isNaN(d.getTime())
72
+ ? safeText(value).slice(0, 10)
73
+ : d.toISOString().slice(0, 10);
74
+ }
75
+ return "—";
76
+ }
77
+ export function table(headers, rows) {
78
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)));
79
+ const fmt = (row) => row.map((cell, i) => (cell ?? "").padEnd(widths[i])).join(" ").trimEnd();
80
+ return [fmt(headers), fmt(headers.map((h) => "-".repeat(h.length))), ...rows.map(fmt)].join("\n");
81
+ }
82
+ export function accountLine(account) {
83
+ const id = safeTextOr(account.accountId, "(no id)");
84
+ const name = safeText(account.name);
85
+ const currency = safeText(account.currency);
86
+ const suffix = [name, currency ? `(${currency})` : ""].filter(Boolean).join(" ");
87
+ return suffix ? ` ${id} ${suffix}` : ` ${id}`;
88
+ }
89
+ export function mediaUrl(apiBase, relative) {
90
+ const rel = safeText(relative);
91
+ if (!rel)
92
+ return null;
93
+ if (/^https?:\/\//i.test(rel))
94
+ return rel;
95
+ const base = (apiBase || "").replace(/\/$/, "");
96
+ return `${base}${rel.startsWith("/") ? "" : "/"}${rel}`;
97
+ }
98
+ export function errorCode(data) {
99
+ if (typeof data !== "object" || data === null)
100
+ return undefined;
101
+ const rec = data;
102
+ if (typeof rec.code === "string" && rec.code)
103
+ return rec.code;
104
+ const err = rec.error;
105
+ if (typeof err === "object" && err !== null) {
106
+ const nested = err.code;
107
+ if (typeof nested === "string" && nested)
108
+ return nested;
109
+ }
110
+ return undefined;
111
+ }
112
+ export function accountsOf(data) {
113
+ if (typeof data !== "object" || data === null)
114
+ return [];
115
+ const raw = data.accounts;
116
+ return Array.isArray(raw) ? raw : [];
117
+ }
118
+ export function accountRequiredLines(res, example) {
119
+ const accounts = accountsOf(res.data);
120
+ const lines = [formatApiError(res)];
121
+ if (accounts.length > 0) {
122
+ lines.push("");
123
+ lines.push("Connected ad accounts:");
124
+ for (const account of accounts)
125
+ lines.push(accountLine(account));
126
+ const first = accounts[0]?.accountId;
127
+ if (first) {
128
+ lines.push("");
129
+ lines.push("Re-run naming the one you mean:");
130
+ lines.push(` ${example(first)}`);
131
+ }
132
+ }
133
+ return lines;
134
+ }
135
+ export function errorFor(res, verb, json, deps, example) {
136
+ if (res.ok)
137
+ return undefined;
138
+ if (json) {
139
+ return { code: 1, lines: [JSON.stringify({ ok: false, status: res.status, data: res.data })] };
140
+ }
141
+ const unsupported = missingRouteLine(res, verb, deps.channel);
142
+ if (unsupported)
143
+ return { code: 1, lines: [unsupported] };
144
+ if (res.status === 400 && errorCode(res.data) === "ACCOUNT_REQUIRED") {
145
+ return { code: 1, lines: accountRequiredLines(res, example) };
146
+ }
147
+ const lines = [formatApiError(res)];
148
+ if (res.status === 401) {
149
+ lines.push("");
150
+ lines.push(auth401Hint(deps.apiUrl()));
151
+ }
152
+ else if (res.status === 400 && errorCode(res.data) === "NO_BRAND") {
153
+ lines.push("Pick a brand first: exodus brand use <slug>");
154
+ }
155
+ return { code: 1, lines };
156
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicopycoders/exodus",
3
- "version": "2026.9.200",
3
+ "version": "2026.9.1100-beta.1",
4
4
  "description": "CLI + Claude Code skills for the Copycoders Exodus ad pipeline",
5
5
  "repository": {
6
6
  "type": "git",