@aicopycoders/exodus 2026.7.200-beta.3 → 2026.7.200-beta.5

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,274 @@
1
+ import fs from "node:fs";
2
+ import { apiGet, apiPost } from "../lib/client.js";
3
+ import { formatError } from "../lib/format.js";
4
+ export const helpText = `
5
+ exodus segment — Segment map + personas (WHAT / WHO / WHY), agentic I/O
6
+
7
+ The JSON contract is the edit surface: export the map, edit the file, import it
8
+ back. Import is a FULL REPLACE, slug-stable by name-match — existing outcomes and
9
+ values keep their slugs, new names get new ones, and names absent from the file
10
+ are deleted. Personas the new map orphans are ARCHIVED (never deleted).
11
+
12
+ Usage:
13
+ exodus segment show Human-readable summary of the map
14
+ exodus segment show --json Raw normalized map JSON
15
+ exodus segment export [--out <file>] Contract JSON (map + personas) to stdout or a file
16
+ exodus segment import <file> [--yes] Import a contract JSON file (dry-run first)
17
+ exodus segment personas List saved personas (incl. archived)
18
+ exodus segment personas --json Raw persona list JSON
19
+
20
+ Flags:
21
+ --json Machine-readable JSON output (show / personas)
22
+ --out <file> Write export to a file instead of stdout
23
+ --yes Apply a destructive import (deletes segments / archives personas)
24
+
25
+ Notes:
26
+ • All operations scope to your active brand. Check with: exodus brand current
27
+ • 'import' ALWAYS previews via a server dry-run first and prints what would
28
+ change. If anything would be deleted at ANY layer (outcome, sub-slice,
29
+ demo/facet value) or a persona archived/deleted, it refuses to apply
30
+ without --yes and exits non-zero (never prompts — agents hang on prompts).
31
+ • 'import' on a workspace with no map yet just creates it.
32
+ • 'export' output imports back unchanged — it is the contract document.
33
+
34
+ Examples:
35
+ exodus segment show
36
+ exodus segment export --out segments.json
37
+ exodus segment import segments.json
38
+ exodus segment import segments.json --yes
39
+ exodus segment personas --json
40
+ `.trim();
41
+ export function asSummary(data) {
42
+ const d = (typeof data === "object" && data !== null ? data : {});
43
+ const list = (v) => (Array.isArray(v) ? v : []);
44
+ return {
45
+ added: list(d.added),
46
+ keptRenamed: list(d.keptRenamed),
47
+ deleted: list(d.deleted),
48
+ subsDeleted: list(d.subsDeleted),
49
+ demoValuesDeleted: list(d.demoValuesDeleted),
50
+ facetValuesDeleted: list(d.facetValuesDeleted),
51
+ personasArchived: list(d.personasArchived),
52
+ personasDeleted: list(d.personasDeleted),
53
+ };
54
+ }
55
+ export function isDestructive(summary) {
56
+ return (summary.deleted.length > 0 ||
57
+ summary.subsDeleted.length > 0 ||
58
+ summary.demoValuesDeleted.length > 0 ||
59
+ summary.facetValuesDeleted.length > 0 ||
60
+ summary.personasArchived.length > 0 ||
61
+ summary.personasDeleted.length > 0);
62
+ }
63
+ function line(label, names) {
64
+ const detail = names.length > 0 ? ` (${names.join(", ")})` : "";
65
+ return ` ${label.padEnd(24)} ${String(names.length).padStart(3)}${detail}`;
66
+ }
67
+ export function formatImportSummary(summary) {
68
+ return [
69
+ "Import preview:",
70
+ line("+ outcomes added", summary.added),
71
+ line("~ outcomes kept", summary.keptRenamed),
72
+ line("- outcomes deleted", summary.deleted),
73
+ line("- subs deleted", summary.subsDeleted),
74
+ line("- demo values deleted", summary.demoValuesDeleted),
75
+ line("- facet values deleted", summary.facetValuesDeleted),
76
+ line("personas archived", summary.personasArchived),
77
+ line("personas deleted", summary.personasDeleted),
78
+ ].join("\n");
79
+ }
80
+ export function formatMapSummary(map) {
81
+ const lines = [];
82
+ lines.push(`Segment map — ${map.brandLabel} (product: ${map.productWord})`);
83
+ lines.push(`\nOutcomes (${map.outcomes.length}):`);
84
+ for (const o of map.outcomes) {
85
+ const lens = o.lens ? ` [${o.lens}]` : "";
86
+ const subs = o.subs.length > 0 ? ` · subs: ${o.subs.map((s) => s.name).join(", ")}` : "";
87
+ lines.push(` ${o.slug.padEnd(24)} ${o.name}${lens}${subs}`);
88
+ }
89
+ const groupBlock = (title, groups) => {
90
+ lines.push(`\n${title}:`);
91
+ for (const g of groups) {
92
+ const vals = g.values.length > 0 ? g.values.map((val) => val.name).join(", ") : "—";
93
+ lines.push(` ${g.name} (${g.values.length}): ${vals}`);
94
+ }
95
+ };
96
+ groupBlock("Demographics (WHO)", map.demoGroups);
97
+ groupBlock("Facets (WHY)", map.facetFamilies);
98
+ return lines.join("\n");
99
+ }
100
+ const IMPORT_PATH = "/api/v2/segments/import";
101
+ export async function importFlow(file, opts, deps) {
102
+ const calls = [];
103
+ let text;
104
+ try {
105
+ text = deps.readFile(file);
106
+ }
107
+ catch {
108
+ return { code: 1, lines: [`Error: file not found: ${file}`], calls };
109
+ }
110
+ let contract;
111
+ try {
112
+ contract = JSON.parse(text);
113
+ }
114
+ catch (e) {
115
+ const msg = e instanceof Error ? e.message : String(e);
116
+ return { code: 1, lines: [`Error: ${file} is not valid JSON: ${msg}`], calls };
117
+ }
118
+ if (typeof contract !== "object" || contract === null || Array.isArray(contract)) {
119
+ return { code: 1, lines: [`Error: ${file} is not a segment map (expected a JSON object).`], calls };
120
+ }
121
+ const clean = { ...contract };
122
+ delete clean.dryRun;
123
+ calls.push({ dryRun: true });
124
+ const dry = await deps.post(IMPORT_PATH, { ...clean, dryRun: true });
125
+ if (!dry.ok) {
126
+ const lines = opts.json
127
+ ? [JSON.stringify({ ok: false, status: dry.status, data: dry.data })]
128
+ : [formatError({ ok: dry.ok, status: dry.status, data: dry.data })];
129
+ return { code: 1, lines, calls };
130
+ }
131
+ const summary = asSummary(dry.data);
132
+ const destructive = isDestructive(summary);
133
+ if (destructive && !opts.yes) {
134
+ const lines = opts.json
135
+ ? [JSON.stringify({ ok: false, applied: false, destructive: true, summary })]
136
+ : [
137
+ formatImportSummary(summary),
138
+ "",
139
+ "This import is destructive (deletes outcomes, sub-slices, or values,",
140
+ "and/or archives or deletes personas). Re-run with --yes to apply it.",
141
+ ];
142
+ return { code: 1, lines, calls };
143
+ }
144
+ calls.push({ dryRun: false });
145
+ const real = await deps.post(IMPORT_PATH, clean);
146
+ if (!real.ok) {
147
+ const lines = opts.json
148
+ ? [JSON.stringify({ ok: false, status: real.status, data: real.data })]
149
+ : [formatError({ ok: real.ok, status: real.status, data: real.data })];
150
+ return { code: 1, lines, calls };
151
+ }
152
+ const applied = asSummary(real.data);
153
+ const lines = opts.json
154
+ ? [JSON.stringify({ ok: true, applied: true, destructive, summary: applied })]
155
+ : [formatImportSummary(applied), "", "Imported."];
156
+ return { code: 0, lines, calls };
157
+ }
158
+ export async function run(flags) {
159
+ const positional = parsePositional();
160
+ const [sub, ...rest] = positional;
161
+ if (!sub || sub === "show")
162
+ return runShow(flags);
163
+ if (sub === "export")
164
+ return runExport(flags);
165
+ if (sub === "import")
166
+ return runImport(rest, flags);
167
+ if (sub === "personas")
168
+ return runPersonas(flags);
169
+ console.error(`Unknown subcommand: "${sub}"\n`);
170
+ console.log(helpText);
171
+ process.exit(1);
172
+ }
173
+ function parsePositional() {
174
+ const args = process.argv.slice(3);
175
+ const out = [];
176
+ let i = 0;
177
+ while (i < args.length) {
178
+ const a = args[i];
179
+ if (a.startsWith("--")) {
180
+ const next = args[i + 1];
181
+ if (next !== undefined && !next.startsWith("--")) {
182
+ i += 2;
183
+ }
184
+ else {
185
+ i++;
186
+ }
187
+ continue;
188
+ }
189
+ out.push(a);
190
+ i++;
191
+ }
192
+ return out;
193
+ }
194
+ async function runShow(flags) {
195
+ const json = !!flags["json"];
196
+ const res = await apiGet("/api/v2/segments");
197
+ if (!res.ok) {
198
+ if (json)
199
+ console.log(JSON.stringify({ ok: false, status: res.status, data: res.data }));
200
+ else
201
+ console.log(formatError(res));
202
+ process.exit(1);
203
+ }
204
+ if (json) {
205
+ console.log(JSON.stringify(res.data.map));
206
+ return;
207
+ }
208
+ if (!res.data.map) {
209
+ console.log("No segment map yet for the active brand.");
210
+ console.log("Build one and import it: exodus segment import <file>");
211
+ return;
212
+ }
213
+ console.log(formatMapSummary(res.data.map));
214
+ }
215
+ async function runExport(flags) {
216
+ const out = typeof flags["out"] === "string" ? flags["out"] : undefined;
217
+ const res = await apiGet("/api/v2/segments/export");
218
+ if (!res.ok) {
219
+ console.log(formatError(res));
220
+ process.exit(1);
221
+ }
222
+ const doc = JSON.stringify(res.data, null, 2);
223
+ if (out) {
224
+ fs.writeFileSync(out, doc + "\n", "utf-8");
225
+ console.log(`Wrote segment contract to ${out}.`);
226
+ return;
227
+ }
228
+ console.log(doc);
229
+ }
230
+ async function runImport(positional, flags) {
231
+ const file = positional[0];
232
+ if (!file) {
233
+ console.error("Error: a contract JSON file is required");
234
+ console.log("Usage: exodus segment import <file> [--yes]");
235
+ process.exit(1);
236
+ }
237
+ const result = await importFlow(file, { yes: !!flags["yes"], json: !!flags["json"] }, {
238
+ post: (path, body) => apiPost(path, body),
239
+ readFile: (p) => fs.readFileSync(p, "utf-8"),
240
+ });
241
+ for (const l of result.lines)
242
+ console.log(l);
243
+ if (result.code !== 0)
244
+ process.exit(result.code);
245
+ }
246
+ async function runPersonas(flags) {
247
+ const json = !!flags["json"];
248
+ const res = await apiGet("/api/v2/segments/personas");
249
+ if (!res.ok) {
250
+ if (json)
251
+ console.log(JSON.stringify({ ok: false, status: res.status, data: res.data }));
252
+ else
253
+ console.log(formatError(res));
254
+ process.exit(1);
255
+ }
256
+ const personas = res.data.personas ?? [];
257
+ if (json) {
258
+ console.log(JSON.stringify({ personas }));
259
+ return;
260
+ }
261
+ if (personas.length === 0) {
262
+ console.log("No personas saved yet for the active brand.");
263
+ return;
264
+ }
265
+ console.log(`Personas (${personas.length}):`);
266
+ for (const p of personas) {
267
+ const tag = p.archived ? " [archived]" : "";
268
+ const code = p.code ? `${p.code} ` : "";
269
+ const type = p.type ? ` (${p.type})` : "";
270
+ console.log(` ${code}${p.name}${type}${tag}`);
271
+ if (p.description)
272
+ console.log(` ${p.description}`);
273
+ }
274
+ }
@@ -0,0 +1,271 @@
1
+ import { type ApiResponse } from "../lib/client.js";
2
+ import { type PollOptions, type PollResult } from "../lib/poll.js";
3
+ export declare const helpText: string;
4
+ export type WorkflowNodeKind = "brief" | "bot" | "primer" | "image" | "output";
5
+ export interface WorkflowNode {
6
+ id: string;
7
+ kind: WorkflowNodeKind;
8
+ position: {
9
+ x: number;
10
+ y: number;
11
+ };
12
+ config: Record<string, unknown>;
13
+ }
14
+ export interface WorkflowEdge {
15
+ id: string;
16
+ source: string;
17
+ sourceHandle: string;
18
+ target: string;
19
+ targetHandle: string;
20
+ }
21
+ export interface WorkflowContractJson {
22
+ contract: "exodus-workflow";
23
+ version: number;
24
+ workflowId?: string;
25
+ updatedAt?: string;
26
+ name: string;
27
+ description?: string;
28
+ nodes: WorkflowNode[];
29
+ edges: WorkflowEdge[];
30
+ }
31
+ export interface UnresolvedWorkflowRef {
32
+ nodeId: string;
33
+ ref: "persona" | "primer";
34
+ value: string;
35
+ message: string;
36
+ }
37
+ export interface WorkflowImportResult {
38
+ name: string;
39
+ created: boolean;
40
+ workflowId?: string;
41
+ nodeCount: number;
42
+ edgeCount: number;
43
+ unresolved: UnresolvedWorkflowRef[];
44
+ warnings: string[];
45
+ }
46
+ export type GraphIssueCode = "bad-shape" | "unknown-kind" | "duplicate-node-id" | "dangling-edge" | "unknown-port" | "type-mismatch" | "duplicate-input" | "cycle" | "missing-required-input" | "bad-config";
47
+ export interface GraphIssue {
48
+ code: GraphIssueCode;
49
+ message: string;
50
+ nodeId?: string;
51
+ edgeId?: string;
52
+ portId?: string;
53
+ remedy?: string;
54
+ }
55
+ export type WorkflowImportErrorCode = "invalid-graph" | "conflict" | "forbidden" | "not-found";
56
+ export interface WorkflowImportError {
57
+ code: WorkflowImportErrorCode;
58
+ message: string;
59
+ issues?: GraphIssue[];
60
+ currentUpdatedAt?: string;
61
+ }
62
+ export type WorkflowPortType = "text" | "primer" | "image";
63
+ export type WorkflowPrimerKind = "body" | "hook" | "headline" | "summary";
64
+ export type WorkflowParamKind = "select" | "text" | "textarea" | "toggle" | "number" | "multiselect";
65
+ export interface CatalogInput {
66
+ id: string;
67
+ label: string;
68
+ accepts: WorkflowPortType[];
69
+ required: boolean;
70
+ multi?: boolean;
71
+ primerKinds?: WorkflowPrimerKind[];
72
+ }
73
+ export interface CatalogParam {
74
+ key: string;
75
+ label: string;
76
+ kind: WorkflowParamKind;
77
+ options?: string[];
78
+ min?: number;
79
+ max?: number;
80
+ required?: boolean;
81
+ help?: string;
82
+ default?: string;
83
+ }
84
+ export interface CatalogBot {
85
+ slug: string;
86
+ name: string;
87
+ blurb: string;
88
+ category: string;
89
+ categoryLabel: string;
90
+ inputs: CatalogInput[];
91
+ params: CatalogParam[];
92
+ outputType: string;
93
+ splitsOutput: boolean;
94
+ }
95
+ export interface WorkflowCatalog {
96
+ catalog: "exodus-workflow-catalog";
97
+ version: 1;
98
+ bots: CatalogBot[];
99
+ vocabulary: {
100
+ nodeKinds: string[];
101
+ briefSources: string[];
102
+ primerKinds: string[];
103
+ imageModels: string[];
104
+ aspectRatios: string[];
105
+ imageQuantityModes: string[];
106
+ categories: Array<{
107
+ id: string;
108
+ label: string;
109
+ }>;
110
+ };
111
+ customBot: {
112
+ slug: "custom";
113
+ configKey: "customSlug";
114
+ inputs: CatalogInput[];
115
+ summaryOnlySlugs: string[];
116
+ notes: string;
117
+ };
118
+ }
119
+ export type WorkflowBriefSource = "text" | "swipe-ad" | "swipe-bundle" | "organic-url" | "ad-url";
120
+ export interface WorkflowInputDescriptor {
121
+ fieldName: string;
122
+ nodeId: string;
123
+ source: WorkflowBriefSource;
124
+ required: boolean;
125
+ description?: string;
126
+ bundleSize?: number;
127
+ }
128
+ export interface WorkflowPrerequisiteDescriptor {
129
+ primerKind: WorkflowPrimerKind;
130
+ nodeIds: string[];
131
+ }
132
+ export interface WorkflowOutputDescriptor {
133
+ type: "text" | "image";
134
+ label: string;
135
+ nodeId: string;
136
+ botSlug?: string;
137
+ }
138
+ export interface WorkflowDescribeResponse {
139
+ workflowId: string;
140
+ name: string;
141
+ description?: string;
142
+ updatedAt: string;
143
+ inputs: WorkflowInputDescriptor[];
144
+ prerequisites: Array<WorkflowPrerequisiteDescriptor & {
145
+ stored: boolean;
146
+ }>;
147
+ outputs: WorkflowOutputDescriptor[];
148
+ }
149
+ export interface WorkflowListItem {
150
+ _id: string;
151
+ name: string;
152
+ description?: string;
153
+ nodeCount: number;
154
+ edgeCount: number;
155
+ createdAt: string;
156
+ updatedAt: string;
157
+ }
158
+ export interface WorkflowListResponse {
159
+ workflows: WorkflowListItem[];
160
+ }
161
+ export type WorkflowRunStatus = "queued" | "running" | "completed" | "partial" | "failed";
162
+ export type WorkflowNodeRunStatus = "idle" | "running" | "done" | "failed" | "skipped";
163
+ export type WorkflowArtifact = {
164
+ type: "text";
165
+ text: string;
166
+ label?: string;
167
+ } | {
168
+ type: "primer";
169
+ text: string;
170
+ primerKind: string;
171
+ } | {
172
+ type: "image";
173
+ storageId: string;
174
+ imageUrl?: string;
175
+ };
176
+ export interface WorkflowRunOutput {
177
+ nodeId: string;
178
+ botSlug?: string;
179
+ type: "text" | "image";
180
+ label: string;
181
+ text?: string;
182
+ imageUrl?: string;
183
+ imageId?: string;
184
+ }
185
+ export interface WorkflowRunNode {
186
+ nodeId: string;
187
+ kind: string;
188
+ status: WorkflowNodeRunStatus;
189
+ error?: string;
190
+ startedAt?: number;
191
+ completedAt?: number;
192
+ outputs: WorkflowArtifact[];
193
+ }
194
+ export interface WorkflowCounts {
195
+ done: number;
196
+ failed: number;
197
+ skipped: number;
198
+ total: number;
199
+ }
200
+ export interface WorkflowRun {
201
+ _id: string;
202
+ workflowId: string;
203
+ workflowName: string;
204
+ status: WorkflowRunStatus;
205
+ error?: string;
206
+ counts?: WorkflowCounts;
207
+ inputs: Record<string, string>;
208
+ triggerRunId?: string;
209
+ createdAt: number;
210
+ startedAt?: number;
211
+ completedAt?: number;
212
+ isTerminal: boolean;
213
+ nodes: WorkflowRunNode[];
214
+ outputs?: WorkflowRunOutput[];
215
+ }
216
+ export type WorkflowRunProjection = Omit<WorkflowRun, "nodes"> & {
217
+ nodes?: never;
218
+ };
219
+ export interface WorkflowRunDeps {
220
+ get: (path: string) => Promise<ApiResponse<unknown>>;
221
+ post: (path: string, body: unknown) => Promise<ApiResponse<unknown>>;
222
+ readFile: (path: string) => string;
223
+ writeFile: (path: string, text: string) => void;
224
+ poll: (opts: PollOptions) => Promise<PollResult>;
225
+ }
226
+ export interface FlowResult {
227
+ code: number;
228
+ lines: string[];
229
+ }
230
+ interface RunFlowOptions {
231
+ inputs: Record<string, string>;
232
+ wait: boolean;
233
+ json: boolean;
234
+ onProgressLine?: (line: string) => void;
235
+ }
236
+ export declare function parseInputFlags(args: string[], readFile?: (path: string) => string): Record<string, string>;
237
+ export declare function formatWorkflowList(workflows: WorkflowListItem[]): string;
238
+ export declare function formatRecentRuns(runs: WorkflowRunProjection[]): string;
239
+ export declare function formatImportSummary(result: WorkflowImportResult, mode?: {
240
+ dryRun?: boolean;
241
+ update?: boolean;
242
+ }): string;
243
+ export declare function formatWorkflowRun(run: WorkflowRun): string;
244
+ export declare function formatDescribe(res: WorkflowDescribeResponse): string;
245
+ export declare function formatBotsList(catalog: WorkflowCatalog, category?: string): string;
246
+ export declare function formatBotDetail(bot: CatalogBot): string;
247
+ export declare function resolveWorkflowId(ref: string, deps: WorkflowRunDeps): Promise<string>;
248
+ export declare function listFlow(json: boolean, deps: WorkflowRunDeps): Promise<FlowResult>;
249
+ export declare function describeFlow(workflowRef: string, opts: {
250
+ json: boolean;
251
+ }, deps: WorkflowRunDeps): Promise<FlowResult>;
252
+ export declare function botsFlow(opts: {
253
+ category?: string;
254
+ slug?: string;
255
+ json: boolean;
256
+ }, deps: WorkflowRunDeps): Promise<FlowResult>;
257
+ export declare function runFlow(workflowRef: string, opts: RunFlowOptions, deps: WorkflowRunDeps): Promise<FlowResult>;
258
+ export declare function statusFlow(opts: {
259
+ id?: string;
260
+ json: boolean;
261
+ }, deps: WorkflowRunDeps): Promise<FlowResult>;
262
+ export declare function exportFlow(workflowRef: string, opts: {
263
+ out?: string;
264
+ }, deps: WorkflowRunDeps): Promise<FlowResult>;
265
+ export declare function importFlow(file: string, opts: {
266
+ dryRun: boolean;
267
+ json: boolean;
268
+ update?: string;
269
+ }, deps: WorkflowRunDeps): Promise<FlowResult>;
270
+ export declare function run(flags: Record<string, string | boolean>): Promise<void>;
271
+ export {};