@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.
- package/assets/references/awareness-framework.md +156 -0
- package/assets/references/cast-video.md +165 -0
- package/assets/references/copy-blocks.md +138 -0
- package/assets/references/creative-strategy.md +990 -0
- package/assets/references/editing-rules.md +81 -0
- package/assets/references/hook-quality-checklist.md +153 -0
- package/assets/references/iterations.md +73 -0
- package/assets/references/scrawls.md +483 -0
- package/assets/references/segments.md +79 -0
- package/assets/skills/exodus/SKILL.md +50 -0
- package/assets/skills/exodus-brand/SKILL.md +117 -0
- package/assets/skills/exodus-browse/SKILL.md +96 -0
- package/assets/skills/exodus-creative/SKILL.md +94 -0
- package/assets/skills/exodus-drive/SKILL.md +118 -0
- package/assets/skills/exodus-foundation/SKILL.md +170 -0
- package/assets/skills/exodus-genesis/SKILL.md +159 -0
- package/assets/skills/exodus-idea/SKILL.md +78 -0
- package/assets/skills/exodus-image/SKILL.md +224 -0
- package/assets/skills/exodus-meme/SKILL.md +183 -0
- package/assets/skills/exodus-primer/SKILL.md +145 -0
- package/assets/skills/exodus-strategist/SKILL.md +217 -0
- package/assets/skills/exodus-swipe/SKILL.md +85 -0
- package/assets/skills/exodus-template/SKILL.md +106 -0
- package/assets/skills/exodus-write/SKILL.md +157 -0
- package/assets/skills/genesis-bots/.env.example +12 -0
- package/assets/skills/genesis-bots/README.txt +29 -0
- package/assets/skills/genesis-bots/SKILL.md +219 -0
- package/assets/skills/genesis-bots/scripts/genesis-stream.mjs +307 -0
- package/assets/skills/genesis-bots/scripts/genesis-stream.py +220 -0
- package/dist/bin/exodus.d.ts +2 -0
- package/dist/bin/exodus.js +177 -0
- package/dist/commands/__tests__/genesis.hookgate.test.d.ts +1 -0
- package/dist/commands/__tests__/genesis.hookgate.test.js +12 -0
- package/dist/commands/brand.d.ts +24 -0
- package/dist/commands/brand.js +292 -0
- package/dist/commands/browse.d.ts +9 -0
- package/dist/commands/browse.js +132 -0
- package/dist/commands/creative.d.ts +2 -0
- package/dist/commands/creative.js +223 -0
- package/dist/commands/doctor.d.ts +21 -0
- package/dist/commands/doctor.js +590 -0
- package/dist/commands/drive.d.ts +2 -0
- package/dist/commands/drive.js +154 -0
- package/dist/commands/foundation.d.ts +2 -0
- package/dist/commands/foundation.js +468 -0
- package/dist/commands/genesis.d.ts +64 -0
- package/dist/commands/genesis.js +564 -0
- package/dist/commands/idea.d.ts +53 -0
- package/dist/commands/idea.js +333 -0
- package/dist/commands/image.d.ts +2 -0
- package/dist/commands/image.js +249 -0
- package/dist/commands/init.d.ts +13 -0
- package/dist/commands/init.js +70 -0
- package/dist/commands/keys.d.ts +2 -0
- package/dist/commands/keys.js +101 -0
- package/dist/commands/meme.d.ts +12 -0
- package/dist/commands/meme.js +272 -0
- package/dist/commands/migrate.d.ts +3 -0
- package/dist/commands/migrate.js +158 -0
- package/dist/commands/primer.d.ts +2 -0
- package/dist/commands/primer.js +540 -0
- package/dist/commands/read-doc.d.ts +3 -0
- package/dist/commands/read-doc.js +62 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +205 -0
- package/dist/commands/swipe.d.ts +2 -0
- package/dist/commands/swipe.js +398 -0
- package/dist/commands/template.d.ts +2 -0
- package/dist/commands/template.js +225 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +51 -0
- package/dist/lib/ad-library-write.d.ts +15 -0
- package/dist/lib/ad-library-write.js +67 -0
- package/dist/lib/assets.d.ts +3 -0
- package/dist/lib/assets.js +23 -0
- package/dist/lib/backend-hint.d.ts +2 -0
- package/dist/lib/backend-hint.js +22 -0
- package/dist/lib/cc-command.d.ts +1 -0
- package/dist/lib/cc-command.js +13 -0
- package/dist/lib/client.d.ts +23 -0
- package/dist/lib/client.js +213 -0
- package/dist/lib/format.d.ts +8 -0
- package/dist/lib/format.js +172 -0
- package/dist/lib/intel-format.d.ts +1 -0
- package/dist/lib/intel-format.js +38 -0
- package/dist/lib/keys-sync.d.ts +16 -0
- package/dist/lib/keys-sync.js +88 -0
- package/dist/lib/layout.d.ts +38 -0
- package/dist/lib/layout.js +153 -0
- package/dist/lib/load-env.d.ts +1 -0
- package/dist/lib/load-env.js +47 -0
- package/dist/lib/pixar-orchestrator-types.d.ts +54 -0
- package/dist/lib/pixar-orchestrator-types.js +1 -0
- package/dist/lib/poll.d.ts +14 -0
- package/dist/lib/poll.js +32 -0
- package/dist/lib/preflight.d.ts +18 -0
- package/dist/lib/preflight.js +56 -0
- package/dist/lib/prompts.d.ts +9 -0
- package/dist/lib/prompts.js +99 -0
- package/dist/lib/reel-write.d.ts +14 -0
- package/dist/lib/reel-write.js +59 -0
- package/dist/lib/release.d.ts +4 -0
- package/dist/lib/release.js +81 -0
- package/dist/lib/scaffold.d.ts +8 -0
- package/dist/lib/scaffold.js +67 -0
- package/dist/lib/scout-hydrate.d.ts +1 -0
- package/dist/lib/scout-hydrate.js +10 -0
- package/dist/lib/state.d.ts +6 -0
- package/dist/lib/state.js +49 -0
- package/dist/lib/template-constants.d.ts +4 -0
- package/dist/lib/template-constants.js +85 -0
- package/dist/lib/version.d.ts +1 -0
- package/dist/lib/version.js +19 -0
- package/package.json +30 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { apiGet, apiPost, apiPostDashboard, getDashboardUrl } from "../lib/client.js";
|
|
2
|
+
import { formatError } from "../lib/format.js";
|
|
3
|
+
import { AD_TYPES, AD_TYPE_NAMES, REPTILE_TRIGGERS, } from "../lib/template-constants.js";
|
|
4
|
+
export const helpText = `
|
|
5
|
+
exodus template — Fernando's Template pipeline (33 AD_TYPES, resume)
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
exodus template run --input "<ad brief or numbered ads>" [options]
|
|
9
|
+
exodus template resume --id <runId>
|
|
10
|
+
exodus template ad-types
|
|
11
|
+
exodus template reptile-triggers
|
|
12
|
+
|
|
13
|
+
Run options:
|
|
14
|
+
--input "<text>" Input ads block. One brief, or numbered list
|
|
15
|
+
("1. ad copy ... 2. ad copy ..."). Required.
|
|
16
|
+
--mode auto|manual Generation mode (default: auto)
|
|
17
|
+
--render-mode images|prompts Skip render and return prompts only (default: images)
|
|
18
|
+
--aspect 1:1|9:16 Image aspect ratio (default: 1:1)
|
|
19
|
+
--model gpt-image-2|nano-banana-pro Kie.ai model (default: gpt-image-2)
|
|
20
|
+
--realism off|realistic Realism enforcement (default: off)
|
|
21
|
+
--quantities <slug:N,slug:N> Manual-mode per-type counts (e.g. "testimonial:3,hero:2")
|
|
22
|
+
--requested-count N Auto-mode total render target (optional)
|
|
23
|
+
--no-wait (default — no CLI-side polling endpoint yet)
|
|
24
|
+
|
|
25
|
+
Resume options:
|
|
26
|
+
--id <runId> Template run id to resume (#56 orphan finalization)
|
|
27
|
+
|
|
28
|
+
Status polling:
|
|
29
|
+
The Convex HTTP route is POST-only. View live progress at the dashboard URL
|
|
30
|
+
printed at kickoff (or the dashboard's /creative-suite/template/sessions list).
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
exodus template run --input "silver-threaded grounding sheets for inflammation"
|
|
34
|
+
exodus template run --input "..." --aspect 9:16 --model nano-banana-pro --realism realistic
|
|
35
|
+
exodus template run --input "..." --mode manual --quantities "testimonial:3,hero:2,ugc:4"
|
|
36
|
+
exodus template resume --id <runId>
|
|
37
|
+
exodus template ad-types
|
|
38
|
+
`.trim();
|
|
39
|
+
function dashboardUrlForRun(runId) {
|
|
40
|
+
return `${getDashboardUrl()}/creative-suite/template/sessions/${runId}`;
|
|
41
|
+
}
|
|
42
|
+
function runAdTypes() {
|
|
43
|
+
console.log(`AD_TYPES (${AD_TYPES.length} total):`);
|
|
44
|
+
console.log("");
|
|
45
|
+
for (const slug of AD_TYPES) {
|
|
46
|
+
const name = AD_TYPE_NAMES[slug];
|
|
47
|
+
console.log(` ${slug.padEnd(28)} ${name}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function runReptileTriggers() {
|
|
51
|
+
console.log(`REPTILE_TRIGGERS (${REPTILE_TRIGGERS.length} total):`);
|
|
52
|
+
console.log("");
|
|
53
|
+
for (const trigger of REPTILE_TRIGGERS) {
|
|
54
|
+
console.log(` ${trigger}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function parseQuantities(raw) {
|
|
58
|
+
const out = {};
|
|
59
|
+
for (const pair of raw.split(",")) {
|
|
60
|
+
const [slug, nRaw] = pair.split(":").map((s) => s.trim());
|
|
61
|
+
if (!slug || !nRaw) {
|
|
62
|
+
throw new Error(`Malformed --quantities entry "${pair}". Use slug:N format.`);
|
|
63
|
+
}
|
|
64
|
+
if (!AD_TYPES.includes(slug)) {
|
|
65
|
+
throw new Error(`Unknown ad type "${slug}" in --quantities. Run \`exodus template ad-types\` for the full list.`);
|
|
66
|
+
}
|
|
67
|
+
const n = parseInt(nRaw, 10);
|
|
68
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
69
|
+
throw new Error(`--quantities count for "${slug}" must be a positive integer (got "${nRaw}").`);
|
|
70
|
+
}
|
|
71
|
+
out[slug] = n;
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
async function runTemplate(flags) {
|
|
76
|
+
const input = typeof flags["input"] === "string" ? flags["input"] : undefined;
|
|
77
|
+
if (!input || input.trim().length < 3) {
|
|
78
|
+
console.error("Error: template run requires --input \"<text>\" (min 3 chars).");
|
|
79
|
+
process.exit(1);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const mode = typeof flags["mode"] === "string" ? flags["mode"] : "auto";
|
|
83
|
+
if (mode !== "auto" && mode !== "manual") {
|
|
84
|
+
console.error(`Error: --mode must be auto | manual (got "${mode}").`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const renderMode = typeof flags["render-mode"] === "string" ? flags["render-mode"] : "images";
|
|
89
|
+
if (renderMode !== "images" && renderMode !== "prompts") {
|
|
90
|
+
console.error(`Error: --render-mode must be images | prompts (got "${renderMode}").`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const aspect = typeof flags["aspect"] === "string" ? flags["aspect"] : "1:1";
|
|
95
|
+
if (aspect !== "1:1" && aspect !== "9:16") {
|
|
96
|
+
console.error(`Error: --aspect must be 1:1 | 9:16 (got "${aspect}").`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const model = typeof flags["model"] === "string" ? flags["model"] : "gpt-image-2";
|
|
101
|
+
if (model !== "gpt-image-2" && model !== "nano-banana-pro") {
|
|
102
|
+
console.error(`Error: --model must be gpt-image-2 | nano-banana-pro (got "${model}").`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const realism = typeof flags["realism"] === "string" ? flags["realism"] : "off";
|
|
107
|
+
if (realism !== "off" && realism !== "realistic") {
|
|
108
|
+
console.error(`Error: --realism must be off | realistic (got "${realism}").`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
let manualQuantities;
|
|
113
|
+
if (typeof flags["quantities"] === "string") {
|
|
114
|
+
try {
|
|
115
|
+
manualQuantities = parseQuantities(flags["quantities"]);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (mode === "manual" && !manualQuantities) {
|
|
124
|
+
console.error("Error: --mode manual requires --quantities \"slug:N,slug:N\".");
|
|
125
|
+
process.exit(1);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const requestedRaw = flags["requested-count"];
|
|
129
|
+
const requestedImageCount = typeof requestedRaw === "string" ? parseInt(requestedRaw, 10) : undefined;
|
|
130
|
+
if (requestedImageCount !== undefined && (!Number.isFinite(requestedImageCount) || requestedImageCount <= 0)) {
|
|
131
|
+
console.error(`Error: --requested-count must be a positive integer.`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const who = await apiGet("/api/v2/whoami");
|
|
136
|
+
if (!who.ok || !who.data.userId || !who.data.workspaceId) {
|
|
137
|
+
console.error("Error: whoami failed. Check EXODUS_API_KEY and EXODUS_API_URL/CONVEX_SITE_URL.");
|
|
138
|
+
console.error(formatError(who));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const body = {
|
|
143
|
+
workspaceId: who.data.workspaceId,
|
|
144
|
+
submittedBy: who.data.userId,
|
|
145
|
+
mode,
|
|
146
|
+
renderMode,
|
|
147
|
+
inputAds: input,
|
|
148
|
+
aspectRatio: aspect,
|
|
149
|
+
model,
|
|
150
|
+
realismMode: realism,
|
|
151
|
+
...(requestedImageCount !== undefined ? { requestedImageCount } : {}),
|
|
152
|
+
...(manualQuantities ? { manualQuantities } : {}),
|
|
153
|
+
};
|
|
154
|
+
console.log(`Template run: brand=${who.data.workspaceSlug ?? who.data.workspaceId}, mode=${mode}, ` +
|
|
155
|
+
`aspect=${aspect}, model=${model}, realism=${realism}, render=${renderMode}`);
|
|
156
|
+
const res = await apiPost("/api/creative-suite-template/run", body);
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
console.error(formatError(res));
|
|
159
|
+
process.exit(1);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const { runId, triggerRunId } = res.data;
|
|
163
|
+
console.log("");
|
|
164
|
+
console.log(`✓ Template run started`);
|
|
165
|
+
console.log(` runId: ${runId}`);
|
|
166
|
+
if (triggerRunId)
|
|
167
|
+
console.log(` triggerRunId: ${triggerRunId}`);
|
|
168
|
+
console.log(` dashboard: ${dashboardUrlForRun(runId)}`);
|
|
169
|
+
console.log("");
|
|
170
|
+
console.log("Status updates render live in the dashboard (no CLI status endpoint yet).");
|
|
171
|
+
}
|
|
172
|
+
async function runResume(flags) {
|
|
173
|
+
const runId = typeof flags["id"] === "string" ? flags["id"] : undefined;
|
|
174
|
+
if (!runId) {
|
|
175
|
+
console.error("Error: template resume requires --id <runId>.");
|
|
176
|
+
process.exit(1);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const res = await apiPostDashboard(`/api/creative-suite-template/runs/${encodeURIComponent(runId)}/resume`, {});
|
|
180
|
+
if (!res.ok) {
|
|
181
|
+
console.error(formatError(res));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const { resumeCandidateCount, triggerRunId } = res.data;
|
|
186
|
+
console.log("");
|
|
187
|
+
console.log(`✓ Resume enqueued`);
|
|
188
|
+
console.log(` runId: ${runId}`);
|
|
189
|
+
if (resumeCandidateCount !== undefined) {
|
|
190
|
+
console.log(` resumeCandidateCount: ${resumeCandidateCount}`);
|
|
191
|
+
}
|
|
192
|
+
if (triggerRunId)
|
|
193
|
+
console.log(` triggerRunId: ${triggerRunId}`);
|
|
194
|
+
console.log(` dashboard: ${dashboardUrlForRun(runId)}`);
|
|
195
|
+
}
|
|
196
|
+
export async function run(flags) {
|
|
197
|
+
const sub = process.argv[3] ?? "";
|
|
198
|
+
if (sub === "ad-types") {
|
|
199
|
+
runAdTypes();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (sub === "reptile-triggers") {
|
|
203
|
+
runReptileTriggers();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (sub === "resume") {
|
|
207
|
+
await runResume(flags);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (sub === "run") {
|
|
211
|
+
await runTemplate(flags);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (!sub || sub.startsWith("--")) {
|
|
215
|
+
console.error("Error: template requires a subcommand (run | resume | ad-types | reptile-triggers).");
|
|
216
|
+
console.error("");
|
|
217
|
+
console.error(helpText);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
console.error(`Error: unknown template subcommand "${sub}".`);
|
|
222
|
+
console.error("");
|
|
223
|
+
console.error(helpText);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { apiGet, getApiUrl } from "../lib/client.js";
|
|
2
|
+
import { resolveActiveBrand } from "../lib/layout.js";
|
|
3
|
+
import { auth401Hint } from "../lib/backend-hint.js";
|
|
4
|
+
export const helpText = `
|
|
5
|
+
exodus whoami — show identity and brand context for the current API key
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
exodus whoami
|
|
9
|
+
|
|
10
|
+
Reads EXODUS_API_KEY from your .env (or env var) and asks the dashboard which
|
|
11
|
+
workspace this key is bound to. Prints:
|
|
12
|
+
• Brand (workspace slug + name)
|
|
13
|
+
• User email + role
|
|
14
|
+
• Whether the brand has full Genesis foundation knowledge filled in
|
|
15
|
+
|
|
16
|
+
Run this BEFORE any pipeline command to confirm you're pointed at the right
|
|
17
|
+
brand. If "foundation: NOT READY" appears, ask an admin to complete the
|
|
18
|
+
brand's foundation at /settings?tab=brands&brand=<slug>.
|
|
19
|
+
`.trim();
|
|
20
|
+
export async function run() {
|
|
21
|
+
const res = await apiGet("/api/v2/whoami");
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
if (res.status === 401 || res.status === 403) {
|
|
24
|
+
console.error(auth401Hint(getApiUrl()));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const message = res.data?.error ??
|
|
28
|
+
`whoami failed (HTTP ${res.status})`;
|
|
29
|
+
console.error(message);
|
|
30
|
+
}
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const d = res.data;
|
|
34
|
+
const ready = d.foundationReady ? "READY" : "NOT READY";
|
|
35
|
+
const missing = d.foundationMissing.length
|
|
36
|
+
? `\n missing: ${d.foundationMissing.join(", ")}`
|
|
37
|
+
: "";
|
|
38
|
+
const resolved = resolveActiveBrand();
|
|
39
|
+
const sourceNote = resolved.source === "folder" ? " (brand folder)" : "";
|
|
40
|
+
const activeLine = resolved.slug
|
|
41
|
+
? `\nactive: ${resolved.slug}${sourceNote}${resolved.slug !== d.workspaceSlug ? " ← X-Active-Brand override" : ""}`
|
|
42
|
+
: d.userRole === "admin"
|
|
43
|
+
? `\nactive: (key default)`
|
|
44
|
+
: "";
|
|
45
|
+
console.log(`
|
|
46
|
+
brand: ${d.workspaceName ?? "—"} (${d.workspaceSlug ?? "—"})
|
|
47
|
+
user: ${d.userEmail ?? "—"} (${d.userRole ?? "—"})
|
|
48
|
+
key: ${d.keyPrefix ?? "—"}${activeLine}
|
|
49
|
+
foundation: ${ready}${missing}
|
|
50
|
+
`.trim());
|
|
51
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface SwipeWriteResult {
|
|
2
|
+
dispatched: Array<{
|
|
3
|
+
url: string;
|
|
4
|
+
key: string;
|
|
5
|
+
runId: string;
|
|
6
|
+
}>;
|
|
7
|
+
bankedFailed: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function captureSwipeAndWrite(urls: string[], opts: {
|
|
10
|
+
awarenessLevel: string;
|
|
11
|
+
variantCount?: number;
|
|
12
|
+
steering?: string;
|
|
13
|
+
}, rt?: {
|
|
14
|
+
cc?: string;
|
|
15
|
+
}): Promise<SwipeWriteResult>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { apiGet, apiPost } from "./client.js";
|
|
2
|
+
import { formatError } from "./format.js";
|
|
3
|
+
function normalizeUrl(u) {
|
|
4
|
+
return u.trim().replace(/\/+$/, "").toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
7
|
+
async function listAdLibrary() {
|
|
8
|
+
const res = await apiGet("/api/v2/idea-bank?source=ad-library&limit=200");
|
|
9
|
+
if (!res.ok)
|
|
10
|
+
return [];
|
|
11
|
+
return Array.isArray(res.data.ideas) ? res.data.ideas : [];
|
|
12
|
+
}
|
|
13
|
+
export async function captureSwipeAndWrite(urls, opts, rt = {}) {
|
|
14
|
+
const wanted = urls.map((u) => u.trim()).filter(Boolean);
|
|
15
|
+
if (wanted.length === 0)
|
|
16
|
+
return { dispatched: [], bankedFailed: [] };
|
|
17
|
+
const wantedNorm = new Map(wanted.map((u) => [normalizeUrl(u), u]));
|
|
18
|
+
const before = new Set((await listAdLibrary()).map((r) => r.key).filter(Boolean));
|
|
19
|
+
const ideate = await apiPost("/api/v2/idea-bank/ideate", { source: "ad-library", urls: wanted }, { ccCommand: rt.cc });
|
|
20
|
+
if (!ideate.ok) {
|
|
21
|
+
console.log(formatError(ideate));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
25
|
+
const INTERVAL_MS = 4_000;
|
|
26
|
+
const start = Date.now();
|
|
27
|
+
const found = new Map();
|
|
28
|
+
process.stdout.write(` scraping ${wanted.length} ad${wanted.length === 1 ? "" : "s"} into ideas`);
|
|
29
|
+
while (found.size < wantedNorm.size && Date.now() - start < TIMEOUT_MS) {
|
|
30
|
+
await sleep(INTERVAL_MS);
|
|
31
|
+
process.stdout.write(".");
|
|
32
|
+
for (const row of await listAdLibrary()) {
|
|
33
|
+
if (!row.key || before.has(row.key) || found.has(normalizeUrl(row.sourceRef ?? "")))
|
|
34
|
+
continue;
|
|
35
|
+
const ref = row.sourceRef ? normalizeUrl(row.sourceRef) : "";
|
|
36
|
+
if (ref && wantedNorm.has(ref))
|
|
37
|
+
found.set(ref, row.key);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
process.stdout.write("\n");
|
|
41
|
+
const bankedFailed = [...wantedNorm.entries()]
|
|
42
|
+
.filter(([norm]) => !found.has(norm))
|
|
43
|
+
.map(([, original]) => original);
|
|
44
|
+
if (found.size === 0)
|
|
45
|
+
return { dispatched: [], bankedFailed };
|
|
46
|
+
const keys = [...found.values()];
|
|
47
|
+
const steering = opts.steering?.trim();
|
|
48
|
+
if (steering) {
|
|
49
|
+
for (const key of keys) {
|
|
50
|
+
const noted = await apiPost("/api/v2/idea-bank/notes", { key, notes: steering }, { ccCommand: rt.cc });
|
|
51
|
+
if (!noted.ok)
|
|
52
|
+
console.error(` (couldn't attach steering to ${key}: ${formatError(noted)})`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const dispatch = await apiPost("/api/v2/idea-bank/dispatch", { keys, awarenessLevel: opts.awarenessLevel, variantCount: opts.variantCount }, { ccCommand: rt.cc });
|
|
56
|
+
if (!dispatch.ok) {
|
|
57
|
+
console.log(formatError(dispatch));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
const keyToUrl = new Map([...found.entries()].map(([norm, key]) => [key, wantedNorm.get(norm)]));
|
|
61
|
+
const dispatched = (dispatch.data.dispatched ?? []).map((d) => ({
|
|
62
|
+
url: keyToUrl.get(d.key) ?? "",
|
|
63
|
+
key: d.key,
|
|
64
|
+
runId: d.runId,
|
|
65
|
+
}));
|
|
66
|
+
return { dispatched, bankedFailed };
|
|
67
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
export function assetsRoot(override) {
|
|
5
|
+
let dir = override ?? path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const { root } = path.parse(dir);
|
|
7
|
+
for (;;) {
|
|
8
|
+
const candidate = path.join(dir, "assets");
|
|
9
|
+
if (fs.existsSync(candidate))
|
|
10
|
+
return candidate;
|
|
11
|
+
if (dir === root) {
|
|
12
|
+
throw new Error(`bundled assets not found (searched up from ${override ?? "module dir"}). ` +
|
|
13
|
+
`Run \`npm run bundle-assets\` in dev, or reinstall the package.`);
|
|
14
|
+
}
|
|
15
|
+
dir = path.dirname(dir);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function skillsDir(override) {
|
|
19
|
+
return path.join(assetsRoot(override), "skills");
|
|
20
|
+
}
|
|
21
|
+
export function referencesDir(override) {
|
|
22
|
+
return path.join(assetsRoot(override), "references");
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const PROD_BACKEND = "accomplished-tapir-106";
|
|
2
|
+
const DEV_BACKEND = "good-cod-360";
|
|
3
|
+
const PLACEHOLDER_MARK = "YOUR-EXODUS-BACKEND";
|
|
4
|
+
export function isPlaceholderApiUrl(apiUrl) {
|
|
5
|
+
return !apiUrl || apiUrl.includes(PLACEHOLDER_MARK);
|
|
6
|
+
}
|
|
7
|
+
export function auth401Hint(apiUrl) {
|
|
8
|
+
let host = apiUrl || "(no EXODUS_API_URL set)";
|
|
9
|
+
try {
|
|
10
|
+
host = new URL(apiUrl).host;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
}
|
|
14
|
+
return [
|
|
15
|
+
`This API key was rejected by ${host} (HTTP 401).`,
|
|
16
|
+
`Keys are environment-bound — this key may belong to the other environment.`,
|
|
17
|
+
`Set EXODUS_API_URL in your .env to match the dashboard you signed up on:`,
|
|
18
|
+
` • xo.copycoders.ai → EXODUS_API_URL=https://${PROD_BACKEND}.convex.site`,
|
|
19
|
+
` • dev.xo.copycoders.ai → EXODUS_API_URL=https://${DEV_BACKEND}.convex.site`,
|
|
20
|
+
`Then re-run \`node exodus/dist/bin/exodus.js whoami\`.`,
|
|
21
|
+
].join("\n");
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatCcCommand(argv: string[]): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function formatCcCommand(argv) {
|
|
2
|
+
const [cmd, ...rest] = argv;
|
|
3
|
+
if (!cmd)
|
|
4
|
+
return "";
|
|
5
|
+
const parts = rest.map((arg) => {
|
|
6
|
+
if (arg.startsWith("--"))
|
|
7
|
+
return arg;
|
|
8
|
+
if (/\s/.test(arg))
|
|
9
|
+
return `"${arg.replace(/"/g, '\\"')}"`;
|
|
10
|
+
return arg;
|
|
11
|
+
});
|
|
12
|
+
return `/${cmd}${parts.length ? " " + parts.join(" ") : ""}`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ApiResponse<T> {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
status: number;
|
|
4
|
+
data: T;
|
|
5
|
+
}
|
|
6
|
+
export declare function getApiUrl(): string;
|
|
7
|
+
export declare function apiGet<T>(path: string, opts?: {
|
|
8
|
+
skipActiveBrand?: boolean;
|
|
9
|
+
activeBrandOverride?: string;
|
|
10
|
+
}): Promise<ApiResponse<T>>;
|
|
11
|
+
export declare function apiPost<T>(path: string, body: unknown, opts?: {
|
|
12
|
+
ccCommand?: string;
|
|
13
|
+
}): Promise<ApiResponse<T>>;
|
|
14
|
+
export declare function resolveDashboardUrl(opts: {
|
|
15
|
+
override?: string;
|
|
16
|
+
apiUrl?: string;
|
|
17
|
+
}): string;
|
|
18
|
+
export declare function getDashboardUrl(): string;
|
|
19
|
+
export declare function apiGetDashboard<T>(path: string, opts?: {
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
activeBrandOverride?: string;
|
|
22
|
+
}): Promise<ApiResponse<T>>;
|
|
23
|
+
export declare function apiPostDashboard<T>(path: string, body: unknown): Promise<ApiResponse<T>>;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveActiveBrand } from "./layout.js";
|
|
4
|
+
let cachedConfig = null;
|
|
5
|
+
function parseEnvFile(content) {
|
|
6
|
+
const result = {};
|
|
7
|
+
for (const line of content.split("\n")) {
|
|
8
|
+
const trimmed = line.trim();
|
|
9
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
10
|
+
continue;
|
|
11
|
+
const eqIdx = trimmed.indexOf("=");
|
|
12
|
+
if (eqIdx === -1)
|
|
13
|
+
continue;
|
|
14
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
15
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
16
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
17
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
18
|
+
value = value.slice(1, -1);
|
|
19
|
+
}
|
|
20
|
+
result[key] = value;
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
function loadConfig() {
|
|
25
|
+
let dir = process.cwd();
|
|
26
|
+
for (let i = 0; i < 8; i++) {
|
|
27
|
+
const envPath = path.join(dir, ".env");
|
|
28
|
+
if (fs.existsSync(envPath)) {
|
|
29
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
30
|
+
const vars = parseEnvFile(content);
|
|
31
|
+
const apiUrl = vars["EXODUS_API_URL"] ??
|
|
32
|
+
vars["CONVEX_SITE_URL"] ??
|
|
33
|
+
process.env["EXODUS_API_URL"] ??
|
|
34
|
+
process.env["CONVEX_SITE_URL"] ??
|
|
35
|
+
"";
|
|
36
|
+
const apiKey = vars["EXODUS_API_KEY"] ??
|
|
37
|
+
vars["VAD_API_KEY"] ??
|
|
38
|
+
process.env["EXODUS_API_KEY"] ??
|
|
39
|
+
process.env["VAD_API_KEY"] ??
|
|
40
|
+
"";
|
|
41
|
+
if (apiUrl) {
|
|
42
|
+
return { apiUrl: apiUrl.replace(/\/$/, ""), apiKey };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const parent = path.dirname(dir);
|
|
46
|
+
if (parent === dir)
|
|
47
|
+
break;
|
|
48
|
+
dir = parent;
|
|
49
|
+
}
|
|
50
|
+
const apiUrl = process.env["EXODUS_API_URL"] ??
|
|
51
|
+
process.env["CONVEX_SITE_URL"] ??
|
|
52
|
+
"";
|
|
53
|
+
const apiKey = process.env["EXODUS_API_KEY"] ??
|
|
54
|
+
process.env["VAD_API_KEY"] ??
|
|
55
|
+
"";
|
|
56
|
+
return { apiUrl: apiUrl.replace(/\/$/, ""), apiKey };
|
|
57
|
+
}
|
|
58
|
+
function getConfig() {
|
|
59
|
+
if (!cachedConfig) {
|
|
60
|
+
cachedConfig = loadConfig();
|
|
61
|
+
}
|
|
62
|
+
return cachedConfig;
|
|
63
|
+
}
|
|
64
|
+
export function getApiUrl() {
|
|
65
|
+
return getConfig().apiUrl;
|
|
66
|
+
}
|
|
67
|
+
function buildHeaders(apiKey, ccCommand, opts) {
|
|
68
|
+
const headers = {
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
};
|
|
71
|
+
if (apiKey) {
|
|
72
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
73
|
+
}
|
|
74
|
+
if (!opts?.skipActiveBrand) {
|
|
75
|
+
const activeBrand = opts?.activeBrandOverride ?? resolveActiveBrand().slug;
|
|
76
|
+
if (activeBrand) {
|
|
77
|
+
headers["X-Active-Brand"] = activeBrand;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (ccCommand) {
|
|
81
|
+
const sanitized = ccCommand
|
|
82
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
83
|
+
.replace(/[^\x20-\x7e]/g, "")
|
|
84
|
+
.slice(0, 500);
|
|
85
|
+
if (sanitized)
|
|
86
|
+
headers["X-CC-Command"] = sanitized;
|
|
87
|
+
}
|
|
88
|
+
return headers;
|
|
89
|
+
}
|
|
90
|
+
export async function apiGet(path, opts) {
|
|
91
|
+
const { apiUrl, apiKey } = getConfig();
|
|
92
|
+
const url = `${apiUrl}${path}`;
|
|
93
|
+
const res = await fetch(url, {
|
|
94
|
+
method: "GET",
|
|
95
|
+
headers: buildHeaders(apiKey, undefined, opts),
|
|
96
|
+
});
|
|
97
|
+
const text = await res.text();
|
|
98
|
+
let data;
|
|
99
|
+
try {
|
|
100
|
+
data = JSON.parse(text);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
const snippet = text.replace(/\s+/g, " ").trim().slice(0, 300);
|
|
104
|
+
data = {
|
|
105
|
+
error: `Non-JSON ${res.status} from ${path}: ${snippet}`,
|
|
106
|
+
httpStatus: res.status,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return { ok: res.ok, status: res.status, data: data };
|
|
110
|
+
}
|
|
111
|
+
export async function apiPost(path, body, opts) {
|
|
112
|
+
const { apiUrl, apiKey } = getConfig();
|
|
113
|
+
const url = `${apiUrl}${path}`;
|
|
114
|
+
const res = await fetch(url, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: buildHeaders(apiKey, opts?.ccCommand),
|
|
117
|
+
body: JSON.stringify(body),
|
|
118
|
+
});
|
|
119
|
+
const text = await res.text();
|
|
120
|
+
let data;
|
|
121
|
+
try {
|
|
122
|
+
data = JSON.parse(text);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
const snippet = text.replace(/\s+/g, " ").trim().slice(0, 300);
|
|
126
|
+
data = {
|
|
127
|
+
error: `Non-JSON ${res.status} from ${path}: ${snippet}`,
|
|
128
|
+
httpStatus: res.status,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return { ok: res.ok, status: res.status, data: data };
|
|
132
|
+
}
|
|
133
|
+
const DEV_BACKEND_DASHBOARDS = [
|
|
134
|
+
{ match: "good-cod-360", dashboard: "https://dev.xo.copycoders.ai" },
|
|
135
|
+
];
|
|
136
|
+
const PROD_DASHBOARD = "https://xo.copycoders.ai";
|
|
137
|
+
export function resolveDashboardUrl(opts) {
|
|
138
|
+
if (opts.override)
|
|
139
|
+
return opts.override.replace(/\/$/, "");
|
|
140
|
+
const api = opts.apiUrl ?? "";
|
|
141
|
+
for (const { match, dashboard } of DEV_BACKEND_DASHBOARDS) {
|
|
142
|
+
if (api.includes(match))
|
|
143
|
+
return dashboard;
|
|
144
|
+
}
|
|
145
|
+
return PROD_DASHBOARD;
|
|
146
|
+
}
|
|
147
|
+
export function getDashboardUrl() {
|
|
148
|
+
return resolveDashboardUrl({
|
|
149
|
+
override: process.env["EXODUS_DASHBOARD_URL"],
|
|
150
|
+
apiUrl: getConfig().apiUrl,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
export async function apiGetDashboard(path, opts) {
|
|
154
|
+
const { apiKey } = getConfig();
|
|
155
|
+
const url = `${getDashboardUrl()}${path}`;
|
|
156
|
+
const headers = buildHeaders(apiKey, undefined, {
|
|
157
|
+
activeBrandOverride: opts?.activeBrandOverride,
|
|
158
|
+
});
|
|
159
|
+
const controller = opts?.timeoutMs ? new AbortController() : undefined;
|
|
160
|
+
const timer = controller
|
|
161
|
+
? setTimeout(() => controller.abort(), opts.timeoutMs)
|
|
162
|
+
: undefined;
|
|
163
|
+
let res;
|
|
164
|
+
try {
|
|
165
|
+
res = await fetch(url, {
|
|
166
|
+
method: "GET",
|
|
167
|
+
headers,
|
|
168
|
+
signal: controller?.signal,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
if (timer)
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
}
|
|
175
|
+
const text = await res.text();
|
|
176
|
+
let data;
|
|
177
|
+
try {
|
|
178
|
+
data = JSON.parse(text);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
const snippet = text.replace(/\s+/g, " ").trim().slice(0, 300);
|
|
182
|
+
data = {
|
|
183
|
+
error: `Non-JSON ${res.status} from ${path}: ${snippet}`,
|
|
184
|
+
httpStatus: res.status,
|
|
185
|
+
contentType: res.headers.get("content-type") ?? undefined,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return { ok: res.ok, status: res.status, data: data };
|
|
189
|
+
}
|
|
190
|
+
export async function apiPostDashboard(path, body) {
|
|
191
|
+
const { apiKey } = getConfig();
|
|
192
|
+
const url = `${getDashboardUrl()}${path}`;
|
|
193
|
+
const headers = buildHeaders(apiKey);
|
|
194
|
+
const res = await fetch(url, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers,
|
|
197
|
+
body: JSON.stringify(body),
|
|
198
|
+
});
|
|
199
|
+
const text = await res.text();
|
|
200
|
+
let data;
|
|
201
|
+
try {
|
|
202
|
+
data = JSON.parse(text);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
const snippet = text.replace(/\s+/g, " ").trim().slice(0, 300);
|
|
206
|
+
data = {
|
|
207
|
+
error: `Non-JSON ${res.status} from ${path}: ${snippet}`,
|
|
208
|
+
httpStatus: res.status,
|
|
209
|
+
contentType: res.headers.get("content-type") ?? undefined,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return { ok: res.ok, status: res.status, data: data };
|
|
213
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function formatGeneration(data: Record<string, unknown>): string;
|
|
2
|
+
export declare function formatGenesisRun(data: Record<string, unknown>): string;
|
|
3
|
+
export declare function formatBrowse(generations: unknown[]): string;
|
|
4
|
+
export declare function formatError(res: {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
status: number;
|
|
7
|
+
data: unknown;
|
|
8
|
+
}): string;
|