@aicopycoders/exodus 2026.6.2400

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 (114) hide show
  1. package/assets/references/awareness-framework.md +156 -0
  2. package/assets/references/cast-video.md +165 -0
  3. package/assets/references/copy-blocks.md +138 -0
  4. package/assets/references/creative-strategy.md +990 -0
  5. package/assets/references/editing-rules.md +81 -0
  6. package/assets/references/hook-quality-checklist.md +153 -0
  7. package/assets/references/iterations.md +73 -0
  8. package/assets/references/scrawls.md +483 -0
  9. package/assets/references/segments.md +79 -0
  10. package/assets/skills/exodus/SKILL.md +50 -0
  11. package/assets/skills/exodus-brand/SKILL.md +117 -0
  12. package/assets/skills/exodus-browse/SKILL.md +96 -0
  13. package/assets/skills/exodus-creative/SKILL.md +94 -0
  14. package/assets/skills/exodus-drive/SKILL.md +118 -0
  15. package/assets/skills/exodus-foundation/SKILL.md +170 -0
  16. package/assets/skills/exodus-genesis/SKILL.md +159 -0
  17. package/assets/skills/exodus-idea/SKILL.md +78 -0
  18. package/assets/skills/exodus-image/SKILL.md +224 -0
  19. package/assets/skills/exodus-meme/SKILL.md +183 -0
  20. package/assets/skills/exodus-primer/SKILL.md +145 -0
  21. package/assets/skills/exodus-strategist/SKILL.md +217 -0
  22. package/assets/skills/exodus-swipe/SKILL.md +85 -0
  23. package/assets/skills/exodus-template/SKILL.md +106 -0
  24. package/assets/skills/exodus-write/SKILL.md +157 -0
  25. package/assets/skills/genesis-bots/.env.example +12 -0
  26. package/assets/skills/genesis-bots/README.txt +29 -0
  27. package/assets/skills/genesis-bots/SKILL.md +219 -0
  28. package/assets/skills/genesis-bots/scripts/genesis-stream.mjs +307 -0
  29. package/assets/skills/genesis-bots/scripts/genesis-stream.py +220 -0
  30. package/dist/bin/exodus.d.ts +2 -0
  31. package/dist/bin/exodus.js +177 -0
  32. package/dist/commands/__tests__/genesis.hookgate.test.d.ts +1 -0
  33. package/dist/commands/__tests__/genesis.hookgate.test.js +12 -0
  34. package/dist/commands/brand.d.ts +24 -0
  35. package/dist/commands/brand.js +292 -0
  36. package/dist/commands/browse.d.ts +9 -0
  37. package/dist/commands/browse.js +132 -0
  38. package/dist/commands/creative.d.ts +2 -0
  39. package/dist/commands/creative.js +223 -0
  40. package/dist/commands/doctor.d.ts +21 -0
  41. package/dist/commands/doctor.js +590 -0
  42. package/dist/commands/drive.d.ts +2 -0
  43. package/dist/commands/drive.js +154 -0
  44. package/dist/commands/foundation.d.ts +2 -0
  45. package/dist/commands/foundation.js +468 -0
  46. package/dist/commands/genesis.d.ts +64 -0
  47. package/dist/commands/genesis.js +564 -0
  48. package/dist/commands/idea.d.ts +53 -0
  49. package/dist/commands/idea.js +333 -0
  50. package/dist/commands/image.d.ts +2 -0
  51. package/dist/commands/image.js +249 -0
  52. package/dist/commands/init.d.ts +13 -0
  53. package/dist/commands/init.js +70 -0
  54. package/dist/commands/keys.d.ts +2 -0
  55. package/dist/commands/keys.js +101 -0
  56. package/dist/commands/meme.d.ts +12 -0
  57. package/dist/commands/meme.js +272 -0
  58. package/dist/commands/migrate.d.ts +3 -0
  59. package/dist/commands/migrate.js +158 -0
  60. package/dist/commands/primer.d.ts +2 -0
  61. package/dist/commands/primer.js +540 -0
  62. package/dist/commands/read-doc.d.ts +3 -0
  63. package/dist/commands/read-doc.js +62 -0
  64. package/dist/commands/status.d.ts +6 -0
  65. package/dist/commands/status.js +205 -0
  66. package/dist/commands/swipe.d.ts +2 -0
  67. package/dist/commands/swipe.js +398 -0
  68. package/dist/commands/template.d.ts +2 -0
  69. package/dist/commands/template.js +225 -0
  70. package/dist/commands/whoami.d.ts +2 -0
  71. package/dist/commands/whoami.js +51 -0
  72. package/dist/lib/ad-library-write.d.ts +15 -0
  73. package/dist/lib/ad-library-write.js +67 -0
  74. package/dist/lib/assets.d.ts +3 -0
  75. package/dist/lib/assets.js +23 -0
  76. package/dist/lib/backend-hint.d.ts +2 -0
  77. package/dist/lib/backend-hint.js +22 -0
  78. package/dist/lib/cc-command.d.ts +1 -0
  79. package/dist/lib/cc-command.js +13 -0
  80. package/dist/lib/client.d.ts +23 -0
  81. package/dist/lib/client.js +213 -0
  82. package/dist/lib/format.d.ts +8 -0
  83. package/dist/lib/format.js +172 -0
  84. package/dist/lib/intel-format.d.ts +1 -0
  85. package/dist/lib/intel-format.js +38 -0
  86. package/dist/lib/keys-sync.d.ts +16 -0
  87. package/dist/lib/keys-sync.js +88 -0
  88. package/dist/lib/layout.d.ts +38 -0
  89. package/dist/lib/layout.js +153 -0
  90. package/dist/lib/load-env.d.ts +1 -0
  91. package/dist/lib/load-env.js +47 -0
  92. package/dist/lib/pixar-orchestrator-types.d.ts +54 -0
  93. package/dist/lib/pixar-orchestrator-types.js +1 -0
  94. package/dist/lib/poll.d.ts +14 -0
  95. package/dist/lib/poll.js +32 -0
  96. package/dist/lib/preflight.d.ts +18 -0
  97. package/dist/lib/preflight.js +56 -0
  98. package/dist/lib/prompts.d.ts +9 -0
  99. package/dist/lib/prompts.js +99 -0
  100. package/dist/lib/reel-write.d.ts +14 -0
  101. package/dist/lib/reel-write.js +59 -0
  102. package/dist/lib/release.d.ts +4 -0
  103. package/dist/lib/release.js +81 -0
  104. package/dist/lib/scaffold.d.ts +8 -0
  105. package/dist/lib/scaffold.js +67 -0
  106. package/dist/lib/scout-hydrate.d.ts +1 -0
  107. package/dist/lib/scout-hydrate.js +10 -0
  108. package/dist/lib/state.d.ts +6 -0
  109. package/dist/lib/state.js +49 -0
  110. package/dist/lib/template-constants.d.ts +4 -0
  111. package/dist/lib/template-constants.js +85 -0
  112. package/dist/lib/version.d.ts +1 -0
  113. package/dist/lib/version.js +19 -0
  114. package/package.json +30 -0
@@ -0,0 +1,205 @@
1
+ import { apiGet } from "../lib/client.js";
2
+ import { formatGeneration, formatGenesisRun, formatError } from "../lib/format.js";
3
+ import { hydrateScoutIdeasCount } from "../lib/scout-hydrate.js";
4
+ import { formatIntelResult as formatIntelResultRich } from "../lib/intel-format.js";
5
+ export const helpText = `
6
+ exodus status — Check the status of a pipeline run
7
+
8
+ Usage:
9
+ exodus status --id <runId> [--type <pipeline>]
10
+
11
+ Required:
12
+ --id <runId> The generation or run ID returned by a prior exodus command
13
+
14
+ Options:
15
+ --type <pipeline> Pipeline type: generation (default) | genesis | intel | pulse | scout
16
+ | creative | template
17
+ Spark, Viral, Mirror, Remix runs use --type generation (default).
18
+ Intel, Pulse, Scout require --type intel|pulse|scout.
19
+ Creative-suite (native/copy-derived/ref-match/meme)
20
+ use --type creative; Template uses --type template.
21
+ `.trim();
22
+ async function fetchScoutIdeasCount(runId) {
23
+ const res = await apiGet(`/api/v2/scout/ideas?runId=${runId}&limit=1`);
24
+ if (res.ok && typeof res.data.count === "number")
25
+ return res.data.count;
26
+ return null;
27
+ }
28
+ export function formatIntelResult(data) {
29
+ return formatIntelResultRich(data).replace("## Intel Analysis Result", "## Intel Status");
30
+ }
31
+ export function formatPulseResult(data) {
32
+ const lines = [];
33
+ lines.push("## Pulse Status");
34
+ lines.push(`**Status:** ${data["status"] ?? "unknown"}`);
35
+ if (data["accountId"])
36
+ lines.push(`**Account ID:** ${data["accountId"]}`);
37
+ if (data["commentsAnalyzed"] !== undefined)
38
+ lines.push(`**Comments Analyzed:** ${data["commentsAnalyzed"]}`);
39
+ if (data["summary"])
40
+ lines.push(`**Summary:** ${data["summary"]}`);
41
+ if (data["phase1DocUrl"])
42
+ lines.push(`**Phase 1 Doc:** ${data["phase1DocUrl"]}`);
43
+ if (data["phase2DocUrl"])
44
+ lines.push(`**Phase 2 Doc:** ${data["phase2DocUrl"]}`);
45
+ if (data["reportUrl"])
46
+ lines.push(`**Report URL:** ${data["reportUrl"]}`);
47
+ if (data["error"])
48
+ lines.push(`**Error:** ${data["error"]}`);
49
+ const ideas = data["ideas"];
50
+ if (Array.isArray(ideas) && ideas.length > 0) {
51
+ lines.push(`\n**Ideas (${ideas.length}):**`);
52
+ for (const idea of ideas) {
53
+ if (typeof idea === "object" && idea !== null) {
54
+ const i = idea;
55
+ lines.push(` ${i["id"] ?? "?"} — ${i["name"] ?? i["title"] ?? i["idea"] ?? "(untitled)"}`);
56
+ }
57
+ }
58
+ }
59
+ return lines.join("\n");
60
+ }
61
+ export function formatScoutResult(data) {
62
+ const lines = [];
63
+ lines.push("## Scout Status");
64
+ lines.push(`**Status:** ${data["status"] ?? "unknown"}`);
65
+ const pipelineSlug = data["pipelineSlug"];
66
+ if (pipelineSlug)
67
+ lines.push(`**Pipeline:** ${pipelineSlug}`);
68
+ const mode = data["mode"];
69
+ if (mode)
70
+ lines.push(`**Mode:** ${mode}`);
71
+ const ideasGenerated = data["ideasGenerated"] ?? data["ideaCount"];
72
+ if (ideasGenerated !== undefined)
73
+ lines.push(`**Ideas Generated:** ${ideasGenerated}`);
74
+ const ideas = data["ideas"];
75
+ const ideasArr = Array.isArray(ideas)
76
+ ? ideas.filter((i) => typeof i === "object" && i !== null)
77
+ : [];
78
+ const docIdeas = ideasArr.filter((i) => typeof i["googleDocUrl"] === "string" && i["googleDocUrl"].length > 0);
79
+ if (docIdeas.length === 1) {
80
+ lines.push(`**Doc:** ${docIdeas[0]["googleDocUrl"]}`);
81
+ }
82
+ const sheetUrl = data["sheetUrl"];
83
+ if (sheetUrl)
84
+ lines.push(`**Sheet:** ${sheetUrl}`);
85
+ const sheetError = data["sheetError"];
86
+ if (!sheetUrl && sheetError)
87
+ lines.push(`**Sheet error:** ${sheetError}`);
88
+ if (ideasArr.length > 0) {
89
+ lines.push("");
90
+ lines.push(`**Ideas (${ideasArr.length}):**`);
91
+ for (const i of ideasArr) {
92
+ const status = String(i["status"] ?? "?");
93
+ const username = i["sourceUsername"] ?? "";
94
+ const hook = i["hook"] ?? "";
95
+ const docUrl = i["googleDocUrl"];
96
+ const pipelineError = i["pipelineError"];
97
+ const label = hook || username || i["sourceUrl"] || "(idea)";
98
+ const truncated = label.length > 80 ? `${label.slice(0, 80)}…` : label;
99
+ lines.push(` • [${status}] ${truncated}`);
100
+ if (docUrl)
101
+ lines.push(` ${docUrl}`);
102
+ else if (pipelineError)
103
+ lines.push(` error: ${pipelineError}`);
104
+ }
105
+ }
106
+ const scanSteps = data["scanSteps"];
107
+ if (Array.isArray(scanSteps) && scanSteps.length > 0) {
108
+ lines.push("");
109
+ lines.push("Scan breakdown:");
110
+ for (const step of scanSteps) {
111
+ if (typeof step === "object" && step !== null) {
112
+ const s = step;
113
+ const label = String(s["label"] ?? s["key"] ?? "step");
114
+ const detail = s["detail"] ? ` — ${s["detail"]}` : "";
115
+ lines.push(` ${label.padEnd(20)}${detail}`);
116
+ }
117
+ }
118
+ }
119
+ if (data["error"])
120
+ lines.push(`**Error:** ${data["error"]}`);
121
+ return lines.join("\n");
122
+ }
123
+ export function formatCreativeSuiteStatus(title, data) {
124
+ const lines = [];
125
+ lines.push(`## ${title}`);
126
+ lines.push(`**Status:** ${data["status"] ?? "unknown"}`);
127
+ const show = (label, key) => {
128
+ const v = data[key];
129
+ if (v !== undefined && v !== null && v !== "")
130
+ lines.push(`**${label}:** ${v}`);
131
+ };
132
+ show("Engine", "engine");
133
+ show("Mode", "mode");
134
+ show("Render mode", "renderMode");
135
+ show("Concepts", "totalConcepts");
136
+ show("Detected ads", "detectedAdCount");
137
+ if (data["totalImages"] !== undefined) {
138
+ lines.push(`**Images:** ${data["completedImages"] ?? 0}/${data["totalImages"]}`);
139
+ }
140
+ if (data["completedImageCount"] !== undefined) {
141
+ const failed = data["failedImageCount"] ? ` (${data["failedImageCount"]} failed)` : "";
142
+ lines.push(`**Images:** ${data["completedImageCount"]}${failed}`);
143
+ }
144
+ show("Final video", "finalVideoUrl");
145
+ show("Video", "videoUrl");
146
+ show("Cost (cents)", "totalCostCents");
147
+ if (data["errorMessage"])
148
+ lines.push(`**Error:** ${data["errorMessage"]}`);
149
+ else if (data["error"])
150
+ lines.push(`**Error:** ${data["error"]}`);
151
+ return lines.join("\n");
152
+ }
153
+ export async function run(flags) {
154
+ const id = flags["id"];
155
+ if (!id || typeof id !== "string") {
156
+ console.error("Error: --id <runId> is required");
157
+ process.exit(1);
158
+ }
159
+ const type = flags["type"] ?? "generation";
160
+ const pathMap = {
161
+ generation: `/api/v2/generations?id=${id}`,
162
+ genesis: `/api/v2/genesis?id=${id}`,
163
+ intel: `/api/v2/intel?id=${id}`,
164
+ pulse: `/api/v2/pulse?id=${id}`,
165
+ scout: `/api/v2/scout?runId=${id}`,
166
+ creative: `/api/v2/creative?runId=${id}`,
167
+ template: `/api/v2/template?runId=${id}`,
168
+ };
169
+ const path = pathMap[type];
170
+ if (!path) {
171
+ console.error(`Unknown type: "${type}". Valid types: generation, genesis, intel, pulse, scout, creative, template`);
172
+ process.exit(1);
173
+ }
174
+ const res = await apiGet(path);
175
+ if (!res.ok) {
176
+ console.log(formatError(res));
177
+ process.exit(1);
178
+ }
179
+ if (type === "scout") {
180
+ await hydrateScoutIdeasCount(id, res.data, fetchScoutIdeasCount);
181
+ }
182
+ switch (type) {
183
+ case "generation":
184
+ console.log(formatGeneration(res.data));
185
+ break;
186
+ case "genesis":
187
+ console.log(formatGenesisRun(res.data));
188
+ break;
189
+ case "intel":
190
+ console.log(formatIntelResult(res.data));
191
+ break;
192
+ case "pulse":
193
+ console.log(formatPulseResult(res.data));
194
+ break;
195
+ case "scout":
196
+ console.log(formatScoutResult(res.data));
197
+ break;
198
+ case "creative":
199
+ console.log(formatCreativeSuiteStatus("Creative Suite Status", res.data));
200
+ break;
201
+ case "template":
202
+ console.log(formatCreativeSuiteStatus("Template Status", res.data));
203
+ break;
204
+ }
205
+ }
@@ -0,0 +1,2 @@
1
+ export declare const helpText: string;
2
+ export declare function run(flags: Record<string, string | boolean>): Promise<void>;
@@ -0,0 +1,398 @@
1
+ import fs from "node:fs";
2
+ import { apiGet, apiPost } from "../lib/client.js";
3
+ import { formatError } from "../lib/format.js";
4
+ export const helpText = `
5
+ exodus swipe — Swipe pipeline lifecycle + competitor watchlist
6
+
7
+ The Swipe pipeline produces a weekly Google Doc per brand of Mirror-style
8
+ creative inspired by your watchlist's top competitor ads.
9
+
10
+ Usage:
11
+ exodus swipe run Trigger a run for the active brand
12
+ exodus swipe mine [--batch N] Scrape watchlist competitors (default batch=50)
13
+ exodus swipe status <runId> Poll a run by id
14
+ exodus swipe latest Show the latest run for the active brand
15
+ exodus swipe history [--limit N] Show recent runs (default 20, max 100)
16
+
17
+ exodus swipe brands list Show the watchlist
18
+ exodus swipe brands add "<Name>" [flags] Add a competitor
19
+ exodus swipe brands remove <id> Remove a competitor (use 'list' to find id)
20
+ exodus swipe brands toggle <id> Pause / resume a competitor
21
+ exodus swipe brands bulk-import <csvFile> Bulk add from CSV
22
+
23
+ Lifecycle flags:
24
+ --json Machine-readable JSON output (lifecycle subcommands)
25
+ --batch <n> Watchlist batch size for 'mine' (1-500, default 50)
26
+
27
+ Brand-add flags:
28
+ --ig <handle> Instagram handle (@ optional)
29
+ --fb <pageId> Facebook page ID
30
+ --website <url> Brand site (optional)
31
+ --cat <category> Free-form category
32
+ --youtube <handle> YouTube handle (@ optional)
33
+
34
+ Notes:
35
+ • All operations scope to your active brand. Check with: exodus brand current
36
+ • 'swipe run' returns the runId immediately; the pipeline runs async.
37
+ Poll with 'swipe status <id>' or call 'swipe latest' once done.
38
+ • Brand watchlist drives selection. Pausing a brand stops new scrapes
39
+ without losing history. Removing deletes the entry — past ads remain.
40
+ • CSV format: name,igHandle,fbPageId,category[,website][,youtubeHandle]
41
+
42
+ Examples:
43
+ exodus swipe run
44
+ exodus swipe status qn7eccmtrq3cn0khz6wpcaeyf186jn97
45
+ exodus swipe latest --json
46
+ exodus swipe brands list
47
+ exodus swipe brands add "AG1" --ig drinkag1 --fb 123456789 --cat supplements
48
+ `.trim();
49
+ export async function run(flags) {
50
+ const positional = parsePositional();
51
+ const [sub, ...rest] = positional;
52
+ if (sub === "run")
53
+ return runRunPipeline(flags);
54
+ if (sub === "mine")
55
+ return runMinePipeline(flags);
56
+ if (sub === "status")
57
+ return runStatus(rest, flags);
58
+ if (sub === "latest")
59
+ return runLatest(flags);
60
+ if (sub === "history")
61
+ return runHistory(flags);
62
+ if (sub === "brands") {
63
+ const [brandSub, ...brandRest] = rest;
64
+ if (!brandSub || brandSub === "list")
65
+ return runBrandsList();
66
+ if (brandSub === "add")
67
+ return runBrandsAdd(brandRest, flags);
68
+ if (brandSub === "remove")
69
+ return runBrandsRemove(brandRest);
70
+ if (brandSub === "toggle")
71
+ return runBrandsToggle(brandRest);
72
+ if (brandSub === "bulk-import")
73
+ return runBrandsBulkImport(brandRest);
74
+ console.error(`Unknown brands subcommand: "${brandSub}"\n`);
75
+ console.log(helpText);
76
+ process.exit(1);
77
+ }
78
+ if (!sub) {
79
+ console.log(helpText);
80
+ return;
81
+ }
82
+ console.error(`Unknown subcommand: "${sub}"\n`);
83
+ console.log(helpText);
84
+ process.exit(1);
85
+ }
86
+ function parsePositional() {
87
+ const args = process.argv.slice(3);
88
+ const out = [];
89
+ let i = 0;
90
+ while (i < args.length) {
91
+ const a = args[i];
92
+ if (a.startsWith("--")) {
93
+ const next = args[i + 1];
94
+ if (next !== undefined && !next.startsWith("--")) {
95
+ i += 2;
96
+ }
97
+ else {
98
+ i++;
99
+ }
100
+ continue;
101
+ }
102
+ out.push(a);
103
+ i++;
104
+ }
105
+ return out;
106
+ }
107
+ async function runRunPipeline(flags) {
108
+ const json = !!flags["json"];
109
+ const res = await apiPost("/api/v2/swipe/run", {});
110
+ if (!res.ok || !res.data.runId) {
111
+ if (json) {
112
+ console.log(JSON.stringify({ ok: false, status: res.status, data: res.data }));
113
+ }
114
+ else {
115
+ console.log(formatError(res));
116
+ }
117
+ process.exit(1);
118
+ }
119
+ if (json) {
120
+ console.log(JSON.stringify({ ok: true, runId: res.data.runId, triggerRunId: res.data.triggerRunId, status: res.data.status }));
121
+ return;
122
+ }
123
+ console.log(`Run started: ${res.data.runId}`);
124
+ console.log(` status: ${res.data.status ?? "selecting"}`);
125
+ if (res.data.triggerRunId)
126
+ console.log(` triggerRunId: ${res.data.triggerRunId}`);
127
+ console.log(`\nPoll: exodus swipe status ${res.data.runId}`);
128
+ console.log(`Latest: exodus swipe latest`);
129
+ }
130
+ async function runMinePipeline(flags) {
131
+ const json = !!flags["json"];
132
+ const batchRaw = flags["batch"];
133
+ const batchSize = typeof batchRaw === "string"
134
+ ? Math.max(1, Math.min(500, parseInt(batchRaw, 10) || 50))
135
+ : 50;
136
+ const res = await apiPost("/api/v2/swipe/mine", { batchSize });
137
+ if (!res.ok || !res.data.triggerRunId) {
138
+ if (json)
139
+ console.log(JSON.stringify({ ok: false, status: res.status, data: res.data }));
140
+ else
141
+ console.log(formatError(res));
142
+ process.exit(1);
143
+ }
144
+ if (json) {
145
+ console.log(JSON.stringify({ ok: true, triggerRunId: res.data.triggerRunId, batchSize: res.data.batchSize }));
146
+ return;
147
+ }
148
+ console.log(`Mine started: triggerRunId=${res.data.triggerRunId}`);
149
+ console.log(` batchSize: ${res.data.batchSize ?? batchSize}`);
150
+ console.log(`\nScrapes the active watchlist sequentially.`);
151
+ console.log(`Check progress: exodus swipe brands list`);
152
+ }
153
+ async function runStatus(positional, flags) {
154
+ const runId = positional[0];
155
+ if (!runId) {
156
+ console.error("Error: runId is required");
157
+ console.log("Usage: exodus swipe status <runId>");
158
+ process.exit(1);
159
+ }
160
+ const json = !!flags["json"];
161
+ const res = await apiGet(`/api/v2/swipe/status/${encodeURIComponent(runId)}`);
162
+ if (!res.ok) {
163
+ if (json)
164
+ console.log(JSON.stringify({ ok: false, status: res.status, data: res.data }));
165
+ else
166
+ console.log(formatError(res));
167
+ process.exit(1);
168
+ }
169
+ if (json) {
170
+ console.log(JSON.stringify({ ok: true, ...res.data }));
171
+ return;
172
+ }
173
+ printRunStatus(res.data);
174
+ }
175
+ async function runLatest(flags) {
176
+ const json = !!flags["json"];
177
+ const res = await apiGet("/api/v2/swipe/latest");
178
+ if (!res.ok) {
179
+ if (res.status === 404) {
180
+ if (json)
181
+ console.log(JSON.stringify({ ok: false, status: 404, error: "no runs yet" }));
182
+ else
183
+ console.log("No swipe runs yet for the active brand. Trigger one: exodus swipe run");
184
+ process.exit(1);
185
+ }
186
+ if (json)
187
+ console.log(JSON.stringify({ ok: false, status: res.status, data: res.data }));
188
+ else
189
+ console.log(formatError(res));
190
+ process.exit(1);
191
+ }
192
+ if (json) {
193
+ console.log(JSON.stringify({ ok: true, ...res.data }));
194
+ return;
195
+ }
196
+ printRunStatus(res.data);
197
+ if (!res.data.docUrl && res.data.fallbackMarkdown) {
198
+ console.log("\nDoc creation failed; fallback markdown follows (first 500 chars):");
199
+ console.log(res.data.fallbackMarkdown.slice(0, 500));
200
+ }
201
+ }
202
+ async function runHistory(flags) {
203
+ const json = !!flags["json"];
204
+ const limitRaw = flags["limit"];
205
+ const limit = typeof limitRaw === "string" ? Math.max(1, Math.min(100, parseInt(limitRaw, 10) || 20)) : 20;
206
+ const res = await apiGet(`/api/v2/swipe/history?limit=${limit}`);
207
+ if (!res.ok) {
208
+ if (json)
209
+ console.log(JSON.stringify({ ok: false, status: res.status, data: res.data }));
210
+ else
211
+ console.log(formatError(res));
212
+ process.exit(1);
213
+ }
214
+ const runs = res.data.runs ?? [];
215
+ if (json) {
216
+ console.log(JSON.stringify({ ok: true, runs }));
217
+ return;
218
+ }
219
+ if (runs.length === 0) {
220
+ console.log("No swipe runs yet. Trigger one: exodus swipe run");
221
+ return;
222
+ }
223
+ console.log(`Recent runs (${runs.length}):`);
224
+ for (const r of runs) {
225
+ const when = r.startedAt ? new Date(r.startedAt).toLocaleString() : "?";
226
+ const doc = r.docUrl ? ` doc=${r.docUrl}` : r.error ? ` error=${r.error}` : "";
227
+ console.log(` ${r.runId} ${r.status} ads=${r.adsSelectedCount} by=${r.triggeredBy} ${when}${doc}`);
228
+ }
229
+ }
230
+ function printRunStatus(r) {
231
+ console.log(`Run ${r.runId ?? "?"}`);
232
+ console.log(` brand: ${r.targetBrandSlug ?? "?"}`);
233
+ console.log(` status: ${r.status ?? "?"}`);
234
+ console.log(` by: ${r.triggeredBy ?? "?"}`);
235
+ console.log(` ads: ${r.adsSelectedCount ?? 0}`);
236
+ if (r.startedAt)
237
+ console.log(` started: ${new Date(r.startedAt).toLocaleString()}`);
238
+ if (r.completedAt)
239
+ console.log(` finished: ${new Date(r.completedAt).toLocaleString()}`);
240
+ if (r.docUrl)
241
+ console.log(` doc: ${r.docUrl}`);
242
+ if (r.error)
243
+ console.log(` error: ${r.error}`);
244
+ if (r.perAdSummary && r.perAdSummary.length > 0) {
245
+ const done = r.perAdSummary.filter((s) => s.status === "done").length;
246
+ const failed = r.perAdSummary.filter((s) => s.status === "failed").length;
247
+ console.log(` perAd: ${done} done, ${failed} failed, ${r.perAdSummary.length - done - failed} pending`);
248
+ }
249
+ }
250
+ async function runBrandsList() {
251
+ const res = await apiGet("/api/v2/brands");
252
+ if (!res.ok) {
253
+ console.log(formatError(res));
254
+ process.exit(1);
255
+ }
256
+ const brands = res.data.brands ?? [];
257
+ if (brands.length === 0) {
258
+ console.log("No brands on your watchlist yet.");
259
+ console.log("Add one: exodus swipe brands add \"<Brand Name>\" --fb <pageId>");
260
+ return;
261
+ }
262
+ console.log(`Watchlist (${brands.length}):`);
263
+ for (const b of brands) {
264
+ const sources = [
265
+ b.igHandle ? `ig=@${b.igHandle}` : null,
266
+ b.fbPageId ? `fb=${b.fbPageId}` : null,
267
+ b.youtubeHandle ? `yt=${b.youtubeHandle}` : null,
268
+ b.website ? `web=${b.website.replace(/^https?:\/\//, "")}` : null,
269
+ ]
270
+ .filter(Boolean)
271
+ .join(" ");
272
+ const cat = b.category ? `[${b.category}]` : "";
273
+ const last = b.lastScrapedAt
274
+ ? `last=${new Date(b.lastScrapedAt).toLocaleDateString()}`
275
+ : "never-scraped";
276
+ console.log(` ${b.id} ${b.name} ${cat}`);
277
+ if (sources)
278
+ console.log(` ${sources}`);
279
+ console.log(` ${last}`);
280
+ }
281
+ }
282
+ async function runBrandsAdd(positional, flags) {
283
+ const name = positional[0];
284
+ if (!name) {
285
+ console.error("Error: brand name is required");
286
+ console.log("Usage: exodus swipe brands add \"<Brand Name>\" [--ig handle] [--fb pageId]");
287
+ process.exit(1);
288
+ }
289
+ const body = { name };
290
+ const ig = strFlag(flags, "ig");
291
+ const fb = strFlag(flags, "fb");
292
+ const website = strFlag(flags, "website");
293
+ const cat = strFlag(flags, "cat") ?? strFlag(flags, "category");
294
+ const yt = strFlag(flags, "youtube") ?? strFlag(flags, "yt");
295
+ if (ig)
296
+ body.igHandle = ig.replace(/^@/, "");
297
+ if (fb)
298
+ body.fbPageId = fb;
299
+ if (website)
300
+ body.website = website;
301
+ if (cat)
302
+ body.category = cat;
303
+ if (yt)
304
+ body.youtubeHandle = yt.replace(/^@/, "");
305
+ const res = await apiPost("/api/v2/brands", body);
306
+ if (!res.ok) {
307
+ console.log(formatError(res));
308
+ process.exit(1);
309
+ }
310
+ console.log(`Added "${name}".`);
311
+ }
312
+ async function runBrandsRemove(positional) {
313
+ const id = positional[0];
314
+ if (!id) {
315
+ console.error("Error: brand id is required (run 'exodus swipe brands list' to find it)");
316
+ process.exit(1);
317
+ }
318
+ const res = await apiPost("/api/v2/brands/remove", { id });
319
+ if (!res.ok) {
320
+ console.log(formatError(res));
321
+ process.exit(1);
322
+ }
323
+ console.log(`Removed brand ${id}.`);
324
+ }
325
+ async function runBrandsToggle(positional) {
326
+ const id = positional[0];
327
+ if (!id) {
328
+ console.error("Error: brand id is required (run 'exodus swipe brands list' to find it)");
329
+ process.exit(1);
330
+ }
331
+ const res = await apiPost("/api/v2/brands/toggle", { id });
332
+ if (!res.ok) {
333
+ console.log(formatError(res));
334
+ process.exit(1);
335
+ }
336
+ const status = res.data.isActive ? "active" : "paused";
337
+ console.log(`Brand ${id} now ${status}.`);
338
+ }
339
+ async function runBrandsBulkImport(positional) {
340
+ const csvPath = positional[0];
341
+ if (!csvPath) {
342
+ console.error("Error: csv file path is required");
343
+ console.log("Usage: exodus swipe brands bulk-import <path-to-csv>");
344
+ process.exit(1);
345
+ }
346
+ if (!fs.existsSync(csvPath)) {
347
+ console.error(`Error: file not found: ${csvPath}`);
348
+ process.exit(1);
349
+ }
350
+ const text = fs.readFileSync(csvPath, "utf-8");
351
+ const brands = parseCsv(text);
352
+ if (brands.length === 0) {
353
+ console.error("Error: no rows parsed. CSV must have at least one row with a 'name' column.");
354
+ process.exit(1);
355
+ }
356
+ const res = await apiPost("/api/v2/brands/bulk", { brands });
357
+ if (!res.ok) {
358
+ console.log(formatError(res));
359
+ process.exit(1);
360
+ }
361
+ console.log(`Imported ${res.data.added ?? 0} new brand(s). Skipped ${res.data.skipped ?? 0} duplicate(s).`);
362
+ }
363
+ function parseCsv(text) {
364
+ const lines = text
365
+ .split(/\r?\n/)
366
+ .map((l) => l.trim())
367
+ .filter((l) => l.length > 0 && !l.startsWith("#"));
368
+ if (lines.length === 0)
369
+ return [];
370
+ const headerLine = lines[0];
371
+ const headers = headerLine.split(",").map((h) => h.trim());
372
+ const rows = [];
373
+ for (let i = 1; i < lines.length; i++) {
374
+ const cells = lines[i].split(",").map((c) => c.trim());
375
+ if (cells.length === 0 || !cells[0])
376
+ continue;
377
+ const row = {};
378
+ for (let j = 0; j < headers.length; j++) {
379
+ const h = headers[j];
380
+ const v = cells[j] ?? "";
381
+ if (!h || !v)
382
+ continue;
383
+ if (h === "igHandle" || h === "youtubeHandle") {
384
+ row[h] = v.replace(/^@/, "");
385
+ }
386
+ else {
387
+ row[h] = v;
388
+ }
389
+ }
390
+ if (row.name)
391
+ rows.push(row);
392
+ }
393
+ return rows;
394
+ }
395
+ function strFlag(flags, key) {
396
+ const v = flags[key];
397
+ return typeof v === "string" ? v : undefined;
398
+ }
@@ -0,0 +1,2 @@
1
+ export declare const helpText: string;
2
+ export declare function run(flags: Record<string, string | boolean>): Promise<void>;