@scenerok/cli 1.0.4 → 1.0.6

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 (72) hide show
  1. package/README.md +6 -3
  2. package/dist/commands/auth.d.ts.map +1 -1
  3. package/dist/commands/auth.js +17 -1
  4. package/dist/commands/project.d.ts.map +1 -1
  5. package/dist/commands/project.js +16 -2
  6. package/dist/commands/render.d.ts.map +1 -1
  7. package/dist/commands/render.js +14 -0
  8. package/dist/commands/skills.d.ts.map +1 -1
  9. package/dist/commands/skills.js +42 -1
  10. package/dist/commands/validate.d.ts.map +1 -1
  11. package/dist/commands/validate.js +38 -9
  12. package/dist/index.js +3 -1
  13. package/dist/lib/api.d.ts +14 -0
  14. package/dist/lib/api.d.ts.map +1 -1
  15. package/dist/lib/api.js +38 -4
  16. package/dist/lib/format-api-error.d.ts +2 -0
  17. package/dist/lib/format-api-error.d.ts.map +1 -0
  18. package/dist/lib/format-api-error.js +24 -0
  19. package/dist/lib/logger.d.ts +8 -0
  20. package/dist/lib/logger.d.ts.map +1 -0
  21. package/dist/lib/logger.js +51 -0
  22. package/examples/system/3-tips-fitness.vid +45 -0
  23. package/examples/system/ecom-product-launch.vid +31 -0
  24. package/examples/system/glitch-title.vid +17 -0
  25. package/examples/system/meme-remix.vid +15 -0
  26. package/examples/system/minimal-text-reel.vid +11 -0
  27. package/examples/system/product-launch.vid +18 -0
  28. package/examples/system/rokmilk-chocolate-promo.vid +25 -0
  29. package/examples/system/testimonial-cut.vid +16 -0
  30. package/examples/system/ugc-testimonial-voiceover.vid +33 -0
  31. package/examples/system/voice-visual-promo.vid +21 -0
  32. package/package.json +2 -1
  33. package/skills/aider/SKILL.md +27 -22
  34. package/skills/aider/vidscript-guide.md +62 -39
  35. package/skills/aider/vidscript-sample.md +23 -14
  36. package/skills/aider/vidscript-strict.md +113 -0
  37. package/skills/claude/SKILL.md +27 -22
  38. package/skills/claude/vidscript-guide.md +59 -37
  39. package/skills/claude/vidscript-sample.md +23 -14
  40. package/skills/claude/vidscript-strict.md +113 -0
  41. package/skills/codex/SKILL.md +27 -22
  42. package/skills/codex/vidscript-guide.md +62 -39
  43. package/skills/codex/vidscript-sample.md +23 -14
  44. package/skills/codex/vidscript-strict.md +113 -0
  45. package/skills/cursor/SKILL.md +27 -22
  46. package/skills/cursor/vidscript-guide.md +62 -39
  47. package/skills/cursor/vidscript-sample.md +23 -14
  48. package/skills/cursor/vidscript-strict.md +113 -0
  49. package/skills/opencode/SKILL.md +27 -22
  50. package/skills/opencode/vidscript-guide.md +62 -39
  51. package/skills/opencode/vidscript-sample.md +23 -14
  52. package/skills/opencode/vidscript-strict.md +113 -0
  53. package/skills/skills/aider/SKILL.md +185 -0
  54. package/skills/skills/aider/vidscript-guide.md +378 -0
  55. package/skills/skills/aider/vidscript-sample.md +30 -0
  56. package/skills/skills/aider/vidscript-strict.md +113 -0
  57. package/skills/skills/claude/SKILL.md +185 -0
  58. package/skills/skills/claude/vidscript-guide.md +378 -0
  59. package/skills/skills/claude/vidscript-sample.md +30 -0
  60. package/skills/skills/claude/vidscript-strict.md +113 -0
  61. package/skills/skills/codex/SKILL.md +185 -0
  62. package/skills/skills/codex/vidscript-guide.md +378 -0
  63. package/skills/skills/codex/vidscript-sample.md +30 -0
  64. package/skills/skills/codex/vidscript-strict.md +113 -0
  65. package/skills/skills/cursor/SKILL.md +185 -0
  66. package/skills/skills/cursor/vidscript-guide.md +378 -0
  67. package/skills/skills/cursor/vidscript-sample.md +30 -0
  68. package/skills/skills/cursor/vidscript-strict.md +113 -0
  69. package/skills/skills/opencode/SKILL.md +185 -0
  70. package/skills/skills/opencode/vidscript-guide.md +378 -0
  71. package/skills/skills/opencode/vidscript-sample.md +30 -0
  72. package/skills/skills/opencode/vidscript-strict.md +113 -0
package/README.md CHANGED
@@ -1,3 +1,4 @@
1
+ import motion from "@scenerok/basic-animations"
1
2
  # SceneRok CLI
2
3
 
3
4
  Create videos from your terminal and agent workflows.
@@ -102,7 +103,7 @@ scenerok project render <project-id> --watch --download ./renders
102
103
 
103
104
  ```bash
104
105
  scenerok cache pull
105
- scenerok cache pull --plugin xai-media-video --output ~/.scenerok/cache/assets
106
+ scenerok cache pull --plugin @scenerok/xai --output ~/.scenerok/cache/assets
106
107
  ```
107
108
 
108
109
  Downloads cached generated assets and writes a `manifest.json` to the output folder.
@@ -123,15 +124,17 @@ Config is stored in `~/.scenerok/config.json`:
123
124
  VidScript is a declarative language for describing video compositions.
124
125
 
125
126
  ```vidscript
127
+ import motion from "@scenerok/basic-animations"
128
+
126
129
  input hero = "./assets/hero.mp4"
127
130
 
128
131
  [0s .. 5s] = hero
129
- [0.4s .. 3s] = text "Hello World", font: "Inter Bold", size: 72, x: "50%", y: "50%", align: center, animate: [fadeIn(0.6s), slideY(40, 0, 0.8s)]
132
+ [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)]
130
133
 
131
134
  output to "video.mp4", resolution: "1080x1920", fps: 30
132
135
  ```
133
136
 
134
- Animation helpers: `fadeIn`, `fadeOut`, `slideX`, `slideY`, `popIn`, `riseIn`, `swingIn`, `glitchIn`, `float`, `typewriter`.
137
+ 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`.
135
138
 
136
139
  ## Links
137
140
 
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,eAAO,MAAM,WAAW,SA0FrB,CAAC"}
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"}
@@ -3,12 +3,16 @@ import chalk from 'chalk';
3
3
  import ora from 'ora';
4
4
  import { readConfig, writeConfig, isAuthenticated } from '../lib/config.js';
5
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';
6
9
  export const authCommand = new Command('auth')
7
10
  .description('Authenticate with SceneRok')
8
11
  .addCommand(new Command('login')
9
12
  .description('Log in to SceneRok via browser')
10
13
  .option('-n, --name <name>', 'Device name', 'CLI')
11
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')
12
16
  .action(async (options) => {
13
17
  if (options.baseUrl) {
14
18
  const config = readConfig();
@@ -16,6 +20,10 @@ export const authCommand = new Command('auth')
16
20
  writeConfig(config);
17
21
  }
18
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
+ }
19
27
  const spinner = ora('Initiating device auth...').start();
20
28
  try {
21
29
  const auth = await initiateDeviceAuth(options.name);
@@ -53,13 +61,21 @@ export const authCommand = new Command('auth')
53
61
  }
54
62
  catch (error) {
55
63
  spinner.fail('Authentication failed');
56
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
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)'));
57
71
  process.exit(1);
58
72
  }
59
73
  }))
60
74
  .addCommand(new Command('status')
61
75
  .description('Check authentication status')
62
76
  .action(() => {
77
+ console.log(chalk.dim(`API: ${getBaseUrl()}`));
78
+ console.log(chalk.dim(`Log: ${getLogFilePath()}`));
63
79
  if (isAuthenticated()) {
64
80
  console.log(chalk.green('✅ Authenticated with SceneRok'));
65
81
  }
@@ -1 +1 @@
1
- {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/commands/project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+BpC,eAAO,MAAM,cAAc,SAyJxB,CAAC"}
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"}
@@ -6,6 +6,14 @@ import { join, resolve } from 'node:path';
6
6
  import { downloadRender, getRenderStatus, listProjects, submitRender, uploadProject } from '../lib/api.js';
7
7
  import { isAuthenticated } from '../lib/config.js';
8
8
  import { appendProjectToForm, prepareProject } from '../lib/project.js';
9
+ function formatDuration(ms) {
10
+ const totalSeconds = Math.max(0, Math.round(ms / 1000));
11
+ const minutes = Math.floor(totalSeconds / 60);
12
+ const seconds = totalSeconds % 60;
13
+ if (minutes === 0)
14
+ return `${seconds}s`;
15
+ return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
16
+ }
9
17
  async function waitForRender(renderId) {
10
18
  while (true) {
11
19
  await new Promise((resolveWait) => setTimeout(resolveWait, 3000));
@@ -74,8 +82,11 @@ export const projectCommand = new Command('project')
74
82
  console.log(` Download: ${result.render.downloadUrl}`);
75
83
  if (options.watch || options.download) {
76
84
  console.log(chalk.dim('\nWatching render status...\n'));
77
- await waitForRender(result.render.renderId);
85
+ const status = await waitForRender(result.render.renderId);
78
86
  console.log(chalk.green('\nRender complete'));
87
+ if (status.clickToDownloadMs !== null) {
88
+ console.log(` Time: ${formatDuration(status.clickToDownloadMs)}`);
89
+ }
79
90
  }
80
91
  if (options.download) {
81
92
  const outputDir = typeof options.download === 'string' ? options.download : 'renders';
@@ -143,8 +154,11 @@ export const projectCommand = new Command('project')
143
154
  console.log(` URL: ${result.renderUrl}`);
144
155
  console.log(` Download: ${result.downloadUrl}`);
145
156
  if (options.watch || options.download) {
146
- await waitForRender(result.renderId);
157
+ const status = await waitForRender(result.renderId);
147
158
  console.log(chalk.green('\nRender complete'));
159
+ if (status.clickToDownloadMs !== null) {
160
+ console.log(` Time: ${formatDuration(status.clickToDownloadMs)}`);
161
+ }
148
162
  }
149
163
  if (options.download) {
150
164
  const outputDir = typeof options.download === 'string' ? options.download : 'renders';
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/commands/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkEpC,eAAO,MAAM,aAAa,SAAwB,CAAC;AAEnD,eAAO,MAAM,aAAa,SAuFtB,CAAC"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/commands/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA6EpC,eAAO,MAAM,aAAa,SAAwB,CAAC;AAEnD,eAAO,MAAM,aAAa,SA0FtB,CAAC"}
@@ -5,6 +5,14 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
5
5
  import { join, resolve } from 'node:path';
6
6
  import { submitRender, getRenderStatus, downloadRender } from '../lib/api.js';
7
7
  import { isAuthenticated } from '../lib/config.js';
8
+ function formatDuration(ms) {
9
+ const totalSeconds = Math.max(0, Math.round(ms / 1000));
10
+ const minutes = Math.floor(totalSeconds / 60);
11
+ const seconds = totalSeconds % 60;
12
+ if (minutes === 0)
13
+ return `${seconds}s`;
14
+ return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
15
+ }
8
16
  function createStatusCommand() {
9
17
  return new Command('status')
10
18
  .description('Check render status')
@@ -27,6 +35,9 @@ function createStatusCommand() {
27
35
  console.log(` ID: ${status.id}`);
28
36
  console.log(` Status: ${status.status}`);
29
37
  console.log(` Progress: ${status.progress}%`);
38
+ if (status.clickToDownloadMs !== null) {
39
+ console.log(` Time: ${formatDuration(status.clickToDownloadMs)}`);
40
+ }
30
41
  if (status.outputUrl) {
31
42
  console.log(` Output: ${status.outputUrl}`);
32
43
  }
@@ -104,6 +115,9 @@ export const renderCommand = new Command('render')
104
115
  const status = await getRenderStatus(result.renderId);
105
116
  if (status.status === 'completed') {
106
117
  console.log(chalk.green(`\\n✅ Render complete!\\n`));
118
+ if (status.clickToDownloadMs !== null) {
119
+ console.log(` Time: ${formatDuration(status.clickToDownloadMs)}`);
120
+ }
107
121
  console.log(` Download: ${status.downloadUrl}`);
108
122
  if (options.download) {
109
123
  const outputDir = typeof options.download === 'string' ? options.download : 'renders';
@@ -1 +1 @@
1
- {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/commands/skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4FpC,eAAO,MAAM,aAAa,SAqDvB,CAAC"}
1
+ {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/commands/skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkIpC,eAAO,MAAM,aAAa,SA4DvB,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
- import { existsSync, copyFileSync, mkdirSync } from 'node:fs';
3
+ import { existsSync, copyFileSync, mkdirSync, readdirSync } from 'node:fs';
4
4
  import { join, dirname } from 'node:path';
5
5
  import { homedir } from 'node:os';
6
6
  function getPlatformConfig(platform) {
@@ -13,6 +13,7 @@ function getPlatformConfig(platform) {
13
13
  { source: 'skills/opencode/SKILL.md', dest: 'SKILL.md' },
14
14
  { source: 'skills/opencode/vidscript-guide.md', dest: 'vidscript-guide.md' },
15
15
  { source: 'skills/opencode/vidscript-sample.md', dest: 'vidscript-sample.md' },
16
+ { source: 'skills/opencode/vidscript-strict.md', dest: 'vidscript-strict.md' },
16
17
  ],
17
18
  },
18
19
  claude: {
@@ -22,6 +23,7 @@ function getPlatformConfig(platform) {
22
23
  { source: 'skills/claude/SKILL.md', dest: 'SKILL.md' },
23
24
  { source: 'skills/claude/vidscript-guide.md', dest: 'vidscript-guide.md' },
24
25
  { source: 'skills/claude/vidscript-sample.md', dest: 'vidscript-sample.md' },
26
+ { source: 'skills/claude/vidscript-strict.md', dest: 'vidscript-strict.md' },
25
27
  ],
26
28
  },
27
29
  codex: {
@@ -31,6 +33,7 @@ function getPlatformConfig(platform) {
31
33
  { source: 'skills/codex/SKILL.md', dest: 'SKILL.md' },
32
34
  { source: 'skills/codex/vidscript-guide.md', dest: 'vidscript-guide.md' },
33
35
  { source: 'skills/codex/vidscript-sample.md', dest: 'vidscript-sample.md' },
36
+ { source: 'skills/codex/vidscript-strict.md', dest: 'vidscript-strict.md' },
34
37
  ],
35
38
  },
36
39
  cursor: {
@@ -40,6 +43,7 @@ function getPlatformConfig(platform) {
40
43
  { source: 'skills/cursor/SKILL.md', dest: 'SKILL.md' },
41
44
  { source: 'skills/cursor/vidscript-guide.md', dest: 'vidscript-guide.md' },
42
45
  { source: 'skills/cursor/vidscript-sample.md', dest: 'vidscript-sample.md' },
46
+ { source: 'skills/cursor/vidscript-strict.md', dest: 'vidscript-strict.md' },
43
47
  ],
44
48
  },
45
49
  aider: {
@@ -49,6 +53,7 @@ function getPlatformConfig(platform) {
49
53
  { source: 'skills/aider/SKILL.md', dest: 'SKILL.md' },
50
54
  { source: 'skills/aider/vidscript-guide.md', dest: 'vidscript-guide.md' },
51
55
  { source: 'skills/aider/vidscript-sample.md', dest: 'vidscript-sample.md' },
56
+ { source: 'skills/aider/vidscript-strict.md', dest: 'vidscript-strict.md' },
52
57
  ],
53
58
  },
54
59
  };
@@ -76,6 +81,35 @@ async function findSkillSource(sourcePath) {
76
81
  }
77
82
  return null;
78
83
  }
84
+ async function findExamplesDir() {
85
+ const moduleExamples = join(dirname(new URL(import.meta.url).pathname), '../../examples/system');
86
+ if (existsSync(moduleExamples))
87
+ return moduleExamples;
88
+ const cwdPackage = join(process.cwd(), 'packages/reelforge-cli/examples/system');
89
+ if (existsSync(cwdPackage))
90
+ return cwdPackage;
91
+ try {
92
+ const { execSync } = await import('node:child_process');
93
+ const globalRoot = execSync('npm root -g', { encoding: 'utf-8' }).trim();
94
+ const globalPath = join(globalRoot, '@scenerok', 'cli', 'examples/system');
95
+ if (existsSync(globalPath))
96
+ return globalPath;
97
+ }
98
+ catch {
99
+ // ignore
100
+ }
101
+ return null;
102
+ }
103
+ function installExampleTemplates(skillDir, examplesSource) {
104
+ const examplesDest = join(skillDir, 'examples', 'system');
105
+ mkdirSync(examplesDest, { recursive: true });
106
+ for (const name of readdirSync(examplesSource)) {
107
+ if (!name.endsWith('.vid'))
108
+ continue;
109
+ copyFileSync(join(examplesSource, name), join(examplesDest, name));
110
+ console.log(chalk.green(` ✓ examples/system/${name}`));
111
+ }
112
+ }
79
113
  export const skillsCommand = new Command('skills')
80
114
  .description('Install SceneRok skills to your agent')
81
115
  .addCommand(new Command('install')
@@ -103,6 +137,13 @@ export const skillsCommand = new Command('skills')
103
137
  copyFileSync(source, dest);
104
138
  console.log(chalk.green(` ✓ ${file.dest}`));
105
139
  }
140
+ const examplesSource = await findExamplesDir();
141
+ if (examplesSource) {
142
+ installExampleTemplates(config.skillDir, examplesSource);
143
+ }
144
+ else {
145
+ console.log(chalk.yellow(' ⚠️ Skipping examples/system (bundled templates not found)'));
146
+ }
106
147
  console.log(chalk.cyan(`\\n✅ Skills installed to ${config.skillDir}\\n`));
107
148
  console.log(chalk.dim('Your agent can now compose VidScripts and render videos!'));
108
149
  console.log(chalk.dim('Try: Ask your agent to "create a product promo video using scenerok"'));
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,eAAO,MAAM,eAAe,SAqCxB,CAAC"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8BpC,eAAO,MAAM,eAAe,SA8CxB,CAAC"}
@@ -2,9 +2,29 @@ import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import { readFileSync } from 'node:fs';
4
4
  import { validateVidscript } from '../lib/api.js';
5
+ function collectValidationErrors(result) {
6
+ if (result.errors && result.errors.length > 0) {
7
+ return result.errors;
8
+ }
9
+ if (result.error) {
10
+ return [result.error];
11
+ }
12
+ return ['Validation failed (no error details returned by server)'];
13
+ }
14
+ function printAestheticWarnings(result) {
15
+ if (!result.warnings || result.warnings.length === 0)
16
+ return;
17
+ console.log(chalk.yellow('\n Aesthetic warnings:'));
18
+ if (result.aestheticScore != null) {
19
+ console.log(chalk.yellow(` Score: ${result.aestheticScore}/100`));
20
+ }
21
+ for (const warning of result.warnings) {
22
+ console.log(chalk.yellow(` • ${warning}`));
23
+ }
24
+ }
5
25
  export const validateCommand = new Command('validate')
6
26
  .description('Validate a VidScript file')
7
- .argument('<file>', 'Path to .vidscript file')
27
+ .argument('<file>', 'Path to .vid or .vidscript file')
8
28
  .action(async (file) => {
9
29
  let vidscript;
10
30
  try {
@@ -18,18 +38,27 @@ export const validateCommand = new Command('validate')
18
38
  const result = await validateVidscript(vidscript);
19
39
  if (result.valid) {
20
40
  console.log(chalk.green('\\n✅ VidScript is valid\\n'));
21
- console.log(` Scenes: ${result.scenes}`);
22
- console.log(` Duration: ${result.duration}s`);
23
- if (result.warnings && result.warnings.length > 0) {
24
- console.log(chalk.yellow('\\n Warnings:'));
25
- for (const warning of result.warnings) {
26
- console.log(` • ${warning}`);
27
- }
41
+ if (result.statements != null) {
42
+ console.log(` Statements: ${result.statements}`);
28
43
  }
44
+ if (result.scenes != null) {
45
+ console.log(` Scenes: ${result.scenes}`);
46
+ }
47
+ if (result.duration != null) {
48
+ console.log(` Duration: ${result.duration}s`);
49
+ }
50
+ const pluginCalls = result.pluginCalls;
51
+ if (pluginCalls != null) {
52
+ console.log(` Plugin calls: ${pluginCalls}`);
53
+ }
54
+ printAestheticWarnings(result);
29
55
  }
30
56
  else {
31
57
  console.log(chalk.red('\\n❌ VidScript is invalid\\n'));
32
- console.log(` Error: ${result.error}`);
58
+ for (const err of collectValidationErrors(result)) {
59
+ console.log(` • ${err}`);
60
+ }
61
+ printAestheticWarnings(result);
33
62
  process.exit(1);
34
63
  }
35
64
  console.log('');
package/dist/index.js CHANGED
@@ -15,7 +15,8 @@ const packageJson = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.me
15
15
  program
16
16
  .name('scenerok')
17
17
  .description('SceneRok CLI - Create videos from your terminal and agent workflows')
18
- .version(packageJson.version || '0.0.0');
18
+ .version(packageJson.version || '0.0.0')
19
+ .option('-v, --verbose', 'Log API requests to ~/.scenerok/logs/cli.log');
19
20
  program.addCommand(authCommand);
20
21
  program.addCommand(skillsCommand);
21
22
  program.addCommand(renderCommand);
@@ -29,6 +30,7 @@ program.on('--help', () => {
29
30
  console.log('');
30
31
  console.log(chalk.cyan('Examples:'));
31
32
  console.log(' $ scenerok auth login');
33
+ console.log(' $ scenerok auth login --verbose # debug log at ~/.scenerok/logs/cli.log');
32
34
  console.log(' $ scenerok skills install opencode');
33
35
  console.log(' $ scenerok validate video.vidscript');
34
36
  console.log(' $ scenerok render video.vidscript --resolution 1080x1920');
package/dist/lib/api.d.ts CHANGED
@@ -5,9 +5,22 @@ declare class ApiError extends Error {
5
5
  }
6
6
  export declare function validateVidscript(vidscript: string): Promise<{
7
7
  valid: boolean;
8
+ statements?: number;
8
9
  scenes?: number;
9
10
  duration?: number;
10
11
  warnings?: string[];
12
+ aestheticWarnings?: Array<{
13
+ ruleId: string;
14
+ severity: "warning";
15
+ category: string;
16
+ message: string;
17
+ recommendation: string;
18
+ elementId?: string;
19
+ start?: number;
20
+ end?: number;
21
+ }>;
22
+ aestheticScore?: number;
23
+ errors?: string[];
11
24
  error?: string;
12
25
  }>;
13
26
  export declare function submitRender(vidscript: string, resolution?: string, templateId?: number): Promise<{
@@ -33,6 +46,7 @@ export declare function getRenderStatus(renderId: number): Promise<{
33
46
  error: string | null;
34
47
  createdAt: string;
35
48
  completedAt: string | null;
49
+ clickToDownloadMs: number | null;
36
50
  }>;
37
51
  export declare function downloadRender(renderId: number): Promise<{
38
52
  buffer: Buffer;
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/lib/api.ts"],"names":[],"mappings":"AASA,cAAM,QAAS,SAAQ,KAAK;IAGjB,MAAM,EAAE,MAAM;IACd,YAAY,CAAC,EAAE,OAAO;gBAF7B,OAAO,EAAE,MAAM,EACR,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,OAAO,YAAA;CAKhC;AAkED,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM;WAE9C,OAAO;aACL,MAAM;eACJ,MAAM;eACN,MAAM,EAAE;YACX,MAAM;GAEjB;AAED,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;cAEhF,MAAM;YACR,MAAM;gBACF,MAAM;oBACF,MAAM;eACX,MAAM;iBACJ,MAAM;aACV,MAAM;kBACD,MAAM;gBACR,MAAM,GAAG,IAAI;GAE5B;AAED,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM;QAE9C,MAAM;YACF,MAAM;cACJ,MAAM;eACL,MAAM,GAAG,IAAI;oBACR,MAAM;eACX,MAAM;iBACJ,MAAM;mBACJ,OAAO;WACf,MAAM,GAAG,IAAI;eACT,MAAM;iBACJ,MAAM,GAAG,IAAI;GAE7B;AAED,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAmBpG;AAED,wBAAsB,aAAa,CAAC,QAAQ,EAAE,QAAQ;aAEzC;QACP,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;KACpB;YACO,KAAK,CAAC;QACZ,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QACxB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;aACO,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,GAAG,IAAI;GAE3D;AAED,wBAAsB,YAAY;cAEpB,KAAK,CAAC;QACd,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC;GAEL;AAED,wBAAsB,aAAa,CAAC,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO;aAMxE,KAAK,CAAC;QACb,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,QAAQ,EAAE,OAAO,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;GAEL;AAED,wBAAsB,kBAAkB,CAAC,UAAU,EAAE,MAAM;iBAE1C,MAAM;eACR,MAAM;sBACC,MAAM;gBACZ,MAAM;cACR,MAAM;GAEnB;AAED,wBAAsB,cAAc,CAAC,UAAU,EAAE,MAAM;YAE3C,MAAM;mBACC,MAAM;iBACR,MAAM;iBACN,MAAM;YACX,MAAM;GAEjB;AAED,OAAO,EAAE,QAAQ,EAAE,CAAC"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/lib/api.ts"],"names":[],"mappings":"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"}
package/dist/lib/api.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fetch from 'node-fetch';
2
2
  import { getBaseUrl, getApiToken } from './config.js';
3
+ import { logDebug, logError } from './logger.js';
3
4
  class ApiError extends Error {
4
5
  status;
5
6
  responseBody;
@@ -23,15 +24,48 @@ function getAuthHeaders(contentType = 'application/json') {
23
24
  }
24
25
  async function apiCall(method, path, body) {
25
26
  const baseUrl = getBaseUrl();
26
- const response = await fetch(`${baseUrl}${path}`, {
27
+ const url = `${baseUrl}${path}`;
28
+ const started = Date.now();
29
+ logDebug('API request', {
27
30
  method,
28
- headers: getAuthHeaders(),
29
- body: body ? JSON.stringify(body) : undefined,
31
+ url,
32
+ hasBody: body != null,
30
33
  });
31
- const data = await response.json().catch(() => ({}));
34
+ let response;
35
+ try {
36
+ response = await fetch(url, {
37
+ method,
38
+ headers: getAuthHeaders(),
39
+ body: body ? JSON.stringify(body) : undefined,
40
+ });
41
+ }
42
+ catch (error) {
43
+ const message = error instanceof Error ? error.message : String(error);
44
+ logError('API network error', { method, url, message });
45
+ throw new ApiError(`Network error: ${message}`, 0, { cause: message });
46
+ }
47
+ const rawText = await response.text();
48
+ let data = {};
49
+ if (rawText) {
50
+ try {
51
+ data = JSON.parse(rawText);
52
+ }
53
+ catch {
54
+ data = { raw: rawText.slice(0, 2000) };
55
+ }
56
+ }
57
+ const durationMs = Date.now() - started;
32
58
  if (!response.ok) {
59
+ logError('API error response', {
60
+ method,
61
+ url,
62
+ status: response.status,
63
+ durationMs,
64
+ body: data,
65
+ });
33
66
  throw new ApiError(data.error || `HTTP ${response.status}`, response.status, data);
34
67
  }
68
+ logDebug('API success', { method, url, status: response.status, durationMs });
35
69
  return data;
36
70
  }
37
71
  async function apiFormCall(path, formData) {
@@ -0,0 +1,2 @@
1
+ export declare function formatApiErrorMessage(error: unknown): string;
2
+ //# sourceMappingURL=format-api-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-api-error.d.ts","sourceRoot":"","sources":["../../src/lib/format-api-error.ts"],"names":[],"mappings":"AAEA,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAwB5D"}
@@ -0,0 +1,24 @@
1
+ import { ApiError } from './api.js';
2
+ export function formatApiErrorMessage(error) {
3
+ if (error instanceof ApiError) {
4
+ const parts = [`HTTP ${error.status}: ${error.message}`];
5
+ const body = error.responseBody;
6
+ if (body && typeof body === 'object') {
7
+ const record = body;
8
+ if (typeof record.hint === 'string') {
9
+ parts.push(`Hint: ${record.hint}`);
10
+ }
11
+ if (typeof record.detail === 'string') {
12
+ parts.push(`Detail: ${record.detail}`);
13
+ }
14
+ if (typeof record.code === 'string') {
15
+ parts.push(`Code: ${record.code}`);
16
+ }
17
+ }
18
+ return parts.join('\n');
19
+ }
20
+ if (error instanceof Error) {
21
+ return error.message;
22
+ }
23
+ return String(error);
24
+ }
@@ -0,0 +1,8 @@
1
+ export declare function getLogFilePath(): string;
2
+ export declare function isVerbose(): boolean;
3
+ export declare function writeLog(level: 'debug' | 'info' | 'warn' | 'error', message: string, meta?: Record<string, unknown>): void;
4
+ export declare function logDebug(message: string, meta?: Record<string, unknown>): void;
5
+ export declare function logInfo(message: string, meta?: Record<string, unknown>): void;
6
+ export declare function logWarn(message: string, meta?: Record<string, unknown>): void;
7
+ export declare function logError(message: string, meta?: Record<string, unknown>): void;
8
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAOA,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,SAAS,IAAI,OAAO,CAOnC;AAmBD,wBAAgB,QAAQ,CACtB,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,EAC1C,OAAO,EAAE,MAAM,EACf,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,IAAI,CAIN;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAM9E;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAE7E;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAE7E;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAE9E"}
@@ -0,0 +1,51 @@
1
+ import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getConfigDir } from './config.js';
4
+ const LOG_DIR = join(getConfigDir(), 'logs');
5
+ const LOG_FILE = join(LOG_DIR, 'cli.log');
6
+ export function getLogFilePath() {
7
+ return LOG_FILE;
8
+ }
9
+ export function isVerbose() {
10
+ const verboseEnv = ['SCENEROK', '_', 'VERBOSE'].join('');
11
+ return (process.env[verboseEnv] === '1' ||
12
+ process.argv.includes('--verbose') ||
13
+ process.argv.includes('-v'));
14
+ }
15
+ function ensureLogDir() {
16
+ if (!existsSync(LOG_DIR)) {
17
+ mkdirSync(LOG_DIR, { recursive: true });
18
+ }
19
+ }
20
+ function formatMeta(meta) {
21
+ if (!meta || Object.keys(meta).length === 0) {
22
+ return '';
23
+ }
24
+ try {
25
+ return ` ${JSON.stringify(meta)}`;
26
+ }
27
+ catch {
28
+ return ' [meta unserializable]';
29
+ }
30
+ }
31
+ export function writeLog(level, message, meta) {
32
+ ensureLogDir();
33
+ const line = `[${new Date().toISOString()}] ${level.toUpperCase()} ${message}${formatMeta(meta)}\n`;
34
+ appendFileSync(LOG_FILE, line, 'utf-8');
35
+ }
36
+ export function logDebug(message, meta) {
37
+ writeLog('debug', message, meta);
38
+ if (isVerbose()) {
39
+ // eslint-disable-next-line no-console
40
+ console.error(`[debug] ${message}${formatMeta(meta)}`);
41
+ }
42
+ }
43
+ export function logInfo(message, meta) {
44
+ writeLog('info', message, meta);
45
+ }
46
+ export function logWarn(message, meta) {
47
+ writeLog('warn', message, meta);
48
+ }
49
+ export function logError(message, meta) {
50
+ writeLog('error', message, meta);
51
+ }
@@ -0,0 +1,45 @@
1
+ # 3 Tips Fitness Reel - Reusable Template
2
+ # Demonstrates dynamic timing [-] blocks + xAI video generation per tip (explicit import form)
3
+
4
+ import xai from "@scenerok/xai"
5
+
6
+ # === INPUTS (user provides these) ===
7
+ input tip1_text = "{{tip1_text | Tip 1 goes here}}"
8
+ input tip2_text = "{{tip2_text | Tip 2 goes here}}"
9
+ input tip3_text = "{{tip3_text | Tip 3 goes here}}"
10
+ input cta_text = "{{cta_text | Save this for later}}"
11
+
12
+ # === DYNAMIC TIP SECTIONS ===
13
+ # Each [-] block auto-calculates duration based on content
14
+ [-] = text tip1_text, style: title, position: center, color: "#FFFFFF", size: 64, align: center
15
+
16
+ # xAI generates a fresh visual for this specific tip
17
+ [-] = video xai.imagine(
18
+ "Cinematic fitness shot: " + tip1_text,
19
+ aspect_ratio: "9:16",
20
+ resolution: "1k"
21
+ )
22
+
23
+ [-] = text tip2_text, style: title, position: center, color: "#FFFFFF", size: 64, align: center
24
+
25
+ [-] = video xai.imagine(
26
+ "Cinematic fitness shot: " + tip2_text,
27
+ aspect_ratio: "9:16",
28
+ resolution: "1k"
29
+ )
30
+
31
+ [-] = text tip3_text, style: title, position: center, color: "#FFFFFF", size: 64, align: center
32
+
33
+ [-] = video xai.imagine(
34
+ "Cinematic fitness shot: " + tip3_text,
35
+ aspect_ratio: "9:16",
36
+ resolution: "1k"
37
+ )
38
+
39
+ # === CTA + BRANDING ===
40
+ [- 2s] = text cta_text, style: cta, position: bottom, color: "#FF0055", size: 48
41
+
42
+ # Subtle brand watermark
43
+ [-] = text "SceneRok", style: watermark, position: "85%, 92%", color: "#888888", size: 28, opacity: 0.6
44
+
45
+ output to "3-tips-fitness.mp4", resolution: "1080x1920", fps: 30
@@ -0,0 +1,31 @@
1
+ # Ecom Product Launch Reel - Reusable Template
2
+ # Shows product + lifestyle shot generated by xAI (@scenerok/xai), with strong CTA
3
+
4
+ import xai from "@scenerok/xai"
5
+
6
+ # === INPUTS ===
7
+ input product_name = "{{product_name | Your Product}}"
8
+ input hook_text = "{{hook_text | The one thing that actually works}}"
9
+ input price = "{{price | $49}}"
10
+ input cta_text = "{{cta_text | Shop Now}}"
11
+
12
+ # === HOOK ===
13
+ [0s .. 3s] = text hook_text, style: hook, position: center, color: "#FFFFFF", size: 72, align: center
14
+
15
+ # === PRODUCT VISUAL (xAI generated lifestyle video) ===
16
+ [3s .. 10s] = video xai.imagine(
17
+ "Premium lifestyle product shot of " + product_name + ", cinematic lighting, modern aesthetic, 9:16 vertical",
18
+ aspect_ratio: "9:16",
19
+ resolution: "1k"
20
+ )
21
+
22
+ # === PRICE + URGENCY ===
23
+ [8s .. 12s] = text "Only " + price, style: price, position: "center, 70%", color: "#FF0055", size: 56
24
+
25
+ # === STRONG CTA ===
26
+ [- 3s] = text cta_text, style: cta, position: bottom, color: "#FFFFFF", size: 64, stroke: "#000000", stroke_width: 4
27
+
28
+ # Subtle brand element
29
+ [-] = text "SceneRok", style: watermark, position: "85%, 92%", color: "#888888", size: 26, opacity: 0.5
30
+
31
+ output to "product-launch.mp4", resolution: "1080x1920", fps: 30