@scenerok/cli 2.2.0

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.
Files changed (79) hide show
  1. package/README.md +173 -0
  2. package/dist/commands/auth.d.ts +3 -0
  3. package/dist/commands/auth.d.ts.map +1 -0
  4. package/dist/commands/auth.js +94 -0
  5. package/dist/commands/cache.d.ts +3 -0
  6. package/dist/commands/cache.d.ts.map +1 -0
  7. package/dist/commands/cache.js +65 -0
  8. package/dist/commands/lint-video.d.ts +21 -0
  9. package/dist/commands/lint-video.d.ts.map +1 -0
  10. package/dist/commands/lint-video.js +122 -0
  11. package/dist/commands/project.d.ts +3 -0
  12. package/dist/commands/project.d.ts.map +1 -0
  13. package/dist/commands/project.js +173 -0
  14. package/dist/commands/render.d.ts +4 -0
  15. package/dist/commands/render.d.ts.map +1 -0
  16. package/dist/commands/render.js +242 -0
  17. package/dist/commands/skills.d.ts +3 -0
  18. package/dist/commands/skills.d.ts.map +1 -0
  19. package/dist/commands/skills.js +208 -0
  20. package/dist/commands/validate.d.ts +3 -0
  21. package/dist/commands/validate.d.ts.map +1 -0
  22. package/dist/commands/validate.js +254 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +51 -0
  26. package/dist/lib/api.d.ts +268 -0
  27. package/dist/lib/api.d.ts.map +1 -0
  28. package/dist/lib/api.js +160 -0
  29. package/dist/lib/config.d.ts +13 -0
  30. package/dist/lib/config.d.ts.map +1 -0
  31. package/dist/lib/config.js +53 -0
  32. package/dist/lib/format-api-error.d.ts +2 -0
  33. package/dist/lib/format-api-error.d.ts.map +1 -0
  34. package/dist/lib/format-api-error.js +24 -0
  35. package/dist/lib/language.d.ts +3 -0
  36. package/dist/lib/language.d.ts.map +1 -0
  37. package/dist/lib/language.js +9 -0
  38. package/dist/lib/logger.d.ts +8 -0
  39. package/dist/lib/logger.d.ts.map +1 -0
  40. package/dist/lib/logger.js +51 -0
  41. package/dist/lib/project.d.ts +29 -0
  42. package/dist/lib/project.d.ts.map +1 -0
  43. package/dist/lib/project.js +156 -0
  44. package/dist/lib/sdk-scene-modules.d.ts +7 -0
  45. package/dist/lib/sdk-scene-modules.d.ts.map +1 -0
  46. package/dist/lib/sdk-scene-modules.js +35 -0
  47. package/dist/utils/skillsManifest.d.ts +42 -0
  48. package/dist/utils/skillsManifest.d.ts.map +1 -0
  49. package/dist/utils/skillsManifest.js +122 -0
  50. package/examples/system/3-tips-fitness.vid +45 -0
  51. package/examples/system/ecom-product-launch.vid +31 -0
  52. package/examples/system/glitch-title.vid +17 -0
  53. package/examples/system/meme-remix.vid +15 -0
  54. package/examples/system/minimal-text-reel.vid +11 -0
  55. package/examples/system/product-launch.vid +18 -0
  56. package/examples/system/rokmilk-chocolate-promo.vid +25 -0
  57. package/examples/system/testimonial-cut.vid +16 -0
  58. package/examples/system/ugc-testimonial-voiceover.vid +33 -0
  59. package/examples/system/voice-visual-promo.vid +21 -0
  60. package/package.json +51 -0
  61. package/skills/remote/ai-models.md +65 -0
  62. package/skills/remote/animations-catalog.md +89 -0
  63. package/skills/remote/animations-picker.md +77 -0
  64. package/skills/remote/brand-setup.md +69 -0
  65. package/skills/remote/catalog-index.json +1810 -0
  66. package/skills/remote/environments.md +73 -0
  67. package/skills/remote/intent-router.md +81 -0
  68. package/skills/remote/media-stdlib.md +66 -0
  69. package/skills/remote/music.md +69 -0
  70. package/skills/remote/sdk-guide.md +201 -0
  71. package/skills/remote/shaders-filters.md +97 -0
  72. package/skills/remote/text-styles.md +108 -0
  73. package/skills/remote/transitions-catalog.md +55 -0
  74. package/skills/remote/tts.md +82 -0
  75. package/skills/remote/vidscript-guide.md +574 -0
  76. package/skills/remote/vidscript-sample.md +28 -0
  77. package/skills/remote/vidscript-strict.md +251 -0
  78. package/skills/remote/workflow.md +72 -0
  79. package/skills/shared/SKILL.md +157 -0
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # SceneRok CLI
2
+
3
+ Create videos from your terminal and agent workflows.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @scenerok/cli
9
+ ```
10
+
11
+ Or use with `npx`:
12
+ ```bash
13
+ npx @scenerok/cli auth login
14
+ ```
15
+
16
+ ## Quick Start
17
+
18
+ ```bash
19
+ # Authenticate
20
+ scenerok auth login
21
+
22
+ # Validate a VidScript
23
+ scenerok validate video.vidscript
24
+
25
+ # Render a video
26
+ scenerok render video.vidscript --resolution 1080x1920 --watch
27
+
28
+ # Upload a local project and assets
29
+ scenerok project upload video.vidscript --assets ./assets
30
+
31
+ # Upload, render, watch, and download
32
+ scenerok project upload video.vidscript --assets ./assets --render --watch --download ./renders
33
+
34
+ # Pull generated asset cache
35
+ scenerok cache pull
36
+
37
+ # Check render status
38
+ scenerok status 42
39
+ ```
40
+
41
+ ## Commands
42
+
43
+ ### `auth`
44
+
45
+ ```bash
46
+ scenerok auth login # Log in via browser
47
+ scenerok auth status # Check auth status
48
+ scenerok auth logout # Remove credentials
49
+ ```
50
+
51
+ ### `skills`
52
+
53
+ ```bash
54
+ scenerok skills install <platform> [--sync] # Install router skill (+ optional full sync)
55
+ scenerok skills sync # Download remote skill bundle
56
+ scenerok skills check [--json] # Compare local vs remote manifest
57
+ scenerok skills update # Refresh outdated skills
58
+ scenerok skills list # List remote skills + local status
59
+ scenerok skills platforms # List supported agent platforms
60
+ ```
61
+
62
+ Supported platforms: `opencode`, `claude`, `codex`, `cursor`, `aider`
63
+
64
+ Remote skills are served from https://scenerok.com/.well-known/agent-skills/ and updated on every deploy — no CLI release needed for skill content changes.
65
+
66
+ ### `validate`
67
+
68
+ ```bash
69
+ scenerok validate <file.vidscript>
70
+ scenerok validate <file.vidscript> --generate-assets --report-json
71
+ scenerok validate <file.vidscript> --render --cost medium
72
+ ```
73
+
74
+ Validates VidScript syntax and returns scene count, duration, and warnings.
75
+
76
+ Options:
77
+ - `--generate-assets` - Real compile: plugins execute, assets generate + cache, durations and text geometry are measured (billed; requires auth)
78
+ - `--render` - Full pipeline: validate with generated assets → render → validate the output MP4 (ffprobe + LLM frame review, billed). Exit code 0 only when every stage passes — built for agent write→lint→render→lint loops
79
+ - `--cost <tier>` / `--frames <n>` - Frame-grab budget for the MP4 review (see `lint-video`)
80
+ - `--budget <usd>` - Total USD cap for the whole `--render` pipeline (plugin generation + render + MP4 review); the pipeline aborts before any billed call if the free dry-run estimate exceeds it, and the MP4 review draws only from what remains
81
+ - `--render-budget <usd>` - USD cap on plugin generation + render spend alone
82
+ - `--validation-budget <usd>` - USD cap on the MP4 LLM review; frame count is clamped to fit
83
+ - `--resolution <resolution>` - Output resolution for `--render`
84
+ - `--report-json` - Print reports as JSON (for agents)
85
+
86
+ ### `lint-video`
87
+
88
+ ```bash
89
+ scenerok lint-video <render-id> [--cost low|medium|high] [--frames <n>] [--budget <usd>] [--json]
90
+ ```
91
+
92
+ Validates a completed render's actual MP4:
93
+ 1. **ffprobe checks** (free): readable container, video stream present, duration/aspect/audio vs the VidScript
94
+ 2. **Frame review** (billed by token usage): frames are grabbed at timestamps where the script says content appears (text midpoints, clip midpoints, opening, ending) and a vision LLM compares each frame with what the script expects — missing/illegible text, blank frames, artifacts
95
+
96
+ `--cost` sets the frame budget (low=4, medium=8, high=14); `--frames` sets it exactly (max 20); `--budget <usd>` is a hard USD cap that clamps the frame count to the most that fit. More frames cost more and cover more. All LLM cost is billed to your token balance and itemized in the output.
97
+
98
+ ### `render`
99
+
100
+ ```bash
101
+ scenerok render <file.vidscript> [options]
102
+ ```
103
+
104
+ Options:
105
+ - `-r, --resolution <resolution>` - Output resolution (e.g. 1080x1920)
106
+ - `-p, --project-id <id>` - Attach the render to an uploaded project
107
+ - `-w, --watch` - Watch render status until complete
108
+ - `--download [dir]` - Download the completed MP4
109
+ - `--validate [cost]` - After rendering, validate the output MP4 (implies watching; billed frame review, cost tier low/medium/high)
110
+ - `--validation-budget <usd>` - Hard USD cap on the MP4 review when using `--validate`
111
+
112
+ ### `status`
113
+
114
+ ```bash
115
+ scenerok status <render-id>
116
+ ```
117
+
118
+ Check the status of a render job.
119
+
120
+ Use `scenerok status <render-id> --download ./renders` to download a completed render.
121
+
122
+ ### `project`
123
+
124
+ ```bash
125
+ scenerok project upload <file.vidscript> --assets ./assets
126
+ scenerok project upload <file.vidscript> --assets ./assets --render --watch --download ./renders
127
+ scenerok project list
128
+ scenerok project render <project-id> --watch --download ./renders
129
+ ```
130
+
131
+ `project upload` creates a private SceneRok project on the main server, uploads referenced local assets, rewrites local `input` paths to server asset URLs, and returns a project id. Project renders are visible on the website.
132
+
133
+ ### `cache`
134
+
135
+ ```bash
136
+ scenerok cache pull
137
+ scenerok cache pull --plugin @scenerok/xai --output ~/.scenerok/cache/assets
138
+ ```
139
+
140
+ Downloads cached generated assets and writes a `manifest.json` to the output folder.
141
+
142
+ ## Configuration
143
+
144
+ Config is stored in `~/.scenerok/config.json`:
145
+
146
+ ```json
147
+ {
148
+ "apiToken": "rf_...",
149
+ "baseUrl": "https://scenerok.com"
150
+ }
151
+ ```
152
+
153
+ ## VidScript
154
+
155
+ VidScript is a declarative language for describing video compositions.
156
+
157
+ ```vidscript
158
+ import motion from "@scenerok/basic-animations"
159
+
160
+ input hero = "./assets/hero.mp4"
161
+
162
+ [0s .. 5s] = hero
163
+ [0.4s .. 3s] = text "Hello World", font: "Inter Bold", size: 72, x: "50%", y: "50%", align: center, animate: [motion.fadeIn(0.6s), motion.slideY(40, 0, 0.8s)]
164
+
165
+ output to "video.mp4", resolution: "1080x1920", fps: 30
166
+ ```
167
+
168
+ Animation helpers are package functions: `motion.fadeIn`, `motion.fadeOut`, `motion.slideX`, `motion.slideY`, `motion.popIn`, `motion.riseIn`, `motion.swingIn`, `motion.glitchIn`, `motion.float`, `motion.typewriter`.
169
+
170
+ ## Links
171
+
172
+ - Website: https://scenerok.com
173
+ - Docs: https://scenerok.com/agents/get-started.md
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare const authCommand: Command;
3
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,eAAO,MAAM,WAAW,SA2GrB,CAAC"}
@@ -0,0 +1,94 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { readConfig, writeConfig, isAuthenticated } from '../lib/config.js';
5
+ import { initiateDeviceAuth, pollDeviceAuth } from '../lib/api.js';
6
+ import { getBaseUrl } from '../lib/config.js';
7
+ import { formatApiErrorMessage } from '../lib/format-api-error.js';
8
+ import { getLogFilePath, isVerbose, logError } from '../lib/logger.js';
9
+ export const authCommand = new Command('auth')
10
+ .description('Authenticate with SceneRok')
11
+ .addCommand(new Command('login')
12
+ .description('Log in to SceneRok via browser')
13
+ .option('-n, --name <name>', 'Device name', 'CLI')
14
+ .option('-u, --base-url <url>', 'SceneRok API base URL (overrides ~/.scenerok/config.json and SCENEROK_BASE_URL)')
15
+ .option('-v, --verbose', 'Print debug output and log API calls to ~/.scenerok/logs/cli.log')
16
+ .action(async (options) => {
17
+ if (options.baseUrl) {
18
+ const config = readConfig();
19
+ config.baseUrl = options.baseUrl.replace(/\/$/, '');
20
+ writeConfig(config);
21
+ }
22
+ console.log(chalk.cyan('\n🔐 SceneRok Authentication\n'));
23
+ console.log(chalk.dim(`API: ${getBaseUrl()}`));
24
+ if (options.verbose || isVerbose()) {
25
+ console.log(chalk.dim(`Log file: ${getLogFilePath()}\n`));
26
+ }
27
+ const spinner = ora('Initiating device auth...').start();
28
+ try {
29
+ const auth = await initiateDeviceAuth(options.name);
30
+ spinner.succeed('Device auth initiated');
31
+ console.log(chalk.yellow('\n1. Open this URL in your browser:'));
32
+ console.log(chalk.underline(auth.verification_uri));
33
+ console.log('');
34
+ console.log(chalk.yellow('2. Enter this code if prompted:'));
35
+ console.log(chalk.bold(` ${auth.user_code}`));
36
+ console.log('');
37
+ console.log(chalk.yellow('3. Or run this command:'));
38
+ console.log(` open "${auth.verification_uri}"`);
39
+ console.log('');
40
+ console.log(chalk.dim(`Waiting for authentication... (expires in ${auth.expires_in}s)\n`));
41
+ const startTime = Date.now();
42
+ const expiresIn = auth.expires_in * 1000;
43
+ while (Date.now() - startTime < expiresIn) {
44
+ await new Promise((resolve) => setTimeout(resolve, auth.interval * 1000));
45
+ const result = await pollDeviceAuth(auth.device_code);
46
+ if (result.status === 'complete' && result.access_token) {
47
+ const config = readConfig();
48
+ config.apiToken = result.access_token;
49
+ writeConfig(config);
50
+ console.log(chalk.green('\n✅ Successfully authenticated with SceneRok!\n'));
51
+ console.log(chalk.dim('Your API token has been saved to ~/.scenerok/config.json'));
52
+ return;
53
+ }
54
+ if (result.error === 'expired_token') {
55
+ console.log(chalk.red('\n❌ Authentication expired. Please try again.\n'));
56
+ process.exit(1);
57
+ }
58
+ }
59
+ console.log(chalk.red('\n❌ Authentication timed out. Please try again.\n'));
60
+ process.exit(1);
61
+ }
62
+ catch (error) {
63
+ spinner.fail('Authentication failed');
64
+ logError('auth login failed', {
65
+ baseUrl: getBaseUrl(),
66
+ error: formatApiErrorMessage(error),
67
+ });
68
+ console.error(chalk.red(formatApiErrorMessage(error)));
69
+ console.error(chalk.dim(`\nLog: ${getLogFilePath()}`));
70
+ console.error(chalk.dim('Retry with: scenerok auth login --verbose (or SCENEROK_VERBOSE=1 scenerok auth login)'));
71
+ process.exit(1);
72
+ }
73
+ }))
74
+ .addCommand(new Command('status')
75
+ .description('Check authentication status')
76
+ .action(() => {
77
+ console.log(chalk.dim(`API: ${getBaseUrl()}`));
78
+ console.log(chalk.dim(`Log: ${getLogFilePath()}`));
79
+ if (isAuthenticated()) {
80
+ console.log(chalk.green('✅ Authenticated with SceneRok'));
81
+ }
82
+ else {
83
+ console.log(chalk.yellow('⚠️ Not authenticated'));
84
+ console.log(chalk.dim('Run: scenerok auth login'));
85
+ }
86
+ }))
87
+ .addCommand(new Command('logout')
88
+ .description('Log out and remove stored credentials')
89
+ .action(() => {
90
+ const config = readConfig();
91
+ delete config.apiToken;
92
+ writeConfig(config);
93
+ console.log(chalk.green('✅ Logged out successfully'));
94
+ }));
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare const cacheCommand: Command;
3
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/commands/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwBpC,eAAO,MAAM,YAAY,SA6CtB,CAAC"}
@@ -0,0 +1,65 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { mkdirSync, writeFileSync } from 'node:fs';
5
+ import { basename, extname, join, resolve } from 'node:path';
6
+ import fetch from 'node-fetch';
7
+ import { getAssetCache } from '../lib/api.js';
8
+ import { getCacheDir, isAuthenticated } from '../lib/config.js';
9
+ function safeName(value) {
10
+ return value.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120);
11
+ }
12
+ function filenameFor(url, cacheKey) {
13
+ try {
14
+ const parsed = new URL(url);
15
+ const fromUrl = basename(parsed.pathname);
16
+ if (fromUrl && extname(fromUrl))
17
+ return safeName(fromUrl);
18
+ }
19
+ catch {
20
+ // fall through
21
+ }
22
+ return `${safeName(cacheKey)}.bin`;
23
+ }
24
+ export const cacheCommand = new Command('cache')
25
+ .description('Manage local SceneRok asset cache')
26
+ .addCommand(new Command('pull')
27
+ .description('Download cached generated assets to a local folder')
28
+ .option('-o, --output <dir>', 'Output directory', join(getCacheDir(), 'assets'))
29
+ .option('--plugin <name>', 'Filter by plugin name')
30
+ .option('--limit <count>', 'Maximum assets to pull', '100')
31
+ .action(async (options) => {
32
+ if (!isAuthenticated()) {
33
+ console.log(chalk.yellow('Not authenticated'));
34
+ console.log(chalk.dim('Run: scenerok auth login'));
35
+ process.exit(1);
36
+ }
37
+ const outputDir = resolve(options.output);
38
+ mkdirSync(outputDir, { recursive: true });
39
+ const spinner = ora('Fetching asset cache manifest...').start();
40
+ try {
41
+ const manifest = await getAssetCache({
42
+ plugin: options.plugin,
43
+ limit: Number.parseInt(options.limit, 10) || 100,
44
+ });
45
+ spinner.text = `Downloading ${manifest.entries.length} cached asset${manifest.entries.length === 1 ? '' : 's'}...`;
46
+ const downloaded = [];
47
+ for (const entry of manifest.entries) {
48
+ const response = await fetch(entry.r2Url);
49
+ if (!response.ok)
50
+ continue;
51
+ const filename = filenameFor(entry.r2Url, entry.cacheKey);
52
+ const filePath = join(outputDir, filename);
53
+ writeFileSync(filePath, Buffer.from(await response.arrayBuffer()));
54
+ downloaded.push({ ...entry, filePath });
55
+ }
56
+ writeFileSync(join(outputDir, 'manifest.json'), JSON.stringify(downloaded, null, 2));
57
+ spinner.succeed(`Downloaded ${downloaded.length} cached asset${downloaded.length === 1 ? '' : 's'}`);
58
+ console.log(chalk.dim(`Manifest: ${join(outputDir, 'manifest.json')}`));
59
+ }
60
+ catch (error) {
61
+ spinner.fail('Asset cache pull failed');
62
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
63
+ process.exit(1);
64
+ }
65
+ }));
@@ -0,0 +1,21 @@
1
+ import { Command } from 'commander';
2
+ import { type VideoLintResult } from '../lib/api.js';
3
+ export declare const LINT_COST_CHOICES: string[];
4
+ export declare function parseLintOptions(opts: {
5
+ cost?: string;
6
+ frames?: string;
7
+ budget?: string;
8
+ }): {
9
+ cost?: string;
10
+ frames?: number;
11
+ budgetUsd?: number;
12
+ };
13
+ export declare function printVideoLintResult(result: VideoLintResult, jsonMode?: boolean): void;
14
+ export declare function runVideoLint(renderId: number, options: {
15
+ cost?: string;
16
+ frames?: number;
17
+ budgetUsd?: number;
18
+ json?: boolean;
19
+ }): Promise<boolean>;
20
+ export declare const lintVideoCommand: Command;
21
+ //# sourceMappingURL=lint-video.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lint-video.d.ts","sourceRoot":"","sources":["../../src/commands/lint-video.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAGrE,eAAO,MAAM,iBAAiB,UAA4B,CAAC;AAE3D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAC3F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAsBA;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,eAAe,EAAE,QAAQ,UAAQ,GAAG,IAAI,CA8DpF;AAED,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9E,OAAO,CAAC,OAAO,CAAC,CAgBlB;AAED,eAAO,MAAM,gBAAgB,SAqBzB,CAAC"}
@@ -0,0 +1,122 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { validateRender } from '../lib/api.js';
5
+ import { isAuthenticated } from '../lib/config.js';
6
+ export const LINT_COST_CHOICES = ['low', 'medium', 'high'];
7
+ export function parseLintOptions(opts) {
8
+ if (opts.cost && !LINT_COST_CHOICES.includes(opts.cost)) {
9
+ console.log(chalk.red(`❌ Invalid --cost "${opts.cost}" — use ${LINT_COST_CHOICES.join(', ')}`));
10
+ process.exit(1);
11
+ }
12
+ let frames;
13
+ if (opts.frames !== undefined) {
14
+ frames = Number.parseInt(opts.frames, 10);
15
+ if (Number.isNaN(frames) || frames < 1) {
16
+ console.log(chalk.red('❌ --frames must be a positive integer'));
17
+ process.exit(1);
18
+ }
19
+ }
20
+ let budgetUsd;
21
+ if (opts.budget !== undefined) {
22
+ budgetUsd = Number.parseFloat(opts.budget);
23
+ if (Number.isNaN(budgetUsd) || budgetUsd <= 0) {
24
+ console.log(chalk.red('❌ --budget must be a positive USD amount'));
25
+ process.exit(1);
26
+ }
27
+ }
28
+ return { cost: opts.cost, frames, budgetUsd };
29
+ }
30
+ export function printVideoLintResult(result, jsonMode = false) {
31
+ if (jsonMode) {
32
+ console.log(JSON.stringify(result, null, 2));
33
+ return;
34
+ }
35
+ console.log(result.valid
36
+ ? chalk.green('\n✅ Rendered video passed validation\n')
37
+ : chalk.red('\n❌ Rendered video failed validation\n'));
38
+ if (result.probe) {
39
+ const p = result.probe;
40
+ console.log(chalk.cyan(' Probe:'));
41
+ console.log(` ${p.width}x${p.height} @ ${p.fps?.toFixed(1) ?? '?'}fps, ` +
42
+ `${p.durationSeconds?.toFixed(2) ?? '?'}s, ${p.videoCodec ?? 'unknown codec'}, ` +
43
+ `audio: ${p.hasAudio ? 'yes' : 'no'}`);
44
+ }
45
+ for (const error of result.errors ?? []) {
46
+ console.log(chalk.red(` ✗ ${error}`));
47
+ }
48
+ for (const warning of result.warnings ?? []) {
49
+ console.log(chalk.yellow(` ⚠ ${warning}`));
50
+ }
51
+ const review = result.llmReview;
52
+ if (review) {
53
+ console.log(chalk.cyan(`\n Frame review (${result.framesAnalyzed} frames, score ${review.overallScore}/100):`));
54
+ for (const frame of review.frames) {
55
+ const mark = frame.matches ? chalk.green('✓') : chalk.red('✗');
56
+ console.log(` ${mark} ${frame.timestamp.toFixed(1)}s — ${frame.observed}`);
57
+ for (const issue of frame.issues) {
58
+ console.log(chalk.red(` ${issue}`));
59
+ }
60
+ }
61
+ if (review.summary) {
62
+ console.log(`\n ${review.summary}`);
63
+ }
64
+ if (review.suggestions.length > 0) {
65
+ console.log(chalk.cyan('\n Suggestions:'));
66
+ for (const suggestion of review.suggestions) {
67
+ console.log(` • ${suggestion}`);
68
+ }
69
+ }
70
+ }
71
+ else if (result.note) {
72
+ console.log(chalk.dim(`\n ${result.note}`));
73
+ }
74
+ if (result.billing) {
75
+ if (result.billing.budgetUsd !== undefined) {
76
+ console.log(chalk.dim(`\n Budget: $${result.billing.budgetUsd.toFixed(4)} cap`));
77
+ }
78
+ console.log(chalk.dim(`${result.billing.budgetUsd !== undefined ? '' : '\n'} LLM cost: $${result.billing.costUsd.toFixed(4)} ` +
79
+ `(${result.billing.totalTokens} LLM tokens, $${(result.billing.tokenUnitsCharged / 1000).toFixed(3)} wallet)`));
80
+ }
81
+ console.log('');
82
+ }
83
+ export async function runVideoLint(renderId, options) {
84
+ const spinner = options.json ? null : ora('Validating rendered video (probe + frame review)...').start();
85
+ try {
86
+ const result = await validateRender(renderId, {
87
+ cost: options.cost,
88
+ frames: options.frames,
89
+ budgetUsd: options.budgetUsd,
90
+ });
91
+ spinner?.stop();
92
+ printVideoLintResult(result, options.json);
93
+ return result.valid;
94
+ }
95
+ catch (error) {
96
+ spinner?.fail('Video validation failed');
97
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
98
+ return false;
99
+ }
100
+ }
101
+ export const lintVideoCommand = new Command('lint-video')
102
+ .description('Validate a completed render\'s MP4: ffprobe checks plus LLM review of frame grabs compared against the VidScript (billed by usage)')
103
+ .argument('<renderId>', 'Completed render job ID')
104
+ .option('--cost <tier>', `Frame-grab budget: ${LINT_COST_CHOICES.join(', ')} (more frames = higher LLM cost, better coverage)`, 'medium')
105
+ .option('--frames <n>', 'Exact number of frame grabs (overrides --cost, max 20)')
106
+ .option('--budget <usd>', 'Hard USD cap: clamp frame grabs to the most that fit this budget')
107
+ .option('--json', 'Print the full report as JSON (for agents)')
108
+ .action(async (renderIdArg, opts) => {
109
+ if (!isAuthenticated()) {
110
+ console.log(chalk.yellow('⚠️ Not authenticated'));
111
+ console.log(chalk.dim('Run: scenerok auth login'));
112
+ process.exit(1);
113
+ }
114
+ const renderId = Number.parseInt(renderIdArg, 10);
115
+ if (Number.isNaN(renderId)) {
116
+ console.log(chalk.red('❌ Invalid render ID'));
117
+ process.exit(1);
118
+ }
119
+ const { cost, frames, budgetUsd } = parseLintOptions(opts);
120
+ const passed = await runVideoLint(renderId, { cost, frames, budgetUsd, json: opts.json });
121
+ process.exit(passed ? 0 : 1);
122
+ });
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare const projectCommand: Command;
3
+ //# sourceMappingURL=project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/commands/project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuCpC,eAAO,MAAM,cAAc,SA+JxB,CAAC"}