@scenerok/cli 1.0.0 → 1.0.1

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/dist/lib/api.js CHANGED
@@ -10,18 +10,22 @@ class ApiError extends Error {
10
10
  this.name = 'ApiError';
11
11
  }
12
12
  }
13
- async function apiCall(method, path, body) {
14
- const baseUrl = getBaseUrl();
13
+ function getAuthHeaders(contentType = 'application/json') {
15
14
  const token = getApiToken();
16
- const headers = {
17
- 'Content-Type': 'application/json',
18
- };
15
+ const headers = {};
16
+ if (contentType) {
17
+ headers['Content-Type'] = contentType;
18
+ }
19
19
  if (token) {
20
20
  headers['Authorization'] = `Bearer ${token}`;
21
21
  }
22
+ return headers;
23
+ }
24
+ async function apiCall(method, path, body) {
25
+ const baseUrl = getBaseUrl();
22
26
  const response = await fetch(`${baseUrl}${path}`, {
23
27
  method,
24
- headers,
28
+ headers: getAuthHeaders(),
25
29
  body: body ? JSON.stringify(body) : undefined,
26
30
  });
27
31
  const data = await response.json().catch(() => ({}));
@@ -30,15 +34,62 @@ async function apiCall(method, path, body) {
30
34
  }
31
35
  return data;
32
36
  }
37
+ async function apiFormCall(path, formData) {
38
+ const baseUrl = getBaseUrl();
39
+ const headers = getAuthHeaders('');
40
+ const response = await fetch(`${baseUrl}${path}`, {
41
+ method: 'POST',
42
+ headers,
43
+ body: formData,
44
+ });
45
+ const data = await response.json().catch(() => ({}));
46
+ if (!response.ok) {
47
+ throw new ApiError(data.error || `HTTP ${response.status}`, response.status, data);
48
+ }
49
+ return data;
50
+ }
33
51
  export async function validateVidscript(vidscript) {
34
52
  return apiCall('POST', '/api/cli/validate', { vidscript });
35
53
  }
36
- export async function submitRender(vidscript, resolution) {
37
- return apiCall('POST', '/api/cli/render', { vidscript, resolution });
54
+ export async function submitRender(vidscript, resolution, templateId) {
55
+ return apiCall('POST', '/api/cli/render', { vidscript, resolution, templateId });
38
56
  }
39
57
  export async function getRenderStatus(renderId) {
40
58
  return apiCall('GET', `/api/cli/status?id=${renderId}`);
41
59
  }
60
+ export async function downloadRender(renderId) {
61
+ const baseUrl = getBaseUrl();
62
+ const response = await fetch(`${baseUrl}/api/cli/render/download?id=${renderId}`, {
63
+ method: 'GET',
64
+ headers: getAuthHeaders(''),
65
+ });
66
+ if (!response.ok) {
67
+ const data = await response.json().catch(() => ({}));
68
+ throw new ApiError(data.error || `HTTP ${response.status}`, response.status, data);
69
+ }
70
+ const disposition = response.headers.get('content-disposition') || '';
71
+ const filenameMatch = disposition.match(/filename="([^"]+)"/);
72
+ const filename = filenameMatch?.[1] || `${renderId}.mp4`;
73
+ return {
74
+ buffer: Buffer.from(await response.arrayBuffer()),
75
+ filename,
76
+ };
77
+ }
78
+ export async function uploadProject(formData) {
79
+ return apiFormCall('/api/cli/projects', formData);
80
+ }
81
+ export async function listProjects() {
82
+ return apiCall('GET', '/api/cli/projects');
83
+ }
84
+ export async function getAssetCache(options = {}) {
85
+ const params = new URLSearchParams();
86
+ if (options.plugin)
87
+ params.set('plugin', options.plugin);
88
+ if (options.limit)
89
+ params.set('limit', String(options.limit));
90
+ const suffix = params.toString() ? `?${params.toString()}` : '';
91
+ return apiCall('GET', `/api/cli/asset-cache${suffix}`);
92
+ }
42
93
  export async function initiateDeviceAuth(deviceName) {
43
94
  return apiCall('POST', '/api/cli/auth/device', { deviceName });
44
95
  }
@@ -8,4 +8,6 @@ export declare function writeConfig(config: CliConfig): void;
8
8
  export declare function getBaseUrl(): string;
9
9
  export declare function getApiToken(): string | undefined;
10
10
  export declare function isAuthenticated(): boolean;
11
+ export declare function getConfigDir(): string;
12
+ export declare function getCacheDir(): string;
11
13
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAQD,wBAAgB,UAAU,IAAI,SAAS,CAUtC;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,QAG5C;AAED,wBAAgB,UAAU,IAAI,MAAM,CAGnC;AAED,wBAAgB,WAAW,IAAI,MAAM,GAAG,SAAS,CAGhD;AAED,wBAAgB,eAAe,IAAI,OAAO,CAEzC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAQD,wBAAgB,UAAU,IAAI,SAAS,CAUtC;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,QAG5C;AAED,wBAAgB,UAAU,IAAI,MAAM,CAGnC;AAED,wBAAgB,WAAW,IAAI,MAAM,GAAG,SAAS,CAGhD;AAED,wBAAgB,eAAe,IAAI,OAAO,CAEzC;AAED,wBAAgB,YAAY,IAAI,MAAM,CAGrC;AAED,wBAAgB,WAAW,IAAI,MAAM,CAKpC"}
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  const CONFIG_DIR = join(homedir(), '.scenerok');
5
5
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
6
+ const CACHE_DIR = join(CONFIG_DIR, 'cache');
6
7
  function ensureConfigDir() {
7
8
  if (!existsSync(CONFIG_DIR)) {
8
9
  mkdirSync(CONFIG_DIR, { recursive: true });
@@ -35,3 +36,13 @@ export function getApiToken() {
35
36
  export function isAuthenticated() {
36
37
  return !!getApiToken();
37
38
  }
39
+ export function getConfigDir() {
40
+ ensureConfigDir();
41
+ return CONFIG_DIR;
42
+ }
43
+ export function getCacheDir() {
44
+ if (!existsSync(CACHE_DIR)) {
45
+ mkdirSync(CACHE_DIR, { recursive: true });
46
+ }
47
+ return CACHE_DIR;
48
+ }
@@ -0,0 +1,14 @@
1
+ export interface PreparedProject {
2
+ rootDir: string;
3
+ entryFile: string;
4
+ originalVidscript: string;
5
+ rewrittenVidscript: string;
6
+ assets: Array<{
7
+ localPath: string;
8
+ absolutePath: string;
9
+ }>;
10
+ }
11
+ export declare function listAssetFiles(assetDir: string): string[];
12
+ export declare function prepareProject(entryFile: string, assetDirs?: string[]): PreparedProject;
13
+ export declare function appendProjectToForm(formData: FormData, project: PreparedProject): Promise<void>;
14
+ //# sourceMappingURL=project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../../src/lib/project.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,MAAM,EAAE,KAAK,CAAC;QACZ,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;CACJ;AAuBD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAoBzD;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,GAAE,MAAM,EAAO,GAAG,eAAe,CAgD3F;AAED,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAcrG"}
@@ -0,0 +1,97 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
3
+ const INPUT_RE = /^(\s*input\s+[A-Za-z_][A-Za-z0-9_]*\s*=\s*)(["'])([^"']+)(\2)/gm;
4
+ const HTTP_RE = /^https?:\/\//i;
5
+ function isRenderableLocalPath(value) {
6
+ return Boolean(value) && !HTTP_RE.test(value) && !value.startsWith('/uploads/') && !value.startsWith('data:');
7
+ }
8
+ function getMimeType(filePath) {
9
+ const ext = filePath.toLowerCase().split('.').pop();
10
+ switch (ext) {
11
+ case 'mp4': return 'video/mp4';
12
+ case 'mov': return 'video/quicktime';
13
+ case 'webm': return 'video/webm';
14
+ case 'mp3': return 'audio/mpeg';
15
+ case 'wav': return 'audio/wav';
16
+ case 'png': return 'image/png';
17
+ case 'jpg':
18
+ case 'jpeg': return 'image/jpeg';
19
+ case 'webp': return 'image/webp';
20
+ case 'gif': return 'image/gif';
21
+ default: return 'application/octet-stream';
22
+ }
23
+ }
24
+ export function listAssetFiles(assetDir) {
25
+ const results = [];
26
+ function walk(dir) {
27
+ for (const entry of readdirSync(dir)) {
28
+ const absolute = join(dir, entry);
29
+ const stats = statSync(absolute);
30
+ if (stats.isDirectory()) {
31
+ walk(absolute);
32
+ }
33
+ else if (stats.isFile()) {
34
+ results.push(absolute);
35
+ }
36
+ }
37
+ }
38
+ if (existsSync(assetDir)) {
39
+ walk(assetDir);
40
+ }
41
+ return results;
42
+ }
43
+ export function prepareProject(entryFile, assetDirs = []) {
44
+ const absoluteEntry = resolve(entryFile);
45
+ if (!existsSync(absoluteEntry)) {
46
+ throw new Error(`VidScript file not found: ${entryFile}`);
47
+ }
48
+ const rootDir = dirname(absoluteEntry);
49
+ const originalVidscript = readFileSync(absoluteEntry, 'utf-8');
50
+ const assetsByPath = new Map();
51
+ let rewrittenVidscript = originalVidscript.replace(INPUT_RE, (match, prefix, quote, rawPath, suffix) => {
52
+ if (!isRenderableLocalPath(rawPath)) {
53
+ return match;
54
+ }
55
+ const absolutePath = isAbsolute(rawPath) ? rawPath : resolve(rootDir, rawPath);
56
+ if (!existsSync(absolutePath)) {
57
+ return match;
58
+ }
59
+ const localPath = relative(rootDir, absolutePath).replace(/\\/g, '/');
60
+ assetsByPath.set(localPath, absolutePath);
61
+ return `${prefix}${quote}{{asset:${localPath}}}${suffix}`;
62
+ });
63
+ for (const dir of assetDirs) {
64
+ const absoluteDir = resolve(dir);
65
+ for (const absolutePath of listAssetFiles(absoluteDir)) {
66
+ const localPath = relative(rootDir, absolutePath).replace(/\\/g, '/');
67
+ assetsByPath.set(localPath, absolutePath);
68
+ }
69
+ }
70
+ for (const [localPath] of assetsByPath) {
71
+ const token = `{{asset:${localPath}}}`;
72
+ rewrittenVidscript = rewrittenVidscript.replaceAll(token, `__SCENEROK_ASSET_${Buffer.from(localPath).toString('base64url')}__`);
73
+ }
74
+ return {
75
+ rootDir,
76
+ entryFile: basename(absoluteEntry),
77
+ originalVidscript,
78
+ rewrittenVidscript,
79
+ assets: Array.from(assetsByPath.entries()).map(([localPath, absolutePath]) => ({
80
+ localPath,
81
+ absolutePath,
82
+ })),
83
+ };
84
+ }
85
+ export async function appendProjectToForm(formData, project) {
86
+ formData.set('localRoot', project.rootDir);
87
+ formData.set('entryFile', project.entryFile);
88
+ formData.set('originalVidscript', project.originalVidscript);
89
+ let serverVidscript = project.rewrittenVidscript;
90
+ for (const asset of project.assets) {
91
+ const bytes = readFileSync(asset.absolutePath);
92
+ const blob = new Blob([new Uint8Array(bytes)], { type: getMimeType(asset.absolutePath) });
93
+ formData.append('asset', blob, basename(asset.absolutePath));
94
+ formData.append('assetPath', asset.localPath);
95
+ }
96
+ formData.set('vidscript', serverVidscript);
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scenerok/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "SceneRok CLI - Create videos from your terminal and agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
+ "skills",
12
13
  "README.md"
13
14
  ],
14
15
  "scripts": {
@@ -0,0 +1,171 @@
1
+ # SceneRok Skill for Claude Code
2
+
3
+ ## Overview
4
+
5
+ You are a VidScript composer and video generation expert integrated with Claude Code. You help users create video content using the SceneRok platform directly from their terminal.
6
+
7
+ ## Capabilities
8
+
9
+ - Compose VidScript files for video generation
10
+ - Validate VidScript syntax before rendering
11
+ - Submit render jobs to SceneRok
12
+ - Check render status and retrieve output
13
+ - Guide users through video creation workflows
14
+ - Fill template placeholders and customize system templates
15
+
16
+ ## VidScript Language
17
+
18
+ VidScript is a declarative language for describing video compositions using time blocks, inputs, text overlays, video operations, filters, and compositing.
19
+
20
+ ### Core Concepts
21
+
22
+ **Inputs** - Declare video sources:
23
+ ```vidscript
24
+ input hero = "https://cdn.example.com/hero.mp4"
25
+ input logo = "/uploads/logo.png"
26
+ ```
27
+
28
+ **Time Blocks** - Define what happens during a time range:
29
+ ```vidscript
30
+ [-] = hero # auto-append: starts after previous block
31
+ hero.Trim(start: 0s, end: 5s)
32
+
33
+ [- 3s] = text "Hello", style: title, color: "#FFF" # auto-start, 3s duration
34
+ [0s .. 5s] = hero # explicit range
35
+ [prev + 0.5s .. prev + 2s] = filter "glow" # expression-based
36
+ ```
37
+
38
+ **Video Operations** - Modify how video plays:
39
+ ```vidscript
40
+ hero.Trim(start: 0s, end: 5s) # trim clip
41
+ hero.Speed(factor: 1.5) # 50% faster
42
+ hero.Resize(width: 1080, height: 1920)
43
+ hero.Loop(count: 3) # play 3 times
44
+ hero.Opacity(value: 0.5, duration: 2s)
45
+ ```
46
+
47
+ **Compositing** - Layer videos:
48
+ ```vidscript
49
+ base.Overlay(logo, x: 50, y: 50, opacity: 0.8)
50
+ ```
51
+
52
+ **Filters & Shaders** - Post-processing effects:
53
+ ```vidscript
54
+ [0s .. 5s] = filter "vignette", intensity: 0.4
55
+ [2s .. 4s] = filter "sepia", intensity: 0.6
56
+ ```
57
+
58
+ Built-in filters: `monochrome`, `sepia`, `blur`, `chromatic`, `glitch`, `vignette`, `contrast`, `saturation`, `brightness`
59
+
60
+ **Text Overlays** - Add on-screen text:
61
+ ```vidscript
62
+ [0.5s .. 3s] = text "Headline", style: title, position: center, color: "#FFF", size: 72, stroke: "#000", stroke_width: 3
63
+ ```
64
+
65
+ **Output** - Specify render settings:
66
+ ```vidscript
67
+ output to "video.mp4", resolution: "1080x1920", fps: 30
68
+ ```
69
+
70
+ ### Module System
71
+
72
+ ```vidscript
73
+ export const BRAND_COLOR = "#FF5733"
74
+ export timeline intro(clip: string, headline: string) {
75
+ [-] = clip # auto-append
76
+ clip.Trim(start: 0s, end: 3s)
77
+ [- 2s] = text headline, style: title # auto-start, 2s duration
78
+ }
79
+ ```
80
+
81
+ ```vidscript
82
+ import { intro } from "./timelines.vid"
83
+ import * as pack from "./effects.vid"
84
+ import eleven from "@elevenlabs/tts" # future scoped package # from npm registry
85
+ import music from "@elevenlabs/music" # generative music
86
+ use intro at [-] with { clip: main, headline: "Welcome" }
87
+ ```
88
+
89
+ ### Plugin Calls
90
+
91
+ ```vidscript
92
+ import eleven from "/tts"
93
+
94
+ [-] = audio eleven.tts("Welcome to SceneRok", voice: "Rachel")
95
+ [-] = video xai.imagine("Cinematic product shot", aspect_ratio: "9:16")
96
+ [-] = audio music("upbeat launch soundtrack", duration: 12, instrumental: true)
97
+ [-] = audio xai.tts("Next line", voice: "eve")
98
+ ```
99
+
100
+ Package-style plugin calls are supported inside time blocks. Use `video ...` for generated visual clips and `audio ...` for generated speech, music, and other generated audio.
101
+
102
+ ### Placeholders
103
+
104
+ Templates use `{{name | default}}` for user-supplied values:
105
+ ```vidscript
106
+ input hero = "{{hero_clip}}"
107
+ [0.5s .. 3s] = text "{{headline | Hello}}", style: title
108
+ ```
109
+
110
+ ## Workflow
111
+
112
+ 1. **Understand the goal** — What video does the user want? (promo, testimonial, meme, etc.)
113
+ 2. **Plan the structure** — Time blocks, durations, inputs
114
+ 3. **Gather assets** — Video URLs or local paths
115
+ 4. **Compose VidScript** — Write the full script
116
+ 5. **Validate** — Run `scenerok validate script.vid`
117
+ 6. **Render** — Run `scenerok render script.vid --watch`
118
+ 7. **Deliver** — Share the download URL when complete
119
+
120
+ ## Best Practices
121
+
122
+ - **Use dynamic timeblocks** — `[-]` auto-advances the cursor, reducing calculation errors
123
+ - **Use `prev` for offsets** — `[prev + 0.5s .. prev + 2s]` for gaps between content
124
+ - **Named arguments for clarity** — `hero.Trim(start: 0s, end: 5s)` over `hero.Trim(0s, 5s)`
125
+ - Use 1080x1920 for vertical content (TikTok/Instagram)
126
+ - Use 1920x1080 for horizontal content (YouTube)
127
+ - Hook viewers in the first 3 seconds
128
+ - Use high-contrast text on video backgrounds with `stroke` and `stroke_width`
129
+ - Include a clear call-to-action
130
+ - Test with `scenerok validate` before rendering
131
+ - Each render costs 1 credit
132
+
133
+ ## CLI Commands
134
+
135
+ ```bash
136
+ scenerok auth login # Authenticate
137
+ scenerok validate script.vid # Validate a VidScript
138
+ scenerok render script.vid --watch # Render a video
139
+ scenerok status <render-id> # Check status
140
+ scenerok skills install <platform> # Install/update agent skill
141
+ scenerok skills list # List available skills
142
+ scenerok secrets set KEY=VALUE # Store API key for plugins
143
+ ```
144
+
145
+ ## Error Handling
146
+
147
+ If a render fails:
148
+ 1. Check the error message with `scenerok status <id>`
149
+ 2. Common issues: missing assets (404 URLs), invalid file paths, syntax errors
150
+ 3. Fix the VidScript and re-render
151
+ 4. If credits are insufficient, the user needs to purchase more
152
+
153
+ ## Sample Video Types
154
+
155
+ - **Product Promo** — Hero clip + headline + description + CTA + vignette filter
156
+ - **Social Media Hook** — Fast cuts, bold text, speed adjustments
157
+ - **Testimonial** — Speaker clip + quote text + sepia filter
158
+ - **Meme Remix** — Reaction clip + top/bottom punchline overlays
159
+ - **Title Sequence** — Background clip + glitch shader + bold typography
160
+
161
+ Always ask clarifying questions about:
162
+ - Target platform (TikTok, Instagram, YouTube, etc.)
163
+ - Brand colors and fonts
164
+ - Existing assets (video clips, logo, etc.)
165
+ - Desired duration and style
166
+ - Whether they want to start from a template or from scratch
167
+
168
+ ## Full Reference
169
+
170
+ For exhaustive grammar documentation, visit:
171
+ https://scenerok.com/docs/vidscript