@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,540 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { apiGetDashboard, apiPostDashboard } from "../lib/client.js";
4
+ import { promptChoice, promptMultiline, openInEditor } from "../lib/prompts.js";
5
+ import { refreshBrandProfileMd } from "./brand.js";
6
+ export const helpText = `
7
+ exodus primer — Build a brand's primer (the single foundation step)
8
+
9
+ A brand needs ONE thing before it can run the pipelines: a primer. The primer
10
+ builder reads your winning ads + product facts and writes a modular primer
11
+ document the writers use to construct new ads. Once it's saved, the brand is
12
+ ready for Genesis.
13
+
14
+ The build also seeds a HOOK BANK from the primer's HOOK section. Add a HEADLINE
15
+ BANK separately with \`exodus primer headlines\`. Both banks are injected into the
16
+ hook/headline writing stages as brand-specific examples.
17
+
18
+ Subcommands:
19
+ exodus primer Interactive build (paste ads → review → save)
20
+ exodus primer status Show whether this brand has a primer / is ready
21
+ exodus primer show Print the saved primer
22
+ exodus primer set --file <path>
23
+ exodus primer set --value <text>
24
+ exodus primer set --stdin Save a primer you already have, skipping the bot
25
+ exodus primer headlines Save the headline bank (paste, or --file/--stdin/--value)
26
+ exodus primer steering Set a primer's "always use / don't use" steering
27
+ exodus primer steering show Print the steering saved for all four primers
28
+
29
+ Steering picks one of the four primers and sets the brand's guidance for it —
30
+ the same "always use / don't use" the dashboard Primer editor writes. The
31
+ Genesis writer for that primer honors it on every run.
32
+
33
+ exodus primer steering --lane <lane> --always "<text>" --never "<text>"
34
+ exodus primer steering --lane <lane> --always-file <path>
35
+ exodus primer steering (interactive: pick a primer, then enter guidance)
36
+
37
+ <lane> is one of: bodyUnaware | bodyAware | hooks | headlines
38
+ (aliases: body-unaware/unaware/problem-aware, body-aware/solution-aware/
39
+ product-aware, hook, headline). Setting just one of --always / --never
40
+ leaves the other field as-is.
41
+
42
+ Interactive build flags:
43
+ --file <path> Read the submission (winning ads + product info) from a file
44
+ instead of pasting it interactively. Use --file - to read
45
+ the submission from stdin.
46
+ --yes Build and save without the interactive confirmation — for
47
+ non-interactive shells like Claude Code. Pairs with --file
48
+ (or --file -); rebuilds in place if a primer already exists.
49
+
50
+ The submission should include 10+ winning ads (full copy) and the product name
51
+ and core function. Differentiators, proof points, customer stories, offer
52
+ details, and villain framing are recommended but optional.
53
+ `.trim();
54
+ const INPUT_REQUIREMENTS = `
55
+ BRAND PRIMER BUILDER — INPUT REQUIREMENTS
56
+
57
+ REQUIRED:
58
+ - Winning ads — 10+ recommended (paste full ad copy for each)
59
+ - Product name and core function
60
+
61
+ RECOMMENDED:
62
+ - Product differentiators
63
+ - Key statistics or proof points
64
+ - Customer stories
65
+ - Pricing and offer details
66
+ - Villain framing
67
+ - Urgency or scarcity language in use
68
+
69
+ Paste everything together below.
70
+ `.trim();
71
+ function fail(msg, code = 1) {
72
+ console.error(`exodus primer: ${msg}`);
73
+ process.exit(code);
74
+ }
75
+ function printApiError(prefix, status, data) {
76
+ const body = data;
77
+ const detail = body?.hint ?? body?.detail ?? body?.error ?? `HTTP ${status}`;
78
+ console.error(`exodus primer: ${prefix}: ${detail}`);
79
+ }
80
+ function box(title, body) {
81
+ const line = "─".repeat(64);
82
+ console.log(`\n┌${line}`);
83
+ console.log(`│ ${title}`);
84
+ console.log(`├${line}`);
85
+ for (const row of body.split("\n")) {
86
+ console.log(`│ ${row}`);
87
+ }
88
+ console.log(`└${line}\n`);
89
+ }
90
+ async function readAllStdin() {
91
+ return await new Promise((resolve, reject) => {
92
+ let buf = "";
93
+ process.stdin.setEncoding("utf-8");
94
+ process.stdin.on("data", (chunk) => {
95
+ buf += chunk;
96
+ });
97
+ process.stdin.on("end", () => resolve(buf));
98
+ process.stdin.on("error", reject);
99
+ });
100
+ }
101
+ async function fetchStatus() {
102
+ const res = await apiGetDashboard("/api/primer/status");
103
+ if (!res.ok) {
104
+ printApiError("status check failed", res.status, res.data);
105
+ process.exit(1);
106
+ }
107
+ return res.data;
108
+ }
109
+ function printStatus(s) {
110
+ const brandLabel = s.brand ? `${s.brand.name} (${s.brand.slug})` : "(brand not resolved)";
111
+ console.log(`\nBrand: ${brandLabel}`);
112
+ if (s.ready) {
113
+ console.log(`Status: ✓ Ready — primer saved (${s.chars} chars)`);
114
+ }
115
+ else {
116
+ console.log(`Status: ✗ Not ready — no primer yet. Run: exodus primer`);
117
+ }
118
+ console.log("");
119
+ }
120
+ function sleep(ms) {
121
+ return new Promise((resolve) => setTimeout(resolve, ms));
122
+ }
123
+ async function buildPrimer(submission) {
124
+ console.log("\nBuilding primer… this reads every ad and writes the full module");
125
+ console.log("taxonomy. It runs in the background — I'll poll until it's done");
126
+ console.log("(Ctrl-C is safe; the build keeps running).\n");
127
+ const enqueue = await apiPostDashboard("/api/primer/build", {
128
+ submission,
129
+ });
130
+ if (!enqueue.ok) {
131
+ printApiError("primer build failed to start", enqueue.status, enqueue.data);
132
+ process.exit(1);
133
+ }
134
+ const buildId = enqueue.data.buildId;
135
+ if (!buildId)
136
+ fail("primer build did not return a buildId");
137
+ const POLL_INTERVAL_MS = 5_000;
138
+ const MAX_POLLS = 180;
139
+ for (let i = 0; i < MAX_POLLS; i++) {
140
+ await sleep(POLL_INTERVAL_MS);
141
+ const res = await apiGetDashboard(`/api/primer/build/${buildId}`);
142
+ if (!res.ok) {
143
+ if (res.status >= 400 && res.status < 500) {
144
+ printApiError("primer build status check failed", res.status, res.data);
145
+ process.exit(1);
146
+ }
147
+ process.stdout.write("·");
148
+ continue;
149
+ }
150
+ const data = res.data;
151
+ if (!data.isTerminal) {
152
+ process.stdout.write("·");
153
+ continue;
154
+ }
155
+ if (data.status === "failed") {
156
+ console.log("");
157
+ fail(`primer build failed: ${data.error ?? "unknown error"}`);
158
+ }
159
+ const primer = (data.primer ?? "").trim();
160
+ if (!primer)
161
+ fail("primer build returned empty output");
162
+ console.log("");
163
+ return { primer, missing: data.missing ?? [] };
164
+ }
165
+ fail("primer build timed out; check the dashboard and retry");
166
+ }
167
+ async function savePrimer(value) {
168
+ const res = await apiPostDashboard("/api/primer/set", { value });
169
+ if (!res.ok) {
170
+ printApiError("save failed", res.status, res.data);
171
+ process.exit(1);
172
+ }
173
+ return res.data;
174
+ }
175
+ async function saveFoundationField(fieldKey, value) {
176
+ const res = await apiPostDashboard("/api/primer/set", { value, fieldKey });
177
+ if (!res.ok) {
178
+ printApiError(`${fieldKey} save failed`, res.status, res.data);
179
+ process.exit(1);
180
+ }
181
+ return res.data;
182
+ }
183
+ function reportSplit(split) {
184
+ if (!split)
185
+ return;
186
+ if (split.saved.length) {
187
+ console.log(` ✓ split into ${split.saved.length} primers: ${split.saved.join(", ")}.`);
188
+ }
189
+ if (split.missing.length) {
190
+ console.log(` ⚠ no ads classified into: ${split.missing.join("; ")}.`);
191
+ console.log(" These primers are empty. Re-run `exodus primer` with ads for those");
192
+ console.log(" categories to populate them. Until then, runs for those categories");
193
+ console.log(" fall back to your other primer.");
194
+ }
195
+ }
196
+ async function refreshProfileAfterSave() {
197
+ try {
198
+ const r = await refreshBrandProfileMd();
199
+ if (r.written) {
200
+ console.log(` ✓ refreshed ${r.pathRel} from the saved foundation.`);
201
+ }
202
+ else {
203
+ console.log(` ⚠ could not refresh ${r.pathRel}: ${r.reason}`);
204
+ }
205
+ }
206
+ catch (err) {
207
+ console.log(` ⚠ could not refresh the brand profile: ${err instanceof Error ? err.message : String(err)}`);
208
+ }
209
+ }
210
+ function readFileArg(filePath) {
211
+ if (filePath === "-")
212
+ return "";
213
+ const resolved = path.resolve(process.cwd(), filePath);
214
+ let text;
215
+ try {
216
+ text = fs.readFileSync(resolved, "utf-8");
217
+ }
218
+ catch (err) {
219
+ fail(`could not read ${resolved}: ${err instanceof Error ? err.message : String(err)}`);
220
+ }
221
+ if (!text.trim())
222
+ fail(`file is empty: ${resolved}`);
223
+ return text;
224
+ }
225
+ async function collectSubmission(flags) {
226
+ if (typeof flags.file === "string" && flags.file) {
227
+ if (flags.file === "-") {
228
+ const text = await readAllStdin();
229
+ if (!text.trim())
230
+ fail("no submission on stdin");
231
+ return text;
232
+ }
233
+ return readFileArg(flags.file);
234
+ }
235
+ console.log(`\n${INPUT_REQUIREMENTS}\n`);
236
+ const text = await promptMultiline("Paste your winning ads + product info (Ctrl-D when done):");
237
+ if (!text.trim())
238
+ fail("no submission entered");
239
+ return text;
240
+ }
241
+ async function afterPrimerSaved(result) {
242
+ console.log(result.ready
243
+ ? " ✓ Brand is now ready for Genesis."
244
+ : ` ✗ Still not ready: ${result.missing.join(", ")}`);
245
+ reportSplit(result.split);
246
+ await refreshProfileAfterSave();
247
+ }
248
+ async function confirmAndSave(build, autoYes = false) {
249
+ if (autoYes) {
250
+ const result = await savePrimer(build.primer);
251
+ console.log(`\n ✓ saved primer (${result.chars} chars).`);
252
+ await afterPrimerSaved(result);
253
+ return true;
254
+ }
255
+ box("PRIMER — review", build.primer);
256
+ for (;;) {
257
+ const choice = await promptChoice("Save this primer to the brand?", [
258
+ { key: "a", label: "accept & save" },
259
+ { key: "e", label: "edit in $EDITOR, then save" },
260
+ { key: "v", label: "view again" },
261
+ { key: "q", label: "quit without saving" },
262
+ ], "a");
263
+ if (choice === "a") {
264
+ const result = await savePrimer(build.primer);
265
+ console.log(`\n ✓ saved primer (${result.chars} chars).`);
266
+ await afterPrimerSaved(result);
267
+ return true;
268
+ }
269
+ if (choice === "e") {
270
+ const edited = openInEditor(build.primer);
271
+ if (edited === null || !edited.trim()) {
272
+ console.log(" → editor exited empty / non-zero. Try again.");
273
+ continue;
274
+ }
275
+ const result = await savePrimer(edited);
276
+ console.log(`\n ✓ saved primer (${result.chars} chars, edited).`);
277
+ await afterPrimerSaved(result);
278
+ return true;
279
+ }
280
+ if (choice === "v") {
281
+ box("PRIMER — review", build.primer);
282
+ continue;
283
+ }
284
+ if (choice === "q") {
285
+ console.log(" → not saved.");
286
+ return false;
287
+ }
288
+ }
289
+ }
290
+ async function runStatus() {
291
+ printStatus(await fetchStatus());
292
+ }
293
+ async function runShow() {
294
+ const res = await apiGetDashboard("/api/primer");
295
+ if (!res.ok) {
296
+ printApiError("show failed", res.status, res.data);
297
+ process.exit(1);
298
+ }
299
+ const data = res.data;
300
+ if (!data.primer) {
301
+ console.log("No primer saved for this brand yet. Run: exodus primer");
302
+ return;
303
+ }
304
+ console.log(data.primer);
305
+ }
306
+ async function runSet(flags) {
307
+ let value = null;
308
+ if (typeof flags.value === "string") {
309
+ value = flags.value;
310
+ }
311
+ else if (flags.stdin === true) {
312
+ value = await readAllStdin();
313
+ }
314
+ else if (typeof flags.file === "string") {
315
+ value = flags.file === "-" ? await readAllStdin() : readFileArg(flags.file);
316
+ }
317
+ if (value === null || !value.trim()) {
318
+ fail("missing or empty content. provide --value <text> | --file <path> | --stdin (or --file -).");
319
+ }
320
+ const result = await savePrimer(value);
321
+ console.log(`✓ saved primer (${result.chars} chars).`);
322
+ console.log(result.ready
323
+ ? "✓ Brand is now ready for Genesis."
324
+ : `✗ Still not ready: ${result.missing.join(", ")}`);
325
+ reportSplit(result.split);
326
+ await refreshProfileAfterSave();
327
+ }
328
+ async function runHeadlines(flags) {
329
+ let value = null;
330
+ if (typeof flags.value === "string") {
331
+ value = flags.value;
332
+ }
333
+ else if (flags.stdin === true) {
334
+ value = await readAllStdin();
335
+ }
336
+ else if (typeof flags.file === "string") {
337
+ value = flags.file === "-" ? await readAllStdin() : readFileArg(flags.file);
338
+ }
339
+ else {
340
+ console.log("\nPaste your 10–15 winning headlines (one per line). Ctrl-D when done:\n");
341
+ value = await promptMultiline("Winning headlines:");
342
+ }
343
+ if (value === null || !value.trim()) {
344
+ fail("no headlines provided. Use --file <path> | --stdin | --value <text>, or paste interactively.");
345
+ }
346
+ const result = await saveFoundationField("headlineBank", value);
347
+ console.log(`✓ saved headline bank (${result.chars} chars).`);
348
+ console.log(" These headlines now prime the headline writing stage as brand examples.");
349
+ }
350
+ async function runInteractive(flags) {
351
+ const autoYes = flags.yes === true;
352
+ const status = await fetchStatus();
353
+ printStatus(status);
354
+ if (status.hasPrimer && !autoYes) {
355
+ const choice = await promptChoice("This brand already has a primer. Rebuild it?", [
356
+ { key: "r", label: "rebuild (replace it)" },
357
+ { key: "s", label: "show the current one" },
358
+ { key: "q", label: "quit" },
359
+ ], "q");
360
+ if (choice === "q")
361
+ return;
362
+ if (choice === "s") {
363
+ await runShow();
364
+ return;
365
+ }
366
+ }
367
+ const submission = await collectSubmission(flags);
368
+ for (;;) {
369
+ const build = await buildPrimer(submission);
370
+ const saved = await confirmAndSave(build, autoYes);
371
+ if (saved)
372
+ return;
373
+ const again = await promptChoice("Build again from the same submission?", [
374
+ { key: "r", label: "rebuild" },
375
+ { key: "q", label: "quit" },
376
+ ], "q");
377
+ if (again === "q")
378
+ return;
379
+ }
380
+ }
381
+ const LANE_LABELS = {
382
+ bodyUnaware: "PRIMER 1 — body copy (unaware / problem-aware)",
383
+ bodyAware: "PRIMER 2 — body copy (solution / product-aware)",
384
+ hooks: "PRIMER 3 — hooks",
385
+ headlines: "PRIMER 4 — headlines",
386
+ };
387
+ const LANE_ALIASES = {
388
+ bodyunaware: "bodyUnaware",
389
+ "body-unaware": "bodyUnaware",
390
+ unaware: "bodyUnaware",
391
+ "problem-aware": "bodyUnaware",
392
+ problemaware: "bodyUnaware",
393
+ bodyaware: "bodyAware",
394
+ "body-aware": "bodyAware",
395
+ aware: "bodyAware",
396
+ "solution-aware": "bodyAware",
397
+ solutionaware: "bodyAware",
398
+ "product-aware": "bodyAware",
399
+ productaware: "bodyAware",
400
+ hooks: "hooks",
401
+ hook: "hooks",
402
+ headlines: "headlines",
403
+ headline: "headlines",
404
+ };
405
+ function resolveLane(input) {
406
+ return LANE_ALIASES[input.trim().toLowerCase()] ?? null;
407
+ }
408
+ function resolveSteeringField(flags, key) {
409
+ const fileVal = flags[`${key}-file`];
410
+ if (typeof fileVal === "string")
411
+ return readFileArg(fileVal);
412
+ const val = flags[key];
413
+ if (typeof val === "string")
414
+ return val;
415
+ if (val === true) {
416
+ fail(`--${key} needs a value, e.g. --${key} "lead with the 273% stat"`);
417
+ }
418
+ return undefined;
419
+ }
420
+ function printSteeringField(label, val) {
421
+ const text = (val ?? "").trim();
422
+ console.log(` ${label} ${text || "— none —"}`);
423
+ }
424
+ function printSteeringShow(data) {
425
+ const brandLabel = data.brand ? `${data.brand.name} (${data.brand.slug})` : "(brand not resolved)";
426
+ console.log(`\nBrand: ${brandLabel}\n`);
427
+ const ps = data.primerSteering ?? {};
428
+ Object.keys(LANE_LABELS).forEach((lane) => {
429
+ console.log(LANE_LABELS[lane]);
430
+ printSteeringField("always use:", ps[lane]?.alwaysDo);
431
+ printSteeringField("don't use: ", ps[lane]?.neverMention);
432
+ console.log("");
433
+ });
434
+ }
435
+ async function runSteering(flags) {
436
+ if (readPositionals()[1] === "show") {
437
+ const res = await apiGetDashboard("/api/primer/steering");
438
+ if (!res.ok) {
439
+ printApiError("steering show failed", res.status, res.data);
440
+ process.exit(1);
441
+ }
442
+ printSteeringShow(res.data);
443
+ return;
444
+ }
445
+ let lane;
446
+ let alwaysDo;
447
+ let neverMention;
448
+ if (typeof flags.lane === "string") {
449
+ const resolved = resolveLane(flags.lane);
450
+ if (!resolved) {
451
+ fail(`unknown --lane "${flags.lane}". Use one of: ${Object.keys(LANE_LABELS).join(", ")} (or an alias).`);
452
+ }
453
+ lane = resolved;
454
+ alwaysDo = resolveSteeringField(flags, "always");
455
+ neverMention = resolveSteeringField(flags, "never");
456
+ if (alwaysDo === undefined && neverMention === undefined) {
457
+ fail('nothing to set. Provide --always "<text>" and/or --never "<text>" (or --always-file/--never-file).');
458
+ }
459
+ }
460
+ else {
461
+ const byKey = {
462
+ "1": "bodyUnaware",
463
+ "2": "bodyAware",
464
+ "3": "hooks",
465
+ "4": "headlines",
466
+ };
467
+ const choiceKey = await promptChoice("Which primer's steering do you want to set?", [
468
+ { key: "1", label: LANE_LABELS.bodyUnaware },
469
+ { key: "2", label: LANE_LABELS.bodyAware },
470
+ { key: "3", label: LANE_LABELS.hooks },
471
+ { key: "4", label: LANE_LABELS.headlines },
472
+ ], "1");
473
+ lane = byKey[choiceKey];
474
+ console.log(`\nSetting steering for ${LANE_LABELS[lane]}.`);
475
+ console.log("Leave a field empty (just Ctrl-D) to leave it unchanged.\n");
476
+ const a = (await promptMultiline("ALWAYS use (Ctrl-D when done):")).trim();
477
+ const n = (await promptMultiline("DON'T use (Ctrl-D when done):")).trim();
478
+ alwaysDo = a || undefined;
479
+ neverMention = n || undefined;
480
+ if (alwaysDo === undefined && neverMention === undefined) {
481
+ console.log("\n → nothing entered; steering unchanged.");
482
+ return;
483
+ }
484
+ }
485
+ const payload = { lane };
486
+ if (alwaysDo !== undefined)
487
+ payload.alwaysDo = alwaysDo;
488
+ if (neverMention !== undefined)
489
+ payload.neverMention = neverMention;
490
+ const res = await apiPostDashboard("/api/primer/steering", payload);
491
+ if (!res.ok) {
492
+ printApiError("steering save failed", res.status, res.data);
493
+ process.exit(1);
494
+ }
495
+ const data = res.data;
496
+ console.log(`\n ✓ saved steering for ${LANE_LABELS[lane]}.`);
497
+ printSteeringField("always use:", data.steering?.alwaysDo);
498
+ printSteeringField("don't use: ", data.steering?.neverMention);
499
+ console.log("");
500
+ }
501
+ function readPositionals() {
502
+ const argv = process.argv.slice(3);
503
+ const out = [];
504
+ let i = 0;
505
+ while (i < argv.length) {
506
+ const a = argv[i];
507
+ if (a.startsWith("--")) {
508
+ const next = argv[i + 1];
509
+ i += next !== undefined && !next.startsWith("--") ? 2 : 1;
510
+ continue;
511
+ }
512
+ out.push(a);
513
+ i++;
514
+ }
515
+ return out;
516
+ }
517
+ export async function run(flags) {
518
+ const sub = readPositionals()[0];
519
+ if (sub === "status") {
520
+ await runStatus();
521
+ return;
522
+ }
523
+ if (sub === "show") {
524
+ await runShow();
525
+ return;
526
+ }
527
+ if (sub === "set") {
528
+ await runSet(flags);
529
+ return;
530
+ }
531
+ if (sub === "headlines") {
532
+ await runHeadlines(flags);
533
+ return;
534
+ }
535
+ if (sub === "steering") {
536
+ await runSteering(flags);
537
+ return;
538
+ }
539
+ await runInteractive(flags);
540
+ }
@@ -0,0 +1,3 @@
1
+ export declare const helpText: string;
2
+ export declare function runReadDoc(args: string[]): Promise<void>;
3
+ export declare function run(flags: Record<string, string | boolean>): Promise<void>;
@@ -0,0 +1,62 @@
1
+ import { apiGetDashboard } from "../lib/client.js";
2
+ export const helpText = `
3
+ exodus read-doc — Print all tabs of a run's Google Doc(s) as markdown
4
+
5
+ Usage:
6
+ exodus read-doc <runId>
7
+
8
+ Exit codes:
9
+ 0 Content returned (gws-live or cache)
10
+ 2 All docs unavailable — fall back to Drive MCP
11
+ 1 Error (network failure, runId not found, missing arg)
12
+ `.trim();
13
+ export async function runReadDoc(args) {
14
+ const runId = args[0];
15
+ if (!runId) {
16
+ console.error("Usage: exodus read-doc <runId>");
17
+ process.exit(1);
18
+ return;
19
+ }
20
+ const res = await apiGetDashboard(`/api/runs/${encodeURIComponent(runId)}/document`);
21
+ if (!res.ok) {
22
+ const err = res.data?.error ?? `HTTP ${res.status}`;
23
+ console.error(`exodus read-doc: ${err}`);
24
+ process.exit(1);
25
+ return;
26
+ }
27
+ const body = res.data;
28
+ const allUnavailable = body.docs.every((d) => d.source === "unavailable");
29
+ let printedAny = false;
30
+ body.docs.forEach((doc, idx) => {
31
+ if (idx > 0)
32
+ console.log("\n---\n");
33
+ const provenance = `<!-- source: ${doc.source} | docId: ${doc.docId} | label: ${doc.label}` +
34
+ `${doc.generatedAt ? ` | generatedAt: ${doc.generatedAt}` : ""} -->`;
35
+ console.log(provenance);
36
+ if (doc.content) {
37
+ console.log(doc.content);
38
+ printedAny = true;
39
+ }
40
+ else {
41
+ console.log(`<!-- driveUrl: ${doc.driveUrl} -->`);
42
+ if (doc.hint)
43
+ console.error(`exodus read-doc: ${doc.hint}`);
44
+ }
45
+ });
46
+ if (allUnavailable) {
47
+ process.exit(2);
48
+ return;
49
+ }
50
+ if (!printedAny) {
51
+ process.exit(1);
52
+ return;
53
+ }
54
+ process.exit(0);
55
+ }
56
+ export async function run(flags) {
57
+ const argv = process.argv.slice(2);
58
+ const positionals = argv.filter((a, i) => !a.startsWith("--") &&
59
+ (i === 0 || !argv[i - 1]?.startsWith("--") || argv[i - 1] === "--"));
60
+ const runId = positionals[1];
61
+ await runReadDoc(runId ? [runId] : []);
62
+ }
@@ -0,0 +1,6 @@
1
+ export declare const helpText: string;
2
+ export declare function formatIntelResult(data: Record<string, unknown>): string;
3
+ export declare function formatPulseResult(data: Record<string, unknown>): string;
4
+ export declare function formatScoutResult(data: Record<string, unknown>): string;
5
+ export declare function formatCreativeSuiteStatus(title: string, data: Record<string, unknown>): string;
6
+ export declare function run(flags: Record<string, string | boolean>): Promise<void>;