@aicopycoders/exodus 2026.7.200-beta.8 → 2026.8.1100
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/README.md +51 -0
- package/assets/docs/CLAUDE.md +3 -0
- package/assets/docs/PIPELINES.md +50 -0
- package/assets/skills/exodus/SKILL.md +6 -2
- package/assets/skills/exodus-meme/SKILL.md +44 -28
- package/assets/skills/exodus-winners/SKILL.md +239 -0
- package/assets/skills/exodus-workflow/SKILL.md +464 -63
- package/dist/bin/exodus.js +3 -0
- package/dist/commands/bank.d.ts +88 -0
- package/dist/commands/bank.js +346 -0
- package/dist/commands/browse.js +2 -1
- package/dist/commands/creative.js +2 -2
- package/dist/commands/genesis.js +16 -4
- package/dist/commands/keys.js +1 -0
- package/dist/commands/meme.js +9 -6
- package/dist/commands/primer.js +2 -1
- package/dist/commands/session.d.ts +55 -0
- package/dist/commands/session.js +202 -0
- package/dist/commands/status.js +4 -4
- package/dist/commands/swipe.js +4 -4
- package/dist/commands/template.js +2 -2
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +20 -0
- package/dist/commands/winners.js +13 -3
- package/dist/commands/workflow.d.ts +293 -8
- package/dist/commands/workflow.js +2396 -90
- package/dist/lib/client.d.ts +8 -1
- package/dist/lib/client.js +30 -6
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.js +52 -5
- package/dist/lib/intel-format.js +2 -1
- package/dist/lib/keys-sync.js +1 -0
- package/dist/lib/poll.js +7 -4
- package/dist/lib/route-support.d.ts +4 -0
- package/dist/lib/route-support.js +22 -0
- package/dist/lib/runStatus.d.ts +22 -0
- package/dist/lib/runStatus.js +139 -0
- package/dist/lib/workflowText.d.ts +3 -0
- package/dist/lib/workflowText.js +113 -0
- package/package.json +5 -2
package/dist/bin/exodus.js
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type ApiResponse } from "../lib/client.js";
|
|
2
|
+
import { type Channel } from "../lib/channel.js";
|
|
3
|
+
export declare const helpText: string;
|
|
4
|
+
export declare const AWARENESS_PAIRS: readonly ["unawareProblemAware", "solutionProductAware", "mostAware"];
|
|
5
|
+
export type AwarenessPair = (typeof AWARENESS_PAIRS)[number];
|
|
6
|
+
export interface BankSummary {
|
|
7
|
+
key: string;
|
|
8
|
+
name: string;
|
|
9
|
+
type: string;
|
|
10
|
+
awarenessPair?: AwarenessPair;
|
|
11
|
+
entryCount: number;
|
|
12
|
+
}
|
|
13
|
+
export interface BankWinMetrics {
|
|
14
|
+
spend?: number;
|
|
15
|
+
roas?: number;
|
|
16
|
+
ctr?: number;
|
|
17
|
+
note?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface BankProvenance {
|
|
20
|
+
workflowRunId?: string;
|
|
21
|
+
genesisRunId?: string;
|
|
22
|
+
nodeId?: string;
|
|
23
|
+
variantIndex?: number;
|
|
24
|
+
artifactKind?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface BankEntry {
|
|
27
|
+
_id: string;
|
|
28
|
+
text: string;
|
|
29
|
+
source: string;
|
|
30
|
+
awarenessPair?: AwarenessPair;
|
|
31
|
+
winMetrics?: BankWinMetrics;
|
|
32
|
+
humanEdited?: boolean;
|
|
33
|
+
provenance?: BankProvenance;
|
|
34
|
+
createdAt: number | string;
|
|
35
|
+
}
|
|
36
|
+
export interface BankShowResponse {
|
|
37
|
+
bank: {
|
|
38
|
+
key: string;
|
|
39
|
+
name: string;
|
|
40
|
+
type: string;
|
|
41
|
+
awarenessPair?: AwarenessPair;
|
|
42
|
+
};
|
|
43
|
+
entries: BankEntry[];
|
|
44
|
+
}
|
|
45
|
+
export interface BankPromoteBody {
|
|
46
|
+
bankKey: string;
|
|
47
|
+
text: string;
|
|
48
|
+
awarenessPair?: AwarenessPair;
|
|
49
|
+
winMetrics?: BankWinMetrics;
|
|
50
|
+
provenance?: BankProvenance;
|
|
51
|
+
}
|
|
52
|
+
export interface BankPromoteResponse {
|
|
53
|
+
entryId: string;
|
|
54
|
+
bankName: string;
|
|
55
|
+
event?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface FlowResult {
|
|
58
|
+
code: number;
|
|
59
|
+
lines: string[];
|
|
60
|
+
}
|
|
61
|
+
export interface BankDeps {
|
|
62
|
+
get: (path: string) => Promise<ApiResponse<unknown>>;
|
|
63
|
+
post: (path: string, body: unknown) => Promise<ApiResponse<unknown>>;
|
|
64
|
+
readFile: (path: string) => string;
|
|
65
|
+
readStdin: () => string;
|
|
66
|
+
stdinIsTTY: () => boolean;
|
|
67
|
+
channel: Channel;
|
|
68
|
+
}
|
|
69
|
+
export declare function relativeAge(value: number | string, now?: number): string;
|
|
70
|
+
export declare function formatBankList(banks: BankSummary[]): string;
|
|
71
|
+
export declare function formatBankShow(res: BankShowResponse): string;
|
|
72
|
+
interface PromoteTextSources {
|
|
73
|
+
arg?: string;
|
|
74
|
+
file?: string;
|
|
75
|
+
readFile: (path: string) => string;
|
|
76
|
+
readStdin: () => string;
|
|
77
|
+
stdinIsTTY: () => boolean;
|
|
78
|
+
}
|
|
79
|
+
export declare function resolvePromoteText(opts: PromoteTextSources): string;
|
|
80
|
+
export declare function parseWinMetrics(flags: Record<string, string | boolean>): BankWinMetrics | undefined;
|
|
81
|
+
export declare function parseProvenance(flags: Record<string, string | boolean>): BankProvenance | undefined;
|
|
82
|
+
export declare function parseAwareness(flags: Record<string, string | boolean>): AwarenessPair | undefined;
|
|
83
|
+
export declare function buildPromoteBody(bankKey: string, text: string, flags: Record<string, string | boolean>): BankPromoteBody;
|
|
84
|
+
export declare function listFlow(json: boolean, deps: BankDeps): Promise<FlowResult>;
|
|
85
|
+
export declare function showFlow(key: string, json: boolean, deps: BankDeps): Promise<FlowResult>;
|
|
86
|
+
export declare function promoteFlow(key: string, arg: string | undefined, flags: Record<string, string | boolean>, json: boolean, deps: BankDeps): Promise<FlowResult>;
|
|
87
|
+
export declare function run(flags: Record<string, string | boolean>): Promise<void>;
|
|
88
|
+
export {};
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { apiGet, apiPost } from "../lib/client.js";
|
|
3
|
+
import { formatApiError } from "../lib/format.js";
|
|
4
|
+
import { getChannel } from "../lib/channel.js";
|
|
5
|
+
import { missingRouteLine } from "../lib/route-support.js";
|
|
6
|
+
export const helpText = `
|
|
7
|
+
exodus bank — List, inspect, and promote entries into this brand's copy banks
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
exodus bank list [--json]
|
|
11
|
+
exodus bank show <key> [--json]
|
|
12
|
+
exodus bank promote <key> [text] [--file <path>] [options] [--json]
|
|
13
|
+
|
|
14
|
+
Flags:
|
|
15
|
+
--json Machine-readable JSON output (includes ids + provenance)
|
|
16
|
+
--file <path> (promote) Read the entry text from a file instead of an
|
|
17
|
+
argument; piping text on stdin is a third option. A text
|
|
18
|
+
argument and --file conflict (provide one, not both);
|
|
19
|
+
stdin is read only when neither is given.
|
|
20
|
+
--awareness <pair> (promote) Tag a body-bank entry with an awareness pair:
|
|
21
|
+
unawareProblemAware | solutionProductAware | mostAware
|
|
22
|
+
--spend <n> (promote) Win metric: ad spend (number)
|
|
23
|
+
--roas <n> (promote) Win metric: return on ad spend (number)
|
|
24
|
+
--ctr <n> (promote) Win metric: click-through rate (number)
|
|
25
|
+
--note <s> (promote) Win metric: freeform note
|
|
26
|
+
--run <runId> (promote) Provenance: the run this copy came from
|
|
27
|
+
--node <nodeId> (promote) Provenance: the node this copy came from
|
|
28
|
+
|
|
29
|
+
Banks are addressed by their well-known KEY (shown by \`bank list\`), never by an
|
|
30
|
+
internal id. A fresh brand starts with six banks, all empty. \`bank show\` numbers
|
|
31
|
+
entries newest-first with their source, win metrics, and age. \`bank promote\`
|
|
32
|
+
appends a winning line to a bank; the server rejects an unknown key (listing the
|
|
33
|
+
valid ones), empty/over-long text, and a bank that has hit its 2000-entry cap —
|
|
34
|
+
those messages print verbatim.
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
exodus bank list
|
|
38
|
+
exodus bank show hooks
|
|
39
|
+
exodus bank promote hooks "Stop scrolling — your knees will thank you"
|
|
40
|
+
exodus bank promote body:solutionProductAware --file winner.txt --awareness solutionProductAware
|
|
41
|
+
cat winner.txt | exodus bank promote hooks
|
|
42
|
+
exodus bank promote hooks "..." --spend 4200 --roas 3.1 --ctr 1.8 --note "Q3 winner"
|
|
43
|
+
exodus bank promote hooks "..." --run wr_123 --node bot-4
|
|
44
|
+
`.trim();
|
|
45
|
+
export const AWARENESS_PAIRS = [
|
|
46
|
+
"unawareProblemAware",
|
|
47
|
+
"solutionProductAware",
|
|
48
|
+
"mostAware",
|
|
49
|
+
];
|
|
50
|
+
const LIST_PATH = "/api/v2/banks";
|
|
51
|
+
const SHOW_PATH = "/api/v2/banks/show";
|
|
52
|
+
const PROMOTE_PATH = "/api/v2/banks/promote";
|
|
53
|
+
const FLYWHEEL_LINE = 'Winner Flywheel: "winner-promoted" dispatched — any workflow with an enabled ' +
|
|
54
|
+
"trigger starts a background run (watch: exodus workflow inbox)";
|
|
55
|
+
const VALUE_FLAGS = new Set(["file", "awareness", "spend", "roas", "ctr", "note", "run", "node"]);
|
|
56
|
+
const defaultDeps = {
|
|
57
|
+
get: (path) => apiGet(path),
|
|
58
|
+
post: (path, body) => apiPost(path, body),
|
|
59
|
+
readFile: (path) => fs.readFileSync(path, "utf-8"),
|
|
60
|
+
readStdin: () => fs.readFileSync(0, "utf-8"),
|
|
61
|
+
stdinIsTTY: () => !!process.stdin.isTTY,
|
|
62
|
+
channel: getChannel(),
|
|
63
|
+
};
|
|
64
|
+
function asErrorResult(res, json) {
|
|
65
|
+
return {
|
|
66
|
+
code: 1,
|
|
67
|
+
lines: json
|
|
68
|
+
? [JSON.stringify({ ok: false, status: res.status, data: res.data })]
|
|
69
|
+
: [formatApiError(res)],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function table(headers, rows) {
|
|
73
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)));
|
|
74
|
+
const fmt = (row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
|
|
75
|
+
return [fmt(headers), fmt(headers.map((h) => "-".repeat(h.length))), ...rows.map(fmt)].join("\n");
|
|
76
|
+
}
|
|
77
|
+
export function relativeAge(value, now = Date.now()) {
|
|
78
|
+
const t = typeof value === "number" ? value : Date.parse(value);
|
|
79
|
+
if (!t || Number.isNaN(t))
|
|
80
|
+
return "unknown";
|
|
81
|
+
const diff = now - t;
|
|
82
|
+
if (diff < 0)
|
|
83
|
+
return "just now";
|
|
84
|
+
const sec = Math.floor(diff / 1000);
|
|
85
|
+
if (sec < 45)
|
|
86
|
+
return "just now";
|
|
87
|
+
const min = Math.floor(sec / 60);
|
|
88
|
+
if (min < 60)
|
|
89
|
+
return `${min}m ago`;
|
|
90
|
+
const hr = Math.floor(min / 60);
|
|
91
|
+
if (hr < 24)
|
|
92
|
+
return `${hr}h ago`;
|
|
93
|
+
const day = Math.floor(hr / 24);
|
|
94
|
+
if (day < 7)
|
|
95
|
+
return `${day}d ago`;
|
|
96
|
+
const wk = Math.floor(day / 7);
|
|
97
|
+
if (wk < 5)
|
|
98
|
+
return `${wk}w ago`;
|
|
99
|
+
const mo = Math.floor(day / 30);
|
|
100
|
+
if (mo < 12)
|
|
101
|
+
return `${mo}mo ago`;
|
|
102
|
+
return `${Math.floor(day / 365)}y ago`;
|
|
103
|
+
}
|
|
104
|
+
function winMetricsLine(wm) {
|
|
105
|
+
if (!wm)
|
|
106
|
+
return null;
|
|
107
|
+
const parts = [];
|
|
108
|
+
if (typeof wm.spend === "number")
|
|
109
|
+
parts.push(`spend=${wm.spend}`);
|
|
110
|
+
if (typeof wm.roas === "number")
|
|
111
|
+
parts.push(`roas=${wm.roas}`);
|
|
112
|
+
if (typeof wm.ctr === "number")
|
|
113
|
+
parts.push(`ctr=${wm.ctr}`);
|
|
114
|
+
if (wm.note)
|
|
115
|
+
parts.push(`note="${wm.note}"`);
|
|
116
|
+
return parts.length > 0 ? ` win: ${parts.join(" ")}` : null;
|
|
117
|
+
}
|
|
118
|
+
function textBlock(text) {
|
|
119
|
+
const lines = text.split("\n");
|
|
120
|
+
return lines.map((l) => ` ${l}`);
|
|
121
|
+
}
|
|
122
|
+
export function formatBankList(banks) {
|
|
123
|
+
if (banks.length === 0)
|
|
124
|
+
return "No banks found for the active brand.";
|
|
125
|
+
return table(["key", "name", "type", "entries"], banks.map((b) => [b.key, b.name, b.type, String(b.entryCount)]));
|
|
126
|
+
}
|
|
127
|
+
export function formatBankShow(res) {
|
|
128
|
+
const lines = [];
|
|
129
|
+
const { bank, entries } = res;
|
|
130
|
+
lines.push(`Bank — ${bank.name}`);
|
|
131
|
+
lines.push(`key: ${bank.key}`);
|
|
132
|
+
lines.push(`type: ${bank.type}`);
|
|
133
|
+
if (bank.awarenessPair)
|
|
134
|
+
lines.push(`awareness: ${bank.awarenessPair}`);
|
|
135
|
+
lines.push(`entries: ${entries.length}`);
|
|
136
|
+
if (entries.length === 0) {
|
|
137
|
+
lines.push("");
|
|
138
|
+
lines.push(" (no entries yet — promote a winner with `exodus bank promote`)");
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
entries.forEach((entry, i) => {
|
|
142
|
+
lines.push("");
|
|
143
|
+
const badges = [`[${entry.source}]`];
|
|
144
|
+
if (entry.awarenessPair)
|
|
145
|
+
badges.push(entry.awarenessPair);
|
|
146
|
+
if (entry.humanEdited)
|
|
147
|
+
badges.push("edited");
|
|
148
|
+
badges.push(relativeAge(entry.createdAt));
|
|
149
|
+
lines.push(`${i + 1}. ${badges.join(" · ")}`);
|
|
150
|
+
lines.push(...textBlock(entry.text));
|
|
151
|
+
const win = winMetricsLine(entry.winMetrics);
|
|
152
|
+
if (win)
|
|
153
|
+
lines.push(win);
|
|
154
|
+
});
|
|
155
|
+
return lines.join("\n");
|
|
156
|
+
}
|
|
157
|
+
export function resolvePromoteText(opts) {
|
|
158
|
+
if (opts.arg !== undefined && opts.file !== undefined) {
|
|
159
|
+
throw new Error("provide exactly ONE entry text source, got 2: text argument + --file.");
|
|
160
|
+
}
|
|
161
|
+
if (opts.arg !== undefined)
|
|
162
|
+
return opts.arg;
|
|
163
|
+
if (opts.file !== undefined) {
|
|
164
|
+
try {
|
|
165
|
+
return opts.readFile(opts.file);
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
169
|
+
throw new Error(`could not read --file "${opts.file}": ${msg}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!opts.stdinIsTTY()) {
|
|
173
|
+
const text = opts.readStdin();
|
|
174
|
+
if (text.trim() !== "")
|
|
175
|
+
return text;
|
|
176
|
+
}
|
|
177
|
+
throw new Error("no entry text — pass it as an argument, with --file <path>, or pipe it on stdin.");
|
|
178
|
+
}
|
|
179
|
+
export function parseWinMetrics(flags) {
|
|
180
|
+
const wm = {};
|
|
181
|
+
const num = (name) => {
|
|
182
|
+
const raw = flags[name];
|
|
183
|
+
if (raw === undefined)
|
|
184
|
+
return undefined;
|
|
185
|
+
if (typeof raw !== "string" || raw.trim() === "") {
|
|
186
|
+
throw new Error(`--${name} must be a number (got "${String(raw)}")`);
|
|
187
|
+
}
|
|
188
|
+
const n = Number(raw);
|
|
189
|
+
if (Number.isNaN(n))
|
|
190
|
+
throw new Error(`--${name} must be a number (got "${raw}")`);
|
|
191
|
+
return n;
|
|
192
|
+
};
|
|
193
|
+
const spend = num("spend");
|
|
194
|
+
const roas = num("roas");
|
|
195
|
+
const ctr = num("ctr");
|
|
196
|
+
if (spend !== undefined)
|
|
197
|
+
wm.spend = spend;
|
|
198
|
+
if (roas !== undefined)
|
|
199
|
+
wm.roas = roas;
|
|
200
|
+
if (ctr !== undefined)
|
|
201
|
+
wm.ctr = ctr;
|
|
202
|
+
if (typeof flags["note"] === "string")
|
|
203
|
+
wm.note = flags["note"];
|
|
204
|
+
return Object.keys(wm).length > 0 ? wm : undefined;
|
|
205
|
+
}
|
|
206
|
+
export function parseProvenance(flags) {
|
|
207
|
+
const prov = {};
|
|
208
|
+
if (typeof flags["run"] === "string")
|
|
209
|
+
prov.workflowRunId = flags["run"];
|
|
210
|
+
if (typeof flags["node"] === "string")
|
|
211
|
+
prov.nodeId = flags["node"];
|
|
212
|
+
return Object.keys(prov).length > 0 ? prov : undefined;
|
|
213
|
+
}
|
|
214
|
+
export function parseAwareness(flags) {
|
|
215
|
+
const raw = flags["awareness"];
|
|
216
|
+
if (raw === undefined)
|
|
217
|
+
return undefined;
|
|
218
|
+
if (typeof raw !== "string" || !AWARENESS_PAIRS.includes(raw)) {
|
|
219
|
+
throw new Error(`--awareness must be one of: ${AWARENESS_PAIRS.join(", ")} (got "${String(raw)}").`);
|
|
220
|
+
}
|
|
221
|
+
return raw;
|
|
222
|
+
}
|
|
223
|
+
export function buildPromoteBody(bankKey, text, flags) {
|
|
224
|
+
const body = { bankKey, text };
|
|
225
|
+
const awarenessPair = parseAwareness(flags);
|
|
226
|
+
if (awarenessPair)
|
|
227
|
+
body.awarenessPair = awarenessPair;
|
|
228
|
+
const winMetrics = parseWinMetrics(flags);
|
|
229
|
+
if (winMetrics)
|
|
230
|
+
body.winMetrics = winMetrics;
|
|
231
|
+
const provenance = parseProvenance(flags);
|
|
232
|
+
if (provenance)
|
|
233
|
+
body.provenance = provenance;
|
|
234
|
+
return body;
|
|
235
|
+
}
|
|
236
|
+
export async function listFlow(json, deps) {
|
|
237
|
+
const res = await deps.get(LIST_PATH);
|
|
238
|
+
const unsupported = missingRouteLine(res, "bank list", deps.channel);
|
|
239
|
+
if (unsupported)
|
|
240
|
+
return { code: 1, lines: [unsupported] };
|
|
241
|
+
if (!res.ok)
|
|
242
|
+
return asErrorResult(res, json);
|
|
243
|
+
const banks = res.data ?? [];
|
|
244
|
+
return { code: 0, lines: json ? [JSON.stringify(res.data)] : [formatBankList(banks)] };
|
|
245
|
+
}
|
|
246
|
+
export async function showFlow(key, json, deps) {
|
|
247
|
+
const res = await deps.get(`${SHOW_PATH}?key=${encodeURIComponent(key)}`);
|
|
248
|
+
const unsupported = missingRouteLine(res, "bank show", deps.channel);
|
|
249
|
+
if (unsupported)
|
|
250
|
+
return { code: 1, lines: [unsupported] };
|
|
251
|
+
if (!res.ok)
|
|
252
|
+
return asErrorResult(res, json);
|
|
253
|
+
return {
|
|
254
|
+
code: 0,
|
|
255
|
+
lines: json ? [JSON.stringify(res.data)] : [formatBankShow(res.data)],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
export async function promoteFlow(key, arg, flags, json, deps) {
|
|
259
|
+
let body;
|
|
260
|
+
try {
|
|
261
|
+
const text = resolvePromoteText({
|
|
262
|
+
arg,
|
|
263
|
+
file: typeof flags["file"] === "string" ? flags["file"] : undefined,
|
|
264
|
+
readFile: deps.readFile,
|
|
265
|
+
readStdin: deps.readStdin,
|
|
266
|
+
stdinIsTTY: deps.stdinIsTTY,
|
|
267
|
+
});
|
|
268
|
+
body = buildPromoteBody(key, text, flags);
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
return { code: 1, lines: [`Error: ${e instanceof Error ? e.message : String(e)}`] };
|
|
272
|
+
}
|
|
273
|
+
const res = await deps.post(PROMOTE_PATH, body);
|
|
274
|
+
const unsupported = missingRouteLine(res, "bank promote", deps.channel);
|
|
275
|
+
if (unsupported)
|
|
276
|
+
return { code: 1, lines: [unsupported] };
|
|
277
|
+
if (!res.ok)
|
|
278
|
+
return asErrorResult(res, json);
|
|
279
|
+
const data = res.data;
|
|
280
|
+
if (json)
|
|
281
|
+
return { code: 0, lines: [JSON.stringify(res.data)] };
|
|
282
|
+
return {
|
|
283
|
+
code: 0,
|
|
284
|
+
lines: [
|
|
285
|
+
`✓ Promoted to ${data.bankName}.`,
|
|
286
|
+
`entryId: ${data.entryId}`,
|
|
287
|
+
FLYWHEEL_LINE,
|
|
288
|
+
],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function parsePositional(args = process.argv.slice(3)) {
|
|
292
|
+
const out = [];
|
|
293
|
+
let i = 0;
|
|
294
|
+
while (i < args.length) {
|
|
295
|
+
const arg = args[i];
|
|
296
|
+
if (arg.startsWith("--")) {
|
|
297
|
+
const key = arg.slice(2).split("=", 1)[0] ?? "";
|
|
298
|
+
if (!arg.includes("=") && VALUE_FLAGS.has(key))
|
|
299
|
+
i += 2;
|
|
300
|
+
else
|
|
301
|
+
i++;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
out.push(arg);
|
|
305
|
+
i++;
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
async function printResult(result) {
|
|
310
|
+
for (const line of result.lines)
|
|
311
|
+
console.log(line);
|
|
312
|
+
if (result.code !== 0)
|
|
313
|
+
process.exit(result.code);
|
|
314
|
+
}
|
|
315
|
+
export async function run(flags) {
|
|
316
|
+
const positional = parsePositional();
|
|
317
|
+
const [sub, ...rest] = positional;
|
|
318
|
+
const json = !!flags["json"];
|
|
319
|
+
if (!sub) {
|
|
320
|
+
console.log(helpText);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (sub === "list")
|
|
324
|
+
return printResult(await listFlow(json, defaultDeps));
|
|
325
|
+
if (sub === "show") {
|
|
326
|
+
const key = rest[0];
|
|
327
|
+
if (!key) {
|
|
328
|
+
console.error("Error: bank show requires <key>.");
|
|
329
|
+
console.log("Usage: exodus bank show <key> [--json]");
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
return printResult(await showFlow(key, json, defaultDeps));
|
|
333
|
+
}
|
|
334
|
+
if (sub === "promote") {
|
|
335
|
+
const key = rest[0];
|
|
336
|
+
if (!key) {
|
|
337
|
+
console.error("Error: bank promote requires <key>.");
|
|
338
|
+
console.log("Usage: exodus bank promote <key> [text] [--file <path>] [--json]");
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
return printResult(await promoteFlow(key, rest[1], flags, json, defaultDeps));
|
|
342
|
+
}
|
|
343
|
+
console.error(`Unknown subcommand: "${sub}"\n`);
|
|
344
|
+
console.log(helpText);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
package/dist/commands/browse.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { apiGet } from "../lib/client.js";
|
|
2
2
|
import { formatBrowse, formatError } from "../lib/format.js";
|
|
3
|
+
import { isActiveRunStatus } from "../lib/runStatus.js";
|
|
3
4
|
export const helpText = `
|
|
4
5
|
exodus browse — List recent pipeline runs (hooks, ads, image concepts, etc.)
|
|
5
6
|
|
|
@@ -119,7 +120,7 @@ export async function run(flags) {
|
|
|
119
120
|
? all
|
|
120
121
|
: all.filter((g) => {
|
|
121
122
|
const status = g["status"];
|
|
122
|
-
if (status
|
|
123
|
+
if (!status || !isActiveRunStatus(status))
|
|
123
124
|
return true;
|
|
124
125
|
const created = normalizeCreatedAt(g);
|
|
125
126
|
return created > 0 && now - created < STUCK_THRESHOLD_MS;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { apiGet, apiGetDashboard, apiPost, getDashboardUrl, } from "../lib/client.js";
|
|
2
|
-
import { formatError } from "../lib/format.js";
|
|
2
|
+
import { displayRunStatus, formatError } from "../lib/format.js";
|
|
3
3
|
export const helpText = `
|
|
4
4
|
exodus creative — Creative-suite engines (native, copy-derived, ref-match)
|
|
5
5
|
|
|
@@ -165,7 +165,7 @@ async function runStatus(flags) {
|
|
|
165
165
|
console.log(`name: ${d.name}`);
|
|
166
166
|
if (d.engine)
|
|
167
167
|
console.log(`engine: ${d.engine}`);
|
|
168
|
-
console.log(`status: ${d.status
|
|
168
|
+
console.log(`status: ${displayRunStatus(d.status, "—")}${d.isTerminal ? " (terminal)" : ""}`);
|
|
169
169
|
if (d.completedImages !== undefined || d.totalImages !== undefined) {
|
|
170
170
|
console.log(`progress: ${d.completedImages ?? 0} / ${d.totalImages ?? "?"} completed${d.failedImages ? `, ${d.failedImages} failed` : ""}`);
|
|
171
171
|
}
|
package/dist/commands/genesis.js
CHANGED
|
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { apiGet, apiPost, getDashboardUrl } from "../lib/client.js";
|
|
5
5
|
import { pollUntilDone } from "../lib/poll.js";
|
|
6
|
-
import { formatGenesisRun, formatError } from "../lib/format.js";
|
|
6
|
+
import { displayRunStatus, formatGenesisRun, formatError, tickerRunStatus } from "../lib/format.js";
|
|
7
7
|
import { formatCcCommand } from "../lib/cc-command.js";
|
|
8
8
|
import { resolveActiveBrand } from "../lib/layout.js";
|
|
9
9
|
import { captureReelAndWrite } from "../lib/reel-write.js";
|
|
@@ -16,9 +16,21 @@ Google Doc). Write from a typed brief.
|
|
|
16
16
|
|
|
17
17
|
Usage:
|
|
18
18
|
exodus genesis run --brief <file|"text"> Write from a typed brief
|
|
19
|
+
exodus genesis hook-pref [manual|auto] Read (no arg) or save your default
|
|
20
|
+
hook-gate choice
|
|
21
|
+
exodus genesis hooks --id <runId> Show the hook pool of a run paused
|
|
22
|
+
at the hook gate
|
|
23
|
+
exodus genesis continue --id <runId> --hooks 1,3,5 Pick hooks and resume
|
|
24
|
+
exodus genesis regenerate --id <runId> [--steering "…"] Re-roll the hook pool
|
|
25
|
+
|
|
26
|
+
Every run must know whether you review hooks: pass --stop-at-hooks or
|
|
27
|
+
--auto-hooks, or save a default once with hook-pref. A flagless run with no
|
|
28
|
+
saved preference is refused.
|
|
19
29
|
|
|
20
30
|
Options:
|
|
21
31
|
--brief <file|"text"> Typed brief (file path or inline string)
|
|
32
|
+
--stop-at-hooks Pause at the hook gate to review/pick hooks
|
|
33
|
+
--auto-hooks Auto-pick hooks and write straight through
|
|
22
34
|
--seeds <file|"text"> Per-run creative seeds. File: one per line OR a
|
|
23
35
|
JSON array. Inline: a single seed.
|
|
24
36
|
--steering "<text>" With regenerate: re-roll guidance for the hook pool.
|
|
@@ -441,7 +453,7 @@ async function waitForGenesisRun(runId) {
|
|
|
441
453
|
const status = data["status"];
|
|
442
454
|
const currentStep = data["currentStep"];
|
|
443
455
|
if (status) {
|
|
444
|
-
process.stdout.write(`\r ${status}${currentStep ? ` — ${currentStep}` : ""} `);
|
|
456
|
+
process.stdout.write(`\r ${tickerRunStatus(status)}${currentStep ? ` — ${currentStep}` : ""} `);
|
|
445
457
|
}
|
|
446
458
|
},
|
|
447
459
|
});
|
|
@@ -515,7 +527,7 @@ async function runHooks(flags) {
|
|
|
515
527
|
process.exit(1);
|
|
516
528
|
}
|
|
517
529
|
if (res.data.status !== "awaiting_hook_selection") {
|
|
518
|
-
console.log(`Run ${runId} is not awaiting hook selection (status: ${res.data.status
|
|
530
|
+
console.log(`Run ${runId} is not awaiting hook selection (status: ${displayRunStatus(res.data.status)}).`);
|
|
519
531
|
return;
|
|
520
532
|
}
|
|
521
533
|
const pool = Array.isArray(res.data.hookPool) ? res.data.hookPool : [];
|
|
@@ -677,7 +689,7 @@ async function pollScrape(runId, noWait, timeoutMs) {
|
|
|
677
689
|
? ` captured=${captured ?? 0} qualified=${qualified ?? 0}`
|
|
678
690
|
: "";
|
|
679
691
|
if (status)
|
|
680
|
-
process.stdout.write(`\r status: ${status}${counts} `);
|
|
692
|
+
process.stdout.write(`\r status: ${tickerRunStatus(status)}${counts} `);
|
|
681
693
|
},
|
|
682
694
|
});
|
|
683
695
|
console.log();
|
package/dist/commands/keys.js
CHANGED
package/dist/commands/meme.js
CHANGED
|
@@ -19,11 +19,14 @@ Flow:
|
|
|
19
19
|
2. run → pass the picked recommendation objects straight into --formats
|
|
20
20
|
(a JSON array; both recommendation-shape and run-shape entries
|
|
21
21
|
are accepted, 1–50). Returns { runId } — the whole batch renders
|
|
22
|
-
server-side
|
|
23
|
-
3. poll → exodus status --id <runId>
|
|
24
|
-
|
|
22
|
+
server-side; closing your session won't kill it.
|
|
23
|
+
3. poll → exodus workflow status --id <runId>
|
|
24
|
+
Memes render through the Image Rig now, so a meme run IS a
|
|
25
|
+
workflow run — the workflow verb is the one that can read it.
|
|
25
26
|
4. regenerate → re-render a single miss using the format fields from the
|
|
26
27
|
recommend output. Synchronous; returns the new image URL.
|
|
28
|
+
Its optional --id takes a CREATIVE-SUITE run id, which the id
|
|
29
|
+
from step 2 is not — omit --id and use the returned URL.
|
|
27
30
|
|
|
28
31
|
Keys (strict BYOK — checked server-side before anything starts):
|
|
29
32
|
classic (layer 1) memes need your Imgflip login; AI (layer 2/3) memes need
|
|
@@ -32,10 +35,10 @@ Keys (strict BYOK — checked server-side before anything starts):
|
|
|
32
35
|
Examples:
|
|
33
36
|
exodus meme recommend --brief "grounding sheets reduce inflammation"
|
|
34
37
|
exodus meme run --brief "grounding sheets reduce inflammation" --formats '[{"layer":2,"name":"Group Chat","format_id":"group-chat"}]'
|
|
35
|
-
exodus meme regenerate --brief "grounding sheets reduce inflammation" --layer 2 --format group-chat --hint "make the last message land the product"
|
|
38
|
+
exodus meme regenerate --brief "grounding sheets reduce inflammation" --layer 2 --format group-chat --hint "make the last message land the product"
|
|
36
39
|
`.trim();
|
|
37
40
|
function dashboardUrlForRun(runId) {
|
|
38
|
-
return `${getDashboardUrl()}/
|
|
41
|
+
return `${getDashboardUrl()}/runs/${runId}`;
|
|
39
42
|
}
|
|
40
43
|
function flagString(flags, name) {
|
|
41
44
|
const v = flags[name];
|
|
@@ -185,7 +188,7 @@ async function runRun(flags) {
|
|
|
185
188
|
console.log(` memes: ${formats.length} queued`);
|
|
186
189
|
console.log(` dashboard: ${dashboardUrlForRun(res.data.runId)}`);
|
|
187
190
|
console.log("");
|
|
188
|
-
console.log(`Poll: exodus status --id ${res.data.runId}
|
|
191
|
+
console.log(`Poll: exodus workflow status --id ${res.data.runId}`);
|
|
189
192
|
console.log("(The batch renders server-side — closing this session won't kill it.)");
|
|
190
193
|
}
|
|
191
194
|
async function runRegenerate(flags) {
|
package/dist/commands/primer.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { apiGetDashboard, apiPostDashboard } from "../lib/client.js";
|
|
4
4
|
import { promptChoice, promptMultiline, openInEditor } from "../lib/prompts.js";
|
|
5
|
+
import { normalizeRunStatus } from "../lib/runStatus.js";
|
|
5
6
|
import { refreshBrandProfileMd } from "./brand.js";
|
|
6
7
|
export const helpText = `
|
|
7
8
|
exodus primer — Build a brand's primer (the single foundation step)
|
|
@@ -152,7 +153,7 @@ async function buildPrimer(submission) {
|
|
|
152
153
|
process.stdout.write("·");
|
|
153
154
|
continue;
|
|
154
155
|
}
|
|
155
|
-
if (data.status === "failed") {
|
|
156
|
+
if (normalizeRunStatus(data.status) === "failed") {
|
|
156
157
|
console.log("");
|
|
157
158
|
fail(`primer build failed: ${data.error ?? "unknown error"}`);
|
|
158
159
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type ApiResponse } from "../lib/client.js";
|
|
2
|
+
import { type Channel } from "../lib/channel.js";
|
|
3
|
+
export declare const helpText: string;
|
|
4
|
+
export interface SessionSummary {
|
|
5
|
+
_id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
botSlug: string;
|
|
8
|
+
runId?: string;
|
|
9
|
+
nodeId?: string;
|
|
10
|
+
archived: boolean;
|
|
11
|
+
lastTouchedAt: number | string;
|
|
12
|
+
createdAt: number | string;
|
|
13
|
+
}
|
|
14
|
+
export interface SessionListResponse {
|
|
15
|
+
sessions: SessionSummary[];
|
|
16
|
+
}
|
|
17
|
+
export type SessionMessageRole = "system" | "user" | "assistant";
|
|
18
|
+
export interface SessionMessage {
|
|
19
|
+
role: SessionMessageRole;
|
|
20
|
+
body: string;
|
|
21
|
+
createdAt: number | string;
|
|
22
|
+
}
|
|
23
|
+
export interface SessionShowResponse {
|
|
24
|
+
session: SessionSummary & {
|
|
25
|
+
promptText?: string;
|
|
26
|
+
model?: string;
|
|
27
|
+
};
|
|
28
|
+
messages: SessionMessage[];
|
|
29
|
+
}
|
|
30
|
+
export interface SessionChatResponse {
|
|
31
|
+
reply: string;
|
|
32
|
+
}
|
|
33
|
+
export interface SessionDeps {
|
|
34
|
+
get: (path: string) => Promise<ApiResponse<unknown>>;
|
|
35
|
+
postDashboard: (path: string, body: unknown, opts?: {
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
}) => Promise<ApiResponse<unknown>>;
|
|
38
|
+
channel: Channel;
|
|
39
|
+
}
|
|
40
|
+
export interface FlowResult {
|
|
41
|
+
code: number;
|
|
42
|
+
lines: string[];
|
|
43
|
+
}
|
|
44
|
+
export declare function shortId(id: string): string;
|
|
45
|
+
export declare function formatAge(value: number | string | undefined, now?: number): string;
|
|
46
|
+
export declare function formatSessionList(sessions: SessionSummary[], now?: number): string;
|
|
47
|
+
export declare function formatSessionShow(data: SessionShowResponse): string;
|
|
48
|
+
export declare function listFlow(json: boolean, deps: SessionDeps): Promise<FlowResult>;
|
|
49
|
+
export declare function showFlow(sessionId: string, opts: {
|
|
50
|
+
json: boolean;
|
|
51
|
+
}, deps: SessionDeps): Promise<FlowResult>;
|
|
52
|
+
export declare function chatFlow(sessionId: string, message: string, opts: {
|
|
53
|
+
json: boolean;
|
|
54
|
+
}, deps: SessionDeps): Promise<FlowResult>;
|
|
55
|
+
export declare function run(flags: Record<string, string | boolean>): Promise<void>;
|