@cocoviral/cli 1.5.4
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 +159 -0
- package/dist/cli.js +920 -0
- package/dist/client.js +339 -0
- package/package.json +39 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const client_1 = require("./client");
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Config
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// COCOVIRAL_* is the documented name; CLONEVIRAL_* still works for setups made
|
|
14
|
+
// before the rename, since both brands share one backend.
|
|
15
|
+
const API_KEY = process.env.COCOVIRAL_API_KEY || process.env.CLONEVIRAL_API_KEY;
|
|
16
|
+
const API_URL = process.env.COCOVIRAL_API_URL || process.env.CLONEVIRAL_API_URL || 'https://www.cocoviral.ai';
|
|
17
|
+
const VERSION = '1.5.4';
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Output helpers
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const c = {
|
|
22
|
+
reset: '\x1b[0m',
|
|
23
|
+
bold: '\x1b[1m',
|
|
24
|
+
dim: '\x1b[2m',
|
|
25
|
+
green: '\x1b[32m',
|
|
26
|
+
yellow: '\x1b[33m',
|
|
27
|
+
cyan: '\x1b[36m',
|
|
28
|
+
red: '\x1b[31m',
|
|
29
|
+
white: '\x1b[37m',
|
|
30
|
+
gray: '\x1b[90m',
|
|
31
|
+
};
|
|
32
|
+
function print(msg) { process.stdout.write(msg + '\n'); }
|
|
33
|
+
function success(msg) { print(`${c.green}✓${c.reset} ${msg}`); }
|
|
34
|
+
function warn(msg) { print(`${c.yellow}⚠${c.reset} ${msg}`); }
|
|
35
|
+
function error(msg) { print(`${c.red}✗${c.reset} ${msg}`); }
|
|
36
|
+
function dim(msg) { print(`${c.gray}${msg}${c.reset}`); }
|
|
37
|
+
function label(k, v) {
|
|
38
|
+
print(` ${c.dim}${k.padEnd(18)}${c.reset}${v}`);
|
|
39
|
+
}
|
|
40
|
+
function divider() { print(`${c.gray}${'─'.repeat(52)}${c.reset}`); }
|
|
41
|
+
function parseArgs(argv) {
|
|
42
|
+
const args = argv.slice(2);
|
|
43
|
+
const positionals = [];
|
|
44
|
+
const flags = {};
|
|
45
|
+
let command = '';
|
|
46
|
+
let subcommand;
|
|
47
|
+
for (let i = 0; i < args.length; i++) {
|
|
48
|
+
const arg = args[i];
|
|
49
|
+
if (arg.startsWith('--')) {
|
|
50
|
+
const key = arg.slice(2);
|
|
51
|
+
const next = args[i + 1];
|
|
52
|
+
if (next && !next.startsWith('--')) {
|
|
53
|
+
const existing = flags[key];
|
|
54
|
+
if (existing === undefined) {
|
|
55
|
+
flags[key] = next;
|
|
56
|
+
}
|
|
57
|
+
else if (Array.isArray(existing)) {
|
|
58
|
+
existing.push(next);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
flags[key] = [existing, next];
|
|
62
|
+
}
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
flags[key] = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (!command) {
|
|
70
|
+
command = arg;
|
|
71
|
+
}
|
|
72
|
+
else if (!subcommand && !arg.startsWith('-')) {
|
|
73
|
+
subcommand = arg;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
positionals.push(arg);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { command, subcommand, positionals, flags };
|
|
80
|
+
}
|
|
81
|
+
function flag(flags, ...keys) {
|
|
82
|
+
for (const k of keys) {
|
|
83
|
+
const v = flags[k];
|
|
84
|
+
if (Array.isArray(v))
|
|
85
|
+
return v[0];
|
|
86
|
+
if (v && v !== true)
|
|
87
|
+
return v;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
function flagNum(flags, key, def) {
|
|
92
|
+
const v = flags[key];
|
|
93
|
+
if (Array.isArray(v))
|
|
94
|
+
return parseInt(v[0], 10) || def;
|
|
95
|
+
if (v && v !== true)
|
|
96
|
+
return parseInt(v, 10) || def;
|
|
97
|
+
return def;
|
|
98
|
+
}
|
|
99
|
+
/** Repeatable flag (`--ref a --ref b`) collected into a list. */
|
|
100
|
+
function flagList(flags, key) {
|
|
101
|
+
const v = flags[key];
|
|
102
|
+
if (Array.isArray(v))
|
|
103
|
+
return v;
|
|
104
|
+
if (v && v !== true)
|
|
105
|
+
return [v];
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Help
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
function printHelp() {
|
|
112
|
+
print('');
|
|
113
|
+
print(`${c.bold}cviral${c.reset} ${c.gray}CocoViral CLI v${VERSION}${c.reset}`);
|
|
114
|
+
print('');
|
|
115
|
+
print(`${c.bold}Usage${c.reset}`);
|
|
116
|
+
print(` cviral <command> [options]`);
|
|
117
|
+
print('');
|
|
118
|
+
print(`${c.bold}Commands${c.reset}`);
|
|
119
|
+
print(` ${c.cyan}credits${c.reset} Check your credit balance`);
|
|
120
|
+
print(` ${c.cyan}videos${c.reset} [--limit N] [--status S] List recent videos`);
|
|
121
|
+
print(` ${c.cyan}image${c.reset} <prompt> [options] Generate an AI image`);
|
|
122
|
+
print(` ${c.cyan}edit${c.reset} <prompt> --url <url> Edit an existing image with AI`);
|
|
123
|
+
print(` ${c.cyan}video${c.reset} <prompt> [options] Generate an AI video`);
|
|
124
|
+
print(` ${c.cyan}status${c.reset} <predictionId> [--type video|image] Check generation status`);
|
|
125
|
+
print(` ${c.cyan}analyze${c.reset} <videoUrl> Analyze a video for remixing`);
|
|
126
|
+
print(` ${c.cyan}remix${c.reset} <videoUrl> [options] Analyze + build remix variants`);
|
|
127
|
+
print(` ${c.cyan}translate${c.reset} <videoUrl> [options] Translate & dub a video`);
|
|
128
|
+
print(` ${c.cyan}translate-status${c.reset} <jobId> Check translation job status`);
|
|
129
|
+
print(` ${c.cyan}characters${c.reset} List saved AI characters`);
|
|
130
|
+
print('');
|
|
131
|
+
print(`${c.bold}Viral Flow${c.reset} ${c.gray}(local files: the CLI can upload and download, the hosted tools cannot)${c.reset}`);
|
|
132
|
+
print(` ${c.cyan}flows${c.reset} List your flows`);
|
|
133
|
+
print(` ${c.cyan}flow show${c.reset} <flowId> Nodes, readiness and estimated cost`);
|
|
134
|
+
print(` ${c.cyan}flow upload${c.reset} <path> Upload a local file, get a URL for a node`);
|
|
135
|
+
print(` ${c.cyan}flow run${c.reset} <flowId> --node <id> [--watch] Run one node`);
|
|
136
|
+
print(` ${c.cyan}flow status${c.reset} <flowId> --node <id> Check a running node`);
|
|
137
|
+
print(` ${c.cyan}flow pull${c.reset} <flowId> [--out dir] [--generated] Download its assets`);
|
|
138
|
+
print('');
|
|
139
|
+
print(`${c.bold}Director Studio${c.reset}`);
|
|
140
|
+
print(` ${c.cyan}presets${c.reset} [looks|moves|sizes|camera] List looks, camera moves, shot sizes and camera setup`);
|
|
141
|
+
print(` ${c.cyan}frame${c.reset} <scene> [options] Generate a cinematic still frame`);
|
|
142
|
+
print(` ${c.cyan}shot${c.reset} --start <url> [options] Animate a frame with a camera move`);
|
|
143
|
+
print('');
|
|
144
|
+
print(`${c.bold}Image options${c.reset}`);
|
|
145
|
+
print(` --model ${Object.keys(client_1.IMAGE_MODEL_ALIAS_MAP).join(' | ')} ${c.gray}(default: flux-2-pro)${c.reset}`);
|
|
146
|
+
print(` --ratio 1:1 | 16:9 | 9:16 | 4:3 | 3:4 ${c.gray}(default: 1:1)${c.reset}`);
|
|
147
|
+
print(` --ref Reference image URL (repeat for multiple: --ref url1 --ref url2)`);
|
|
148
|
+
print('');
|
|
149
|
+
print(`${c.bold}Edit options${c.reset}`);
|
|
150
|
+
print(` --url ${c.gray}(required)${c.reset} Source image URL to edit`);
|
|
151
|
+
print(` --model nano-banana-2 | gpt-image-2 | nano-banana-pro | seedream-v5-lite | seedream-v5-pro | midjourney-v8 | gpt-image-2.5 ${c.gray}(default: nano-banana-2)${c.reset}`);
|
|
152
|
+
print(` --ratio 1:1 | 16:9 | 9:16 | 4:3 | 3:4 ${c.gray}(default: 1:1)${c.reset}`);
|
|
153
|
+
print('');
|
|
154
|
+
print(`${c.bold}Video options${c.reset}`);
|
|
155
|
+
print(` --model ${Object.keys(client_1.VIDEO_MODEL_ID_MAP).join(' | ')} ${c.gray}(default: veo3.1)${c.reset}`);
|
|
156
|
+
print(` --ratio 9:16 | 16:9 | 1:1 | 4:3 | 3:4 | 21:9 ${c.gray}(default: 9:16; 21:9 is Seedance 2.5 only)${c.reset}`);
|
|
157
|
+
print(` --duration seconds ${c.gray}(default: 5; Seedance 2.5 up to 30, MiniMax H3 5–15)${c.reset}`);
|
|
158
|
+
print(` --res sd | hd | fhd | 2k ${c.gray}(default: hd; MiniMax H3 is 2K only, Seedance 2.5 is sd/hd/fhd)${c.reset}`);
|
|
159
|
+
print(` --start Start frame image URL (image-to-video)`);
|
|
160
|
+
print(` --end End frame image URL ${c.gray}(seedance2.5-i2v, minimax-h3-i2v, minimax-h3-max-i2v, veo3.1, flux3-i2v)${c.reset}`);
|
|
161
|
+
print(` --ref Reference image URL ${c.gray}(repeat; -omni / -ref models)${c.reset}`);
|
|
162
|
+
print(` --refvid Reference video URL ${c.gray}(repeat; -omni / -ref models)${c.reset}`);
|
|
163
|
+
print(` --refaudio Reference audio URL ${c.gray}(repeat; voice reference for -omni / -ref models)${c.reset}`);
|
|
164
|
+
print('');
|
|
165
|
+
print(`${c.bold}Translate options${c.reset}`);
|
|
166
|
+
print(` --lang ja | zh | ko | es | fr | de | pt | ar | hi | ru`);
|
|
167
|
+
print(` --tone casual | professional | energetic | formal ${c.gray}(default: casual)${c.reset}`);
|
|
168
|
+
print('');
|
|
169
|
+
print(`${c.bold}Remix options${c.reset}`);
|
|
170
|
+
print(` --count Number of variants ${c.gray}(default: 3)${c.reset}`);
|
|
171
|
+
print(` --product Product description to feature`);
|
|
172
|
+
print(` --modify Modification instructions`);
|
|
173
|
+
print('');
|
|
174
|
+
print(`${c.bold}Frame options${c.reset} ${c.gray}(cinema still)${c.reset}`);
|
|
175
|
+
print(` --look Film setup — a "film" look id ${c.gray}(default: blockbuster — run: cviral presets looks)${c.reset}`);
|
|
176
|
+
print(` --lighting Lighting — a "lighting" look id ${c.gray}(golden-hour, neon-noir, chiaroscuro…)${c.reset}`);
|
|
177
|
+
print(` --palette Colour palette — a "genre" look id ${c.gray}(cyberpunk, horror-night, desert-western…)${c.reset}`);
|
|
178
|
+
print(` --camera Camera body / format ${c.gray}(35mm, 16mm, dv-camcorder, super-8… default: Auto)${c.reset}`);
|
|
179
|
+
print(` --lens Lens ${c.gray}(halation-vintage, anamorphic-flare, portrait-85mm… default: Auto)${c.reset}`);
|
|
180
|
+
print(` --aperture Aperture ${c.gray}(f1-4, f2-8, f5-6, f11 — default: Auto)${c.reset}`);
|
|
181
|
+
print(` --size extreme-wide | wide | medium | close-up | extreme-close-up ${c.gray}(default: medium)${c.reset}`);
|
|
182
|
+
print(` --ratio 16:9 | 9:16 | 1:1 | 4:3 | 3:4 ${c.gray}(default: 16:9)${c.reset}`);
|
|
183
|
+
print(` --model ${client_1.CINEMA_FRAME_MODELS.join(' | ')} ${c.gray}(default: nano-banana-pro)${c.reset}`);
|
|
184
|
+
print('');
|
|
185
|
+
print(`${c.bold}Shot options${c.reset} ${c.gray}(animate a frame)${c.reset}`);
|
|
186
|
+
print(` --start ${c.gray}(required)${c.reset} Frame URL to animate`);
|
|
187
|
+
print(` --move Camera move preset id ${c.gray}(default: slow-push-in — run: cviral presets moves)${c.reset}`);
|
|
188
|
+
print(` --direction Extra motion direction — what else moves in the shot`);
|
|
189
|
+
print(` --scene Short reminder of what is in the frame`);
|
|
190
|
+
print(` --duration seconds ${c.gray}(default: the camera move's recommended duration)${c.reset}`);
|
|
191
|
+
print(` --ratio 16:9 | 9:16 | 1:1 | 4:3 | 3:4 ${c.gray}(default: 16:9)${c.reset}`);
|
|
192
|
+
print(` --res sd | hd | fhd | uhd | 2k ${c.gray}(default: fhd)${c.reset}`);
|
|
193
|
+
print(` --model ${client_1.CINEMA_SHOT_MODELS.join(' | ')} ${c.gray}(default: kling3-pro-i2v)${c.reset}`);
|
|
194
|
+
print('');
|
|
195
|
+
print(`${c.bold}Preset options${c.reset}`);
|
|
196
|
+
print(` --category film | lighting | genre ${c.gray}(looks)${c.reset} · camera | action | vfx ${c.gray}(moves)${c.reset}`);
|
|
197
|
+
print(` --search Substring match on preset name or description`);
|
|
198
|
+
print('');
|
|
199
|
+
print(`${c.bold}Environment${c.reset}`);
|
|
200
|
+
print(` COCOVIRAL_API_KEY ${c.gray}Your API key (cv_...)${c.reset}`);
|
|
201
|
+
print(` COCOVIRAL_API_URL ${c.gray}Override API URL${c.reset}`);
|
|
202
|
+
print('');
|
|
203
|
+
print(`${c.bold}Examples${c.reset}`);
|
|
204
|
+
print(` cviral credits`);
|
|
205
|
+
print(` cviral image "a sunset over Tokyo" --model gpt-image-2 --ratio 16:9`);
|
|
206
|
+
print(` cviral image "product shot" --model nano-banana-2 --ref https://example.com/a.jpg --ref https://example.com/b.jpg`);
|
|
207
|
+
print(` cviral edit "make the sky purple" --url https://example.com/photo.jpg --model nano-banana-2`);
|
|
208
|
+
print(` cviral video "UGC skincare ad" --model seedance2 --ratio 9:16 --duration 10`);
|
|
209
|
+
print(` cviral video "cinematic walk" --model veo3.1 --start https://example.com/start.jpg`);
|
|
210
|
+
print(` cviral video "one-take product story" --model seedance2.5 --duration 30 --ratio 21:9`);
|
|
211
|
+
print(` cviral video "she turns and speaks" --model seedance2.5-omni --ref https://example.com/hero.jpg --refaudio https://example.com/voice.mp3`);
|
|
212
|
+
print(` cviral video "founder to camera" --model minimax-h3-max --res hd --duration 12`);
|
|
213
|
+
print(` cviral status pred_abc123`);
|
|
214
|
+
print(` cviral status img_abc123 --type image`);
|
|
215
|
+
print(` cviral remix https://tiktok.com/... --count 5 --product "my face cream"`);
|
|
216
|
+
print(` cviral translate https://example.com/video.mp4 --lang ja --tone casual`);
|
|
217
|
+
print(` cviral translate-status job_abc123`);
|
|
218
|
+
print(` cviral presets moves --category vfx`);
|
|
219
|
+
print(` cviral frame "lone astronaut on a rain-soaked rooftop" --look blockbuster --size wide`);
|
|
220
|
+
print(` cviral shot --start https://example.com/frame.png --move crash-zoom-in --duration 5`);
|
|
221
|
+
print('');
|
|
222
|
+
}
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Commands
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
async function cmdCredits(client) {
|
|
227
|
+
print('');
|
|
228
|
+
const d = await client.getCredits();
|
|
229
|
+
divider();
|
|
230
|
+
print(` ${c.bold}CocoViral Credits${c.reset}`);
|
|
231
|
+
divider();
|
|
232
|
+
label('Balance', `${c.bold}${d.balance ?? 0}${c.reset} credits`);
|
|
233
|
+
label('Subscription', d.subscriptionCredits ?? 0);
|
|
234
|
+
label('Purchased', d.purchasedCredits ?? 0);
|
|
235
|
+
label('Consumed', d.totalConsumed ?? 0);
|
|
236
|
+
divider();
|
|
237
|
+
print('');
|
|
238
|
+
}
|
|
239
|
+
async function cmdVideos(client, flags) {
|
|
240
|
+
const limit = flagNum(flags, 'limit', 10);
|
|
241
|
+
const status = flag(flags, 'status');
|
|
242
|
+
const type = flag(flags, 'type');
|
|
243
|
+
print('');
|
|
244
|
+
const videos = await client.listVideos({ limit, status, type });
|
|
245
|
+
if (!videos.length) {
|
|
246
|
+
warn('No videos found.');
|
|
247
|
+
print('');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
divider();
|
|
251
|
+
print(` ${c.bold}Recent Videos${c.reset} ${c.gray}(${videos.length} shown)${c.reset}`);
|
|
252
|
+
divider();
|
|
253
|
+
for (const v of videos) {
|
|
254
|
+
const statusColor = v.status === 'completed' ? c.green : v.status === 'failed' ? c.red : c.yellow;
|
|
255
|
+
print(` ${c.bold}${v.title || 'Untitled'}${c.reset} ${statusColor}[${v.status}]${c.reset}`);
|
|
256
|
+
label('ID', v._id);
|
|
257
|
+
label('Type', v.type || '—');
|
|
258
|
+
label('Created', v.createdAt ? new Date(v.createdAt).toLocaleString() : '—');
|
|
259
|
+
if (v.videoUrl)
|
|
260
|
+
label('URL', v.videoUrl);
|
|
261
|
+
print('');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async function cmdImage(client, prompt, flags) {
|
|
265
|
+
const model = flag(flags, 'model') || 'flux-2-pro';
|
|
266
|
+
const ratio = flag(flags, 'ratio') || '1:1';
|
|
267
|
+
const referenceImages = flagList(flags, 'ref');
|
|
268
|
+
print('');
|
|
269
|
+
print(`${c.gray}Generating image…${c.reset}`);
|
|
270
|
+
const data = await client.generateImage({ prompt, model, aspectRatio: ratio, referenceImages });
|
|
271
|
+
print('');
|
|
272
|
+
if (data.imageUrl) {
|
|
273
|
+
success('Image ready!');
|
|
274
|
+
label('URL', data.imageUrl);
|
|
275
|
+
label('Model', model);
|
|
276
|
+
label('Ratio', ratio);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
const id = data.predictionId || data.id;
|
|
280
|
+
success('Image generation started');
|
|
281
|
+
label('Prediction ID', id);
|
|
282
|
+
dim(` Run: cviral status ${id}`);
|
|
283
|
+
}
|
|
284
|
+
print('');
|
|
285
|
+
}
|
|
286
|
+
async function cmdEditImage(client, prompt, flags) {
|
|
287
|
+
const imageUrl = flag(flags, 'url');
|
|
288
|
+
if (!imageUrl) {
|
|
289
|
+
error('--url is required: cviral edit "<prompt>" --url <image_url>');
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
const model = flag(flags, 'model') || 'nano-banana-2';
|
|
293
|
+
const ratio = flag(flags, 'ratio') || '1:1';
|
|
294
|
+
print('');
|
|
295
|
+
print(`${c.gray}Editing image…${c.reset}`);
|
|
296
|
+
const data = await client.editImage({ prompt, imageUrl, model, aspectRatio: ratio });
|
|
297
|
+
print('');
|
|
298
|
+
if (data.imageUrl) {
|
|
299
|
+
success('Edit ready!');
|
|
300
|
+
label('URL', data.imageUrl);
|
|
301
|
+
label('Model', model);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const id = data.predictionId || data.id;
|
|
305
|
+
success('Image edit started');
|
|
306
|
+
label('Prediction ID', id);
|
|
307
|
+
dim(` Run: cviral status ${id}`);
|
|
308
|
+
}
|
|
309
|
+
print('');
|
|
310
|
+
}
|
|
311
|
+
async function cmdVideo(client, prompt, flags) {
|
|
312
|
+
const model = flag(flags, 'model') || 'veo3.1';
|
|
313
|
+
const ratio = flag(flags, 'ratio') || '9:16';
|
|
314
|
+
const duration = flagNum(flags, 'duration', 5);
|
|
315
|
+
const res = flag(flags, 'res') || 'hd';
|
|
316
|
+
const startImageUrl = flag(flags, 'start');
|
|
317
|
+
const endImageUrl = flag(flags, 'end');
|
|
318
|
+
const referenceImages = flagList(flags, 'ref');
|
|
319
|
+
const referenceVideos = flagList(flags, 'refvid');
|
|
320
|
+
const referenceAudios = flagList(flags, 'refaudio');
|
|
321
|
+
print('');
|
|
322
|
+
print(`${c.gray}Starting video generation…${c.reset}`);
|
|
323
|
+
const data = await client.generateVideo({
|
|
324
|
+
prompt, model, aspectRatio: ratio, duration, resolution: res,
|
|
325
|
+
startImageUrl, endImageUrl, referenceImages, referenceVideos, referenceAudios,
|
|
326
|
+
});
|
|
327
|
+
const id = data.videoId || data.predictionId || data.id;
|
|
328
|
+
print('');
|
|
329
|
+
success('Video generation started!');
|
|
330
|
+
label('Prediction ID', id);
|
|
331
|
+
label('Model', model);
|
|
332
|
+
label('Aspect', `${ratio} · ${duration}s · ${res}`);
|
|
333
|
+
if (startImageUrl)
|
|
334
|
+
label('Start frame', startImageUrl);
|
|
335
|
+
if (endImageUrl)
|
|
336
|
+
label('End frame', endImageUrl);
|
|
337
|
+
if (referenceImages.length)
|
|
338
|
+
label('Ref images', referenceImages.join(', '));
|
|
339
|
+
if (referenceVideos.length)
|
|
340
|
+
label('Ref videos', referenceVideos.join(', '));
|
|
341
|
+
if (referenceAudios.length)
|
|
342
|
+
label('Ref audios', referenceAudios.join(', '));
|
|
343
|
+
dim(` Poll: cviral status ${id}`);
|
|
344
|
+
print('');
|
|
345
|
+
}
|
|
346
|
+
async function cmdStatus(client, predictionId, flags) {
|
|
347
|
+
const type = flag(flags, 'type') || 'video';
|
|
348
|
+
print('');
|
|
349
|
+
if (type === 'image') {
|
|
350
|
+
const data = await client.checkImageStatus(predictionId);
|
|
351
|
+
const statusColor = data.status === 'completed' ? c.green : data.status === 'failed' ? c.red : c.yellow;
|
|
352
|
+
divider();
|
|
353
|
+
print(` ${c.bold}Image Status${c.reset}`);
|
|
354
|
+
divider();
|
|
355
|
+
label('Prediction ID', predictionId);
|
|
356
|
+
label('Status', `${statusColor}${data.status || 'unknown'}${c.reset}`);
|
|
357
|
+
const urls = data.output || data.r2_urls || data.download_urls || [];
|
|
358
|
+
if (urls.length) {
|
|
359
|
+
urls.forEach((u, i) => label(`Image URL ${i + 1}`, u));
|
|
360
|
+
}
|
|
361
|
+
else if (data.image_url) {
|
|
362
|
+
label('Image URL', data.image_url);
|
|
363
|
+
}
|
|
364
|
+
if (data.error)
|
|
365
|
+
label('Error', `${c.red}${data.error}${c.reset}`);
|
|
366
|
+
divider();
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
const data = await client.checkVideoStatus(predictionId);
|
|
370
|
+
const statusColor = data.status === 'completed' ? c.green : data.status === 'failed' ? c.red : c.yellow;
|
|
371
|
+
divider();
|
|
372
|
+
print(` ${c.bold}Video Status${c.reset}`);
|
|
373
|
+
divider();
|
|
374
|
+
label('Prediction ID', predictionId);
|
|
375
|
+
label('Status', `${statusColor}${data.status || 'unknown'}${c.reset}`);
|
|
376
|
+
if (data.videoUrl || data.video_url)
|
|
377
|
+
label('URL', data.videoUrl || data.video_url);
|
|
378
|
+
if (data.thumbnailUrl)
|
|
379
|
+
label('Thumbnail', data.thumbnailUrl);
|
|
380
|
+
if (data.error)
|
|
381
|
+
label('Error', `${c.red}${data.error}${c.reset}`);
|
|
382
|
+
divider();
|
|
383
|
+
}
|
|
384
|
+
print('');
|
|
385
|
+
}
|
|
386
|
+
async function cmdAnalyze(client, videoUrl, flags) {
|
|
387
|
+
const prompt = flag(flags, 'prompt');
|
|
388
|
+
print('');
|
|
389
|
+
print(`${c.gray}Analyzing video…${c.reset}`);
|
|
390
|
+
const analysis = await client.analyzeVideo(videoUrl, prompt);
|
|
391
|
+
print('');
|
|
392
|
+
divider();
|
|
393
|
+
print(` ${c.bold}Video Analysis${c.reset}`);
|
|
394
|
+
divider();
|
|
395
|
+
print(analysis);
|
|
396
|
+
divider();
|
|
397
|
+
print('');
|
|
398
|
+
}
|
|
399
|
+
async function cmdRemix(client, videoUrl, flags) {
|
|
400
|
+
const count = flagNum(flags, 'count', 3);
|
|
401
|
+
const product = flag(flags, 'product') || '';
|
|
402
|
+
const modify = flag(flags, 'modify') || '';
|
|
403
|
+
print('');
|
|
404
|
+
print(`${c.gray}Analyzing video…${c.reset}`);
|
|
405
|
+
const analysis = await client.analyzeVideo(videoUrl);
|
|
406
|
+
print(`${c.gray}Building ${count} remix variant(s)…${c.reset}`);
|
|
407
|
+
const variants = await client.buildRemixVariants({
|
|
408
|
+
basePrompt: analysis,
|
|
409
|
+
count,
|
|
410
|
+
modificationInstructions: modify,
|
|
411
|
+
productDescription: product,
|
|
412
|
+
});
|
|
413
|
+
print('');
|
|
414
|
+
divider();
|
|
415
|
+
print(` ${c.bold}${variants.length} Remix Variant(s)${c.reset}`);
|
|
416
|
+
divider();
|
|
417
|
+
variants.forEach((v, i) => {
|
|
418
|
+
print(` ${c.bold}Variant ${i + 1}${c.reset}`);
|
|
419
|
+
print(` ${v.prompt || v}`);
|
|
420
|
+
print('');
|
|
421
|
+
});
|
|
422
|
+
divider();
|
|
423
|
+
dim(' Use: cviral video "<prompt>" to generate each variant');
|
|
424
|
+
print('');
|
|
425
|
+
}
|
|
426
|
+
async function cmdTranslate(client, videoUrl, flags) {
|
|
427
|
+
const lang = flag(flags, 'lang', 'language') || '';
|
|
428
|
+
const tone = flag(flags, 'tone') || 'casual';
|
|
429
|
+
const voice = flag(flags, 'voice') || '';
|
|
430
|
+
if (!lang) {
|
|
431
|
+
error('--lang is required (e.g. --lang ja)');
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
print('');
|
|
435
|
+
print(`${c.gray}Creating translation job…${c.reset}`);
|
|
436
|
+
const data = await client.translateVideo({ sourceUrl: videoUrl, targetLanguage: lang, tone, voiceStyle: voice });
|
|
437
|
+
const jobId = data.jobId || data.id;
|
|
438
|
+
print('');
|
|
439
|
+
success('Translation job created!');
|
|
440
|
+
label('Job ID', jobId);
|
|
441
|
+
label('Target', lang);
|
|
442
|
+
label('Tone', tone);
|
|
443
|
+
label('Status', data.status || 'queued');
|
|
444
|
+
print('');
|
|
445
|
+
}
|
|
446
|
+
async function cmdTranslateStatus(client, jobId) {
|
|
447
|
+
print('');
|
|
448
|
+
print(`${c.gray}Fetching translation job status…${c.reset}`);
|
|
449
|
+
const data = await client.getTranslateJobStatus(jobId);
|
|
450
|
+
const status = data.status || data.job?.status || 'unknown';
|
|
451
|
+
const statusColor = status === 'completed' ? c.green : status === 'failed' ? c.red : c.yellow;
|
|
452
|
+
print('');
|
|
453
|
+
divider();
|
|
454
|
+
print(` ${c.bold}Translation Job Status${c.reset}`);
|
|
455
|
+
divider();
|
|
456
|
+
label('Job ID', jobId);
|
|
457
|
+
label('Status', `${statusColor}${status}${c.reset}`);
|
|
458
|
+
if (data.progress !== undefined)
|
|
459
|
+
label('Progress', `${data.progress}%`);
|
|
460
|
+
const outputUrl = data.outputUrl || data.output_url || data.videoUrl || data.job?.outputUrl;
|
|
461
|
+
if (outputUrl)
|
|
462
|
+
label('Output URL', outputUrl);
|
|
463
|
+
const lang = data.targetLanguage || data.job?.targetLanguage;
|
|
464
|
+
if (lang)
|
|
465
|
+
label('Language', lang);
|
|
466
|
+
const err = data.error || data.job?.error;
|
|
467
|
+
if (err)
|
|
468
|
+
label('Error', `${c.red}${err}${c.reset}`);
|
|
469
|
+
divider();
|
|
470
|
+
print('');
|
|
471
|
+
}
|
|
472
|
+
async function cmdCharacters(client, flags) {
|
|
473
|
+
const limit = flagNum(flags, 'limit', 20);
|
|
474
|
+
print('');
|
|
475
|
+
const chars = await client.getCharacters(limit);
|
|
476
|
+
if (!chars.length) {
|
|
477
|
+
warn('No characters found.');
|
|
478
|
+
dim(' Create characters at https://www.cocoviral.ai/characters');
|
|
479
|
+
print('');
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
divider();
|
|
483
|
+
print(` ${c.bold}Characters${c.reset} ${c.gray}(${chars.length} found)${c.reset}`);
|
|
484
|
+
divider();
|
|
485
|
+
for (const ch of chars) {
|
|
486
|
+
print(` ${c.bold}${ch.name || 'Unnamed'}${c.reset} ${c.gray}${ch._id || ch.id}${c.reset}`);
|
|
487
|
+
}
|
|
488
|
+
print('');
|
|
489
|
+
}
|
|
490
|
+
// ── Director Studio ──────────────────────────────────────────────────────────
|
|
491
|
+
async function cmdPresets(client, which, flags) {
|
|
492
|
+
const kindMap = {
|
|
493
|
+
looks: 'look', look: 'look',
|
|
494
|
+
moves: 'motion', move: 'motion', motions: 'motion',
|
|
495
|
+
sizes: 'shot_size', size: 'shot_size',
|
|
496
|
+
camera: 'camera_setup', cameras: 'camera_setup', setup: 'camera_setup',
|
|
497
|
+
};
|
|
498
|
+
const kind = which ? kindMap[which] : undefined;
|
|
499
|
+
if (which && !kind) {
|
|
500
|
+
error(`Unknown preset family "${which}". Use: looks | moves | sizes | camera`);
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
const data = await client.listCinemaPresets({
|
|
504
|
+
kind,
|
|
505
|
+
category: flag(flags, 'category'),
|
|
506
|
+
search: flag(flags, 'search'),
|
|
507
|
+
});
|
|
508
|
+
print('');
|
|
509
|
+
if (data.looks?.length) {
|
|
510
|
+
divider();
|
|
511
|
+
print(` ${c.bold}Looks${c.reset} ${c.gray}(${data.looks.length})${c.reset}`);
|
|
512
|
+
divider();
|
|
513
|
+
for (const p of data.looks) {
|
|
514
|
+
print(` ${c.cyan}${p.id}${c.reset} ${c.bold}${p.name}${c.reset} ${c.gray}[${p.category}]${c.reset}`);
|
|
515
|
+
dim(` ${p.description}`);
|
|
516
|
+
}
|
|
517
|
+
print('');
|
|
518
|
+
}
|
|
519
|
+
if (data.motions?.length) {
|
|
520
|
+
divider();
|
|
521
|
+
print(` ${c.bold}Camera moves${c.reset} ${c.gray}(${data.motions.length})${c.reset}`);
|
|
522
|
+
divider();
|
|
523
|
+
for (const p of data.motions) {
|
|
524
|
+
print(` ${c.cyan}${p.id}${c.reset} ${c.bold}${p.name}${c.reset} ${c.gray}[${p.category} · ${p.recommendedDuration}s]${c.reset}`);
|
|
525
|
+
dim(` ${p.description}`);
|
|
526
|
+
}
|
|
527
|
+
print('');
|
|
528
|
+
}
|
|
529
|
+
for (const [title, list] of [
|
|
530
|
+
['Camera bodies', data.cameraBodies],
|
|
531
|
+
['Lenses', data.lenses],
|
|
532
|
+
['Apertures', data.apertures],
|
|
533
|
+
]) {
|
|
534
|
+
if (!list?.length)
|
|
535
|
+
continue;
|
|
536
|
+
divider();
|
|
537
|
+
print(` ${c.bold}${title}${c.reset} ${c.gray}(${list.length} · omit for Auto)${c.reset}`);
|
|
538
|
+
divider();
|
|
539
|
+
for (const entry of list)
|
|
540
|
+
print(` ${c.cyan}${entry.id}${c.reset} ${c.bold}${entry.name}${c.reset}`);
|
|
541
|
+
print('');
|
|
542
|
+
}
|
|
543
|
+
if (data.shotSizes?.length) {
|
|
544
|
+
divider();
|
|
545
|
+
print(` ${c.bold}Shot sizes${c.reset} ${c.gray}(${data.shotSizes.length})${c.reset}`);
|
|
546
|
+
divider();
|
|
547
|
+
for (const s of data.shotSizes)
|
|
548
|
+
print(` ${c.cyan}${s.id}${c.reset} ${c.bold}${s.name}${c.reset}`);
|
|
549
|
+
print('');
|
|
550
|
+
}
|
|
551
|
+
if (!data.looks?.length && !data.motions?.length && !data.shotSizes?.length && !data.cameraBodies?.length) {
|
|
552
|
+
warn('No presets matched.');
|
|
553
|
+
print('');
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
async function cmdFrame(client, scene, flags) {
|
|
557
|
+
const look = flag(flags, 'look') || 'blockbuster';
|
|
558
|
+
const lighting = flag(flags, 'lighting');
|
|
559
|
+
const palette = flag(flags, 'palette');
|
|
560
|
+
const size = flag(flags, 'size', 'shot-size') || 'medium';
|
|
561
|
+
const body = flag(flags, 'camera', 'body');
|
|
562
|
+
const lens = flag(flags, 'lens');
|
|
563
|
+
const aperture = flag(flags, 'aperture');
|
|
564
|
+
const ratio = flag(flags, 'ratio') || '16:9';
|
|
565
|
+
const model = flag(flags, 'model') || 'nano-banana-pro';
|
|
566
|
+
print('');
|
|
567
|
+
print(`${c.gray}Composing the frame…${c.reset}`);
|
|
568
|
+
const data = await client.generateCinemaFrame({
|
|
569
|
+
scene, look, lighting, palette, shotSize: size,
|
|
570
|
+
cameraBody: body, lens, aperture, aspectRatio: ratio, model,
|
|
571
|
+
});
|
|
572
|
+
print('');
|
|
573
|
+
const applied = data.composed?.applied || {};
|
|
574
|
+
if (data.imageUrl) {
|
|
575
|
+
success('Cinema frame ready!');
|
|
576
|
+
label('URL', data.imageUrl);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
success('Cinema frame generating…');
|
|
580
|
+
label('Prediction ID', data.predictionId || data.id);
|
|
581
|
+
}
|
|
582
|
+
label('Looks', (applied.looks || []).map((entry) => entry.name).join(' + ') || look);
|
|
583
|
+
label('Shot size', applied.shotSize?.name || size);
|
|
584
|
+
const setup = [applied.cameraBody, applied.lens, applied.aperture].filter(Boolean).join(' · ');
|
|
585
|
+
if (setup)
|
|
586
|
+
label('Camera', setup);
|
|
587
|
+
label('Model', `${model} · ${ratio}`);
|
|
588
|
+
const frameUrl = data.imageUrl;
|
|
589
|
+
if (frameUrl)
|
|
590
|
+
dim(` Next: cviral shot --start ${frameUrl} --move crash-zoom-in`);
|
|
591
|
+
else
|
|
592
|
+
dim(` Poll: cviral status ${data.predictionId || data.id} --type image`);
|
|
593
|
+
print('');
|
|
594
|
+
}
|
|
595
|
+
async function cmdShot(client, flags) {
|
|
596
|
+
const startImageUrl = flag(flags, 'start');
|
|
597
|
+
if (!startImageUrl) {
|
|
598
|
+
error('--start is required: cviral shot --start <frame_url> --move crash-zoom-in');
|
|
599
|
+
dim(' Generate a frame first: cviral frame "<scene>"');
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
const motion = flag(flags, 'move', 'motion') || 'slow-push-in';
|
|
603
|
+
const direction = flag(flags, 'direction');
|
|
604
|
+
const scene = flag(flags, 'scene');
|
|
605
|
+
const ratio = flag(flags, 'ratio') || '16:9';
|
|
606
|
+
const res = flag(flags, 'res') || 'fhd';
|
|
607
|
+
const model = flag(flags, 'model') || 'kling3-pro-i2v';
|
|
608
|
+
const duration = flags['duration'] !== undefined ? flagNum(flags, 'duration', 5) : undefined;
|
|
609
|
+
print('');
|
|
610
|
+
print(`${c.gray}Starting the shot…${c.reset}`);
|
|
611
|
+
const data = await client.animateCinemaShot({
|
|
612
|
+
startImageUrl, motion, direction, scene, duration, aspectRatio: ratio, model, resolution: res,
|
|
613
|
+
});
|
|
614
|
+
const id = data.videoId || data.predictionId || data.id;
|
|
615
|
+
print('');
|
|
616
|
+
success('Cinema shot started!');
|
|
617
|
+
label('Prediction ID', id);
|
|
618
|
+
label('Camera move', data.composed?.applied?.motion?.name || motion);
|
|
619
|
+
label('Model', `${model} · ${ratio} · ${data.duration}s · ${res}`);
|
|
620
|
+
label('Start frame', startImageUrl);
|
|
621
|
+
dim(` Poll: cviral status ${id}`);
|
|
622
|
+
print('');
|
|
623
|
+
}
|
|
624
|
+
// ---------------------------------------------------------------------------
|
|
625
|
+
// Viral Flow
|
|
626
|
+
//
|
|
627
|
+
// These commands exist because a terminal can do what a hosted MCP server
|
|
628
|
+
// cannot: read and write the user's own filesystem. Uploading a local file and
|
|
629
|
+
// saving finished assets to disk are the two things the remote tools have to
|
|
630
|
+
// hand back to the caller, so they are the reason to reach for the CLI at all.
|
|
631
|
+
// ---------------------------------------------------------------------------
|
|
632
|
+
async function cmdFlows(client) {
|
|
633
|
+
const flows = await client.listFlows();
|
|
634
|
+
if (flows.length === 0) {
|
|
635
|
+
dim(' No flows yet. Create one at https://www.cocoviral.ai/viral-flow');
|
|
636
|
+
print('');
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
print('');
|
|
640
|
+
success('Your flows');
|
|
641
|
+
for (const flow of flows) {
|
|
642
|
+
label(flow.name, `${flow.id} ${c.gray}${flow.nodeCount ?? '?'} nodes${c.reset}`);
|
|
643
|
+
}
|
|
644
|
+
print('');
|
|
645
|
+
}
|
|
646
|
+
async function cmdFlowShow(client, flowId) {
|
|
647
|
+
const flow = await client.getFlow(flowId);
|
|
648
|
+
print('');
|
|
649
|
+
success(`${flow.name} ${c.gray}${flow.id}${c.reset}`);
|
|
650
|
+
for (const node of flow.nodes || []) {
|
|
651
|
+
const state = node.status === 'success' ? `${c.green}done${c.reset}`
|
|
652
|
+
: node.runnable ? `${c.cyan}ready${c.reset}`
|
|
653
|
+
: `${c.gray}blocked${c.reset}`;
|
|
654
|
+
label(node.title, `${node.id} ${state}${node.credits ? ` ${c.gray}~${node.credits} credits${c.reset}` : ''}`);
|
|
655
|
+
if (node.outputUrl)
|
|
656
|
+
dim(` ${node.outputUrl}`);
|
|
657
|
+
}
|
|
658
|
+
print('');
|
|
659
|
+
label('Estimated cost', `~${flow.estimatedCredits} credits${flow.estimateIsPartial ? ' (partial)' : ''}`);
|
|
660
|
+
for (const problem of flow.errors || [])
|
|
661
|
+
error(` ${problem}`);
|
|
662
|
+
print('');
|
|
663
|
+
}
|
|
664
|
+
/** Upload a file from disk. The whole point of the CLI. */
|
|
665
|
+
async function cmdFlowUpload(client, filePath) {
|
|
666
|
+
if (!filePath) {
|
|
667
|
+
error('Usage: cviral flow upload <path/to/file>');
|
|
668
|
+
process.exit(1);
|
|
669
|
+
}
|
|
670
|
+
const resolved = node_path_1.default.resolve(filePath);
|
|
671
|
+
if (!node_fs_1.default.existsSync(resolved)) {
|
|
672
|
+
error(`No such file: ${resolved}`);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
const result = await client.uploadFile(resolved);
|
|
676
|
+
print('');
|
|
677
|
+
success('Uploaded');
|
|
678
|
+
label('Type', result.mediaType);
|
|
679
|
+
label('URL', result.url);
|
|
680
|
+
dim(' Use this URL in a media, image, video or audio node.');
|
|
681
|
+
print('');
|
|
682
|
+
}
|
|
683
|
+
/** Run a node, and with --watch stay until it finishes. */
|
|
684
|
+
async function cmdFlowRun(client, flowId, nodeId, watch) {
|
|
685
|
+
if (!flowId || !nodeId) {
|
|
686
|
+
error('Usage: cviral flow run <flowId> --node <nodeId> [--watch]');
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
const started = await client.runFlowNode(flowId, nodeId);
|
|
690
|
+
if (started.completion !== 'poll') {
|
|
691
|
+
print('');
|
|
692
|
+
success('Done');
|
|
693
|
+
if (started.url)
|
|
694
|
+
label('Output', started.url);
|
|
695
|
+
print('');
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
print('');
|
|
699
|
+
success(`Started ${started.generationKind} generation`);
|
|
700
|
+
if (!watch) {
|
|
701
|
+
dim(` Poll: cviral flow status ${flowId} --node ${nodeId}`);
|
|
702
|
+
print('');
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
// Long polling is the other thing a terminal does better than a tool call:
|
|
706
|
+
// no round trip through a model, no context spent on progress lines.
|
|
707
|
+
for (let attempt = 0; attempt < 120; attempt += 1) {
|
|
708
|
+
await new Promise(resolve => setTimeout(resolve, 8000));
|
|
709
|
+
const status = await client.flowNodeStatus(flowId, nodeId);
|
|
710
|
+
if (status.status === 'succeeded') {
|
|
711
|
+
label('Output', status.url);
|
|
712
|
+
print('');
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (status.status === 'failed') {
|
|
716
|
+
error(status.error || 'Generation failed.');
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
process.stderr.write(`\r ${c.gray}working… ${status.progress ?? 0}%${c.reset} `);
|
|
720
|
+
}
|
|
721
|
+
error('Timed out waiting for the node to finish. It may still be running.');
|
|
722
|
+
}
|
|
723
|
+
async function cmdFlowStatus(client, flowId, nodeId) {
|
|
724
|
+
const status = await client.flowNodeStatus(flowId, nodeId);
|
|
725
|
+
if (status.status === 'succeeded') {
|
|
726
|
+
success('Finished');
|
|
727
|
+
label('Output', status.url);
|
|
728
|
+
}
|
|
729
|
+
else if (status.status === 'failed')
|
|
730
|
+
error(status.error || 'Failed.');
|
|
731
|
+
else if (status.status === 'idle')
|
|
732
|
+
dim(' This node has not been run yet.');
|
|
733
|
+
else
|
|
734
|
+
dim(` Still working - ${status.progress ?? 0}% done.`);
|
|
735
|
+
print('');
|
|
736
|
+
}
|
|
737
|
+
/** Download every asset a flow produced. */
|
|
738
|
+
async function cmdFlowPull(client, flowId, outDir, generatedOnly) {
|
|
739
|
+
const assets = await client.listFlowAssets(flowId);
|
|
740
|
+
const wanted = assets.filter(a => !generatedOnly || a.generated);
|
|
741
|
+
if (wanted.length === 0) {
|
|
742
|
+
dim(' Nothing to download yet.');
|
|
743
|
+
print('');
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const dir = node_path_1.default.resolve(outDir || '.');
|
|
747
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
748
|
+
print('');
|
|
749
|
+
success(`Downloading ${wanted.length} asset(s) to ${dir}`);
|
|
750
|
+
for (const asset of wanted) {
|
|
751
|
+
// Name by node so a pulled folder still says which step made what.
|
|
752
|
+
const extension = asset.url.split('?')[0].split('.').pop() || 'bin';
|
|
753
|
+
const safeTitle = String(asset.title || asset.nodeType).replace(/[^a-zA-Z0-9-_]+/g, '-').toLowerCase();
|
|
754
|
+
const file = node_path_1.default.join(dir, `${safeTitle}-${asset.nodeId}.${extension}`);
|
|
755
|
+
await client.downloadTo(asset.url, file);
|
|
756
|
+
label(node_path_1.default.basename(file), `${c.gray}${asset.mediaType}${c.reset}`);
|
|
757
|
+
}
|
|
758
|
+
print('');
|
|
759
|
+
}
|
|
760
|
+
// ---------------------------------------------------------------------------
|
|
761
|
+
// Main
|
|
762
|
+
// ---------------------------------------------------------------------------
|
|
763
|
+
async function main() {
|
|
764
|
+
const { command, subcommand, positionals, flags } = parseArgs(process.argv);
|
|
765
|
+
if (!command || command === 'help' || flags['help'] || flags['h']) {
|
|
766
|
+
printHelp();
|
|
767
|
+
process.exit(0);
|
|
768
|
+
}
|
|
769
|
+
if (command === 'version' || flags['version'] || flags['v']) {
|
|
770
|
+
print(`cviral v${VERSION}`);
|
|
771
|
+
process.exit(0);
|
|
772
|
+
}
|
|
773
|
+
if (!API_KEY) {
|
|
774
|
+
error('COCOVIRAL_API_KEY is not set.');
|
|
775
|
+
dim(' Get your key at https://www.cocoviral.ai/mcp-skills');
|
|
776
|
+
dim(' Then: export COCOVIRAL_API_KEY=cv_your_key_here');
|
|
777
|
+
print('');
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
const client = new client_1.CocoViralClient({ apiKey: API_KEY, apiUrl: API_URL });
|
|
781
|
+
// First positional after command becomes the main argument
|
|
782
|
+
const arg = subcommand || '';
|
|
783
|
+
try {
|
|
784
|
+
switch (command) {
|
|
785
|
+
case 'flow':
|
|
786
|
+
case 'flows': {
|
|
787
|
+
// `cviral flow run <flowId>` - the flow id is the second positional,
|
|
788
|
+
// since the first is the sub-command.
|
|
789
|
+
const sub = command === 'flows' ? 'list' : (arg || 'list');
|
|
790
|
+
const flowId = command === 'flows' ? '' : String(positionals[0] || flags['flow'] || '');
|
|
791
|
+
if (sub === 'list') {
|
|
792
|
+
await cmdFlows(client);
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
if (sub === 'show') {
|
|
796
|
+
await cmdFlowShow(client, flowId);
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
if (sub === 'upload') {
|
|
800
|
+
await cmdFlowUpload(client, flowId);
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
if (sub === 'run') {
|
|
804
|
+
await cmdFlowRun(client, flowId, String(flags['node'] || ''), Boolean(flags['watch']));
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
if (sub === 'status') {
|
|
808
|
+
await cmdFlowStatus(client, flowId, String(flags['node'] || ''));
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
if (sub === 'pull') {
|
|
812
|
+
await cmdFlowPull(client, flowId, String(flags['out'] || '.'), Boolean(flags['generated']));
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
error(`Unknown flow command: ${sub}`);
|
|
816
|
+
dim(' Try: list | show | upload | run | status | pull');
|
|
817
|
+
process.exit(1);
|
|
818
|
+
}
|
|
819
|
+
case 'credits':
|
|
820
|
+
await cmdCredits(client);
|
|
821
|
+
break;
|
|
822
|
+
case 'videos':
|
|
823
|
+
await cmdVideos(client, flags);
|
|
824
|
+
break;
|
|
825
|
+
case 'image':
|
|
826
|
+
if (!arg) {
|
|
827
|
+
error('Provide a prompt: cviral image "<prompt>"');
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
830
|
+
await cmdImage(client, arg, flags);
|
|
831
|
+
break;
|
|
832
|
+
case 'edit':
|
|
833
|
+
if (!arg) {
|
|
834
|
+
error('Provide a prompt: cviral edit "<prompt>" --url <image_url>');
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
837
|
+
await cmdEditImage(client, arg, flags);
|
|
838
|
+
break;
|
|
839
|
+
case 'video':
|
|
840
|
+
if (!arg) {
|
|
841
|
+
error('Provide a prompt: cviral video "<prompt>"');
|
|
842
|
+
process.exit(1);
|
|
843
|
+
}
|
|
844
|
+
await cmdVideo(client, arg, flags);
|
|
845
|
+
break;
|
|
846
|
+
case 'status':
|
|
847
|
+
if (!arg) {
|
|
848
|
+
error('Provide a prediction ID: cviral status <id>');
|
|
849
|
+
process.exit(1);
|
|
850
|
+
}
|
|
851
|
+
await cmdStatus(client, arg, flags);
|
|
852
|
+
break;
|
|
853
|
+
case 'analyze':
|
|
854
|
+
if (!arg) {
|
|
855
|
+
error('Provide a video URL: cviral analyze <url>');
|
|
856
|
+
process.exit(1);
|
|
857
|
+
}
|
|
858
|
+
await cmdAnalyze(client, arg, flags);
|
|
859
|
+
break;
|
|
860
|
+
case 'remix':
|
|
861
|
+
if (!arg) {
|
|
862
|
+
error('Provide a video URL: cviral remix <url>');
|
|
863
|
+
process.exit(1);
|
|
864
|
+
}
|
|
865
|
+
await cmdRemix(client, arg, flags);
|
|
866
|
+
break;
|
|
867
|
+
case 'translate':
|
|
868
|
+
if (!arg) {
|
|
869
|
+
error('Provide a video URL: cviral translate <url> --lang ja');
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
await cmdTranslate(client, arg, flags);
|
|
873
|
+
break;
|
|
874
|
+
case 'translate-status':
|
|
875
|
+
if (!arg) {
|
|
876
|
+
error('Provide a job ID: cviral translate-status <jobId>');
|
|
877
|
+
process.exit(1);
|
|
878
|
+
}
|
|
879
|
+
await cmdTranslateStatus(client, arg);
|
|
880
|
+
break;
|
|
881
|
+
case 'characters':
|
|
882
|
+
await cmdCharacters(client, flags);
|
|
883
|
+
break;
|
|
884
|
+
case 'presets':
|
|
885
|
+
await cmdPresets(client, arg, flags);
|
|
886
|
+
break;
|
|
887
|
+
case 'frame':
|
|
888
|
+
if (!arg) {
|
|
889
|
+
error('Provide a scene: cviral frame "<scene>" --look blockbuster');
|
|
890
|
+
process.exit(1);
|
|
891
|
+
}
|
|
892
|
+
await cmdFrame(client, arg, flags);
|
|
893
|
+
break;
|
|
894
|
+
case 'shot':
|
|
895
|
+
await cmdShot(client, flags);
|
|
896
|
+
break;
|
|
897
|
+
default:
|
|
898
|
+
error(`Unknown command: ${command}`);
|
|
899
|
+
dim(' Run: cviral help');
|
|
900
|
+
print('');
|
|
901
|
+
process.exit(1);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
catch (err) {
|
|
905
|
+
const status = err?.response?.status;
|
|
906
|
+
const msg = err?.response?.data?.error || err?.message || 'Unknown error';
|
|
907
|
+
print('');
|
|
908
|
+
if (status === 401)
|
|
909
|
+
error('Invalid or expired API key. Regenerate at https://www.cocoviral.ai/mcp-skills');
|
|
910
|
+
else if (status === 402)
|
|
911
|
+
error('Insufficient credits. Purchase at https://www.cocoviral.ai/pricing');
|
|
912
|
+
else if (status === 429)
|
|
913
|
+
error('Rate limit exceeded. Wait a moment and retry.');
|
|
914
|
+
else
|
|
915
|
+
error(`${status ? `[${status}] ` : ''}${msg}`);
|
|
916
|
+
print('');
|
|
917
|
+
process.exit(1);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
main();
|