@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,177 @@
1
+ #!/usr/bin/env node
2
+ import { loadWorkspaceEnv } from "../lib/load-env.js";
3
+ loadWorkspaceEnv();
4
+ import { readdirSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import { dirname, join } from "node:path";
7
+ import { getVersion } from "../lib/version.js";
8
+ const OTHER_COMMANDS = new Set([
9
+ "browse",
10
+ "status",
11
+ "read-doc",
12
+ "whoami",
13
+ "brand",
14
+ "primer",
15
+ "foundation",
16
+ "drive",
17
+ "doctor",
18
+ "init",
19
+ "migrate",
20
+ "idea",
21
+ ]);
22
+ const HIDDEN_COMMANDS = new Set([
23
+ "creative",
24
+ "template",
25
+ ]);
26
+ const EXAMPLES = [
27
+ 'exodus genesis run --brief "joint pain relief in 30 days" --variants 6',
28
+ 'exodus genesis run --brief brief.txt --seeds seeds.txt',
29
+ 'exodus genesis --list-swipes',
30
+ 'exodus genesis --swipe <id>',
31
+ 'exodus image --ad "grounding sheets reduce inflammation"',
32
+ 'exodus image --type template --input "1. first ad ... 2. second ad ..."',
33
+ 'exodus image --type ref-match --refs k57abc123,k57def456 --subject "morning routine"',
34
+ 'exodus template ad-types',
35
+ 'exodus meme recommend --brief "grounding sheets reduce inflammation"',
36
+ 'exodus meme run --brief "grounding sheets reduce inflammation" --formats \'[...]\'',
37
+ 'exodus browse',
38
+ 'exodus status --id <runId> --type genesis',
39
+ ];
40
+ function installedCommands() {
41
+ const here = dirname(fileURLToPath(import.meta.url));
42
+ const cmdDir = join(here, "..", "commands");
43
+ try {
44
+ return readdirSync(cmdDir)
45
+ .filter((f) => f.endsWith(".js"))
46
+ .map((f) => f.slice(0, -3));
47
+ }
48
+ catch {
49
+ return [];
50
+ }
51
+ }
52
+ function summaryOf(helpText) {
53
+ if (!helpText)
54
+ return "";
55
+ const firstLine = helpText.split("\n").map((s) => s.trim()).find(Boolean) ?? "";
56
+ const m = firstLine.match(/—\s*(.+)$/);
57
+ return m ? m[1].trim() : "";
58
+ }
59
+ async function buildHelp() {
60
+ const names = installedCommands().sort();
61
+ const present = new Set(names);
62
+ const pipeline = [];
63
+ const other = [];
64
+ for (const name of names) {
65
+ if (HIDDEN_COMMANDS.has(name))
66
+ continue;
67
+ let summary = "";
68
+ try {
69
+ const mod = await import(`../commands/${name}.js`);
70
+ summary = summaryOf(mod.helpText);
71
+ }
72
+ catch {
73
+ }
74
+ (OTHER_COMMANDS.has(name) ? other : pipeline).push([name, summary]);
75
+ }
76
+ other.push(["help", "Show this help message"]);
77
+ const fmt = (rows) => rows.map(([n, s]) => ` ${n.padEnd(18)} ${s}`.trimEnd()).join("\n");
78
+ const commonOptions = [
79
+ "Common Options:",
80
+ " --awareness <level> Awareness level: unaware, problem-aware, solution-aware, product-aware",
81
+ " --wait Wait for completion (default)",
82
+ " --no-wait Return immediately after starting",
83
+ " --help, -h Show help (top-level or per-subcommand)",
84
+ " --version, -v Print the installed exodus version",
85
+ ].join("\n");
86
+ const examples = EXAMPLES.filter((ex) => {
87
+ const cmd = ex.replace(/^exodus\s+/, "").split(/\s+/)[0];
88
+ return present.has(cmd);
89
+ });
90
+ const sections = [
91
+ [
92
+ "exodus — CLI for the Viral Ad Dashboard API",
93
+ "",
94
+ "Usage:",
95
+ " exodus <command> [options]",
96
+ " exodus <command> --help",
97
+ ].join("\n"),
98
+ `Pipeline Commands:\n${fmt(pipeline)}`,
99
+ `Other Commands:\n${fmt(other)}`,
100
+ commonOptions,
101
+ `Examples:\n${examples.map((e) => ` ${e}`).join("\n")}`,
102
+ ];
103
+ return sections.filter((s) => s.trim() !== "").join("\n\n");
104
+ }
105
+ function parseArgs(argv) {
106
+ const [, , rawCommand = "help", ...rest] = argv;
107
+ const command = ["--help", "-h", "-help"].includes(rawCommand) ? "help" : rawCommand;
108
+ const flags = {};
109
+ let i = 0;
110
+ while (i < rest.length) {
111
+ const arg = rest[i];
112
+ if (arg === "--help" || arg === "-h") {
113
+ flags["help"] = true;
114
+ i++;
115
+ continue;
116
+ }
117
+ if (arg.startsWith("--")) {
118
+ const key = arg.slice(2);
119
+ const next = rest[i + 1];
120
+ if (key.startsWith("no-")) {
121
+ flags[key.slice(3)] = false;
122
+ i++;
123
+ }
124
+ else if (next !== undefined && !next.startsWith("--")) {
125
+ flags[key] = next;
126
+ i += 2;
127
+ }
128
+ else {
129
+ flags[key] = true;
130
+ i++;
131
+ }
132
+ }
133
+ else {
134
+ i++;
135
+ }
136
+ }
137
+ return { command, flags };
138
+ }
139
+ async function main() {
140
+ const { command, flags } = parseArgs(process.argv);
141
+ if (["--version", "-v", "-V", "version"].includes(command)) {
142
+ console.log(getVersion());
143
+ process.exit(0);
144
+ }
145
+ if (!command || command === "help") {
146
+ console.log(await buildHelp());
147
+ process.exit(0);
148
+ }
149
+ let commandModule;
150
+ try {
151
+ commandModule = await import(`../commands/${command}.js`);
152
+ }
153
+ catch {
154
+ console.error(`Unknown command: "${command}"\n`);
155
+ console.log(await buildHelp());
156
+ process.exit(1);
157
+ }
158
+ if (flags["help"] === true) {
159
+ if (commandModule.helpText) {
160
+ console.log(commandModule.helpText);
161
+ }
162
+ else {
163
+ console.log(`No detailed help is available for "${command}" yet.\n`);
164
+ console.log(await buildHelp());
165
+ }
166
+ process.exit(0);
167
+ }
168
+ if (typeof commandModule.run !== "function") {
169
+ console.error(`Command "${command}" does not export a run() function.`);
170
+ process.exit(1);
171
+ }
172
+ await commandModule.run(flags);
173
+ }
174
+ main().catch((err) => {
175
+ console.error("Fatal error:", err instanceof Error ? err.message : String(err));
176
+ process.exit(1);
177
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { buildBriefBody } from "../genesis.js";
3
+ describe("buildBriefBody stopAtHooks", () => {
4
+ it("omits stopAtHooks when unset", () => {
5
+ const b = buildBriefBody("brief text", { awarenessLevel: "problem-aware" });
6
+ expect("stopAtHooks" in b).toBe(false);
7
+ });
8
+ it("includes stopAtHooks:true when set", () => {
9
+ const b = buildBriefBody("brief text", { awarenessLevel: "problem-aware", stopAtHooks: true });
10
+ expect(b.stopAtHooks).toBe(true);
11
+ });
12
+ });
@@ -0,0 +1,24 @@
1
+ export declare const helpText: string;
2
+ interface Brand {
3
+ id: string;
4
+ slug: string;
5
+ name: string;
6
+ owned?: boolean;
7
+ }
8
+ interface MineResp {
9
+ role: "admin" | "member" | "unknown";
10
+ activeBrandId: string | null;
11
+ activeBrandSlug: string | null;
12
+ brands: Brand[];
13
+ }
14
+ export declare function fetchMine(): Promise<MineResp>;
15
+ export declare function fetchMineOrThrow(): Promise<MineResp>;
16
+ export declare function refreshBrandProfileMd(opts?: {
17
+ slug?: string;
18
+ }): Promise<{
19
+ written: boolean;
20
+ pathRel: string;
21
+ reason?: string;
22
+ }>;
23
+ export declare function run(flags: Record<string, string | boolean>): Promise<void>;
24
+ export {};
@@ -0,0 +1,292 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { apiGetDashboard, apiPostDashboard } from "../lib/client.js";
4
+ import { getActiveBrand, setActiveBrand, clearActiveBrand, findWorkspaceRoot, } from "../lib/state.js";
5
+ import { detectLayout, ensureBrandDir, resolveActiveBrand, brandStateDir, brandDirFor, } from "../lib/layout.js";
6
+ import { promptYesNo } from "../lib/prompts.js";
7
+ const AUTO_END_MARKER = "<!-- exodus:auto-section-end — manual brand notes below survive `brand use` refreshes -->";
8
+ export const helpText = `
9
+ exodus brand — list, switch, and inspect brands available to your CLI
10
+
11
+ Subcommands:
12
+ exodus brand list Show all brands your key has access to
13
+ exodus brand create "<name>" Create a new brand you own and switch into it
14
+ exodus brand use <slug> Set the active brand for this CLI
15
+ exodus brand current Print the active brand
16
+ exodus brand clear Stop overriding the brand (use the key's default)
17
+ exodus brand delete <slug> Delete a brand you own (asks before removing the local folder)
18
+
19
+ Any user can create and own unlimited brands (\`brand create\`) — you do NOT
20
+ need to be an admin, and brands are scoped to you (two users can have the
21
+ same brand name). Your key gives access to every brand you own; switch with
22
+ \`brand use\` (admins also see every brand). The only "locked to one brand"
23
+ case is a brand someone ELSE invited you into: you don't own it, so it's the
24
+ single brand your key sees and you can't switch away from it.
25
+
26
+ When the active brand is set, every CLI command (genesis, image, creative,
27
+ foundation, etc.) targets it via the X-Active-Brand header. In a
28
+ multi-brand install, running a command from inside a brand's subfolder
29
+ targets that brand automatically — the folder wins over \`brand use\`.
30
+ The dashboard's brand-switcher in the top-right is independent — your
31
+ CLI active brand and your dashboard active brand can differ.
32
+ `.trim();
33
+ function fail(msg, code = 1) {
34
+ console.error(`exodus brand: ${msg}`);
35
+ process.exit(code);
36
+ }
37
+ export async function fetchMine() {
38
+ const res = await apiGetDashboard("/api/brands/mine");
39
+ if (!res.ok) {
40
+ const body = res.data;
41
+ fail(body?.hint ?? body?.error ?? `HTTP ${res.status}`);
42
+ }
43
+ return res.data;
44
+ }
45
+ export async function fetchMineOrThrow() {
46
+ const res = await apiGetDashboard("/api/brands/mine");
47
+ if (!res.ok) {
48
+ const body = res.data;
49
+ throw new Error(body?.hint ?? body?.error ?? `HTTP ${res.status}`);
50
+ }
51
+ return res.data;
52
+ }
53
+ async function runList() {
54
+ const data = await fetchMine();
55
+ const resolved = resolveActiveBrand();
56
+ const effective = resolved.slug ?? data.activeBrandSlug;
57
+ if (data.brands.length === 0) {
58
+ console.log("No brands available for this key.");
59
+ if (data.role === "member") {
60
+ console.log("Ask an admin to assign you to a brand in Settings → Team.");
61
+ }
62
+ return;
63
+ }
64
+ console.log(`\nRole: ${data.role}`);
65
+ const sourceNote = resolved.source === "folder" ? " (from this brand folder)" : "";
66
+ console.log(`Active: ${effective ?? "(none — using key default)"}${sourceNote}\n`);
67
+ const slugWidth = Math.max(...data.brands.map((b) => b.slug.length), 4);
68
+ for (const b of data.brands) {
69
+ const marker = b.slug === effective ? "★" : " ";
70
+ console.log(` ${marker} ${b.slug.padEnd(slugWidth)} ${b.name}`);
71
+ }
72
+ if (resolved.source === "folder") {
73
+ const pointer = getActiveBrand();
74
+ if (pointer && pointer !== resolved.slug) {
75
+ console.log(`\nNote: you're inside the "${resolved.slug}" brand folder, which`);
76
+ console.log(`overrides the \`brand use\` pointer ("${pointer}") while you're here.`);
77
+ }
78
+ }
79
+ if (effective && !data.brands.some((b) => b.slug === effective)) {
80
+ console.log(`\n⚠ Active brand "${effective}" is set locally but you don't have access to it.`);
81
+ console.log(` Run \`exodus brand use <slug>\` to pick a different brand,`);
82
+ console.log(` or \`exodus brand clear\` to fall back to the key default.`);
83
+ }
84
+ console.log("");
85
+ }
86
+ export async function refreshBrandProfileMd(opts) {
87
+ const res = await apiGetDashboard("/api/brands/profile-md", opts?.slug ? { activeBrandOverride: opts.slug } : undefined);
88
+ const root = findWorkspaceRoot();
89
+ const stateDir = brandStateDir({ slug: opts?.slug, root });
90
+ const filePath = path.join(stateDir, "brand-profile.md");
91
+ const pathRel = path.relative(root, filePath);
92
+ if (!res.ok) {
93
+ const body = res.data;
94
+ return {
95
+ written: false,
96
+ pathRel,
97
+ reason: body?.hint ?? body?.error ?? `HTTP ${res.status}`,
98
+ };
99
+ }
100
+ const data = res.data;
101
+ let next;
102
+ if (!fs.existsSync(filePath)) {
103
+ next = `${data.autoSection}\n${data.manualTemplate}`;
104
+ }
105
+ else {
106
+ const existing = fs.readFileSync(filePath, "utf-8");
107
+ const idx = existing.indexOf(AUTO_END_MARKER);
108
+ if (idx >= 0) {
109
+ const manual = existing.slice(idx + AUTO_END_MARKER.length);
110
+ next = `${data.autoSection}${manual}`;
111
+ }
112
+ else {
113
+ next = `${data.autoSection}\n\n${existing.trimStart()}`;
114
+ }
115
+ }
116
+ fs.mkdirSync(stateDir, { recursive: true });
117
+ fs.writeFileSync(filePath, next, "utf-8");
118
+ return { written: true, pathRel };
119
+ }
120
+ async function runUse(slug) {
121
+ if (!slug)
122
+ fail("missing slug. usage: exodus brand use <slug>");
123
+ const data = await fetchMine();
124
+ const match = data.brands.find((b) => b.slug === slug);
125
+ if (!match) {
126
+ const available = data.brands.map((b) => b.slug).join(", ") || "(none)";
127
+ fail(`brand "${slug}" is not in your accessible list. available: ${available}`);
128
+ }
129
+ setActiveBrand(slug);
130
+ console.log(`✓ Active brand set to ${slug} (${match.name}).`);
131
+ const root = findWorkspaceRoot();
132
+ if (detectLayout(root) === "v2") {
133
+ const ensured = ensureBrandDir(root, { slug, name: match.name });
134
+ if (ensured.created) {
135
+ console.log(`✓ Created brand folder ${path.relative(root, ensured.dir) || ensured.dir}/.`);
136
+ }
137
+ }
138
+ const refresh = await refreshBrandProfileMd({ slug });
139
+ if (refresh.written) {
140
+ console.log(`✓ Refreshed ${refresh.pathRel} from the dashboard foundation.`);
141
+ }
142
+ else {
143
+ console.log(`⚠ Could not refresh ${refresh.pathRel}: ${refresh.reason}.`);
144
+ console.log(" The brand was switched locally; Genesis may use stale brand context");
145
+ console.log(" until you re-run `exodus brand use` with network access.");
146
+ }
147
+ const resolved = resolveActiveBrand();
148
+ if (resolved.source === "folder" && resolved.slug !== slug) {
149
+ console.log(`\nHeads up: you're currently inside the "${resolved.slug}" brand folder,`);
150
+ console.log(`which takes precedence while you're here. Commands run from this folder`);
151
+ console.log(`still target ${resolved.slug}; run from the install root (or`);
152
+ console.log(`the ${slug} folder) to target ${slug}.`);
153
+ }
154
+ }
155
+ async function runCreate(name) {
156
+ if (!name)
157
+ fail('missing name. usage: exodus brand create "<name>"');
158
+ const res = await apiPostDashboard("/api/brands/create", { name });
159
+ if (!res.ok || !res.data?.slug) {
160
+ fail(res.data?.error ?? `HTTP ${res.status}`);
161
+ }
162
+ const slug = res.data.slug;
163
+ const createdName = res.data.name ?? name;
164
+ console.log(`✓ Brand "${createdName}" created (slug: ${slug}).`);
165
+ setActiveBrand(slug);
166
+ console.log(`✓ Active brand set to ${slug}.`);
167
+ const root = findWorkspaceRoot();
168
+ if (detectLayout(root) === "v2") {
169
+ const ensured = ensureBrandDir(root, { slug, name: createdName });
170
+ if (ensured.created) {
171
+ console.log(`✓ Created brand folder ${path.relative(root, ensured.dir) || ensured.dir}/.`);
172
+ }
173
+ }
174
+ const refresh = await refreshBrandProfileMd({ slug });
175
+ if (refresh.written) {
176
+ console.log(`✓ Wrote ${refresh.pathRel}.`);
177
+ }
178
+ console.log(`\nNext: set up the brand's primer to unlock the pipelines —\n` +
179
+ ` say "exodus, set up my brand primer" (or run \`npx @aicopycoders/exodus primer\`).`);
180
+ }
181
+ async function runDelete(slug, flags) {
182
+ if (!slug)
183
+ fail("missing slug. usage: exodus brand delete <slug>");
184
+ const skipConfirm = flags.yes === true || flags.y === true;
185
+ const purgeLocal = flags["purge-local"] === true || skipConfirm;
186
+ if (!skipConfirm) {
187
+ const ok = await confirmSlugTyped(slug);
188
+ if (!ok) {
189
+ console.log("Aborted — nothing was deleted.");
190
+ return;
191
+ }
192
+ }
193
+ const res = await apiPostDashboard("/api/brands/delete", {
194
+ slug,
195
+ confirmSlug: slug,
196
+ });
197
+ if (!res.ok) {
198
+ fail(res.data?.error ?? `HTTP ${res.status}`);
199
+ }
200
+ console.log(`✓ Brand '${slug}' deleted (server).`);
201
+ if (getActiveBrand() === slug) {
202
+ clearActiveBrand();
203
+ console.log("✓ Cleared local active-brand pointer (it pointed at this brand).");
204
+ }
205
+ const root = findWorkspaceRoot();
206
+ if (detectLayout(root) === "v2") {
207
+ const dir = brandDirFor(root, slug);
208
+ if (fs.existsSync(dir)) {
209
+ const rel = path.relative(root, dir) || dir;
210
+ const remove = purgeLocal
211
+ ? true
212
+ : await promptYesNo(`Also remove the local folder ${rel}/?`, false);
213
+ if (remove) {
214
+ fs.rmSync(dir, { recursive: true, force: true });
215
+ console.log(`✓ Removed local folder ${rel}/.`);
216
+ }
217
+ else {
218
+ console.log(`ℹ Left local folder ${rel}/ in place — delete it yourself if you don't need the files.`);
219
+ }
220
+ }
221
+ }
222
+ }
223
+ async function confirmSlugTyped(slug) {
224
+ const { createInterface } = await import("node:readline");
225
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
226
+ try {
227
+ const answer = await new Promise((resolve) => {
228
+ rl.question(`This permanently deletes the brand "${slug}".\nType "${slug}" to confirm: `, resolve);
229
+ });
230
+ return answer.trim() === slug;
231
+ }
232
+ finally {
233
+ rl.close();
234
+ }
235
+ }
236
+ function runCurrent() {
237
+ const resolved = resolveActiveBrand();
238
+ if (resolved.slug) {
239
+ const source = resolved.source === "folder"
240
+ ? "from this brand folder"
241
+ : "from `brand use`";
242
+ console.log(`${resolved.slug} (${source})`);
243
+ if (resolved.source === "folder") {
244
+ const pointer = getActiveBrand();
245
+ if (pointer && pointer !== resolved.slug) {
246
+ console.log(`pointer: ${pointer} (overridden while inside this folder)`);
247
+ }
248
+ }
249
+ }
250
+ else {
251
+ console.log("(none — falling back to the key's default brand)");
252
+ }
253
+ }
254
+ function runClear() {
255
+ clearActiveBrand();
256
+ console.log("✓ Cleared local active brand. Falling back to the key's default.");
257
+ const resolved = resolveActiveBrand();
258
+ if (resolved.source === "folder") {
259
+ console.log(`Note: you're inside the "${resolved.slug}" brand folder — commands run`);
260
+ console.log("from here still target it (the folder wins over the pointer).");
261
+ }
262
+ }
263
+ export async function run(flags) {
264
+ const positionals = process.argv.slice(3).filter((a) => !a.startsWith("--"));
265
+ const sub = positionals[0];
266
+ const arg = positionals[1];
267
+ switch (sub) {
268
+ case "list":
269
+ case undefined:
270
+ await runList();
271
+ return;
272
+ case "create":
273
+ await runCreate(positionals.slice(1).join(" ").trim());
274
+ return;
275
+ case "use":
276
+ await runUse(arg ?? "");
277
+ return;
278
+ case "delete":
279
+ await runDelete(arg ?? "", flags);
280
+ return;
281
+ case "current":
282
+ runCurrent();
283
+ return;
284
+ case "clear":
285
+ runClear();
286
+ return;
287
+ default:
288
+ console.error(`exodus brand: unknown subcommand "${sub}"\n`);
289
+ console.log(helpText);
290
+ process.exit(1);
291
+ }
292
+ }
@@ -0,0 +1,9 @@
1
+ export declare const helpText: string;
2
+ export declare function computeFetchLimit(userLimit: number, pipeline: string | undefined): number;
3
+ export declare function matchesPipeline(item: Record<string, unknown>, pipeline: string): boolean;
4
+ declare const EXTRA_PIPELINES: readonly ["creative", "template"];
5
+ type ExtraPipeline = (typeof EXTRA_PIPELINES)[number];
6
+ export declare function shouldFetchExtra(filter: string | undefined, pipeline: ExtraPipeline): boolean;
7
+ export declare function resolvePipelineFilter(flags: Record<string, string | boolean>): string | undefined;
8
+ export declare function run(flags: Record<string, string | boolean>): Promise<void>;
9
+ export {};
@@ -0,0 +1,132 @@
1
+ import { apiGet } from "../lib/client.js";
2
+ import { formatBrowse, formatError } from "../lib/format.js";
3
+ export const helpText = `
4
+ exodus browse — List recent pipeline runs (hooks, ads, image concepts, etc.)
5
+
6
+ Usage:
7
+ exodus browse [options]
8
+
9
+ Options:
10
+ --limit <n> Max runs to list (default 20)
11
+ --agent <name> Filter by agent: genesis | creative | template | meme
12
+ `.trim();
13
+ export function computeFetchLimit(userLimit, pipeline) {
14
+ if (!pipeline)
15
+ return userLimit;
16
+ const overfetch = Math.min(userLimit * 10, 100);
17
+ return Math.max(userLimit, overfetch);
18
+ }
19
+ export function matchesPipeline(item, pipeline) {
20
+ const needle = pipeline.toLowerCase();
21
+ const fields = ["pipeline", "agentName", "agent", "agentId"];
22
+ for (const f of fields) {
23
+ const v = item[f];
24
+ if (typeof v !== "string")
25
+ continue;
26
+ const value = v.toLowerCase();
27
+ if (value === needle || value.startsWith(`${needle}:`) || value.startsWith(`${needle}-`)) {
28
+ return true;
29
+ }
30
+ }
31
+ return false;
32
+ }
33
+ function normalizeCreatedAt(item) {
34
+ const raw = item["createdAt"] ?? item["_creationTime"];
35
+ if (typeof raw === "number")
36
+ return raw;
37
+ if (typeof raw === "string")
38
+ return new Date(raw).getTime();
39
+ return 0;
40
+ }
41
+ const EXTRA_PIPELINES = ["creative", "template"];
42
+ export function shouldFetchExtra(filter, pipeline) {
43
+ if (!filter)
44
+ return true;
45
+ const needle = filter.toLowerCase();
46
+ if (pipeline === "creative")
47
+ return needle === "creative" || needle === "meme";
48
+ return needle === pipeline;
49
+ }
50
+ export function resolvePipelineFilter(flags) {
51
+ return (flags["agent"] ??
52
+ flags["pipeline"]);
53
+ }
54
+ async function fetchExtraRuns(pipeline, fetchLimit) {
55
+ const res = await apiGet(`/api/v2/${pipeline}?limit=${fetchLimit}`);
56
+ if (!res.ok)
57
+ return [];
58
+ const runs = res.data.runs ?? [];
59
+ return runs.map((r) => {
60
+ const doc = r.latestDocUrl ??
61
+ r.phase2DocUrl ??
62
+ r.userPhase2DocUrl ??
63
+ r.phase1DocUrl ??
64
+ r.userPhase1DocUrl ??
65
+ r.sheetUrl ??
66
+ undefined;
67
+ const label = pipeline === "creative" && r.engine === "meme"
68
+ ? "meme"
69
+ : pipeline;
70
+ return {
71
+ ...r,
72
+ _id: r.id ?? r._id,
73
+ agentId: label,
74
+ pipeline: label,
75
+ ...(doc ? { googleDocUrl: doc } : {}),
76
+ };
77
+ });
78
+ }
79
+ export async function run(flags) {
80
+ const limit = parseInt(flags["limit"] ?? "20", 10);
81
+ const pipeline = resolvePipelineFilter(flags);
82
+ const fetchLimit = computeFetchLimit(limit, pipeline);
83
+ const genRes = await apiGet(`/api/v2/generations?limit=${fetchLimit}`);
84
+ if (!genRes.ok) {
85
+ console.log(formatError(genRes));
86
+ process.exit(1);
87
+ }
88
+ let generations = [];
89
+ if (Array.isArray(genRes.data)) {
90
+ generations = genRes.data;
91
+ }
92
+ else if (genRes.data &&
93
+ typeof genRes.data === "object" &&
94
+ Array.isArray(genRes.data["generations"])) {
95
+ generations = genRes.data["generations"];
96
+ }
97
+ const genesisRes = await apiGet(`/api/v2/genesis?limit=${fetchLimit}`);
98
+ if (!genesisRes.ok) {
99
+ console.log(formatError(genesisRes));
100
+ process.exit(1);
101
+ }
102
+ const genesisRuns = genesisRes.data.runs ?? [];
103
+ const normalizedGenesis = genesisRuns.map((r) => ({
104
+ ...r,
105
+ agentName: r.inputMethod ? `genesis:${r.inputMethod}` : "genesis",
106
+ pipeline: "genesis",
107
+ }));
108
+ const extraFetches = await Promise.all(EXTRA_PIPELINES.map((p) => shouldFetchExtra(pipeline, p) ? fetchExtraRuns(p, fetchLimit) : Promise.resolve([])));
109
+ const all = [
110
+ ...generations.map((g) => g),
111
+ ...normalizedGenesis.map((g) => g),
112
+ ...extraFetches.flat(),
113
+ ];
114
+ all.sort((a, b) => normalizeCreatedAt(b) - normalizeCreatedAt(a));
115
+ const showAll = flags["all"] === true;
116
+ const STUCK_THRESHOLD_MS = 30 * 60 * 1000;
117
+ const now = Date.now();
118
+ const notStuck = showAll
119
+ ? all
120
+ : all.filter((g) => {
121
+ const status = g["status"];
122
+ if (status !== "running" && status !== "pending")
123
+ return true;
124
+ const created = normalizeCreatedAt(g);
125
+ return created > 0 && now - created < STUCK_THRESHOLD_MS;
126
+ });
127
+ const filtered = pipeline
128
+ ? notStuck.filter((g) => matchesPipeline(g, pipeline))
129
+ : notStuck;
130
+ const capped = filtered.slice(0, limit);
131
+ console.log(formatBrowse(capped));
132
+ }
@@ -0,0 +1,2 @@
1
+ export declare const helpText: string;
2
+ export declare function run(flags: Record<string, string | boolean>): Promise<void>;