videodraft 0.0.0 → 0.1.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.
package/README.md CHANGED
@@ -1,5 +1,121 @@
1
1
  # videodraft
2
2
 
3
- The official **VideoDraft** CLI — create AI videos, images, voiceovers and music from your terminal, for humans and coding agents.
3
+ The official [VideoDraft](https://videodraft.ai) CLI — create AI videos, images, voiceovers and music from your terminal. Built for humans **and** coding agents: every command supports `--json`, exit codes are stable, async jobs are first-class.
4
4
 
5
- 🚧 Launching soon — https://videodraft.ai/cli
5
+ ```bash
6
+ npx videodraft login
7
+ npx videodraft generate image "a red fox in snow, cinematic" --download ./out/
8
+ npx videodraft create "30s launch video for our espresso machine" --ar 9:16
9
+ ```
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g videodraft # or: npx videodraft <command>
15
+ ```
16
+
17
+ Requires Node ≥ 20.
18
+
19
+ ## Authenticate
20
+
21
+ ```bash
22
+ videodraft login # opens your browser (OAuth)
23
+ videodraft login --token vd_mcp_… # personal access token from https://app.videodraft.ai/mcp-keys
24
+ export VIDEODRAFT_API_KEY=vd_mcp_… # headless / CI — no login command needed
25
+ ```
26
+
27
+ Credentials are stored in `~/.config/videodraft/config.json` (0600). `videodraft logout` revokes and clears them.
28
+
29
+ ## The pipeline
30
+
31
+ ```bash
32
+ videodraft credits # know your budget
33
+ videodraft create "<idea>" --ar 9:16 # idea → script → visual assets → storyboard
34
+ videodraft shots <project> --grid --estimate # preview the cost…
35
+ videodraft shots <project> --grid # …then batch-generate every shot image
36
+ videodraft produce <project> # voiceovers + captions + production timeline
37
+ videodraft export <project> --download final.mp4
38
+ ```
39
+
40
+ Single assets don't need a project:
41
+
42
+ ```bash
43
+ videodraft generate image "isometric workspace, warm light" --num 4 --download "./out/{job_id}_{index}.{ext}"
44
+ videodraft generate video "slow dolly over a misty lake" --model google-veo3.1 --duration 6 --estimate
45
+ videodraft generate voiceover "Welcome to VideoDraft" --download welcome.mp3
46
+ videodraft generate music "minimal ambient, 60 BPM" --download bgm.mp3
47
+ videodraft upscale image ./photo.png --scale 4x --download ./photo-4x.png
48
+ videodraft avatar create ./founder.jpg --script "$(videodraft avatar script 'our launch' --json | jq -r .script)"
49
+ ```
50
+
51
+ ## Commands
52
+
53
+ | Group | Commands |
54
+ |---|---|
55
+ | Auth | `login` `logout` `whoami` |
56
+ | Account | `credits` `costs [model]` `models [image\|video\|voices\|styles]` `workspaces` `sessions list/create` |
57
+ | Projects | `projects list/get/delete/favorite/open` `checkpoint create/list/restore` |
58
+ | Pipeline | `create` `shots` `produce` (`--mode full_video`) `attach` `finalize` `export` `export-status` `video-prompts` |
59
+ | Generate | `generate image/video/voiceover/music` `upscale image/video` `avatar script/create/render/get/list` |
60
+ | Jobs | `status <job>` `wait <job>` `generations` |
61
+ | Media | `upload <file>` `media list` `describe <url\|file>` `download <url>` |
62
+ | Everything else | `tools list` `tools schema <name>` `call <tool> --args '<json>'` |
63
+ | Agents | `skills install [--agent claude\|codex\|cursor]` `skills path` |
64
+ | Utility | `config get/set/path` `completion bash\|zsh` `docs` `--version` |
65
+
66
+ `call` reaches **every** VideoDraft API tool (the full MCP catalog), including ones without a curated command — new platform features work in the CLI the day they ship.
67
+
68
+ ## For agents and scripts
69
+
70
+ - `--json` on any command prints a single JSON document on stdout.
71
+ - Exit codes: `0` ok · `1` error · `2` usage · `3` auth required · `4` insufficient credits.
72
+ - Async generations wait by default; `--no-wait` returns `{job_id}` immediately, `videodraft wait <job...>` resumes, `--wait-timeout 30m --wait-interval 5s` tune polling.
73
+ - **Many jobs at once?** Submit them all with `--no-wait`, then `videodraft wait <id1> <id2> ...` — one process polls every job with ONE batched request per tick (don't spawn N parallel `wait` processes). Polling backs off adaptively (3s → 15s with jitter) on long jobs unless you pin `--wait-interval`.
74
+ - `--download` templates: `{job_id}`, `{index}`, `{ext}`, `{name}`. Downloads are echoed as `downloaded_files[]` in `--json` output.
75
+ - Local file → public URL anywhere a URL is expected (`--ref photo.jpg`), or explicitly via `videodraft upload`.
76
+ - `NO_COLOR` and `--no-color` are respected; output is uncolored when piped.
77
+
78
+ Install the VideoDraft skill so your agent knows the workflow:
79
+
80
+ ```bash
81
+ npx videodraft skills install # zero-install: npx fetches the CLI and installs the skill
82
+ videodraft skills install # if the CLI is on PATH — auto-detects your installed agents
83
+ videodraft skills install --agent claude,codex # target specific agents (repeatable/comma; --all for every agent)
84
+ npx skills add videodraft-ai/cli # via the vercel-labs skills tool (discovery / 69-agent multiselect)
85
+ ```
86
+
87
+ ## Environment variables
88
+
89
+ | Variable | Purpose |
90
+ |---|---|
91
+ | `VIDEODRAFT_API_KEY` | Bearer token (`vd_mcp_…`) — skips the login flow |
92
+ | `VIDEODRAFT_BASE_URL` | Target server (default `https://app.videodraft.ai`) |
93
+ | `VIDEODRAFT_CONFIG_DIR` | Config location (default `~/.config/videodraft`) |
94
+ | `VIDEODRAFT_TELEMETRY=0` | Disable telemetry |
95
+ | `DO_NOT_TRACK=1` | Disable telemetry (and the update check) |
96
+ | `VIDEODRAFT_NO_UPDATE_CHECK=1` | Disable the update notice |
97
+ | `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` | Proxy support |
98
+
99
+ ## Telemetry
100
+
101
+ The CLI sends anonymous usage events (command name, CLI version, OS, duration, success/error class — **never prompts, file contents, or arguments**) to help us improve it. Opt out any time:
102
+
103
+ ```bash
104
+ videodraft config set telemetry false # or VIDEODRAFT_TELEMETRY=0 / DO_NOT_TRACK=1
105
+ ```
106
+
107
+ ## Embedding (`videodraft/client`)
108
+
109
+ The CLI's core is exported for programmatic use (Node ≥ 20 and Bun):
110
+
111
+ ```ts
112
+ import { VideoDraftClient, resolveAuth } from "videodraft/client";
113
+
114
+ const { tokenProvider, baseUrl } = resolveAuth({}); // flag/env/config-store resolution
115
+ const client = new VideoDraftClient({ tokenProvider, baseUrl });
116
+ const me = await client.callTool("whoami");
117
+ ```
118
+
119
+ ## License
120
+
121
+ MIT © VideoDraft
@@ -0,0 +1,363 @@
1
+ /**
2
+ * JSON-RPC 2.0 client for the VideoDraft MCP endpoint (POST {base}/api/mcp).
3
+ *
4
+ * Runtime-agnostic by design: no commander, no prompts, no process.exit, no
5
+ * console — this file is part of the `videodraft/client` subpath export the
6
+ * macOS app's sidecar consumes. Errors are thrown, never printed.
7
+ */
8
+ interface TokenProvider {
9
+ /** Return a bearer token (vd_mcp_...). Throw AuthError when none is available. */
10
+ getAccessToken(): Promise<string>;
11
+ /**
12
+ * Called once after a 401. Return a fresh token to retry with, or null to
13
+ * give up (the 401 then surfaces as AuthError).
14
+ */
15
+ onUnauthorized?(): Promise<string | null>;
16
+ }
17
+ interface VideoDraftClientOptions {
18
+ tokenProvider: TokenProvider;
19
+ baseUrl?: string;
20
+ fetchImpl?: typeof fetch;
21
+ userAgent?: string;
22
+ /** Per-request timeout in ms. Generation submits can be slow; default 300s (server maxDuration). */
23
+ requestTimeoutMs?: number;
24
+ }
25
+ interface McpToolInfo {
26
+ name: string;
27
+ description: string;
28
+ inputSchema: unknown;
29
+ }
30
+ declare class VideoDraftClient {
31
+ readonly baseUrl: string;
32
+ private readonly tokenProvider;
33
+ private readonly fetchImpl;
34
+ private readonly userAgent;
35
+ private readonly requestTimeoutMs;
36
+ private nextId;
37
+ constructor(options: VideoDraftClientOptions);
38
+ get endpoint(): string;
39
+ rpc<T = any>(method: string, params?: unknown): Promise<T>;
40
+ private post;
41
+ /**
42
+ * Batch several JSON-RPC requests into ONE HTTP round trip (the server
43
+ * implements JSON-RPC 2.0 batching). With N concurrent jobs this turns N
44
+ * polling requests per tick into 1 — the difference between 50 waiting CLIs
45
+ * generating ~17 req/s and ~0.3 req/s against /api/mcp.
46
+ * Results are matched by id (order-independent per spec).
47
+ */
48
+ rpcBatch(calls: Array<{
49
+ method: string;
50
+ params?: unknown;
51
+ }>): Promise<any[]>;
52
+ /**
53
+ * Call several tools in one batched HTTP request. Per-item failures come
54
+ * back as { ok:false, error } instead of throwing, so one failed job can't
55
+ * abort polling of its siblings.
56
+ */
57
+ callToolBatch(calls: Array<{
58
+ name: string;
59
+ args?: Record<string, unknown>;
60
+ }>): Promise<Array<{
61
+ ok: boolean;
62
+ result?: any;
63
+ error?: string;
64
+ }>>;
65
+ /**
66
+ * Call an MCP tool and return its parsed result.
67
+ * The server wraps results as { content: [{type:"text", text}], isError } —
68
+ * text is JSON for successes and "Error: ..." for tool failures.
69
+ */
70
+ callTool<T = any>(name: string, args?: Record<string, unknown>): Promise<T>;
71
+ listTools(): Promise<McpToolInfo[]>;
72
+ ping(): Promise<void>;
73
+ }
74
+ /** Static token provider — for PATs and pre-resolved tokens. */
75
+ declare function staticTokenProvider(token: string): TokenProvider;
76
+
77
+ /**
78
+ * Token resolution + the multi-process-safe OAuth refresh provider.
79
+ *
80
+ * Resolution order (first hit wins):
81
+ * 1. explicit token (--token flag)
82
+ * 2. VIDEODRAFT_API_KEY env
83
+ * 3. the profile in the config store (PAT, or OAuth with auto-refresh)
84
+ *
85
+ * OAuth refresh tokens are SINGLE-USE (the server rotates them), and the CLI
86
+ * shares its config store with other consumers (the macOS app's sidecar). So
87
+ * refresh runs under a cross-process file lock and re-reads the store after
88
+ * acquiring it — if another process already rotated, we use its fresh token
89
+ * instead of redeeming a now-dead refresh token.
90
+ */
91
+
92
+ interface ResolvedAuth {
93
+ tokenProvider: TokenProvider;
94
+ baseUrl: string;
95
+ source: "flag" | "env" | "profile";
96
+ profileName?: string;
97
+ }
98
+ declare function profileTokenProvider(profileName: string, env?: NodeJS.ProcessEnv): TokenProvider;
99
+ declare function resolveAuth(options: {
100
+ token?: string;
101
+ baseUrl?: string;
102
+ profile?: string;
103
+ env?: NodeJS.ProcessEnv;
104
+ }): ResolvedAuth;
105
+
106
+ /**
107
+ * Config store: $VIDEODRAFT_CONFIG_DIR || $XDG_CONFIG_HOME/videodraft || ~/.config/videodraft
108
+ *
109
+ * Deliberately NOT ~/.videodraft — the VideoDraft macOS app owns ~/videodraft/
110
+ * (user workspaces) and dot/no-dot adjacency invites confusion.
111
+ *
112
+ * The schema below is a documented contract: the macOS app's sidecar reads the
113
+ * same store to inherit `videodraft login` credentials (the same way it
114
+ * inherits `claude login` / `codex login`). Bump `version` on breaking changes.
115
+ */
116
+ interface Profile {
117
+ base_url: string;
118
+ auth_kind: "pat" | "oauth";
119
+ access_token: string;
120
+ /** OAuth only. */
121
+ refresh_token?: string;
122
+ /** OAuth only — ISO timestamp the access token expires at. */
123
+ expires_at?: string;
124
+ /** OAuth only — the client_id the grant belongs to. */
125
+ client_id?: string;
126
+ }
127
+ interface CliConfig {
128
+ version: 1;
129
+ active_profile: string;
130
+ profiles: Record<string, Profile>;
131
+ /** Anonymous machine id for opt-out telemetry. */
132
+ anonymous_id?: string;
133
+ /** Explicit telemetry opt-out persisted via `videodraft config set telemetry false`. */
134
+ telemetry?: boolean;
135
+ /** Epoch ms of the last npm update check. */
136
+ last_update_check?: number;
137
+ }
138
+ declare const DEFAULT_BASE_URL = "https://app.videodraft.ai";
139
+ declare function configDir(env?: NodeJS.ProcessEnv): string;
140
+ declare function configPath(env?: NodeJS.ProcessEnv): string;
141
+ declare function readConfig(env?: NodeJS.ProcessEnv): CliConfig;
142
+ /** Atomic write (tmp + rename), 0700 dir / 0600 file. */
143
+ declare function writeConfig(config: CliConfig, env?: NodeJS.ProcessEnv): void;
144
+ declare function updateConfig(mutate: (config: CliConfig) => void, env?: NodeJS.ProcessEnv): CliConfig;
145
+ declare function getProfile(name?: string, env?: NodeJS.ProcessEnv): {
146
+ name: string;
147
+ profile: Profile | undefined;
148
+ config: CliConfig;
149
+ };
150
+
151
+ /**
152
+ * Polling for async work: generation jobs (check_generation_status) and
153
+ * video exports (check_export_status).
154
+ */
155
+
156
+ interface PollOptions {
157
+ intervalMs?: number;
158
+ timeoutMs?: number;
159
+ /**
160
+ * Adaptive backoff (default true): the effective interval grows as a job
161
+ * runs long (base → ×2 after 60s → ×3 after 3min, capped at 15s) with ±10%
162
+ * jitter so many concurrent CLIs don't poll in lockstep. Set false (the CLI
163
+ * does this when --wait-interval is passed explicitly) for a fixed cadence.
164
+ */
165
+ adaptive?: boolean;
166
+ /** Called every poll with the latest payload (drives spinners). */
167
+ onTick?: (status: string, payload: any) => void;
168
+ }
169
+ /** Effective poll delay for a job that's been running `elapsedMs`. */
170
+ declare function nextPollDelay(baseMs: number, elapsedMs: number, adaptive?: boolean): number;
171
+ interface GenerationResult {
172
+ status: string;
173
+ outputUrls: string[];
174
+ payload: any;
175
+ }
176
+ /** Extract output media URLs across the server's result shapes. */
177
+ declare function extractOutputUrls(payload: any): string[];
178
+ declare function pollGeneration(client: VideoDraftClient, jobId: string, options?: PollOptions): Promise<GenerationResult>;
179
+ /**
180
+ * Poll MANY jobs from ONE process with ONE batched JSON-RPC request per tick.
181
+ * This is the multi-CLI answer: N parallel generations should not mean N idle
182
+ * Node processes and N req/tick — submit with --no-wait, then
183
+ * `videodraft wait <id1> <id2> ...` (or the macOS app's sidecar calling this).
184
+ * Per-job tool failures resolve that job as status "failed" without aborting
185
+ * the rest.
186
+ */
187
+ declare function pollGenerationsBatch(client: VideoDraftClient, jobIds: string[], options?: PollOptions & {
188
+ onJobDone?: (jobId: string, result: GenerationResult) => void;
189
+ }): Promise<Map<string, GenerationResult>>;
190
+ declare function pollExport(client: VideoDraftClient, ref: {
191
+ exportId?: string;
192
+ projectId?: string;
193
+ }, options?: PollOptions): Promise<{
194
+ status: string;
195
+ videoUrl?: string;
196
+ payload: any;
197
+ }>;
198
+ /** Poll many jobs concurrently (used by `shots --wait` batches). */
199
+ declare function pollGenerations(client: VideoDraftClient, jobIds: string[], options?: PollOptions): Promise<GenerationResult[]>;
200
+
201
+ /**
202
+ * Download generated media to local files.
203
+ *
204
+ * Path templates (genmedia-style): {job_id} {index} {ext} {name}
205
+ * videodraft generate image "..." --download "./out/{job_id}_{index}.{ext}"
206
+ * A template without placeholders and without an extension is treated as a
207
+ * directory: <dir>/<job_id>_<index>.<ext>.
208
+ */
209
+ interface DownloadedFile {
210
+ url: string;
211
+ path: string;
212
+ bytes: number;
213
+ }
214
+ declare function extFromUrl(url: string, fallback?: string): string;
215
+ declare function renderTemplate(template: string, vars: {
216
+ job_id?: string;
217
+ index: number;
218
+ ext: string;
219
+ name?: string;
220
+ }): string;
221
+ declare function downloadUrl(url: string, destPath: string, fetchImpl?: typeof fetch): Promise<DownloadedFile>;
222
+ declare function downloadOutputs(urls: string[], template: string, vars: {
223
+ job_id?: string;
224
+ name?: string;
225
+ }, fetchImpl?: typeof fetch): Promise<DownloadedFile[]>;
226
+
227
+ /**
228
+ * Local-file upload via the MCP two-step flow:
229
+ * create_media_upload → PUT bytes to the presigned upload_url → finalize_media_upload
230
+ * The bytes go directly to GCS; they never pass through the MCP server.
231
+ */
232
+
233
+ declare function guessContentType(filename: string): string | undefined;
234
+ interface UploadResult {
235
+ url: string;
236
+ file_path?: string;
237
+ [key: string]: unknown;
238
+ }
239
+ declare function uploadFile(client: VideoDraftClient, localPath: string, options?: {
240
+ contentType?: string;
241
+ fetchImpl?: typeof fetch;
242
+ }): Promise<UploadResult>;
243
+
244
+ /**
245
+ * Error taxonomy + stable exit codes.
246
+ *
247
+ * 0 success
248
+ * 1 generic / server / network error
249
+ * 2 usage error (bad flags/args — commander's default)
250
+ * 3 authentication required or invalid
251
+ * 4 insufficient credits
252
+ *
253
+ * Agents and scripts can branch on these; they are part of the CLI contract
254
+ * and documented in the README.
255
+ */
256
+ declare const EXIT: {
257
+ readonly OK: 0;
258
+ readonly ERROR: 1;
259
+ readonly USAGE: 2;
260
+ readonly AUTH: 3;
261
+ readonly CREDITS: 4;
262
+ };
263
+ declare class CliError extends Error {
264
+ exitCode: number;
265
+ /** One-line remediation shown under the error (human mode only). */
266
+ hint?: string;
267
+ constructor(message: string, exitCode?: number, hint?: string);
268
+ }
269
+ declare class AuthError extends CliError {
270
+ constructor(message?: string);
271
+ }
272
+ /** JSON-RPC layer error (the envelope, not the tool). */
273
+ declare class RpcError extends CliError {
274
+ code: number;
275
+ data?: unknown;
276
+ constructor(code: number, message: string, data?: unknown);
277
+ }
278
+ /** A tools/call result that came back isError:true. */
279
+ declare class ToolError extends CliError {
280
+ toolName: string;
281
+ constructor(toolName: string, message: string);
282
+ }
283
+ declare class TimeoutError extends CliError {
284
+ constructor(message: string);
285
+ }
286
+
287
+ /**
288
+ * OAuth 2.1 authorization-code + PKCE flow against the VideoDraft AS,
289
+ * using an RFC 8252 loopback redirect (http://127.0.0.1:<ephemeral-port>/callback).
290
+ *
291
+ * Server contract (app/api/oauth/* in the VideoDraft app):
292
+ * GET /api/oauth/authorize ?response_type=code&client_id&redirect_uri&scope
293
+ * &state&code_challenge&code_challenge_method=S256&resource
294
+ * POST /api/oauth/token form or JSON; authorization_code | refresh_token grants
295
+ * POST /api/oauth/register RFC 7591 DCR (https or http-loopback redirect URIs only)
296
+ * POST /api/oauth/revoke RFC 7009
297
+ *
298
+ * Client identity: the static public client `vd_client_cli` (pre-registered in
299
+ * oauth_clients — see sql/ in this repo). When that id is missing on the target
300
+ * server (e.g. a fresh local stack), we fall back to Dynamic Client Registration
301
+ * and persist the issued client_id in the profile.
302
+ */
303
+ declare const STATIC_CLI_CLIENT_ID = "vd_client_cli";
304
+ interface OAuthTokens {
305
+ access_token: string;
306
+ refresh_token: string;
307
+ expires_in: number;
308
+ scope?: string;
309
+ }
310
+ declare function createPkcePair(): {
311
+ verifier: string;
312
+ challenge: string;
313
+ };
314
+ declare function buildAuthorizeUrl(params: {
315
+ baseUrl: string;
316
+ clientId: string;
317
+ redirectUri: string;
318
+ state: string;
319
+ codeChallenge: string;
320
+ }): string;
321
+ /**
322
+ * Is this client_id known to the server? The authorize endpoint renders a
323
+ * non-redirect 400 HTML page for an unknown client, and 3xx-redirects to the
324
+ * consent page for a valid request — probe with redirect:"manual" so we can
325
+ * fall back to DCR before ever opening a browser tab on an error page.
326
+ */
327
+ declare function probeClientId(baseUrl: string, clientId: string, redirectUri: string, fetchImpl?: typeof fetch): Promise<boolean>;
328
+ /** RFC 7591 dynamic registration fallback. Returns the issued client_id. */
329
+ declare function registerCliClient(baseUrl: string, fetchImpl?: typeof fetch): Promise<string>;
330
+ /**
331
+ * Run the loopback listener + browser round trip and return the authorization
332
+ * code. `onAuthorizeUrl` receives the URL to open (the command layer opens the
333
+ * browser and prints it — this module stays headless/testable).
334
+ */
335
+ declare function authorizeViaLoopback(params: {
336
+ baseUrl: string;
337
+ clientId: string;
338
+ onAuthorizeUrl: (url: string) => void | Promise<void>;
339
+ timeoutMs?: number;
340
+ fetchImpl?: typeof fetch;
341
+ }): Promise<{
342
+ code: string;
343
+ redirectUri: string;
344
+ verifier: string;
345
+ }>;
346
+ declare function exchangeCode(params: {
347
+ baseUrl: string;
348
+ clientId: string;
349
+ code: string;
350
+ redirectUri: string;
351
+ verifier: string;
352
+ fetchImpl?: typeof fetch;
353
+ }): Promise<OAuthTokens>;
354
+ declare function refreshAccessToken(params: {
355
+ baseUrl: string;
356
+ clientId: string;
357
+ refreshToken: string;
358
+ fetchImpl?: typeof fetch;
359
+ }): Promise<OAuthTokens | null>;
360
+ /** RFC 7009 revocation — best-effort, used by `videodraft logout`. */
361
+ declare function revokeToken(baseUrl: string, token: string, fetchImpl?: typeof fetch): Promise<void>;
362
+
363
+ export { AuthError, type CliConfig, CliError, DEFAULT_BASE_URL, type DownloadedFile, EXIT, type GenerationResult, type McpToolInfo, type PollOptions, type Profile, type ResolvedAuth, RpcError, STATIC_CLI_CLIENT_ID, TimeoutError, type TokenProvider, ToolError, type UploadResult, VideoDraftClient, type VideoDraftClientOptions, authorizeViaLoopback, buildAuthorizeUrl, configDir, configPath, createPkcePair, downloadOutputs, downloadUrl, exchangeCode, extFromUrl, extractOutputUrls, getProfile, guessContentType, nextPollDelay, pollExport, pollGeneration, pollGenerations, pollGenerationsBatch, probeClientId, profileTokenProvider, readConfig, refreshAccessToken, registerCliClient, renderTemplate, resolveAuth, revokeToken, staticTokenProvider, updateConfig, uploadFile, writeConfig };