@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,154 @@
1
+ import { apiGetDashboard, apiPostDashboard } from "../lib/client.js";
2
+ export const helpText = `
3
+ exodus drive — Read and write Google Drive / Docs / Sheets via the dashboard
4
+
5
+ The dashboard's Google OAuth (Settings → Google Drive) is the only auth
6
+ required. There is no local CLI to install, no keyring, no launchctl.
7
+
8
+ Usage:
9
+ exodus drive get-doc <documentId>
10
+ exodus drive create-doc <title> [--folder <folderId>]
11
+ exodus drive batch-update <documentId> --requests <json>
12
+ exodus drive list-files [--q <query>] [--page-size <n>] [--order-by <field>]
13
+ exodus drive get-sheet <spreadsheetId> --range <A1Notation>
14
+ exodus drive status
15
+
16
+ Examples:
17
+ exodus drive get-doc 1abc...XYZ
18
+ exodus drive create-doc "Draft — joint pain"
19
+ exodus drive batch-update 1abc...XYZ --requests '[{"insertText":{"location":{"index":1},"text":"Hello"}}]'
20
+ exodus drive list-files --q "name contains 'spark'" --page-size 10
21
+ exodus drive get-sheet 1sht...ABC --range "Sheet1!A1:Z100"
22
+ exodus drive status
23
+
24
+ Exit codes:
25
+ 0 Success — JSON response printed to stdout
26
+ 1 Error — message printed to stderr
27
+ 2 Drive not connected — user must visit Settings → Google Drive
28
+ `.trim();
29
+ function readPositionals() {
30
+ const argv = process.argv.slice(2);
31
+ return argv.filter((a, i) => !a.startsWith("--") &&
32
+ (i === 0 || !argv[i - 1]?.startsWith("--") || argv[i - 1] === "--"));
33
+ }
34
+ function fail(msg, code = 1) {
35
+ console.error(`exodus drive: ${msg}`);
36
+ process.exit(code);
37
+ }
38
+ function printOrFail(res) {
39
+ if (res.ok) {
40
+ console.log(JSON.stringify(res.data, null, 2));
41
+ process.exit(0);
42
+ }
43
+ const body = res.data;
44
+ const msg = body?.hint ?? body?.error ?? `HTTP ${res.status}`;
45
+ if (body?.error === "drive_not_connected" || res.status === 412) {
46
+ console.error(`exodus drive: ${msg}`);
47
+ process.exit(2);
48
+ }
49
+ if (body?.googleError) {
50
+ console.error(`exodus drive: ${msg}\n${JSON.stringify(body.googleError, null, 2)}`);
51
+ }
52
+ else {
53
+ console.error(`exodus drive: ${msg}`);
54
+ }
55
+ process.exit(1);
56
+ }
57
+ async function getDoc(documentId) {
58
+ if (!documentId)
59
+ fail("usage: exodus drive get-doc <documentId>");
60
+ const res = await apiPostDashboard("/api/drive/docs/get", { documentId });
61
+ printOrFail(res);
62
+ }
63
+ async function createDoc(title, flags) {
64
+ if (!title)
65
+ fail("usage: exodus drive create-doc <title> [--folder <id>]");
66
+ const folderId = typeof flags["folder"] === "string" ? flags["folder"] : undefined;
67
+ const res = await apiPostDashboard("/api/drive/docs/create", {
68
+ title,
69
+ folderId,
70
+ });
71
+ printOrFail(res);
72
+ }
73
+ async function batchUpdate(documentId, flags) {
74
+ if (!documentId) {
75
+ fail("usage: exodus drive batch-update <documentId> --requests <json>");
76
+ }
77
+ const raw = flags["requests"];
78
+ if (typeof raw !== "string") {
79
+ fail("--requests <json> is required (a JSON array of Docs API requests)");
80
+ }
81
+ let requests;
82
+ try {
83
+ requests = JSON.parse(raw);
84
+ }
85
+ catch (err) {
86
+ fail(`--requests is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
87
+ }
88
+ if (!Array.isArray(requests)) {
89
+ fail("--requests must be a JSON array");
90
+ }
91
+ const res = await apiPostDashboard("/api/drive/docs/batch-update", {
92
+ documentId,
93
+ requests,
94
+ });
95
+ printOrFail(res);
96
+ }
97
+ async function listFiles(flags) {
98
+ const body = {};
99
+ if (typeof flags["q"] === "string")
100
+ body.q = flags["q"];
101
+ if (typeof flags["page-size"] === "string") {
102
+ const n = Number(flags["page-size"]);
103
+ if (Number.isFinite(n))
104
+ body.pageSize = n;
105
+ }
106
+ if (typeof flags["order-by"] === "string")
107
+ body.orderBy = flags["order-by"];
108
+ const res = await apiPostDashboard("/api/drive/files/list", body);
109
+ printOrFail(res);
110
+ }
111
+ async function getSheet(spreadsheetId, flags) {
112
+ if (!spreadsheetId) {
113
+ fail("usage: exodus drive get-sheet <spreadsheetId> --range <A1>");
114
+ }
115
+ const range = flags["range"];
116
+ if (typeof range !== "string") {
117
+ fail("--range <A1Notation> is required (e.g. 'Sheet1!A1:Z100')");
118
+ }
119
+ const res = await apiPostDashboard("/api/drive/sheets/values/get", {
120
+ spreadsheetId,
121
+ range,
122
+ });
123
+ printOrFail(res);
124
+ }
125
+ async function status() {
126
+ const res = await apiGetDashboard("/api/drive/status");
127
+ printOrFail(res);
128
+ }
129
+ export async function run(flags) {
130
+ const positionals = readPositionals();
131
+ const sub = positionals[1];
132
+ const arg1 = positionals[2] ?? "";
133
+ switch (sub) {
134
+ case "get-doc":
135
+ return getDoc(arg1);
136
+ case "create-doc":
137
+ return createDoc(positionals.slice(2).join(" "), flags);
138
+ case "batch-update":
139
+ return batchUpdate(arg1, flags);
140
+ case "list-files":
141
+ return listFiles(flags);
142
+ case "get-sheet":
143
+ return getSheet(arg1, flags);
144
+ case "status":
145
+ return status();
146
+ default:
147
+ console.log(helpText);
148
+ if (sub) {
149
+ console.error(`\nUnknown subcommand: ${sub}`);
150
+ process.exit(1);
151
+ }
152
+ process.exit(0);
153
+ }
154
+ }
@@ -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,468 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { apiGetDashboard, apiPostDashboard } from "../lib/client.js";
4
+ import { promptYesNo, promptChoice, promptText, promptMultiline, openInEditor, } from "../lib/prompts.js";
5
+ export const helpText = `
6
+ exodus foundation — (legacy) Manage a brand's 2-track Genesis foundation
7
+
8
+ NOTE: New brands should onboard with \`exodus primer\` — a single primer doc is
9
+ now all a brand needs to run the pipelines. This command edits the older
10
+ audienceConcerns + 2-track-primer fields and remains for brands set up that way.
11
+
12
+ Subcommands:
13
+ exodus foundation Interactive walkthrough (extracts from a source)
14
+ exodus foundation status Show ready/missing state, no writes
15
+ exodus foundation save <path> Save an already-structured markdown file
16
+ directly into the brand's foundation.
17
+ No bot extraction, no interactive prompts.
18
+ exodus foundation set <field> --value <text>
19
+ exodus foundation set <field> --file <path>
20
+ Set ONE field directly. Field must be one of:
21
+ audienceConcerns, brandVoice,
22
+ primerUnawareProblemAware,
23
+ primerSolutionProductAware.
24
+ Legacy 4-level keys (primerUnaware,
25
+ primerProblemAware, primerSolutionAware,
26
+ primerProductAware) are still accepted and
27
+ collapse server-side into the 2-track shape.
28
+
29
+ Interactive walkthrough flags (skip the source picker):
30
+ --doc <url> Google Doc URL
31
+ --url <url> Web page URL
32
+ --file <path> Local text file (.md / .txt / any text)
33
+ --text <inline> Use the literal string as the source text
34
+
35
+ WHEN TO USE WHICH
36
+ - \`save <path>\`: when the user (or you) already produced a fully-formatted
37
+ foundation in markdown with section headers. No extraction needed; just
38
+ write it through. This is the path Claude Code should use when it has
39
+ generated structured content already.
40
+ - \`set <field>\`: when only one field needs updating.
41
+ - (interactive): when starting from a raw source (a brand brief, a website,
42
+ notes the user pasted) that needs extracting + reviewing per section.
43
+
44
+ \`save\` parses H2 headers in your markdown to map sections to fields. The
45
+ foundation is now a 2-track shape (Genesis methodology): the unaware and
46
+ problem-aware levels share one primer; solution-aware and product-aware
47
+ share the other. Recognized headers (case-insensitive, dashes/parens
48
+ optional):
49
+ Audience Concerns | Brand Voice |
50
+ Primer Unaware/Problem-Aware (or "Cold", "TOFU") |
51
+ Primer Solution/Product-Aware (or "Warm", "MOFU/BOFU")
52
+ Legacy single-level headers (Primer Unaware, Primer Problem Aware, Primer
53
+ Solution Aware, Primer Product Aware) still parse and route to the
54
+ correct 2-track destination.
55
+ `.trim();
56
+ const FIELD_ORDER = [
57
+ "audienceConcerns",
58
+ "primerUnawareProblemAware",
59
+ "primerSolutionProductAware",
60
+ "brandVoice",
61
+ ];
62
+ function fail(msg, code = 1) {
63
+ console.error(`exodus foundation: ${msg}`);
64
+ process.exit(code);
65
+ }
66
+ function printApiError(prefix, status, data) {
67
+ const body = data;
68
+ const detail = body?.hint ?? body?.detail ?? body?.error ?? `HTTP ${status}`;
69
+ console.error(`exodus foundation: ${prefix}: ${detail}`);
70
+ }
71
+ async function fetchStatus() {
72
+ const res = await apiGetDashboard("/api/foundation/status");
73
+ if (!res.ok) {
74
+ printApiError("status check failed", res.status, res.data);
75
+ process.exit(1);
76
+ }
77
+ return res.data;
78
+ }
79
+ function printStatus(s) {
80
+ const brandLabel = s.brand
81
+ ? `${s.brand.name} (${s.brand.slug})`
82
+ : "(brand not resolved)";
83
+ console.log(`\nBrand: ${brandLabel}`);
84
+ console.log(`Status: ${s.ready ? "✓ Ready for Genesis" : `✗ Missing: ${s.missing.join(", ")}`}`);
85
+ console.log("");
86
+ for (const f of s.fields) {
87
+ const dot = f.populated ? "●" : "○";
88
+ const tag = f.required ? "" : " (optional)";
89
+ const right = f.populated
90
+ ? `${f.chars} chars — ${f.preview}`
91
+ : "empty";
92
+ console.log(` ${dot} ${f.label}${tag}: ${right}`);
93
+ }
94
+ console.log("");
95
+ }
96
+ async function pickSource(flags) {
97
+ if (typeof flags.doc === "string" && flags.doc) {
98
+ return { kind: "googleDoc", value: flags.doc };
99
+ }
100
+ if (typeof flags.url === "string" && flags.url) {
101
+ return { kind: "url", value: flags.url };
102
+ }
103
+ if (typeof flags.file === "string" && flags.file) {
104
+ const filePath = path.resolve(process.cwd(), flags.file);
105
+ let text;
106
+ try {
107
+ text = fs.readFileSync(filePath, "utf-8");
108
+ }
109
+ catch (err) {
110
+ const msg = err instanceof Error ? err.message : String(err);
111
+ fail(`could not read file ${filePath}: ${msg}`);
112
+ }
113
+ if (!text.trim())
114
+ fail(`file is empty: ${filePath}`);
115
+ return { kind: "text", value: text };
116
+ }
117
+ if (typeof flags.text === "string" && flags.text) {
118
+ return { kind: "text", value: flags.text };
119
+ }
120
+ const choice = await promptChoice("Where should we pull the brand info from?", [
121
+ { key: "1", label: "Google Doc URL" },
122
+ { key: "2", label: "Web page URL" },
123
+ { key: "3", label: "Local text file (.md / .txt)" },
124
+ { key: "4", label: "Paste / type" },
125
+ { key: "q", label: "quit" },
126
+ ], "1");
127
+ if (choice === "q")
128
+ return null;
129
+ if (choice === "1") {
130
+ const url = await promptText("Google Doc URL or ID:");
131
+ if (!url)
132
+ fail("no URL provided");
133
+ return { kind: "googleDoc", value: url };
134
+ }
135
+ if (choice === "2") {
136
+ const url = await promptText("Web page URL:");
137
+ if (!url)
138
+ fail("no URL provided");
139
+ return { kind: "url", value: url };
140
+ }
141
+ if (choice === "3") {
142
+ const filePath = await promptText("Local file path:");
143
+ if (!filePath)
144
+ fail("no file path provided");
145
+ const resolved = path.resolve(process.cwd(), filePath);
146
+ let text;
147
+ try {
148
+ text = fs.readFileSync(resolved, "utf-8");
149
+ }
150
+ catch (err) {
151
+ const msg = err instanceof Error ? err.message : String(err);
152
+ fail(`could not read file ${resolved}: ${msg}`);
153
+ }
154
+ if (!text.trim())
155
+ fail(`file is empty: ${resolved}`);
156
+ return { kind: "text", value: text };
157
+ }
158
+ const text = await promptMultiline("Paste your brand brief / notes below:");
159
+ if (!text.trim())
160
+ fail("no text entered");
161
+ return { kind: "text", value: text };
162
+ }
163
+ async function fetchSource(kind, value) {
164
+ const res = await apiPostDashboard("/api/foundation/source", { kind, value });
165
+ if (!res.ok) {
166
+ printApiError("source fetch failed", res.status, res.data);
167
+ process.exit(1);
168
+ }
169
+ return res.data;
170
+ }
171
+ async function extract(fieldKey, sourceText) {
172
+ const res = await apiPostDashboard("/api/foundation/extract", { fieldKey, sourceText });
173
+ if (!res.ok) {
174
+ printApiError(`extract ${fieldKey} failed`, res.status, res.data);
175
+ process.exit(1);
176
+ }
177
+ return res.data;
178
+ }
179
+ async function saveField(fieldKey, value) {
180
+ const res = await apiPostDashboard("/api/foundation/set", { fieldKey, value });
181
+ if (!res.ok) {
182
+ printApiError(`save ${fieldKey} failed`, res.status, res.data);
183
+ process.exit(1);
184
+ }
185
+ return res.data;
186
+ }
187
+ function box(title, body) {
188
+ const line = "─".repeat(64);
189
+ console.log(`\n┌${line}`);
190
+ console.log(`│ ${title}`);
191
+ console.log(`├${line}`);
192
+ for (const row of body.split("\n")) {
193
+ console.log(`│ ${row}`);
194
+ }
195
+ console.log(`└${line}\n`);
196
+ }
197
+ async function walkField(field, sourceText) {
198
+ if (field.populated) {
199
+ const regen = await promptYesNo(`${field.label} is already populated (${field.chars} chars). Regenerate?`, false);
200
+ if (!regen) {
201
+ console.log(` → keeping existing ${field.label}.`);
202
+ return;
203
+ }
204
+ }
205
+ for (;;) {
206
+ process.stdout.write(`\nExtracting ${field.label}…\n`);
207
+ const result = await extract(field.key, sourceText);
208
+ box(`${field.label} — suggestion (${result.model})`, result.suggestion);
209
+ const choice = await promptChoice(`What do you want to do with this ${field.label} suggestion?`, [
210
+ { key: "a", label: "accept" },
211
+ { key: "e", label: "edit in $EDITOR" },
212
+ { key: "r", label: "regenerate" },
213
+ { key: "s", label: "skip" },
214
+ ], "a");
215
+ if (choice === "a") {
216
+ await saveField(field.key, result.suggestion);
217
+ console.log(` ✓ saved ${field.label}.`);
218
+ return;
219
+ }
220
+ if (choice === "e") {
221
+ const edited = openInEditor(result.suggestion);
222
+ if (edited === null || !edited.trim()) {
223
+ console.log(" → editor exited empty / non-zero. Try again.");
224
+ continue;
225
+ }
226
+ await saveField(field.key, edited);
227
+ console.log(` ✓ saved ${field.label} (edited).`);
228
+ return;
229
+ }
230
+ if (choice === "r") {
231
+ continue;
232
+ }
233
+ if (choice === "s") {
234
+ console.log(` → skipped ${field.label}.`);
235
+ return;
236
+ }
237
+ }
238
+ }
239
+ async function runStatus() {
240
+ const status = await fetchStatus();
241
+ printStatus(status);
242
+ }
243
+ const FIELD_KEYS = new Set([
244
+ "audienceConcerns",
245
+ "brandVoice",
246
+ "primerUnawareProblemAware",
247
+ "primerSolutionProductAware",
248
+ ]);
249
+ function headerToFieldKey(rawHeader) {
250
+ const norm = rawHeader
251
+ .toLowerCase()
252
+ .replace(/[\(\)\-–—_:\/]+/g, " ")
253
+ .replace(/\s+/g, " ")
254
+ .trim();
255
+ if (norm === "audience concerns" || norm === "audience" || norm === "concerns") {
256
+ return "audienceConcerns";
257
+ }
258
+ if (norm === "brand voice" || norm === "voice") {
259
+ return "brandVoice";
260
+ }
261
+ const hasUnaware = norm.includes("unaware");
262
+ const hasProblem = norm.includes("problem");
263
+ const hasSolution = norm.includes("solution");
264
+ const hasProduct = norm.includes("product");
265
+ const hasCold = /\b(cold|tofu|top of funnel)\b/.test(norm);
266
+ const hasWarm = /\b(warm|mofu|bofu|mid funnel|bottom of funnel)\b/.test(norm);
267
+ if ((hasUnaware && hasProblem) || hasCold)
268
+ return "primerUnawareProblemAware";
269
+ if ((hasSolution && hasProduct) || hasWarm)
270
+ return "primerSolutionProductAware";
271
+ if (norm.startsWith("primer")) {
272
+ if (hasUnaware)
273
+ return "primerUnawareProblemAware";
274
+ if (hasProblem)
275
+ return "primerUnawareProblemAware";
276
+ if (hasSolution)
277
+ return "primerSolutionProductAware";
278
+ if (hasProduct)
279
+ return "primerSolutionProductAware";
280
+ }
281
+ if (norm === "unaware")
282
+ return "primerUnawareProblemAware";
283
+ if (norm.startsWith("problem aware"))
284
+ return "primerUnawareProblemAware";
285
+ if (norm.startsWith("solution aware"))
286
+ return "primerSolutionProductAware";
287
+ if (norm.startsWith("product aware"))
288
+ return "primerSolutionProductAware";
289
+ return null;
290
+ }
291
+ function parseFoundationMarkdown(md) {
292
+ const lines = md.split(/\r?\n/);
293
+ const sections = [];
294
+ let current = null;
295
+ for (const line of lines) {
296
+ const h2 = line.match(/^##\s+(.+?)\s*$/);
297
+ if (h2) {
298
+ if (current)
299
+ sections.push(finalizeSection(current));
300
+ const key = headerToFieldKey(h2[1]);
301
+ if (key) {
302
+ current = { fieldKey: key, body: "", headerLine: h2[1] };
303
+ }
304
+ else {
305
+ current = null;
306
+ }
307
+ continue;
308
+ }
309
+ if (current) {
310
+ current.body += line + "\n";
311
+ }
312
+ }
313
+ if (current)
314
+ sections.push(finalizeSection(current));
315
+ const byField = new Map();
316
+ for (const s of sections)
317
+ byField.set(s.fieldKey, s);
318
+ return [...byField.values()];
319
+ }
320
+ function finalizeSection(s) {
321
+ return { ...s, body: s.body.replace(/^\s*\n+/, "").trimEnd() };
322
+ }
323
+ async function readAllStdin() {
324
+ return await new Promise((resolve, reject) => {
325
+ let buf = "";
326
+ process.stdin.setEncoding("utf-8");
327
+ process.stdin.on("data", (chunk) => {
328
+ buf += chunk;
329
+ });
330
+ process.stdin.on("end", () => resolve(buf));
331
+ process.stdin.on("error", reject);
332
+ });
333
+ }
334
+ async function runSave(filePath) {
335
+ if (!filePath) {
336
+ fail("missing path. usage: exodus foundation save <path> (use \"-\" to read from stdin)");
337
+ }
338
+ let md;
339
+ if (filePath === "-") {
340
+ md = await readAllStdin();
341
+ }
342
+ else {
343
+ const resolved = path.resolve(process.cwd(), filePath);
344
+ try {
345
+ md = fs.readFileSync(resolved, "utf-8");
346
+ }
347
+ catch (err) {
348
+ fail(`could not read ${resolved}: ${err instanceof Error ? err.message : String(err)}`);
349
+ }
350
+ }
351
+ if (!md.trim())
352
+ fail(`source is empty (path: ${filePath})`);
353
+ const sourceLabel = filePath === "-" ? "stdin" : path.resolve(process.cwd(), filePath);
354
+ const sections = parseFoundationMarkdown(md);
355
+ if (sections.length === 0) {
356
+ fail(`no recognized H2 sections found in ${sourceLabel}. ` +
357
+ `Use headers like "## Audience Concerns", "## Brand Voice", ` +
358
+ `"## Primer (Unaware)", etc.`);
359
+ }
360
+ console.log(`Found ${sections.length} section(s) in ${sourceLabel}:`);
361
+ for (const s of sections)
362
+ console.log(` - ${s.fieldKey} (from "${s.headerLine}")`);
363
+ console.log("");
364
+ let saved = 0;
365
+ for (const s of sections) {
366
+ if (!s.body.trim()) {
367
+ console.log(` → skipping ${s.fieldKey}: section body is empty`);
368
+ continue;
369
+ }
370
+ process.stdout.write(`Saving ${s.fieldKey} (${s.body.length} chars)…`);
371
+ await saveField(s.fieldKey, s.body);
372
+ process.stdout.write(" ✓\n");
373
+ saved++;
374
+ }
375
+ console.log(`\nSaved ${saved} of ${sections.length} fields. Final status:`);
376
+ const status = await fetchStatus();
377
+ printStatus(status);
378
+ }
379
+ async function runSet(fieldKey, flags) {
380
+ if (!fieldKey)
381
+ fail("missing field. usage: exodus foundation set <field> --value <text> | --file <path>");
382
+ if (!FIELD_KEYS.has(fieldKey)) {
383
+ fail(`invalid field "${fieldKey}". must be one of: ${[...FIELD_KEYS].join(", ")}`);
384
+ }
385
+ let value = null;
386
+ if (typeof flags.value === "string")
387
+ value = flags.value;
388
+ else if (flags.stdin === true) {
389
+ value = await readAllStdin();
390
+ }
391
+ else if (typeof flags.file === "string") {
392
+ if (flags.file === "-") {
393
+ value = await readAllStdin();
394
+ }
395
+ else {
396
+ const resolved = path.resolve(process.cwd(), flags.file);
397
+ try {
398
+ value = fs.readFileSync(resolved, "utf-8");
399
+ }
400
+ catch (err) {
401
+ fail(`could not read ${resolved}: ${err instanceof Error ? err.message : String(err)}`);
402
+ }
403
+ }
404
+ }
405
+ if (value === null || !value.trim()) {
406
+ fail("missing or empty content. provide --value <text> | --file <path> | --stdin (or --file -).");
407
+ }
408
+ await saveField(fieldKey, value);
409
+ console.log(`✓ saved ${fieldKey} (${value.length} chars).`);
410
+ const status = await fetchStatus();
411
+ printStatus(status);
412
+ }
413
+ async function runInteractive(flags) {
414
+ const status = await fetchStatus();
415
+ printStatus(status);
416
+ const source = await pickSource(flags);
417
+ if (!source) {
418
+ console.log("Aborted.");
419
+ return;
420
+ }
421
+ process.stdout.write("\nFetching source…\n");
422
+ const sourceResp = await fetchSource(source.kind, source.value);
423
+ const truncNote = sourceResp.truncated ? " (truncated)" : "";
424
+ console.log(`Source: ${sourceResp.label} — ${sourceResp.chars} chars${truncNote}`);
425
+ const fieldByKey = new Map(status.fields.map((f) => [f.key, f]));
426
+ for (const key of FIELD_ORDER) {
427
+ const field = fieldByKey.get(key);
428
+ if (!field)
429
+ continue;
430
+ await walkField(field, sourceResp.text);
431
+ }
432
+ console.log("\n── Final status ──");
433
+ const finalStatus = await fetchStatus();
434
+ printStatus(finalStatus);
435
+ }
436
+ function readPositionals() {
437
+ const argv = process.argv.slice(3);
438
+ const out = [];
439
+ let i = 0;
440
+ while (i < argv.length) {
441
+ const a = argv[i];
442
+ if (a.startsWith("--")) {
443
+ const next = argv[i + 1];
444
+ i += next !== undefined && !next.startsWith("--") ? 2 : 1;
445
+ continue;
446
+ }
447
+ out.push(a);
448
+ i++;
449
+ }
450
+ return out;
451
+ }
452
+ export async function run(flags) {
453
+ const positionals = readPositionals();
454
+ const sub = positionals[0];
455
+ if (sub === "status") {
456
+ await runStatus();
457
+ return;
458
+ }
459
+ if (sub === "save") {
460
+ await runSave(positionals[1] ?? "");
461
+ return;
462
+ }
463
+ if (sub === "set") {
464
+ await runSet(positionals[1] ?? "", flags);
465
+ return;
466
+ }
467
+ await runInteractive(flags);
468
+ }
@@ -0,0 +1,64 @@
1
+ export declare const helpText: string;
2
+ export interface GenesisOpts {
3
+ awarenessLevel: string;
4
+ seeds?: string[];
5
+ variantCount?: number;
6
+ adAccountId?: string;
7
+ stopAtHooks?: boolean;
8
+ }
9
+ export interface GenesisBody {
10
+ brief: string;
11
+ awarenessLevel: string;
12
+ inputMethod: "brief" | "paste";
13
+ sourcePayload?: {
14
+ winningAd?: string;
15
+ };
16
+ seeds?: string[];
17
+ variantCount?: number;
18
+ adAccountId?: string;
19
+ stopAtHooks?: boolean;
20
+ }
21
+ export declare function buildBriefBody(brief: string, opts: GenesisOpts): GenesisBody;
22
+ export declare function buildPasteBody(text: string, opts: GenesisOpts): GenesisBody;
23
+ export interface SwipeRow {
24
+ _id?: string;
25
+ brandName?: string;
26
+ format?: string;
27
+ transcript?: string;
28
+ bodyText?: string;
29
+ headline?: string;
30
+ ctaText?: string;
31
+ }
32
+ export declare function resolveSwipeText(swipe: SwipeRow): string;
33
+ export type GenesisAction = {
34
+ kind: "submit";
35
+ body: GenesisBody;
36
+ } | {
37
+ kind: "reel";
38
+ urls: string[];
39
+ opts: GenesisOpts;
40
+ } | {
41
+ kind: "swipe-url";
42
+ urls: string[];
43
+ opts: GenesisOpts;
44
+ steering?: string;
45
+ } | {
46
+ kind: "list-bank";
47
+ limit: number;
48
+ } | {
49
+ kind: "from-bank";
50
+ ideaId: string;
51
+ opts: GenesisOpts;
52
+ } | {
53
+ kind: "list-swipes";
54
+ limit: number;
55
+ } | {
56
+ kind: "from-swipe";
57
+ swipeId: string;
58
+ opts: GenesisOpts;
59
+ } | {
60
+ kind: "error";
61
+ message: string;
62
+ };
63
+ export declare function resolveGenesisAction(flags: Record<string, string | boolean>): GenesisAction;
64
+ export declare function run(flags: Record<string, string | boolean>): Promise<void>;