@aicopycoders/exodus 2026.7.200-beta.9 → 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 +2 -0
  3. package/assets/docs/PIPELINES.md +26 -0
  4. package/assets/skills/exodus/SKILL.md +5 -2
  5. package/assets/skills/exodus-meme/SKILL.md +44 -28
  6. package/assets/skills/exodus-winners/SKILL.md +29 -20
  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
@@ -0,0 +1,202 @@
1
+ import { apiGet, apiPostDashboard } from "../lib/client.js";
2
+ import { formatError } from "../lib/format.js";
3
+ import { getChannel } from "../lib/channel.js";
4
+ import { missingRouteLine } from "../lib/route-support.js";
5
+ export const helpText = `
6
+ exodus session — Inspect and continue workflow chat sessions
7
+
8
+ Usage:
9
+ exodus session list [--json]
10
+ exodus session show <sessionId> [--json]
11
+ exodus session chat <sessionId> "message" [--json]
12
+
13
+ Flags:
14
+ --json Machine-readable JSON output (the raw API response)
15
+
16
+ Notes:
17
+ Sessions are born from workflow runs whose bots run in session-mode — you do
18
+ not create them here. "session list" shows the sessions on the active brand;
19
+ "session show" prints a session's turns (system rows are hidden in the human
20
+ view but kept in --json); "session chat" appends your message and prints the
21
+ assistant's reply. The chat route can take up to ~5 minutes to answer.
22
+
23
+ Examples:
24
+ exodus session list
25
+ exodus session show sess_123
26
+ exodus session chat sess_123 "make the hook punchier"
27
+ `.trim();
28
+ const SESSIONS_PATH = "/api/v2/sessions";
29
+ const SESSION_SHOW_PATH = "/api/v2/sessions/show";
30
+ const CHAT_PATH = "/api/sessions/chat";
31
+ const CHAT_TIMEOUT_MS = 320_000;
32
+ const defaultDeps = {
33
+ get: (path) => apiGet(path),
34
+ postDashboard: (path, body, opts) => apiPostDashboard(path, body, opts),
35
+ channel: getChannel(),
36
+ };
37
+ function isRecord(value) {
38
+ return typeof value === "object" && value !== null && !Array.isArray(value);
39
+ }
40
+ export function shortId(id) {
41
+ return id.length <= 12 ? id : `${id.slice(0, 11)}…`;
42
+ }
43
+ export function formatAge(value, now = Date.now()) {
44
+ if (value === undefined || value === null)
45
+ return "-";
46
+ const t = typeof value === "number" ? value : Date.parse(value);
47
+ if (Number.isNaN(t))
48
+ return "-";
49
+ const sec = Math.max(0, Math.floor((now - t) / 1000));
50
+ if (sec < 60)
51
+ return `${sec}s`;
52
+ const min = Math.floor(sec / 60);
53
+ if (min < 60)
54
+ return `${min}m`;
55
+ const hr = Math.floor(min / 60);
56
+ if (hr < 24)
57
+ return `${hr}h`;
58
+ return `${Math.floor(hr / 24)}d`;
59
+ }
60
+ function table(headers, rows) {
61
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)));
62
+ const fmt = (row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
63
+ return [fmt(headers), fmt(headers.map((h) => "-".repeat(h.length))), ...rows.map(fmt)].join("\n");
64
+ }
65
+ const NO_SESSIONS = "No sessions yet. Sessions are born from workflow runs whose bots run in session-mode.";
66
+ export function formatSessionList(sessions, now = Date.now()) {
67
+ if (sessions.length === 0)
68
+ return NO_SESSIONS;
69
+ return table(["id", "title", "bot", "run", "age"], sessions.map((s) => [
70
+ shortId(s._id),
71
+ s.title || "(untitled)",
72
+ s.botSlug,
73
+ s.runId ? shortId(s.runId) : "-",
74
+ formatAge(s.lastTouchedAt, now),
75
+ ]));
76
+ }
77
+ export function formatSessionShow(data) {
78
+ const s = data.session;
79
+ const lines = [];
80
+ lines.push(`Session — ${s.title || "(untitled)"}`);
81
+ lines.push(`sessionId: ${s._id}`);
82
+ lines.push(`bot: ${s.botSlug}`);
83
+ lines.push(`run: ${s.runId ?? "-"}`);
84
+ const turns = (data.messages ?? []).filter((m) => m.role !== "system");
85
+ lines.push("");
86
+ if (turns.length === 0) {
87
+ lines.push("(no messages yet)");
88
+ }
89
+ else {
90
+ for (const m of turns)
91
+ lines.push(`${m.role}: ${m.body}`);
92
+ }
93
+ return lines.join("\n");
94
+ }
95
+ function routeErrorText(data, status) {
96
+ if (isRecord(data)) {
97
+ const err = data.error;
98
+ if (typeof err === "string" && err)
99
+ return err;
100
+ if (isRecord(err) && typeof err.message === "string")
101
+ return err.message;
102
+ if (typeof data.message === "string" && data.message)
103
+ return data.message;
104
+ }
105
+ return `HTTP ${status}`;
106
+ }
107
+ function apiErrorResult(res, verb, json, channel) {
108
+ const missing = missingRouteLine(res, verb, channel);
109
+ if (missing) {
110
+ return {
111
+ code: 1,
112
+ lines: json
113
+ ? [JSON.stringify({ ok: false, status: res.status, error: missing })]
114
+ : [missing],
115
+ };
116
+ }
117
+ return {
118
+ code: 1,
119
+ lines: json
120
+ ? [JSON.stringify({ ok: false, status: res.status, data: res.data })]
121
+ : [formatError(res)],
122
+ };
123
+ }
124
+ export async function listFlow(json, deps) {
125
+ const res = await deps.get(SESSIONS_PATH);
126
+ if (!res.ok)
127
+ return apiErrorResult(res, "session list", json, deps.channel);
128
+ const data = res.data;
129
+ return {
130
+ code: 0,
131
+ lines: json ? [JSON.stringify(data)] : [formatSessionList(data.sessions ?? [])],
132
+ };
133
+ }
134
+ export async function showFlow(sessionId, opts, deps) {
135
+ const res = await deps.get(`${SESSION_SHOW_PATH}?sessionId=${encodeURIComponent(sessionId)}`);
136
+ if (!res.ok)
137
+ return apiErrorResult(res, "session show", opts.json, deps.channel);
138
+ const data = res.data;
139
+ return {
140
+ code: 0,
141
+ lines: opts.json ? [JSON.stringify(data)] : [formatSessionShow(data)],
142
+ };
143
+ }
144
+ export async function chatFlow(sessionId, message, opts, deps) {
145
+ const res = await deps.postDashboard(CHAT_PATH, { sessionId, text: message }, { timeoutMs: CHAT_TIMEOUT_MS });
146
+ if (!res.ok) {
147
+ const err = routeErrorText(res.data, res.status);
148
+ return {
149
+ code: 1,
150
+ lines: opts.json
151
+ ? [JSON.stringify({ ok: false, status: res.status, error: err })]
152
+ : [`exodus session chat: ${err} (HTTP ${res.status})`],
153
+ };
154
+ }
155
+ const data = res.data;
156
+ return {
157
+ code: 0,
158
+ lines: opts.json ? [JSON.stringify(data)] : [data.reply ?? ""],
159
+ };
160
+ }
161
+ function parsePositional(args = process.argv.slice(3)) {
162
+ return args.filter((a) => !a.startsWith("--"));
163
+ }
164
+ async function printResult(result) {
165
+ for (const line of result.lines)
166
+ console.log(line);
167
+ if (result.code !== 0)
168
+ process.exit(result.code);
169
+ }
170
+ export async function run(flags) {
171
+ const positional = parsePositional();
172
+ const [sub, ...rest] = positional;
173
+ const json = !!flags["json"];
174
+ if (!sub) {
175
+ console.log(helpText);
176
+ return;
177
+ }
178
+ if (sub === "list")
179
+ return printResult(await listFlow(json, defaultDeps));
180
+ if (sub === "show") {
181
+ const sessionId = rest[0];
182
+ if (!sessionId) {
183
+ console.error("Error: session show requires <sessionId>.");
184
+ console.log("Usage: exodus session show <sessionId> [--json]");
185
+ process.exit(1);
186
+ }
187
+ return printResult(await showFlow(sessionId, { json }, defaultDeps));
188
+ }
189
+ if (sub === "chat") {
190
+ const sessionId = rest[0];
191
+ const message = rest[1];
192
+ if (!sessionId || !message) {
193
+ console.error('Error: session chat requires <sessionId> and a "message".');
194
+ console.log('Usage: exodus session chat <sessionId> "message" [--json]');
195
+ process.exit(1);
196
+ }
197
+ return printResult(await chatFlow(sessionId, message, { json }, defaultDeps));
198
+ }
199
+ console.error(`Unknown subcommand: "${sub}"\n`);
200
+ console.log(helpText);
201
+ process.exit(1);
202
+ }
@@ -1,5 +1,5 @@
1
1
  import { apiGet } from "../lib/client.js";
2
- import { formatGeneration, formatGenesisRun, formatError } from "../lib/format.js";
2
+ import { formatGeneration, formatGenesisRun, formatError, displayRunStatus } from "../lib/format.js";
3
3
  import { hydrateScoutIdeasCount } from "../lib/scout-hydrate.js";
4
4
  import { formatIntelResult as formatIntelResultRich } from "../lib/intel-format.js";
5
5
  export const helpText = `
@@ -31,7 +31,7 @@ export function formatIntelResult(data) {
31
31
  export function formatPulseResult(data) {
32
32
  const lines = [];
33
33
  lines.push("## Pulse Status");
34
- lines.push(`**Status:** ${data["status"] ?? "unknown"}`);
34
+ lines.push(`**Status:** ${displayRunStatus(data["status"])}`);
35
35
  if (data["accountId"])
36
36
  lines.push(`**Account ID:** ${data["accountId"]}`);
37
37
  if (data["commentsAnalyzed"] !== undefined)
@@ -61,7 +61,7 @@ export function formatPulseResult(data) {
61
61
  export function formatScoutResult(data) {
62
62
  const lines = [];
63
63
  lines.push("## Scout Status");
64
- lines.push(`**Status:** ${data["status"] ?? "unknown"}`);
64
+ lines.push(`**Status:** ${displayRunStatus(data["status"])}`);
65
65
  const pipelineSlug = data["pipelineSlug"];
66
66
  if (pipelineSlug)
67
67
  lines.push(`**Pipeline:** ${pipelineSlug}`);
@@ -123,7 +123,7 @@ export function formatScoutResult(data) {
123
123
  export function formatCreativeSuiteStatus(title, data) {
124
124
  const lines = [];
125
125
  lines.push(`## ${title}`);
126
- lines.push(`**Status:** ${data["status"] ?? "unknown"}`);
126
+ lines.push(`**Status:** ${displayRunStatus(data["status"])}`);
127
127
  const show = (label, key) => {
128
128
  const v = data[key];
129
129
  if (v !== undefined && v !== null && v !== "")
@@ -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 { displayRunStatus, formatError } from "../lib/format.js";
4
4
  export const helpText = `
5
5
  exodus swipe — Swipe pipeline lifecycle + competitor watchlist
6
6
 
@@ -121,7 +121,7 @@ async function runRunPipeline(flags) {
121
121
  return;
122
122
  }
123
123
  console.log(`Run started: ${res.data.runId}`);
124
- console.log(` status: ${res.data.status ?? "selecting"}`);
124
+ console.log(` status: ${displayRunStatus(res.data.status, "Running")}`);
125
125
  if (res.data.triggerRunId)
126
126
  console.log(` triggerRunId: ${res.data.triggerRunId}`);
127
127
  console.log(`\nPoll: exodus swipe status ${res.data.runId}`);
@@ -224,13 +224,13 @@ async function runHistory(flags) {
224
224
  for (const r of runs) {
225
225
  const when = r.startedAt ? new Date(r.startedAt).toLocaleString() : "?";
226
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}`);
227
+ console.log(` ${r.runId} ${displayRunStatus(r.status, "—")} ads=${r.adsSelectedCount} by=${r.triggeredBy} ${when}${doc}`);
228
228
  }
229
229
  }
230
230
  function printRunStatus(r) {
231
231
  console.log(`Run ${r.runId ?? "?"}`);
232
232
  console.log(` brand: ${r.targetBrandSlug ?? "?"}`);
233
- console.log(` status: ${r.status ?? "?"}`);
233
+ console.log(` status: ${displayRunStatus(r.status, "?")}`);
234
234
  console.log(` by: ${r.triggeredBy ?? "?"}`);
235
235
  console.log(` ads: ${r.adsSelectedCount ?? 0}`);
236
236
  if (r.startedAt)
@@ -1,5 +1,5 @@
1
1
  import { apiGet, apiPost, apiPostDashboard, getDashboardUrl } from "../lib/client.js";
2
- import { formatError } from "../lib/format.js";
2
+ import { displayRunStatus, formatError } from "../lib/format.js";
3
3
  import { AD_TYPES, AD_TYPE_NAMES, REPTILE_TRIGGERS, } from "../lib/template-constants.js";
4
4
  export const helpText = `
5
5
  exodus template — Fernando's Template pipeline (${AD_TYPES.length} AD_TYPES, resume)
@@ -225,7 +225,7 @@ async function runStatus(flags) {
225
225
  console.log(`runId: ${d._id ?? runId}`);
226
226
  if (d.name)
227
227
  console.log(`name: ${d.name}`);
228
- console.log(`status: ${d.status ?? "—"}`);
228
+ console.log(`status: ${displayRunStatus(d.status, "—")}`);
229
229
  if (d.completedImageCount !== undefined || d.requestedImageCount !== undefined) {
230
230
  console.log(`progress: ${d.completedImageCount ?? 0} / ${d.requestedImageCount ?? "?"} completed${d.failedImageCount ? `, ${d.failedImageCount} failed` : ""}`);
231
231
  }
@@ -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,20 @@
1
+ import { getVersion } from "../lib/version.js";
2
+ import { run as runInit } from "./init.js";
3
+ export const helpText = `
4
+ exodus update — refresh this workspace from the installed CLI (alias of init)
5
+
6
+ Re-runs the init refresh: rewrites .claude/skills/, the workspace docs
7
+ (CLAUDE.md, PIPELINES.md), and syncs brand folders from the package you
8
+ invoked. Your .env, state/, and outputs are left untouched.
9
+
10
+ Usage:
11
+ exodus update
12
+
13
+ Note: the CLI itself updates because npx resolves your channel fresh on each
14
+ run — so invoke this as \`npx @aicopycoders/exodus update\` (or @beta on the
15
+ beta channel), not from a stale local install.
16
+ `.trim();
17
+ export async function run(flags) {
18
+ console.log(`Exodus ${getVersion()} — refreshing workspace scaffold…`);
19
+ await runInit(flags);
20
+ }
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { apiGet, apiPost } from "../lib/client.js";
4
- import { formatError } from "../lib/format.js";
4
+ import { displayRunStatus, formatError, tickerRunStatus } from "../lib/format.js";
5
5
  import { pollUntilDone } from "../lib/poll.js";
6
6
  export const helpText = `
7
7
  exodus winners — Import your own brand's winning ads as generative fuel
@@ -84,6 +84,11 @@ export function validatePackageLocally(pkg) {
84
84
  if ((typeof w.bodyText !== "string" || !w.bodyText.trim()) &&
85
85
  (typeof w.headline !== "string" || !w.headline.trim()))
86
86
  warnings.push(`${label}${id ? ` (${id})` : ""}: needs bodyText or headline`);
87
+ const assets = (w.assets ?? {});
88
+ const hasPoster = (typeof assets.posterPath === "string" && assets.posterPath.trim()) ||
89
+ (typeof assets.posterStorageId === "string" && assets.posterStorageId.trim());
90
+ if (w.format === "video" && !hasPoster)
91
+ warnings.push(`${label}${id ? ` (${id})` : ""}: video winner has no posterPath — it will show a blank placeholder in the gallery (grab the poster image via ads_get_ad_videos and attach it)`);
87
92
  });
88
93
  return { ok: errors.length === 0, errors, warnings, winnerCount };
89
94
  }
@@ -230,6 +235,11 @@ async function runImport(positional, flags) {
230
235
  ["videoPath", "videoStorageId"],
231
236
  ["posterPath", "posterStorageId"],
232
237
  ];
238
+ for (const [, idKey] of pairs) {
239
+ const existing = assets[idKey];
240
+ if (typeof existing === "string" && existing.trim())
241
+ swapped[idKey] = existing;
242
+ }
233
243
  for (const [pathKey, idKey] of pairs) {
234
244
  const rel = assets[pathKey];
235
245
  if (typeof rel !== "string" || !rel.trim())
@@ -280,7 +290,7 @@ async function runImport(positional, flags) {
280
290
  const status = typeof data.status === "string" ? data.status : "";
281
291
  if (!json && status && status !== lastStatus) {
282
292
  lastStatus = status;
283
- console.log(` status: ${status}`);
293
+ console.log(` status: ${tickerRunStatus(status)}`);
284
294
  }
285
295
  },
286
296
  });
@@ -333,7 +343,7 @@ async function uploadAsset(absPath, json) {
333
343
  }
334
344
  function printOutcomeTable(data) {
335
345
  const winners = data.winners ?? [];
336
- console.log(`\nImport ${data.importId ?? "?"}: ${data.status ?? "?"}`);
346
+ console.log(`\nImport ${data.importId ?? "?"}: ${displayRunStatus(data.status, "?")}`);
337
347
  if (data.error)
338
348
  console.log(` error: ${data.error}`);
339
349
  const created = winners.filter((w) => w.created === true).length;