@aicopycoders/exodus 2026.7.200-beta.5 → 2026.7.200-beta.7
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/cast-video.md +9 -0
- package/assets/references/kie-video-models.md +207 -0
- package/assets/references/lipsync-alternatives.md +217 -0
- package/assets/references/meta-ads-mcp-probe.md +167 -0
- package/assets/references/native-audio-dialogue.md +178 -0
- package/assets/references/own-brand-enrichment-path.md +160 -0
- package/dist/commands/brand.js +2 -1
- package/dist/commands/doctor.js +22 -16
- package/dist/commands/init.js +3 -2
- package/dist/commands/meme.js +5 -4
- package/dist/commands/migrate.js +7 -6
- package/dist/commands/winners.d.ts +10 -0
- package/dist/commands/winners.js +401 -0
- package/dist/commands/workflow.d.ts +32 -5
- package/dist/commands/workflow.js +21 -2
- package/dist/lib/channel.d.ts +5 -0
- package/dist/lib/channel.js +15 -0
- package/dist/lib/scaffold.d.ts +5 -4
- package/dist/lib/scaffold.js +27 -6
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ApiResponse } from "../lib/client.js";
|
|
2
2
|
import { type PollOptions, type PollResult } from "../lib/poll.js";
|
|
3
3
|
export declare const helpText: string;
|
|
4
|
-
export type WorkflowNodeKind = "brief" | "bot" | "primer" | "image" | "output";
|
|
4
|
+
export type WorkflowNodeKind = "brief" | "bot" | "primer" | "image" | "rig" | "storyboard" | "reference" | "scene-frames" | "video" | "voiceover" | "output";
|
|
5
5
|
export interface WorkflowNode {
|
|
6
6
|
id: string;
|
|
7
7
|
kind: WorkflowNodeKind;
|
|
@@ -59,8 +59,22 @@ export interface WorkflowImportError {
|
|
|
59
59
|
issues?: GraphIssue[];
|
|
60
60
|
currentUpdatedAt?: string;
|
|
61
61
|
}
|
|
62
|
-
export type WorkflowPortType = "text" | "primer" | "image";
|
|
62
|
+
export type WorkflowPortType = "text" | "primer" | "image" | "rig" | "storyboard" | "frames" | "video" | "audio";
|
|
63
63
|
export type WorkflowPrimerKind = "body" | "hook" | "headline" | "summary";
|
|
64
|
+
export type WorkflowDurationSpec = {
|
|
65
|
+
kind: "fixed";
|
|
66
|
+
values: number[];
|
|
67
|
+
} | {
|
|
68
|
+
kind: "range";
|
|
69
|
+
min: number;
|
|
70
|
+
max: number;
|
|
71
|
+
};
|
|
72
|
+
export interface CatalogVideoModel {
|
|
73
|
+
id: string;
|
|
74
|
+
label: string;
|
|
75
|
+
durations: WorkflowDurationSpec;
|
|
76
|
+
audioTogglable: boolean;
|
|
77
|
+
}
|
|
64
78
|
export type WorkflowParamKind = "select" | "text" | "textarea" | "toggle" | "number" | "multiselect";
|
|
65
79
|
export interface CatalogInput {
|
|
66
80
|
id: string;
|
|
@@ -103,6 +117,7 @@ export interface WorkflowCatalog {
|
|
|
103
117
|
imageModels: string[];
|
|
104
118
|
aspectRatios: string[];
|
|
105
119
|
imageQuantityModes: string[];
|
|
120
|
+
videoModels: CatalogVideoModel[];
|
|
106
121
|
categories: Array<{
|
|
107
122
|
id: string;
|
|
108
123
|
label: string;
|
|
@@ -130,7 +145,7 @@ export interface WorkflowPrerequisiteDescriptor {
|
|
|
130
145
|
nodeIds: string[];
|
|
131
146
|
}
|
|
132
147
|
export interface WorkflowOutputDescriptor {
|
|
133
|
-
type: "text" | "image";
|
|
148
|
+
type: "text" | "image" | "video" | "audio" | "frames" | "storyboard";
|
|
134
149
|
label: string;
|
|
135
150
|
nodeId: string;
|
|
136
151
|
botSlug?: string;
|
|
@@ -154,11 +169,13 @@ export interface WorkflowListItem {
|
|
|
154
169
|
edgeCount: number;
|
|
155
170
|
createdAt: string;
|
|
156
171
|
updatedAt: string;
|
|
172
|
+
isCrossBrand?: boolean;
|
|
173
|
+
homeBrandName?: string | null;
|
|
157
174
|
}
|
|
158
175
|
export interface WorkflowListResponse {
|
|
159
176
|
workflows: WorkflowListItem[];
|
|
160
177
|
}
|
|
161
|
-
export type WorkflowRunStatus = "queued" | "running" | "completed" | "partial" | "failed";
|
|
178
|
+
export type WorkflowRunStatus = "queued" | "running" | "awaiting-review" | "completed" | "partial" | "failed" | "canceled";
|
|
162
179
|
export type WorkflowNodeRunStatus = "idle" | "running" | "done" | "failed" | "skipped";
|
|
163
180
|
export type WorkflowArtifact = {
|
|
164
181
|
type: "text";
|
|
@@ -176,11 +193,21 @@ export type WorkflowArtifact = {
|
|
|
176
193
|
export interface WorkflowRunOutput {
|
|
177
194
|
nodeId: string;
|
|
178
195
|
botSlug?: string;
|
|
179
|
-
type: "text" | "image";
|
|
196
|
+
type: "text" | "image" | "video" | "audio" | "frames" | "storyboard";
|
|
180
197
|
label: string;
|
|
181
198
|
text?: string;
|
|
182
199
|
imageUrl?: string;
|
|
183
200
|
imageId?: string;
|
|
201
|
+
videoUrl?: string;
|
|
202
|
+
audioUrl?: string;
|
|
203
|
+
durationSec?: number;
|
|
204
|
+
sceneIndex?: number;
|
|
205
|
+
final?: boolean;
|
|
206
|
+
frames?: Array<{
|
|
207
|
+
sceneIndex: number;
|
|
208
|
+
imageUrl?: string;
|
|
209
|
+
}>;
|
|
210
|
+
storyboardJson?: string;
|
|
184
211
|
}
|
|
185
212
|
export interface WorkflowRunNode {
|
|
186
213
|
nodeId: string;
|
|
@@ -171,7 +171,7 @@ export function formatWorkflowList(workflows) {
|
|
|
171
171
|
if (workflows.length === 0)
|
|
172
172
|
return "No workflows found for the active brand.";
|
|
173
173
|
return table(["name", "nodes", "edges", "updated", "id"], workflows.map((w) => [
|
|
174
|
-
w.name,
|
|
174
|
+
w.isCrossBrand && w.homeBrandName ? `${w.name} · from ${w.homeBrandName}` : w.name,
|
|
175
175
|
String(w.nodeCount),
|
|
176
176
|
String(w.edgeCount),
|
|
177
177
|
dateOnly(w.updatedAt),
|
|
@@ -252,6 +252,20 @@ function runOutputLines(output) {
|
|
|
252
252
|
if (output.type === "image") {
|
|
253
253
|
return [` ${output.label} [image]${slug}: ${output.imageUrl ?? output.imageId ?? "(no url)"}`];
|
|
254
254
|
}
|
|
255
|
+
if (output.type === "video") {
|
|
256
|
+
const tag = output.final === true ? "final video" : "video";
|
|
257
|
+
return [` ${output.label} [${tag}]${slug}: ${output.videoUrl ?? "(no url)"}`];
|
|
258
|
+
}
|
|
259
|
+
if (output.type === "audio") {
|
|
260
|
+
return [` ${output.label} [audio]${slug}: ${output.audioUrl ?? "(no url)"}`];
|
|
261
|
+
}
|
|
262
|
+
if (output.type === "frames") {
|
|
263
|
+
const n = output.frames?.length ?? 0;
|
|
264
|
+
return [` ${output.label} [frames]${slug}: ${n} scene${n === 1 ? "" : "s"}`];
|
|
265
|
+
}
|
|
266
|
+
if (output.type === "storyboard") {
|
|
267
|
+
return [` ${output.label} [storyboard]${slug}: use --json for the scene plan`];
|
|
268
|
+
}
|
|
255
269
|
const raw = output.text ?? "";
|
|
256
270
|
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
257
271
|
const body = truncateText(raw, 400);
|
|
@@ -523,14 +537,19 @@ export async function runFlow(workflowRef, opts, deps) {
|
|
|
523
537
|
lines.length = 0;
|
|
524
538
|
}
|
|
525
539
|
const seen = new Map();
|
|
540
|
+
let pausedNotified = false;
|
|
526
541
|
const pollResult = await deps.poll({
|
|
527
542
|
path: `${STATUS_PATH}?runId=${encodeURIComponent(data.runId)}`,
|
|
528
543
|
intervalMs: 3_000,
|
|
529
544
|
timeoutMs: 60 * 60 * 1000,
|
|
530
|
-
terminalStatuses: ["completed", "partial", "failed"],
|
|
545
|
+
terminalStatuses: ["completed", "partial", "failed", "canceled"],
|
|
531
546
|
onProgress: (raw) => {
|
|
532
547
|
if (opts.json || !opts.onProgressLine)
|
|
533
548
|
return;
|
|
549
|
+
if (raw["status"] === "awaiting-review" && !pausedNotified) {
|
|
550
|
+
pausedNotified = true;
|
|
551
|
+
opts.onProgressLine(" ⏸ paused at the cost gate — approve or edit the storyboard in the web app to continue.");
|
|
552
|
+
}
|
|
534
553
|
const nodes = Array.isArray(raw["nodes"]) ? raw["nodes"] : [];
|
|
535
554
|
for (const node of nodes) {
|
|
536
555
|
if (seen.get(node.nodeId) === node.status)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type Channel = "beta" | "latest";
|
|
2
|
+
export declare function channelOf(version: string): Channel;
|
|
3
|
+
export declare function getChannel(): Channel;
|
|
4
|
+
export declare function pkgRef(channel?: Channel): string;
|
|
5
|
+
export declare function stampChannel(content: string, channel?: Channel): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getVersion } from "./version.js";
|
|
2
|
+
export function channelOf(version) {
|
|
3
|
+
return version.includes("-beta") ? "beta" : "latest";
|
|
4
|
+
}
|
|
5
|
+
export function getChannel() {
|
|
6
|
+
return channelOf(getVersion());
|
|
7
|
+
}
|
|
8
|
+
export function pkgRef(channel = getChannel()) {
|
|
9
|
+
return `@aicopycoders/exodus@${channel}`;
|
|
10
|
+
}
|
|
11
|
+
export function stampChannel(content, channel = getChannel()) {
|
|
12
|
+
if (channel !== "beta")
|
|
13
|
+
return content;
|
|
14
|
+
return content.replace(/npx @aicopycoders\/exodus(@[A-Za-z0-9.-]+)?/g, `npx ${pkgRef(channel)}`);
|
|
15
|
+
}
|
package/dist/lib/scaffold.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { type Channel } from "./channel.js";
|
|
1
2
|
export declare const ENV_SCAFFOLD = "# Exodus config \u2014 paste your dashboard .env block below this line.\n# Get it from your Exodus dashboard -> Settings -> Claude Code -> \"Copy .env block\".\n# ONE key covers every brand you own; switch brands with brand subfolders or\n# `npx @aicopycoders/exodus brand use <slug>`, never by editing this file.\n";
|
|
2
|
-
export declare function writeEnvScaffold(root: string): {
|
|
3
|
+
export declare function writeEnvScaffold(root: string, channel?: Channel): {
|
|
3
4
|
created: boolean;
|
|
4
5
|
};
|
|
5
6
|
export declare function ensureGitignore(root: string): void;
|
|
6
|
-
export declare function writeSkills(root: string, srcOverride?: string): string[];
|
|
7
|
-
export declare function writeReferences(root: string, srcOverride?: string): void;
|
|
8
|
-
export declare function writeDocs(root: string, srcOverride?: string): string[];
|
|
7
|
+
export declare function writeSkills(root: string, srcOverride?: string, channel?: Channel): string[];
|
|
8
|
+
export declare function writeReferences(root: string, srcOverride?: string, channel?: Channel): void;
|
|
9
|
+
export declare function writeDocs(root: string, srcOverride?: string, channel?: Channel): string[];
|
|
9
10
|
export declare function ensureBaseDirs(root: string): void;
|
package/dist/lib/scaffold.js
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { skillsDir, referencesDir, docsDir } from "./assets.js";
|
|
4
|
+
import { getChannel, stampChannel } from "./channel.js";
|
|
4
5
|
export const ENV_SCAFFOLD = `# Exodus config — paste your dashboard .env block below this line.
|
|
5
6
|
# Get it from your Exodus dashboard -> Settings -> Claude Code -> "Copy .env block".
|
|
6
7
|
# ONE key covers every brand you own; switch brands with brand subfolders or
|
|
7
8
|
# \`npx @aicopycoders/exodus brand use <slug>\`, never by editing this file.
|
|
8
9
|
`;
|
|
9
|
-
export function writeEnvScaffold(root) {
|
|
10
|
+
export function writeEnvScaffold(root, channel = getChannel()) {
|
|
10
11
|
const envPath = path.join(root, ".env");
|
|
11
12
|
if (fs.existsSync(envPath))
|
|
12
13
|
return { created: false };
|
|
13
|
-
fs.writeFileSync(envPath, ENV_SCAFFOLD);
|
|
14
|
+
fs.writeFileSync(envPath, stampChannel(ENV_SCAFFOLD, channel));
|
|
14
15
|
return { created: true };
|
|
15
16
|
}
|
|
17
|
+
function stampTree(dir, channel) {
|
|
18
|
+
if (channel !== "beta")
|
|
19
|
+
return;
|
|
20
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
21
|
+
const p = path.join(dir, entry.name);
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
stampTree(p, channel);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!entry.isFile())
|
|
27
|
+
continue;
|
|
28
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
29
|
+
const stamped = stampChannel(raw, channel);
|
|
30
|
+
if (stamped !== raw)
|
|
31
|
+
fs.writeFileSync(p, stamped);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
16
34
|
export function ensureGitignore(root) {
|
|
17
35
|
const giPath = path.join(root, ".gitignore");
|
|
18
36
|
const lines = fs.existsSync(giPath)
|
|
@@ -22,7 +40,7 @@ export function ensureGitignore(root) {
|
|
|
22
40
|
lines.push(".env");
|
|
23
41
|
fs.writeFileSync(giPath, lines.join("\n").replace(/\n+$/, "") + "\n");
|
|
24
42
|
}
|
|
25
|
-
export function writeSkills(root, srcOverride) {
|
|
43
|
+
export function writeSkills(root, srcOverride, channel = getChannel()) {
|
|
26
44
|
const src = skillsDir(srcOverride);
|
|
27
45
|
const dest = path.join(root, ".claude", "skills");
|
|
28
46
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -33,11 +51,12 @@ export function writeSkills(root, srcOverride) {
|
|
|
33
51
|
const target = path.join(dest, entry.name);
|
|
34
52
|
fs.rmSync(target, { recursive: true, force: true });
|
|
35
53
|
fs.cpSync(path.join(src, entry.name), target, { recursive: true });
|
|
54
|
+
stampTree(target, channel);
|
|
36
55
|
names.push(entry.name);
|
|
37
56
|
}
|
|
38
57
|
return names;
|
|
39
58
|
}
|
|
40
|
-
export function writeReferences(root, srcOverride) {
|
|
59
|
+
export function writeReferences(root, srcOverride, channel = getChannel()) {
|
|
41
60
|
let src;
|
|
42
61
|
try {
|
|
43
62
|
src = referencesDir(srcOverride);
|
|
@@ -50,8 +69,9 @@ export function writeReferences(root, srcOverride) {
|
|
|
50
69
|
const dest = path.join(root, "references");
|
|
51
70
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
52
71
|
fs.cpSync(src, dest, { recursive: true });
|
|
72
|
+
stampTree(dest, channel);
|
|
53
73
|
}
|
|
54
|
-
export function writeDocs(root, srcOverride) {
|
|
74
|
+
export function writeDocs(root, srcOverride, channel = getChannel()) {
|
|
55
75
|
let src;
|
|
56
76
|
try {
|
|
57
77
|
src = docsDir(srcOverride);
|
|
@@ -65,7 +85,8 @@ export function writeDocs(root, srcOverride) {
|
|
|
65
85
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
66
86
|
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
67
87
|
continue;
|
|
68
|
-
fs.
|
|
88
|
+
const content = fs.readFileSync(path.join(src, entry.name), "utf8");
|
|
89
|
+
fs.writeFileSync(path.join(root, entry.name), stampChannel(content, channel));
|
|
69
90
|
names.push(entry.name);
|
|
70
91
|
}
|
|
71
92
|
return names.sort();
|