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