@aicopycoders/exodus 2026.7.200-beta.11 → 2026.7.200-beta.13

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.
@@ -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
+ }
@@ -26,6 +26,7 @@ const PROVIDER_LABEL = {
26
26
  kie: "Kie.ai",
27
27
  imgflip: "Imgflip",
28
28
  scrapecreators: "ScrapeCreators",
29
+ apify: "Apify",
29
30
  };
30
31
  async function status() {
31
32
  let remote;
@@ -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>;
@@ -0,0 +1,202 @@
1
+ import { apiGet, apiPostDashboard } from "../lib/client.js";
2
+ import { formatError } from "../lib/format.js";
3
+ import { getChannel } from "../lib/channel.js";
4
+ import { missingRouteLine } from "../lib/route-support.js";
5
+ export const helpText = `
6
+ exodus session — Inspect and continue workflow chat sessions
7
+
8
+ Usage:
9
+ exodus session list [--json]
10
+ exodus session show <sessionId> [--json]
11
+ exodus session chat <sessionId> "message" [--json]
12
+
13
+ Flags:
14
+ --json Machine-readable JSON output (the raw API response)
15
+
16
+ Notes:
17
+ Sessions are born from workflow runs whose bots run in session-mode — you do
18
+ not create them here. "session list" shows the sessions on the active brand;
19
+ "session show" prints a session's turns (system rows are hidden in the human
20
+ view but kept in --json); "session chat" appends your message and prints the
21
+ assistant's reply. The chat route can take up to ~5 minutes to answer.
22
+
23
+ Examples:
24
+ exodus session list
25
+ exodus session show sess_123
26
+ exodus session chat sess_123 "make the hook punchier"
27
+ `.trim();
28
+ const SESSIONS_PATH = "/api/v2/sessions";
29
+ const SESSION_SHOW_PATH = "/api/v2/sessions/show";
30
+ const CHAT_PATH = "/api/sessions/chat";
31
+ const CHAT_TIMEOUT_MS = 320_000;
32
+ const defaultDeps = {
33
+ get: (path) => apiGet(path),
34
+ postDashboard: (path, body, opts) => apiPostDashboard(path, body, opts),
35
+ channel: getChannel(),
36
+ };
37
+ function isRecord(value) {
38
+ return typeof value === "object" && value !== null && !Array.isArray(value);
39
+ }
40
+ export function shortId(id) {
41
+ return id.length <= 12 ? id : `${id.slice(0, 11)}…`;
42
+ }
43
+ export function formatAge(value, now = Date.now()) {
44
+ if (value === undefined || value === null)
45
+ return "-";
46
+ const t = typeof value === "number" ? value : Date.parse(value);
47
+ if (Number.isNaN(t))
48
+ return "-";
49
+ const sec = Math.max(0, Math.floor((now - t) / 1000));
50
+ if (sec < 60)
51
+ return `${sec}s`;
52
+ const min = Math.floor(sec / 60);
53
+ if (min < 60)
54
+ return `${min}m`;
55
+ const hr = Math.floor(min / 60);
56
+ if (hr < 24)
57
+ return `${hr}h`;
58
+ return `${Math.floor(hr / 24)}d`;
59
+ }
60
+ function table(headers, rows) {
61
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)));
62
+ const fmt = (row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
63
+ return [fmt(headers), fmt(headers.map((h) => "-".repeat(h.length))), ...rows.map(fmt)].join("\n");
64
+ }
65
+ const NO_SESSIONS = "No sessions yet. Sessions are born from workflow runs whose bots run in session-mode.";
66
+ export function formatSessionList(sessions, now = Date.now()) {
67
+ if (sessions.length === 0)
68
+ return NO_SESSIONS;
69
+ return table(["id", "title", "bot", "run", "age"], sessions.map((s) => [
70
+ shortId(s._id),
71
+ s.title || "(untitled)",
72
+ s.botSlug,
73
+ s.runId ? shortId(s.runId) : "-",
74
+ formatAge(s.lastTouchedAt, now),
75
+ ]));
76
+ }
77
+ export function formatSessionShow(data) {
78
+ const s = data.session;
79
+ const lines = [];
80
+ lines.push(`Session — ${s.title || "(untitled)"}`);
81
+ lines.push(`sessionId: ${s._id}`);
82
+ lines.push(`bot: ${s.botSlug}`);
83
+ lines.push(`run: ${s.runId ?? "-"}`);
84
+ const turns = (data.messages ?? []).filter((m) => m.role !== "system");
85
+ lines.push("");
86
+ if (turns.length === 0) {
87
+ lines.push("(no messages yet)");
88
+ }
89
+ else {
90
+ for (const m of turns)
91
+ lines.push(`${m.role}: ${m.body}`);
92
+ }
93
+ return lines.join("\n");
94
+ }
95
+ function routeErrorText(data, status) {
96
+ if (isRecord(data)) {
97
+ const err = data.error;
98
+ if (typeof err === "string" && err)
99
+ return err;
100
+ if (isRecord(err) && typeof err.message === "string")
101
+ return err.message;
102
+ if (typeof data.message === "string" && data.message)
103
+ return data.message;
104
+ }
105
+ return `HTTP ${status}`;
106
+ }
107
+ function apiErrorResult(res, verb, json, channel) {
108
+ const missing = missingRouteLine(res, verb, channel);
109
+ if (missing) {
110
+ return {
111
+ code: 1,
112
+ lines: json
113
+ ? [JSON.stringify({ ok: false, status: res.status, error: missing })]
114
+ : [missing],
115
+ };
116
+ }
117
+ return {
118
+ code: 1,
119
+ lines: json
120
+ ? [JSON.stringify({ ok: false, status: res.status, data: res.data })]
121
+ : [formatError(res)],
122
+ };
123
+ }
124
+ export async function listFlow(json, deps) {
125
+ const res = await deps.get(SESSIONS_PATH);
126
+ if (!res.ok)
127
+ return apiErrorResult(res, "session list", json, deps.channel);
128
+ const data = res.data;
129
+ return {
130
+ code: 0,
131
+ lines: json ? [JSON.stringify(data)] : [formatSessionList(data.sessions ?? [])],
132
+ };
133
+ }
134
+ export async function showFlow(sessionId, opts, deps) {
135
+ const res = await deps.get(`${SESSION_SHOW_PATH}?sessionId=${encodeURIComponent(sessionId)}`);
136
+ if (!res.ok)
137
+ return apiErrorResult(res, "session show", opts.json, deps.channel);
138
+ const data = res.data;
139
+ return {
140
+ code: 0,
141
+ lines: opts.json ? [JSON.stringify(data)] : [formatSessionShow(data)],
142
+ };
143
+ }
144
+ export async function chatFlow(sessionId, message, opts, deps) {
145
+ const res = await deps.postDashboard(CHAT_PATH, { sessionId, text: message }, { timeoutMs: CHAT_TIMEOUT_MS });
146
+ if (!res.ok) {
147
+ const err = routeErrorText(res.data, res.status);
148
+ return {
149
+ code: 1,
150
+ lines: opts.json
151
+ ? [JSON.stringify({ ok: false, status: res.status, error: err })]
152
+ : [`exodus session chat: ${err} (HTTP ${res.status})`],
153
+ };
154
+ }
155
+ const data = res.data;
156
+ return {
157
+ code: 0,
158
+ lines: opts.json ? [JSON.stringify(data)] : [data.reply ?? ""],
159
+ };
160
+ }
161
+ function parsePositional(args = process.argv.slice(3)) {
162
+ return args.filter((a) => !a.startsWith("--"));
163
+ }
164
+ async function printResult(result) {
165
+ for (const line of result.lines)
166
+ console.log(line);
167
+ if (result.code !== 0)
168
+ process.exit(result.code);
169
+ }
170
+ export async function run(flags) {
171
+ const positional = parsePositional();
172
+ const [sub, ...rest] = positional;
173
+ const json = !!flags["json"];
174
+ if (!sub) {
175
+ console.log(helpText);
176
+ return;
177
+ }
178
+ if (sub === "list")
179
+ return printResult(await listFlow(json, defaultDeps));
180
+ if (sub === "show") {
181
+ const sessionId = rest[0];
182
+ if (!sessionId) {
183
+ console.error("Error: session show requires <sessionId>.");
184
+ console.log("Usage: exodus session show <sessionId> [--json]");
185
+ process.exit(1);
186
+ }
187
+ return printResult(await showFlow(sessionId, { json }, defaultDeps));
188
+ }
189
+ if (sub === "chat") {
190
+ const sessionId = rest[0];
191
+ const message = rest[1];
192
+ if (!sessionId || !message) {
193
+ console.error('Error: session chat requires <sessionId> and a "message".');
194
+ console.log('Usage: exodus session chat <sessionId> "message" [--json]');
195
+ process.exit(1);
196
+ }
197
+ return printResult(await chatFlow(sessionId, message, { json }, defaultDeps));
198
+ }
199
+ console.error(`Unknown subcommand: "${sub}"\n`);
200
+ console.log(helpText);
201
+ process.exit(1);
202
+ }