@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,333 @@
1
+ import { apiGet, apiPost } from "../lib/client.js";
2
+ import { formatError } from "../lib/format.js";
3
+ import { formatCcCommand } from "../lib/cc-command.js";
4
+ import { captureReelAndWrite } from "../lib/reel-write.js";
5
+ export const helpText = `
6
+ exodus idea — capture, curate, and write from your brand-agnostic Idea Bank
7
+
8
+ The Idea Bank holds formed ideas (a hook + a short concept) you can curate and
9
+ escalate to the Genesis writer. Capture ideas three ways, add one directly, or
10
+ write from the ones you like.
11
+
12
+ Usage:
13
+ exodus idea gambit "<brain-dump>" Split a freeform dump into discrete ideas
14
+ exodus idea organic "<url> ..." [--write] Pull one idea from each reel (--write: bank AND write now)
15
+ exodus idea swipe [--limit n] Extract concepts from your saved swipe library
16
+ exodus idea add "<hook>" [--desc "<concept>"] [--source gambit|organic|swipe] [--ref "<x>"] [--notes "<x>"]
17
+ exodus idea list [--source ..] [--since YYYY-MM-DD] [--status raw|writing|written|archived] [--limit n]
18
+ exodus idea note <KEY> "<notes>" Attach persistent steering to an idea
19
+ exodus idea edit <KEY> "<new concept>" Edit an idea's description
20
+ exodus idea write <KEYS> [--awareness <level>] [--passes n] Fire-and-forget; one run per key (default 1 pass = 2 variants)
21
+ exodus idea rm <KEY> [--hard] Archive (or hard-delete with --hard)
22
+
23
+ Examples:
24
+ exodus idea gambit "joint pain at 40, sleep angle, grounding vs pills"
25
+ exodus idea organic "https://www.instagram.com/reel/abc https://www.tiktok.com/@x/video/123"
26
+ exodus idea organic "https://www.instagram.com/reel/abc" --write --awareness solution-aware
27
+ exodus idea swipe --limit 10
28
+ exodus idea list --source gambit --status raw
29
+ exodus idea write G1,S4 --awareness solution-aware
30
+ `.trim();
31
+ const SOURCES = ["gambit", "organic", "swipe"];
32
+ const KEY_RE = /^[GOSgos]\d+$/;
33
+ export function parseKeys(raw) {
34
+ const parts = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
35
+ if (parts.length === 0)
36
+ return null;
37
+ for (const p of parts)
38
+ if (!KEY_RE.test(p))
39
+ return null;
40
+ return parts.map((p) => p.toUpperCase());
41
+ }
42
+ function str(flag) {
43
+ return typeof flag === "string" && flag.trim() ? flag.trim() : undefined;
44
+ }
45
+ function resolveVariantCount(flags) {
46
+ const variantsRaw = str(flags["variants"]);
47
+ if (variantsRaw) {
48
+ const n = parseInt(variantsRaw, 10);
49
+ return Number.isNaN(n) ? 2 : Math.max(1, Math.min(10, n));
50
+ }
51
+ const passesRaw = str(flags["passes"]);
52
+ let passes = 1;
53
+ if (passesRaw) {
54
+ const p = parseInt(passesRaw, 10);
55
+ passes = Number.isNaN(p) ? 1 : Math.max(1, Math.min(5, p));
56
+ }
57
+ return passes * 2;
58
+ }
59
+ export function resolveIdeaAction(positionals, flags) {
60
+ const sub = positionals[0];
61
+ if (sub === "add") {
62
+ const hook = positionals[1];
63
+ if (!hook)
64
+ return { kind: "error", message: 'add needs a hook: exodus idea add "<hook>"' };
65
+ const sourceRaw = str(flags["source"]) ?? "gambit";
66
+ if (!SOURCES.includes(sourceRaw)) {
67
+ return { kind: "error", message: `--source must be one of: ${SOURCES.join(", ")}` };
68
+ }
69
+ return {
70
+ kind: "add",
71
+ hook,
72
+ description: str(flags["desc"]) ?? hook,
73
+ source: sourceRaw,
74
+ sourceRef: str(flags["ref"]),
75
+ notes: str(flags["notes"]),
76
+ };
77
+ }
78
+ if (sub === "list") {
79
+ const sourceRaw = str(flags["source"]);
80
+ if (sourceRaw && !SOURCES.includes(sourceRaw)) {
81
+ return { kind: "error", message: `--source must be one of: ${SOURCES.join(", ")}` };
82
+ }
83
+ const limitRaw = str(flags["limit"]);
84
+ return {
85
+ kind: "list",
86
+ source: sourceRaw,
87
+ since: str(flags["since"]),
88
+ status: str(flags["status"]),
89
+ limit: limitRaw ? Math.max(1, parseInt(limitRaw, 10) || 50) : undefined,
90
+ };
91
+ }
92
+ if (sub === "note") {
93
+ const key = positionals[1];
94
+ const notes = positionals[2];
95
+ if (!key || !notes)
96
+ return { kind: "error", message: 'note needs a key + text: exodus idea note G1 "..."' };
97
+ if (!KEY_RE.test(key))
98
+ return { kind: "error", message: `Bad key "${key}"` };
99
+ return { kind: "note", key: key.toUpperCase(), notes };
100
+ }
101
+ if (sub === "edit") {
102
+ const key = positionals[1];
103
+ const description = positionals[2];
104
+ if (!key || !description)
105
+ return { kind: "error", message: 'edit needs a key + concept: exodus idea edit G1 "..."' };
106
+ if (!KEY_RE.test(key))
107
+ return { kind: "error", message: `Bad key "${key}"` };
108
+ return { kind: "edit", key: key.toUpperCase(), description };
109
+ }
110
+ if (sub === "write") {
111
+ const keys = positionals[1] ? parseKeys(positionals[1]) : null;
112
+ if (!keys)
113
+ return { kind: "error", message: 'write needs keys: exodus idea write G1,S4' };
114
+ return {
115
+ kind: "write",
116
+ keys,
117
+ awarenessLevel: str(flags["awareness"]) ?? "problem-aware",
118
+ variantCount: resolveVariantCount(flags),
119
+ };
120
+ }
121
+ if (sub === "rm") {
122
+ const key = positionals[1];
123
+ if (!key || !KEY_RE.test(key))
124
+ return { kind: "error", message: 'rm needs a key: exodus idea rm G1' };
125
+ return { kind: "rm", key: key.toUpperCase(), hard: flags["hard"] === true };
126
+ }
127
+ if (sub === "gambit") {
128
+ const dump = positionals.slice(1).join(" ").trim();
129
+ if (!dump)
130
+ return { kind: "error", message: 'gambit needs a brain-dump: exodus idea gambit "<text>"' };
131
+ return { kind: "gambit", dump };
132
+ }
133
+ if (sub === "organic") {
134
+ const urls = positionals.slice(1).join(" ").split(/\s+/).map((u) => u.trim()).filter(Boolean);
135
+ if (urls.length === 0)
136
+ return { kind: "error", message: 'organic needs at least one URL: exodus idea organic "<url> <url>"' };
137
+ return {
138
+ kind: "organic",
139
+ urls,
140
+ write: flags["write"] === true,
141
+ awarenessLevel: str(flags["awareness"]) ?? "problem-aware",
142
+ variantCount: resolveVariantCount(flags),
143
+ };
144
+ }
145
+ if (sub === "swipe") {
146
+ const limitRaw = str(flags["limit"]);
147
+ if (limitRaw !== undefined) {
148
+ const n = parseInt(limitRaw, 10);
149
+ if (!Number.isInteger(n) || n < 1) {
150
+ return { kind: "error", message: "--limit must be a positive integer" };
151
+ }
152
+ return { kind: "swipe", limit: n };
153
+ }
154
+ return { kind: "swipe", limit: undefined };
155
+ }
156
+ return {
157
+ kind: "error",
158
+ message: 'Unknown subcommand. Use: gambit | organic | swipe | add | list | note | edit | write | rm.',
159
+ };
160
+ }
161
+ function parsePositional() {
162
+ const args = process.argv.slice(3);
163
+ const out = [];
164
+ let i = 0;
165
+ while (i < args.length) {
166
+ const a = args[i];
167
+ if (a.startsWith("--")) {
168
+ const next = args[i + 1];
169
+ i += next !== undefined && !next.startsWith("--") ? 2 : 1;
170
+ continue;
171
+ }
172
+ out.push(a);
173
+ i++;
174
+ }
175
+ return out;
176
+ }
177
+ function renderList(ideas) {
178
+ if (ideas.length === 0) {
179
+ console.log("Idea Bank is empty.");
180
+ console.log('Add one: exodus idea add "<hook>" --desc "<concept>"');
181
+ return;
182
+ }
183
+ console.log(`## Idea Bank (${ideas.length})`);
184
+ console.log("");
185
+ for (const i of ideas) {
186
+ const date = i.createdAt ? i.createdAt.slice(0, 10) : "";
187
+ console.log(` ${i.key ?? "?"} [${i.source ?? "?"}] ${i.hook ?? ""}`);
188
+ if (i.description)
189
+ console.log(` ${i.description}`);
190
+ const meta = [i.status ?? "raw", date].filter(Boolean).join(" · ");
191
+ console.log(` (${meta})`);
192
+ if (i.notes)
193
+ console.log(` notes: ${i.notes}`);
194
+ if (i.outputDocUrl)
195
+ console.log(` doc: ${i.outputDocUrl}`);
196
+ }
197
+ console.log("");
198
+ console.log("Write from some: exodus idea write G1,S4");
199
+ }
200
+ export async function run(flags) {
201
+ const cc = formatCcCommand(process.argv.slice(2));
202
+ const action = resolveIdeaAction(parsePositional(), flags);
203
+ switch (action.kind) {
204
+ case "error":
205
+ console.error(`Error: ${action.message}`);
206
+ process.exit(1);
207
+ return;
208
+ case "gambit": {
209
+ const res = await apiPost("/api/v2/idea-bank/ideate", { source: "gambit", input: action.dump }, { ccCommand: cc });
210
+ if (!res.ok) {
211
+ console.log(formatError(res));
212
+ process.exit(1);
213
+ }
214
+ console.log("Splitting your dump into ideas — fire-and-forget.");
215
+ console.log("They'll appear in the bank as the run completes: exodus idea list --source gambit");
216
+ return;
217
+ }
218
+ case "organic": {
219
+ if (action.write) {
220
+ console.log(`Transcribing ${action.urls.length} reel(s) → ideas → Genesis…`);
221
+ const result = await captureReelAndWrite(action.urls, { awarenessLevel: action.awarenessLevel, variantCount: action.variantCount }, { cc });
222
+ for (const f of result.bankedFailed) {
223
+ console.error(` ✗ couldn't pull an idea from ${f} (private, region-locked, or no transcript?)`);
224
+ }
225
+ if (result.dispatched.length === 0) {
226
+ console.error("No reel produced a usable idea — nothing written.");
227
+ process.exit(1);
228
+ }
229
+ for (const d of result.dispatched)
230
+ console.log(` ✓ banked ${d.key} → Genesis run ${d.runId}`);
231
+ console.log("");
232
+ console.log("Track them: exodus idea list (status flips to 'written' with a doc link)");
233
+ return;
234
+ }
235
+ const res = await apiPost("/api/v2/idea-bank/ideate", { source: "organic", urls: action.urls }, { ccCommand: cc });
236
+ if (!res.ok) {
237
+ console.log(formatError(res));
238
+ process.exit(1);
239
+ }
240
+ console.log(`Pulling ideas from ${action.urls.length} link(s) — fire-and-forget.`);
241
+ console.log("They'll appear in the bank as the run completes: exodus idea list --source organic");
242
+ return;
243
+ }
244
+ case "swipe": {
245
+ const res = await apiPost("/api/v2/idea-bank/ideate", { source: "swipe", limit: action.limit }, { ccCommand: cc });
246
+ if (!res.ok) {
247
+ console.log(formatError(res));
248
+ process.exit(1);
249
+ }
250
+ console.log("Extracting concepts from your saved swipes — fire-and-forget.");
251
+ console.log("They'll appear in the bank as the run completes: exodus idea list --source swipe");
252
+ return;
253
+ }
254
+ case "add": {
255
+ const res = await apiPost("/api/v2/idea-bank", {
256
+ source: action.source,
257
+ hook: action.hook,
258
+ description: action.description,
259
+ sourceRef: action.sourceRef,
260
+ notes: action.notes,
261
+ }, { ccCommand: cc });
262
+ if (!res.ok) {
263
+ console.log(formatError(res));
264
+ process.exit(1);
265
+ }
266
+ console.log(`Banked ${res.data.key ?? "(idea)"}: ${action.hook}`);
267
+ return;
268
+ }
269
+ case "list": {
270
+ const params = new URLSearchParams();
271
+ if (action.source)
272
+ params.set("source", action.source);
273
+ if (action.since)
274
+ params.set("since", action.since);
275
+ if (action.status)
276
+ params.set("status", action.status);
277
+ if (action.limit)
278
+ params.set("limit", String(action.limit));
279
+ const qs = params.toString();
280
+ const res = await apiGet(`/api/v2/idea-bank${qs ? `?${qs}` : ""}`);
281
+ if (!res.ok) {
282
+ console.log(formatError(res));
283
+ process.exit(1);
284
+ }
285
+ renderList(Array.isArray(res.data.ideas) ? res.data.ideas : []);
286
+ return;
287
+ }
288
+ case "note": {
289
+ const res = await apiPost("/api/v2/idea-bank/notes", { key: action.key, notes: action.notes }, { ccCommand: cc });
290
+ if (!res.ok) {
291
+ console.log(formatError(res));
292
+ process.exit(1);
293
+ }
294
+ console.log(`Notes set on ${action.key}.`);
295
+ return;
296
+ }
297
+ case "edit": {
298
+ const res = await apiPost("/api/v2/idea-bank/update", { key: action.key, description: action.description }, { ccCommand: cc });
299
+ if (!res.ok) {
300
+ console.log(formatError(res));
301
+ process.exit(1);
302
+ }
303
+ console.log(`Updated ${action.key}.`);
304
+ return;
305
+ }
306
+ case "write": {
307
+ const res = await apiPost("/api/v2/idea-bank/dispatch", { keys: action.keys, awarenessLevel: action.awarenessLevel, variantCount: action.variantCount }, { ccCommand: cc });
308
+ if (!res.ok) {
309
+ console.log(formatError(res));
310
+ process.exit(1);
311
+ }
312
+ const dispatched = res.data.dispatched ?? [];
313
+ const skipped = res.data.skipped ?? [];
314
+ console.log(`Dispatched ${dispatched.length} run(s) — fire-and-forget.`);
315
+ for (const d of dispatched)
316
+ console.log(` ${d.key} → run ${d.runId}`);
317
+ for (const s of skipped)
318
+ console.log(` skipped ${s.key}: ${s.reason}`);
319
+ console.log("");
320
+ console.log("Track them: exodus idea list (status flips to 'written' with a doc link)");
321
+ return;
322
+ }
323
+ case "rm": {
324
+ const res = await apiPost("/api/v2/idea-bank/delete", { key: action.key, hard: action.hard }, { ccCommand: cc });
325
+ if (!res.ok) {
326
+ console.log(formatError(res));
327
+ process.exit(1);
328
+ }
329
+ console.log(`${action.hard ? "Deleted" : "Archived"} ${action.key}.`);
330
+ return;
331
+ }
332
+ }
333
+ }
@@ -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,249 @@
1
+ import { apiGet, apiPost, getDashboardUrl } from "../lib/client.js";
2
+ import { formatError } from "../lib/format.js";
3
+ export const helpText = `
4
+ exodus image — Front door for static image ads (routes to creative / template)
5
+
6
+ Usage:
7
+ exodus image --ad "<finished copy>" Infer + run (default: native)
8
+ exodus image --type native --ad "<copy>" Literal render from copy
9
+ exodus image --type copy-derived --ad "<copy>" Variations derived off copy
10
+ exodus image --type template --input "<copy or 1. … 2. …>" Spread across ad-type formats
11
+
12
+ Options:
13
+ --type <engine> native | copy-derived | template
14
+ (omit to infer: numbered --input → template; else native)
15
+ --ad "<text>" Finished ad copy (alias: --input)
16
+ --input "<text>" Finished ad copy, or a numbered list of ads ("1. … 2. …")
17
+ --variations N Renders to generate (creative engines; maps to render target for template)
18
+ --aspect 1:1|4:5|9:16 Image aspect ratio (default: 1:1; template supports 1:1|9:16)
19
+ --name "<label>" Custom run name (creative engines)
20
+ --from copy|idea Source signal (default: copy). --from idea has no copy yet → write first.
21
+ --steer "<direction>" Steer every image in the batch (alias: --direction).
22
+ Native: a --steer with no --ad runs a no-copy native render.
23
+ --realistic Template only: add the realistic-enhancer guardrail
24
+
25
+ Examples:
26
+ exodus image --ad "grounding sheets reduce inflammation"
27
+ exodus image --type copy-derived --ad "..." --variations 10 --aspect 9:16
28
+ exodus image --type template --input "1. first ad ... 2. second ad ..."
29
+ exodus image --type template --input "1. first ad ... 2. second ad ..." --steer "dramatic, moody" --realistic
30
+
31
+ Engines underneath:
32
+ This command picks one engine and runs it. To drive a specific engine yourself,
33
+ use \`exodus creative <native|copy-derived>\` or \`exodus template run\`.
34
+ For meme formats use \`exodus meme\`.
35
+ `.trim();
36
+ function flagString(flags, name) {
37
+ const v = flags[name];
38
+ return typeof v === "string" ? v : undefined;
39
+ }
40
+ function flagInt(flags, name) {
41
+ const v = flags[name];
42
+ if (typeof v !== "string")
43
+ return undefined;
44
+ const n = parseInt(v, 10);
45
+ return Number.isFinite(n) ? n : undefined;
46
+ }
47
+ function looksLikeNumberedList(text) {
48
+ const matches = text.match(/(^|\n)\s*\d+[.)]\s+\S/g);
49
+ return (matches?.length ?? 0) >= 2;
50
+ }
51
+ async function resolveWhoami() {
52
+ const who = await apiGet("/api/v2/whoami");
53
+ if (!who.ok || !who.data.workspaceId) {
54
+ console.error("Error: whoami failed. Check EXODUS_API_KEY and EXODUS_API_URL/CONVEX_SITE_URL.");
55
+ console.error(formatError(who));
56
+ process.exit(1);
57
+ throw new Error("unreachable");
58
+ }
59
+ return who.data;
60
+ }
61
+ function resolveEngine(flags, refs, text) {
62
+ const typeRaw = flagString(flags, "type");
63
+ if (typeRaw) {
64
+ if (typeRaw === "native" ||
65
+ typeRaw === "copy-derived" ||
66
+ typeRaw === "template" ||
67
+ typeRaw === "ref-match") {
68
+ return typeRaw;
69
+ }
70
+ console.error(`Error: --type must be native | copy-derived | template | ref-match (got "${typeRaw}").`);
71
+ process.exit(1);
72
+ throw new Error("unreachable");
73
+ }
74
+ if (refs)
75
+ return "ref-match";
76
+ if (text && looksLikeNumberedList(text))
77
+ return "template";
78
+ return "native";
79
+ }
80
+ function validateAspect(value, allowed) {
81
+ const aspect = value ?? "1:1";
82
+ if (!allowed.includes(aspect)) {
83
+ console.error(`Error: --aspect must be ${allowed.join(" | ")} (got "${aspect}").`);
84
+ process.exit(1);
85
+ throw new Error("unreachable");
86
+ }
87
+ return aspect;
88
+ }
89
+ async function runCreativeText(engine, text, flags, who) {
90
+ const aspect = validateAspect(flagString(flags, "aspect"), ["1:1", "4:5", "9:16"]);
91
+ const variations = flagInt(flags, "variations");
92
+ const name = flagString(flags, "name");
93
+ const steering = flagString(flags, "steer") ?? flagString(flags, "direction");
94
+ const slug = who.workspaceSlug ?? who.workspaceId;
95
+ const hasCopy = !!text && text.trim().length >= 3;
96
+ const body = {
97
+ workspaceId: who.workspaceId,
98
+ engine,
99
+ ...(hasCopy ? { adText: text } : {}),
100
+ ...(steering ? { steering } : {}),
101
+ aspectRatio: aspect,
102
+ ...(variations !== undefined ? { variations } : {}),
103
+ ...(name ? { name } : {}),
104
+ };
105
+ console.log(`Read: ${hasCopy ? "finished copy" : "steering-only"} → creative ${engine} (aspect ${aspect}) for ${slug}.`);
106
+ const res = await apiPost("/api/creative-suite/run", body, {
107
+ ccCommand: `exodus image --type ${engine}`,
108
+ });
109
+ if (!res.ok) {
110
+ console.error(formatError(res));
111
+ process.exit(1);
112
+ return;
113
+ }
114
+ console.log("");
115
+ console.log(`✓ image run started (creative ${engine})`);
116
+ console.log(` runId: ${res.data.runId}`);
117
+ if (res.data.triggerRunId)
118
+ console.log(` triggerRunId: ${res.data.triggerRunId}`);
119
+ console.log(` dashboard: ${getDashboardUrl()}/creative-suite/runs/${res.data.runId}`);
120
+ console.log("");
121
+ console.log(`Poll: exodus creative status --id ${res.data.runId}`);
122
+ }
123
+ async function runRefMatch(flags, who) {
124
+ const refsRaw = flagString(flags, "refs");
125
+ if (!refsRaw) {
126
+ console.error("Error: ref-match requires --refs <id,id,...> (creativeSuiteImages ids from the library).");
127
+ process.exit(1);
128
+ return;
129
+ }
130
+ const referenceImageIds = refsRaw.split(",").map((s) => s.trim()).filter(Boolean);
131
+ if (referenceImageIds.length === 0) {
132
+ console.error("Error: --refs must contain at least one non-empty id.");
133
+ process.exit(1);
134
+ return;
135
+ }
136
+ const aspect = validateAspect(flagString(flags, "aspect"), ["1:1", "4:5", "9:16"]);
137
+ const variations = flagInt(flags, "variations");
138
+ const subject = flagString(flags, "subject");
139
+ const name = flagString(flags, "name");
140
+ const slug = who.workspaceSlug ?? who.workspaceId;
141
+ const body = {
142
+ workspaceId: who.workspaceId,
143
+ engine: "ref-match",
144
+ referenceImageIds,
145
+ aspectRatio: aspect,
146
+ ...(variations !== undefined ? { variations } : {}),
147
+ ...(subject ? { subject } : {}),
148
+ ...(name ? { name } : {}),
149
+ };
150
+ console.log(`Read: reference match → creative ref-match (${referenceImageIds.length} refs, aspect ${aspect}) for ${slug}.`);
151
+ const res = await apiPost("/api/creative-suite/run", body, {
152
+ ccCommand: "exodus image --type ref-match",
153
+ });
154
+ if (!res.ok) {
155
+ console.error(formatError(res));
156
+ process.exit(1);
157
+ return;
158
+ }
159
+ console.log("");
160
+ console.log(`✓ image run started (creative ref-match)`);
161
+ console.log(` runId: ${res.data.runId}`);
162
+ if (res.data.triggerRunId)
163
+ console.log(` triggerRunId: ${res.data.triggerRunId}`);
164
+ console.log(` dashboard: ${getDashboardUrl()}/creative-suite/runs/${res.data.runId}`);
165
+ console.log("");
166
+ console.log(`Poll: exodus creative status --id ${res.data.runId}`);
167
+ }
168
+ async function runTemplate(text, flags, who) {
169
+ if (!who.userId) {
170
+ console.error("Error: whoami did not return a userId (required for template runs).");
171
+ process.exit(1);
172
+ return;
173
+ }
174
+ const aspect = validateAspect(flagString(flags, "aspect"), ["1:1", "9:16"]);
175
+ const requestedImageCount = flagInt(flags, "variations");
176
+ const slug = who.workspaceSlug ?? who.workspaceId;
177
+ const steering = flagString(flags, "steer") ?? flagString(flags, "direction");
178
+ const realisticFlag = flags["realistic"] === true
179
+ || flagString(flags, "realism") === "realistic";
180
+ const body = {
181
+ workspaceId: who.workspaceId,
182
+ submittedBy: who.userId,
183
+ mode: "auto",
184
+ renderMode: "images",
185
+ inputAds: text,
186
+ aspectRatio: aspect,
187
+ model: "gpt-image-2",
188
+ realismMode: realisticFlag ? "realistic" : "off",
189
+ ...(steering ? { steering } : {}),
190
+ ...(requestedImageCount !== undefined ? { requestedImageCount } : {}),
191
+ };
192
+ console.log(`Read: finished copy → template (auto mode, aspect ${aspect}) for ${slug}.`);
193
+ const res = await apiPost("/api/creative-suite-template/run", body, {
194
+ ccCommand: "exodus image --type template",
195
+ });
196
+ if (!res.ok) {
197
+ console.error(formatError(res));
198
+ process.exit(1);
199
+ return;
200
+ }
201
+ console.log("");
202
+ console.log(`✓ image run started (template)`);
203
+ console.log(` runId: ${res.data.runId}`);
204
+ if (res.data.triggerRunId)
205
+ console.log(` triggerRunId: ${res.data.triggerRunId}`);
206
+ console.log(` dashboard: ${getDashboardUrl()}/creative-suite/template/sessions/${res.data.runId}`);
207
+ console.log("");
208
+ console.log("Status updates render live in the dashboard (no CLI status endpoint for template yet).");
209
+ }
210
+ export async function run(flags) {
211
+ const from = flagString(flags, "from");
212
+ if (from === "idea") {
213
+ console.error("image needs finished copy to render. You only have an idea — write copy first:");
214
+ console.error(' exodus genesis run --brief "<your idea>"');
215
+ console.error("Then come back and run image on the resulting copy.");
216
+ process.exit(1);
217
+ return;
218
+ }
219
+ const text = flagString(flags, "ad") ?? flagString(flags, "input");
220
+ const steering = flagString(flags, "steer") ?? flagString(flags, "direction");
221
+ const refs = flagString(flags, "refs");
222
+ const engine = resolveEngine(flags, refs, text);
223
+ if (engine === "ref-match") {
224
+ if (!refs || refs.split(",").map((s) => s.trim()).filter(Boolean).length === 0) {
225
+ console.error("Error: ref-match requires --refs <id,id,...> (creativeSuiteImages ids from the library).");
226
+ process.exit(1);
227
+ return;
228
+ }
229
+ }
230
+ else if (!text || text.trim().length < 3) {
231
+ const hasSteering = !!steering && steering.trim().length > 0;
232
+ if (!(engine === "native" && hasSteering)) {
233
+ console.error(`Error: ${engine} needs ad copy — pass --ad "<text>" (or --input "<text>", min 3 chars).`);
234
+ console.error(' (For a no-copy native render, pass --steer "<direction>". For a reference-image match, use --type ref-match --refs <id,id,...>.)');
235
+ process.exit(1);
236
+ return;
237
+ }
238
+ }
239
+ const who = await resolveWhoami();
240
+ if (engine === "ref-match") {
241
+ await runRefMatch(flags, who);
242
+ return;
243
+ }
244
+ if (engine === "template") {
245
+ await runTemplate(text, flags, who);
246
+ return;
247
+ }
248
+ await runCreativeText(engine, (text ?? ""), flags, who);
249
+ }
@@ -0,0 +1,13 @@
1
+ export declare const helpText: string;
2
+ export interface InitResult {
3
+ existing: boolean;
4
+ envCreated: boolean;
5
+ skills: string[];
6
+ }
7
+ export declare function scaffoldInit(root: string): InitResult;
8
+ export declare function syncBrands(root: string): Promise<{
9
+ synced: string[];
10
+ } | {
11
+ skipped: "no-key" | "api-error";
12
+ }>;
13
+ export declare function run(flags: Record<string, string | boolean>): Promise<void>;
@@ -0,0 +1,70 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { writeEnvScaffold, ensureGitignore, writeSkills, writeReferences, ensureBaseDirs, } from "../lib/scaffold.js";
4
+ import { loadWorkspaceEnv } from "../lib/load-env.js";
5
+ import { ensureBrandDir } from "../lib/layout.js";
6
+ export const helpText = `
7
+ exodus init — Set up (or refresh) an Exodus workspace in the current folder
8
+
9
+ Usage:
10
+ npx @aicopycoders/exodus init Scaffold the workspace + install skills
11
+ npx @aicopycoders/exodus init --root <dir> Target a specific folder
12
+
13
+ Run this in a fresh, empty folder. It creates the workspace layout, writes the
14
+ Exodus + Genesis skills into .claude/skills/, scaffolds a comment-only .env, and
15
+ prints the dashboard paste step. Safe to re-run: it never overwrites your .env
16
+ or brand folders — re-running refreshes the skills and catches up new brands.
17
+ `.trim();
18
+ export function scaffoldInit(root) {
19
+ const existing = fs.existsSync(path.join(root, ".env"));
20
+ ensureBaseDirs(root);
21
+ const { created: envCreated } = writeEnvScaffold(root);
22
+ ensureGitignore(root);
23
+ const skills = writeSkills(root);
24
+ writeReferences(root);
25
+ return { existing, envCreated, skills };
26
+ }
27
+ export async function syncBrands(root) {
28
+ loadWorkspaceEnv(root);
29
+ if (!process.env.EXODUS_API_KEY)
30
+ return { skipped: "no-key" };
31
+ try {
32
+ const { fetchMineOrThrow, refreshBrandProfileMd } = await import("./brand.js");
33
+ const mine = await fetchMineOrThrow();
34
+ const synced = [];
35
+ for (const brand of mine.brands ?? []) {
36
+ ensureBrandDir(root, { slug: brand.slug, name: brand.name });
37
+ await refreshBrandProfileMd({ slug: brand.slug });
38
+ synced.push(brand.slug);
39
+ }
40
+ return { synced };
41
+ }
42
+ catch {
43
+ return { skipped: "api-error" };
44
+ }
45
+ }
46
+ export async function run(flags) {
47
+ const root = typeof flags.root === "string" ? path.resolve(flags.root) : process.cwd();
48
+ const r = scaffoldInit(root);
49
+ console.log(`${r.existing ? "Refreshed" : "Initialized"} Exodus workspace at ${root}`);
50
+ console.log(` Installed ${r.skills.length} skills into .claude/skills/`);
51
+ const brands = await syncBrands(root);
52
+ if ("synced" in brands && brands.synced.length) {
53
+ console.log(` Synced ${brands.synced.length} brand folder(s): ${brands.synced.join(", ")}`);
54
+ }
55
+ else if ("skipped" in brands && brands.skipped === "api-error") {
56
+ console.log(" (Skipped brand sync — couldn't reach the API; check your key/URL, then re-run.)");
57
+ }
58
+ if (r.envCreated) {
59
+ console.log("");
60
+ console.log("NEXT STEP — add your keys:");
61
+ console.log(" 1. Open your Exodus dashboard -> Settings -> Claude Code.");
62
+ console.log(' 2. Click "Copy .env block".');
63
+ console.log(` 3. Paste it into ${path.join(root, ".env")} below the comment, save.`);
64
+ console.log(" 4. Restart this Claude Code session (or /clear) so the skills load.");
65
+ }
66
+ else {
67
+ console.log(" .env already present — left untouched.");
68
+ console.log(" Restart the session (or /clear) to pick up refreshed skills.");
69
+ }
70
+ }
@@ -0,0 +1,2 @@
1
+ export declare const helpText: string;
2
+ export declare function run(): Promise<void>;