faceless-cli 1.1.6 → 1.1.8

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/src/index.mjs CHANGED
@@ -8,773 +8,661 @@ import { print, printError } from "./output.mjs";
8
8
  const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
9
9
  let spec;
10
10
  try {
11
- spec = JSON.parse(
12
- fs.readFileSync(new URL("./generated/operations.json", import.meta.url), "utf8")
13
- );
11
+ spec = JSON.parse(fs.readFileSync(new URL("./generated/operations.json", import.meta.url), "utf8"));
14
12
  } catch {
15
- process.stderr.write(
16
- 'cli/src/generated/operations.json is missing. Run "npm run generate:agents" in the repo root to generate it.\n'
17
- );
18
- process.exit(1);
13
+ process.stderr.write('cli/src/generated/operations.json is missing. Run "npm run generate:agents" in the repo root to generate it.\n');
14
+ process.exit(1);
19
15
  }
20
16
  const operations = spec.operations;
21
17
 
22
18
  function getOp(operationId) {
23
- const op = operations.find((o) => o.operationId === operationId);
24
- if (!op) {
25
- throw new CliError("internal_error", `Unknown operation: ${operationId}`);
26
- }
27
- return op;
19
+ const op = operations.find((o) => o.operationId === operationId);
20
+ if (!op) {
21
+ throw new CliError("internal_error", `Unknown operation: ${operationId}`);
22
+ }
23
+ return op;
28
24
  }
29
25
 
30
26
  function toInt(value) {
31
- const n = parseInt(value, 10);
32
- if (Number.isNaN(n)) throw new CliError("invalid_input", `Not a number: ${value}`);
33
- return n;
27
+ const n = parseInt(value, 10);
28
+ if (Number.isNaN(n)) throw new CliError("invalid_input", `Not a number: ${value}`);
29
+ return n;
34
30
  }
35
31
 
36
32
  function splitList(value) {
37
- return value
38
- .split(",")
39
- .map((v) => v.trim())
40
- .filter(Boolean);
33
+ return value
34
+ .split(",")
35
+ .map((v) => v.trim())
36
+ .filter(Boolean);
41
37
  }
42
38
 
43
39
  function compact(obj) {
44
- const out = {};
45
- for (const [key, value] of Object.entries(obj)) {
46
- if (value !== undefined && value !== null) out[key] = value;
47
- }
48
- return out;
40
+ const out = {};
41
+ for (const [key, value] of Object.entries(obj)) {
42
+ if (value !== undefined && value !== null) out[key] = value;
43
+ }
44
+ return out;
49
45
  }
50
46
 
51
47
  const MODELS = ["storyboard", "motion_lite", "motion_pro"];
52
48
  const PLATFORMS = ["youtube", "tiktok", "instagram", "x", "facebook", "linkedin", "threads"];
53
- const OPTION_KINDS = [
54
- "sources",
55
- "styles",
56
- "niches",
57
- "languages",
58
- "durations",
59
- "models",
60
- "captionThemes",
61
- "music",
62
- "backgrounds",
63
- "subreddits",
64
- ];
65
-
66
- async function execute(
67
- operationId,
68
- { pathParams = {}, query = {}, body, idempotencyKey } = {},
69
- cmd
70
- ) {
71
- const globals = cmd.optsWithGlobals();
72
- const op = getOp(operationId);
73
- const path = op.path.replace(/\{(\w+)\}/g, (_, name) => {
74
- const value = pathParams[name];
75
- if (value === undefined) {
76
- throw new CliError("invalid_input", `Missing required parameter: ${name}`);
77
- }
78
- return encodeURIComponent(String(value));
79
- });
80
- return request({
81
- method: op.method,
82
- path,
83
- query: compact(query),
84
- body: body === undefined ? undefined : compact(body),
85
- apiKey: resolveApiKey(globals),
86
- baseUrl: resolveBaseUrl(globals),
87
- idempotencyKey,
88
- });
49
+ const OPTION_KINDS = ["sources", "styles", "niches", "languages", "durations", "models", "captionThemes", "music", "backgrounds", "subreddits"];
50
+
51
+ async function execute(operationId, { pathParams = {}, query = {}, body, idempotencyKey } = {}, cmd) {
52
+ const globals = cmd.optsWithGlobals();
53
+ const op = getOp(operationId);
54
+ const path = op.path.replace(/\{(\w+)\}/g, (_, name) => {
55
+ const value = pathParams[name];
56
+ if (value === undefined) {
57
+ throw new CliError("invalid_input", `Missing required parameter: ${name}`);
58
+ }
59
+ return encodeURIComponent(String(value));
60
+ });
61
+ return request({
62
+ method: op.method,
63
+ path,
64
+ query: compact(query),
65
+ body: body === undefined ? undefined : compact(body),
66
+ apiKey: resolveApiKey(globals),
67
+ baseUrl: resolveBaseUrl(globals),
68
+ idempotencyKey,
69
+ });
89
70
  }
90
71
 
91
72
  async function waitForTerminal({ operationId, label, id, timeoutSec, cmd }) {
92
- const terminal = getOp(operationId).terminalStates || [];
93
- const deadline = Date.now() + timeoutSec * 1000;
94
- for (;;) {
95
- const result = await execute(operationId, { pathParams: { id } }, cmd);
96
- const data = (result && result.data) || {};
97
- if (terminal.includes(data.status)) {
98
- return result;
99
- }
100
- if (Date.now() >= deadline) {
101
- throw new CliError(
102
- "timeout",
103
- `Timed out after ${timeoutSec}s waiting for ${label} ${id} (last status: ${data.status || "unknown"})`
104
- );
105
- }
106
- await sleep(5000);
107
- }
73
+ const terminal = getOp(operationId).terminalStates || [];
74
+ const deadline = Date.now() + timeoutSec * 1000;
75
+ for (;;) {
76
+ const result = await execute(operationId, { pathParams: { id } }, cmd);
77
+ const data = (result && result.data) || {};
78
+ if (terminal.includes(data.status)) {
79
+ return result;
80
+ }
81
+ if (Date.now() >= deadline) {
82
+ throw new CliError("timeout", `Timed out after ${timeoutSec}s waiting for ${label} ${id} (last status: ${data.status || "unknown"})`);
83
+ }
84
+ await sleep(5000);
85
+ }
108
86
  }
109
87
 
110
88
  // Polls GET /videos/{id}/status until completed or failed.
111
89
  function waitForVideo(id, timeoutSec, cmd) {
112
- return waitForTerminal({ operationId: "getVideoStatus", label: "video", id, timeoutSec, cmd });
90
+ return waitForTerminal({ operationId: "getVideoStatus", label: "video", id, timeoutSec, cmd });
113
91
  }
114
92
 
115
93
  // Polls GET /renders/{id} until done or error.
116
94
  function waitForRender(id, timeoutSec, cmd) {
117
- return waitForTerminal({ operationId: "getRender", label: "render", id, timeoutSec, cmd });
95
+ return waitForTerminal({ operationId: "getRender", label: "render", id, timeoutSec, cmd });
118
96
  }
119
97
 
120
98
  function noteCost(operationId) {
121
- const cost = getOp(operationId).creditCost;
122
- if (cost && cost !== "none") process.stderr.write(`This costs ${cost}.\n`);
99
+ const cost = getOp(operationId).creditCost;
100
+ if (cost && cost !== "none") process.stderr.write(`This costs ${cost}.\n`);
123
101
  }
124
102
 
125
103
  // Async create commands share one shape: state the cost, fire the request,
126
104
  // then optionally poll the created video's status until it is terminal.
127
105
  async function createAndMaybeWait(operationId, body, opts, cmd) {
128
- const globals = cmd.optsWithGlobals();
129
- noteCost(operationId);
130
- const created = await execute(operationId, { body, idempotencyKey: opts.idempotencyKey }, cmd);
131
- const id = created?.data?.id;
132
- if (!opts.wait || !id) {
133
- print(created, { json: globals.json });
134
- return;
135
- }
136
- print(await waitForVideo(id, opts.timeout, cmd), { json: globals.json });
106
+ const globals = cmd.optsWithGlobals();
107
+ noteCost(operationId);
108
+ const created = await execute(operationId, { body, idempotencyKey: opts.idempotencyKey }, cmd);
109
+ const id = created?.data?.id;
110
+ if (!opts.wait || !id) {
111
+ print(created, { json: globals.json });
112
+ return;
113
+ }
114
+ print(await waitForVideo(id, opts.timeout, cmd), { json: globals.json });
137
115
  }
138
116
 
139
117
  function withWaitFlags(command, what) {
140
- return command
141
- .option("--wait", `poll until the ${what} reaches a terminal state`)
142
- .option("--timeout <sec>", "max seconds to wait with --wait", toInt, 600);
118
+ return command.option("--wait", `poll until the ${what} reaches a terminal state`).option("--timeout <sec>", "max seconds to wait with --wait", toInt, 600);
143
119
  }
144
120
 
145
121
  // Series settings shared by "series create" and "series update". Field names
146
122
  // mirror the createSeries request schema.
147
123
  function withSeriesFlags(command) {
148
- return command
149
- .option("--niche <niche>", "content niche, e.g. scary stories (see: faceless options --kind niches)")
150
- .option("--custom-prompt <text>", "custom topic prompt used instead of (or alongside) a niche")
151
- .option("--voice <id>", "TTS voice id for narration (see: faceless voices)")
152
- .option("--style <style>", "visual style for generated imagery (see: faceless options --kind styles)")
153
- .option("--language <language>", "video language, e.g. English")
154
- .addOption(
155
- new Option("--duration <sec>", "target episode length in seconds").choices(["30", "60", "90"])
156
- )
157
- .option("--destination <platform>", "primary auto-post destination, e.g. youtube or tiktok")
158
- .option(
159
- "--destination-accounts <id[,id...]>",
160
- "connected account ids to auto-post to (see: faceless accounts)"
161
- )
162
- .option("--auto-post-time <HH:mm>", "daily auto-post time in the series timezone")
163
- .option(
164
- "--posting-days <days>",
165
- "comma-separated days of the week to post, e.g. Monday,Wednesday (omit for every day)"
166
- )
167
- .option("--timezone <tz>", "IANA timezone for scheduling, e.g. America/New_York")
168
- .option("--caption-style <name>", "caption theme name (see: faceless options --kind captionThemes)")
169
- .option("--subreddit <name>", 'subreddit to pull posts from when source is "Reddit post"')
170
- .option(
171
- "--background-video <id>",
172
- "background gameplay/footage id (see: faceless options --kind backgrounds)"
173
- )
174
- .option("--random-background-video", "pick a random background video per episode")
175
- .option("--layout <layout>", "video layout variant")
176
- .addOption(
177
- new Option("--broll-model <model>", "generation model for visuals").choices(MODELS)
178
- )
179
- .option("--show-emojis", "overlay emojis on captions")
180
- .option("--enable-background-music", "mix background music under the narration")
181
- .option(
182
- "--background-music-mood <mood>",
183
- "background music mood (see: faceless options --kind music)"
184
- )
185
- .option("--hashtags <text>", "hashtags appended to post captions")
186
- .option("--tone <tone>", "writing tone for generated scripts")
187
- .addOption(
188
- new Option("--youtube-privacy <p>", "privacy for auto-posted YouTube videos").choices([
189
- "public",
190
- "unlisted",
191
- "private",
192
- ])
193
- );
124
+ return command
125
+ .option("--niche <niche>", "content niche, e.g. scary stories (see: faceless options --kind niches)")
126
+ .option("--custom-prompt <text>", "custom topic prompt used instead of (or alongside) a niche")
127
+ .option("--voice <id>", "TTS voice id for narration (see: faceless voices)")
128
+ .option("--style <style>", "visual style for generated imagery (see: faceless options --kind styles)")
129
+ .option("--language <language>", "video language, e.g. English")
130
+ .addOption(new Option("--duration <sec>", "target episode length in seconds").choices(["30", "60", "90"]))
131
+ .option("--destination <platform>", "primary auto-post destination, e.g. youtube or tiktok")
132
+ .option("--destination-accounts <id[,id...]>", "connected account ids to auto-post to (see: faceless accounts)")
133
+ .option("--auto-post-time <HH:mm>", "daily auto-post time in the series timezone")
134
+ .option("--posting-days <days>", "comma-separated days of the week to post, e.g. Monday,Wednesday (omit for every day)")
135
+ .option("--timezone <tz>", "IANA timezone for scheduling, e.g. America/New_York")
136
+ .option("--caption-style <name>", "caption theme name (see: faceless options --kind captionThemes)")
137
+ .option("--subreddit <name>", 'subreddit to pull posts from when source is "Reddit post"')
138
+ .option("--background-video <id>", "background gameplay/footage id (see: faceless options --kind backgrounds)")
139
+ .option("--random-background-video", "pick a random background video per episode")
140
+ .option("--layout <layout>", "video layout variant")
141
+ .addOption(new Option("--broll-model <model>", "generation model for visuals").choices(MODELS))
142
+ .option("--show-emojis", "overlay emojis on captions")
143
+ .option("--enable-background-music", "mix background music under the narration")
144
+ .option("--background-music-mood <mood>", "background music mood (see: faceless options --kind music)")
145
+ .option("--hashtags <text>", "hashtags appended to post captions")
146
+ .option("--tone <tone>", "writing tone for generated scripts")
147
+ .addOption(new Option("--youtube-privacy <p>", "privacy for auto-posted YouTube videos").choices(["public", "unlisted", "private"]));
194
148
  }
195
149
 
196
150
  function seriesBody(opts) {
197
- return compact({
198
- name: opts.name,
199
- source: opts.source,
200
- niche: opts.niche,
201
- customPrompt: opts.customPrompt,
202
- voice: opts.voice,
203
- style: opts.style,
204
- language: opts.language,
205
- duration: opts.duration,
206
- destination: opts.destination,
207
- destinationAccounts: opts.destinationAccounts ? splitList(opts.destinationAccounts) : undefined,
208
- autoPostTime: opts.autoPostTime,
209
- postingDays: opts.postingDays ? splitList(opts.postingDays) : undefined,
210
- timezone: opts.timezone,
211
- captionStyle: opts.captionStyle,
212
- subreddit: opts.subreddit,
213
- backgroundVideo: opts.backgroundVideo,
214
- useRandomBackgroundVideo: opts.randomBackgroundVideo,
215
- layout: opts.layout,
216
- brollModel: opts.brollModel,
217
- showEmojis: opts.showEmojis,
218
- enableBackgroundMusic: opts.enableBackgroundMusic,
219
- backgroundMusicMood: opts.backgroundMusicMood,
220
- hashtags: opts.hashtags,
221
- tone: opts.tone,
222
- youtubePrivacyStatus: opts.youtubePrivacy,
223
- paused: opts.paused,
224
- });
151
+ return compact({
152
+ name: opts.name,
153
+ source: opts.source,
154
+ niche: opts.niche,
155
+ customPrompt: opts.customPrompt,
156
+ voice: opts.voice,
157
+ style: opts.style,
158
+ language: opts.language,
159
+ duration: opts.duration,
160
+ destination: opts.destination,
161
+ destinationAccounts: opts.destinationAccounts ? splitList(opts.destinationAccounts) : undefined,
162
+ autoPostTime: opts.autoPostTime,
163
+ postingDays: opts.postingDays ? splitList(opts.postingDays) : undefined,
164
+ timezone: opts.timezone,
165
+ captionStyle: opts.captionStyle,
166
+ subreddit: opts.subreddit,
167
+ backgroundVideo: opts.backgroundVideo,
168
+ useRandomBackgroundVideo: opts.randomBackgroundVideo,
169
+ layout: opts.layout,
170
+ brollModel: opts.brollModel,
171
+ showEmojis: opts.showEmojis,
172
+ enableBackgroundMusic: opts.enableBackgroundMusic,
173
+ backgroundMusicMood: opts.backgroundMusicMood,
174
+ hashtags: opts.hashtags,
175
+ tone: opts.tone,
176
+ youtubePrivacyStatus: opts.youtubePrivacy,
177
+ paused: opts.paused,
178
+ });
225
179
  }
226
180
 
227
181
  const program = new Command();
228
182
 
229
183
  program
230
- .name("faceless")
231
- .description(
232
- "Faceless.so CLI: create AI faceless videos, run automated series and publish to YouTube, TikTok, Instagram and more."
233
- )
234
- .version(pkg.version)
235
- .option("--json", "output raw JSON")
236
- .option("--api-key <key>", "API key (overrides env and config file)")
237
- .option("--api-url <url>", "API base URL (default https://faceless.so/api/v1)");
184
+ .name("faceless")
185
+ .description("Faceless.so CLI: create AI faceless videos, run automated series and publish to YouTube, TikTok, Instagram and more.")
186
+ .version(pkg.version)
187
+ .option("--json", "output raw JSON")
188
+ .option("--api-key <key>", "API key (overrides env and config file)")
189
+ .option("--api-url <url>", "API base URL (default https://faceless.so/api/v1)");
238
190
 
239
191
  program
240
- .command("login")
241
- .description("Save an API key to ~/.faceless/config.json (verified against /me)")
242
- .action(async (_opts, cmd) => {
243
- const globals = cmd.optsWithGlobals();
244
- let key = globals.apiKey;
245
- if (!key) {
246
- const rl = readline.createInterface({
247
- input: process.stdin,
248
- output: process.stderr,
249
- });
250
- key = (await rl.question("API key (fl_live_...): ")).trim();
251
- rl.close();
252
- }
253
- if (!key) throw new CliError("invalid_input", "No API key provided");
254
- const result = await request({
255
- method: "get",
256
- path: "/me",
257
- apiKey: key,
258
- baseUrl: resolveBaseUrl(globals),
259
- });
260
- saveConfig({ apiKey: key });
261
- if (globals.json || !process.stdout.isTTY) {
262
- print(result, { json: true });
263
- return;
264
- }
265
- const data = (result && result.data) || {};
266
- const scopes = data.auth?.scopes || [];
267
- process.stdout.write(`Logged in as ${data.team?.name || "unknown team"}\n`);
268
- process.stdout.write(`Scopes: ${scopes.length ? scopes.join(", ") : "(none)"}\n`);
269
- process.stdout.write(`Saved key to ${CONFIG_PATH}\n`);
270
- });
192
+ .command("login")
193
+ .description("Log in via OAuth in your browser (default), or save an API key with --api-key / --manual")
194
+ .option("--manual", "prompt for an API key instead of the browser OAuth flow")
195
+ .action(async (opts, cmd) => {
196
+ const globals = cmd.optsWithGlobals();
197
+ const baseUrl = resolveBaseUrl(globals);
198
+
199
+ // The key path, kept for CI and anyone who prefers long-lived keys.
200
+ let key = globals.apiKey;
201
+ if (!key && opts.manual) {
202
+ const rl = readline.createInterface({
203
+ input: process.stdin,
204
+ output: process.stderr,
205
+ });
206
+ key = (await rl.question("API key (fl_live_...): ")).trim();
207
+ rl.close();
208
+ if (!key) throw new CliError("invalid_input", "No API key provided");
209
+ }
210
+
211
+ let verifyKey = key;
212
+ if (key) {
213
+ // Exclusive on purpose: exactly one stored credential, so precedence never surprises.
214
+ saveConfig({ apiKey: key, oauth: undefined });
215
+ } else {
216
+ // Default: the browser OAuth flow. This is the path an agent can complete - no key to paste.
217
+ const { loginWithOAuth } = await import("./oauth.mjs");
218
+ const tokens = await loginWithOAuth({ baseUrl, log: (line) => process.stderr.write(line) });
219
+ saveConfig({ oauth: tokens, apiKey: undefined });
220
+ verifyKey = tokens.accessToken;
221
+ }
222
+
223
+ const result = await request({ method: "get", path: "/me", apiKey: verifyKey, baseUrl });
224
+ if (key) saveConfig({ apiKey: key, oauth: undefined }); // keep only after /me verified
225
+ if (globals.json || !process.stdout.isTTY) {
226
+ print(result, { json: true });
227
+ return;
228
+ }
229
+ const data = (result && result.data) || {};
230
+ const scopes = data.auth?.scopes || [];
231
+ process.stdout.write(`Logged in as ${data.team?.name || "unknown team"}\n`);
232
+ process.stdout.write(`Scopes: ${scopes.length ? scopes.join(", ") : "(none)"}\n`);
233
+ process.stdout.write(`Saved ${key ? "key" : "OAuth session"} to ${CONFIG_PATH}\n`);
234
+ });
271
235
 
272
236
  program
273
- .command("logout")
274
- .description("Remove the saved API key from ~/.faceless/config.json")
275
- .action(async () => {
276
- const config = loadConfig();
277
- if (!config.apiKey) {
278
- process.stdout.write("No saved API key.\n");
279
- return;
280
- }
281
- delete config.apiKey;
282
- saveConfig({ ...config, apiKey: undefined });
283
- process.stdout.write(`Removed API key from ${CONFIG_PATH}\n`);
284
- });
237
+ .command("logout")
238
+ .description("Forget the stored credential (revokes the OAuth session server-side when possible)")
239
+ .action(async (_opts, cmd) => {
240
+ const globals = cmd.optsWithGlobals();
241
+ const config = loadConfig();
242
+ if (config.oauth?.refreshToken) {
243
+ // Best-effort revocation: RFC 7009 always answers 200, and a network failure must not stop
244
+ // the local forget - the refresh token is useless without the config file anyway.
245
+ const site = config.oauth.site || resolveBaseUrl(globals).replace(/\/api\/v1$/, "");
246
+ await fetch(`${site}/api/oauth2/revoke`, {
247
+ method: "POST",
248
+ headers: { "Content-Type": "application/json" },
249
+ body: JSON.stringify({ token: config.oauth.refreshToken }),
250
+ }).catch(() => {});
251
+ }
252
+ saveConfig({ apiKey: undefined, oauth: undefined });
253
+ process.stdout.write("Logged out.\n");
254
+ });
285
255
 
286
256
  program
287
- .command("whoami")
288
- .description(getOp("getMe").summary)
289
- .action(async (_opts, cmd) => {
290
- const result = await execute("getMe", {}, cmd);
291
- print(result, { json: cmd.optsWithGlobals().json });
292
- });
257
+ .command("whoami")
258
+ .description(getOp("getMe").summary)
259
+ .action(async (_opts, cmd) => {
260
+ const result = await execute("getMe", {}, cmd);
261
+ print(result, { json: cmd.optsWithGlobals().json });
262
+ });
293
263
 
294
264
  program
295
- .command("credits")
296
- .description(getOp("getCredits").summary)
297
- .option("--page <n>", "page number", toInt)
298
- .option("--limit <n>", "items per page (max 100)", toInt)
299
- .action(async (opts, cmd) => {
300
- const result = await execute("getCredits", { query: { page: opts.page, limit: opts.limit } }, cmd);
301
- print(result, { json: cmd.optsWithGlobals().json });
302
- });
265
+ .command("credits")
266
+ .description(getOp("getCredits").summary)
267
+ .option("--page <n>", "page number", toInt)
268
+ .option("--limit <n>", "items per page (max 100)", toInt)
269
+ .action(async (opts, cmd) => {
270
+ const result = await execute("getCredits", { query: { page: opts.page, limit: opts.limit } }, cmd);
271
+ print(result, { json: cmd.optsWithGlobals().json });
272
+ });
303
273
 
304
274
  const videos = program.command("videos").description("Faceless video projects");
305
275
 
306
276
  withWaitFlags(
307
- videos
308
- .command("create")
309
- .description(getOp("createVideo").summary)
310
- .requiredOption("--script <text>", "the full narration script the video is generated from")
311
- .requiredOption("--voice-id <id>", "TTS voice id for the narration (see: faceless voices)")
312
- .addOption(
313
- new Option(
314
- "--model <model>",
315
- "generation model: storyboard (20 credits), motion_lite (50) or motion_pro (100)"
316
- ).choices(MODELS)
317
- )
318
- .option("--style <style>", "visual style for generated imagery (see: faceless options --kind styles)")
319
- .option("--language <language>", "script language, e.g. English")
320
- .option("--name <name>", "project name; defaults to the start of the script")
321
- .option("--enable-background-music", "mix background music under the narration")
322
- .option("--master-style <text>", "extra style directive applied to every generated scene")
323
- .option("--global-negative-prompt <text>", "things the image model should avoid in every scene")
324
- .option("--idempotency-key <k>", "Idempotency-Key header value"),
325
- "video"
277
+ videos
278
+ .command("create")
279
+ .description(getOp("createVideo").summary)
280
+ .requiredOption("--script <text>", "the full narration script the video is generated from")
281
+ .requiredOption("--voice-id <id>", "TTS voice id for the narration (see: faceless voices)")
282
+ .addOption(new Option("--model <model>", "generation model: storyboard (20 credits), motion_lite (50) or motion_pro (100)").choices(MODELS))
283
+ .option("--style <style>", "visual style for generated imagery (see: faceless options --kind styles)")
284
+ .option("--language <language>", "script language, e.g. English")
285
+ .option("--name <name>", "project name; defaults to the start of the script")
286
+ .option("--enable-background-music", "mix background music under the narration")
287
+ .option("--master-style <text>", "extra style directive applied to every generated scene")
288
+ .option("--global-negative-prompt <text>", "things the image model should avoid in every scene")
289
+ .option("--idempotency-key <k>", "Idempotency-Key header value"),
290
+ "video"
326
291
  ).action(async (opts, cmd) => {
327
- await createAndMaybeWait(
328
- "createVideo",
329
- {
330
- script: opts.script,
331
- voiceId: opts.voiceId,
332
- model: opts.model,
333
- style: opts.style,
334
- language: opts.language,
335
- name: opts.name,
336
- enableBackgroundMusic: opts.enableBackgroundMusic,
337
- masterStyle: opts.masterStyle,
338
- globalNegativePrompt: opts.globalNegativePrompt,
339
- },
340
- opts,
341
- cmd
342
- );
292
+ await createAndMaybeWait(
293
+ "createVideo",
294
+ {
295
+ script: opts.script,
296
+ voiceId: opts.voiceId,
297
+ model: opts.model,
298
+ style: opts.style,
299
+ language: opts.language,
300
+ name: opts.name,
301
+ enableBackgroundMusic: opts.enableBackgroundMusic,
302
+ masterStyle: opts.masterStyle,
303
+ globalNegativePrompt: opts.globalNegativePrompt,
304
+ },
305
+ opts,
306
+ cmd
307
+ );
343
308
  });
344
309
 
345
310
  withWaitFlags(
346
- videos
347
- .command("captions")
348
- .description(getOp("createCaptionVideo").summary)
349
- .option("--video-url <url>", "public URL of the video file to caption")
350
- .option("--audio-url <url>", "public URL of an audio file to turn into a captioned video")
351
- .option("--name <name>", "project name")
352
- .option("--language <language>", "spoken language of the file, e.g. English")
353
- .option("--idempotency-key <k>", "Idempotency-Key header value"),
354
- "video"
311
+ videos
312
+ .command("captions")
313
+ .description(getOp("createCaptionVideo").summary)
314
+ .option("--video-url <url>", "public URL of the video file to caption")
315
+ .option("--audio-url <url>", "public URL of an audio file to turn into a captioned video")
316
+ .option("--name <name>", "project name")
317
+ .option("--language <language>", "spoken language of the file, e.g. English")
318
+ .option("--idempotency-key <k>", "Idempotency-Key header value"),
319
+ "video"
355
320
  ).action(async (opts, cmd) => {
356
- await createAndMaybeWait(
357
- "createCaptionVideo",
358
- {
359
- videoUrl: opts.videoUrl,
360
- audioUrl: opts.audioUrl,
361
- name: opts.name,
362
- language: opts.language,
363
- },
364
- opts,
365
- cmd
366
- );
321
+ await createAndMaybeWait(
322
+ "createCaptionVideo",
323
+ {
324
+ videoUrl: opts.videoUrl,
325
+ audioUrl: opts.audioUrl,
326
+ name: opts.name,
327
+ language: opts.language,
328
+ },
329
+ opts,
330
+ cmd
331
+ );
367
332
  });
368
333
 
369
334
  videos
370
- .command("list")
371
- .description(getOp("listVideos").summary)
372
- .option("--archived", "only archived videos")
373
- .option("--page <n>", "page number", toInt)
374
- .option("--limit <n>", "items per page (max 100)", toInt)
375
- .action(async (opts, cmd) => {
376
- const result = await execute(
377
- "listVideos",
378
- { query: { archived: opts.archived, page: opts.page, limit: opts.limit } },
379
- cmd
380
- );
381
- print(result, { json: cmd.optsWithGlobals().json });
382
- });
335
+ .command("list")
336
+ .description(getOp("listVideos").summary)
337
+ .option("--archived", "only archived videos")
338
+ .option("--page <n>", "page number", toInt)
339
+ .option("--limit <n>", "items per page (max 100)", toInt)
340
+ .action(async (opts, cmd) => {
341
+ const result = await execute("listVideos", { query: { archived: opts.archived, page: opts.page, limit: opts.limit } }, cmd);
342
+ print(result, { json: cmd.optsWithGlobals().json });
343
+ });
383
344
 
384
345
  videos
385
- .command("get <id>")
386
- .description(getOp("getVideo").summary)
387
- .action(async (id, _opts, cmd) => {
388
- const result = await execute("getVideo", { pathParams: { id } }, cmd);
389
- print(result, { json: cmd.optsWithGlobals().json });
390
- });
346
+ .command("get <id>")
347
+ .description(getOp("getVideo").summary)
348
+ .action(async (id, _opts, cmd) => {
349
+ const result = await execute("getVideo", { pathParams: { id } }, cmd);
350
+ print(result, { json: cmd.optsWithGlobals().json });
351
+ });
391
352
 
392
353
  videos
393
- .command("update <id>")
394
- .description(getOp("updateVideo").summary)
395
- .option("--name <name>", "project name")
396
- .option("--youtube-title <text>", "YouTube title (max 100 chars)")
397
- .option("--youtube-description <text>", "YouTube description")
398
- .addOption(
399
- new Option("--youtube-privacy <p>", "YouTube privacy, defaults to public").choices([
400
- "public",
401
- "unlisted",
402
- "private",
403
- ])
404
- )
405
- .option("--tiktok-title <text>", "TikTok caption")
406
- .option("--instagram-caption <text>", "Instagram caption")
407
- .option("--x-text <text>", "X post text (max 280 chars)")
408
- .option("--facebook-description <text>", "Facebook description")
409
- .option("--linkedin-description <text>", "LinkedIn description")
410
- .option("--threads-text <text>", "Threads post text")
411
- .action(async (id, opts, cmd) => {
412
- const youtubePost = compact({
413
- title: opts.youtubeTitle,
414
- description: opts.youtubeDescription,
415
- privacyStatus: opts.youtubePrivacy,
416
- });
417
- const body = compact({
418
- name: opts.name,
419
- youtubePost: Object.keys(youtubePost).length ? youtubePost : undefined,
420
- tiktokPost: opts.tiktokTitle !== undefined ? { title: opts.tiktokTitle } : undefined,
421
- instagramPost:
422
- opts.instagramCaption !== undefined ? { caption: opts.instagramCaption } : undefined,
423
- xPost: opts.xText !== undefined ? { text: opts.xText } : undefined,
424
- facebookPost:
425
- opts.facebookDescription !== undefined
426
- ? { description: opts.facebookDescription }
427
- : undefined,
428
- linkedinPost:
429
- opts.linkedinDescription !== undefined
430
- ? { description: opts.linkedinDescription }
431
- : undefined,
432
- threadsPost: opts.threadsText !== undefined ? { text: opts.threadsText } : undefined,
433
- });
434
- const result = await execute("updateVideo", { pathParams: { id }, body }, cmd);
435
- print(result, { json: cmd.optsWithGlobals().json });
436
- });
354
+ .command("update <id>")
355
+ .description(getOp("updateVideo").summary)
356
+ .option("--name <name>", "project name")
357
+ .option("--youtube-title <text>", "YouTube title (max 100 chars)")
358
+ .option("--youtube-description <text>", "YouTube description")
359
+ .addOption(new Option("--youtube-privacy <p>", "YouTube privacy, defaults to public").choices(["public", "unlisted", "private"]))
360
+ .option("--tiktok-title <text>", "TikTok caption")
361
+ .option("--instagram-caption <text>", "Instagram caption")
362
+ .option("--x-text <text>", "X post text (max 280 chars)")
363
+ .option("--facebook-description <text>", "Facebook description")
364
+ .option("--linkedin-description <text>", "LinkedIn description")
365
+ .option("--threads-text <text>", "Threads post text")
366
+ .action(async (id, opts, cmd) => {
367
+ const youtubePost = compact({
368
+ title: opts.youtubeTitle,
369
+ description: opts.youtubeDescription,
370
+ privacyStatus: opts.youtubePrivacy,
371
+ });
372
+ const body = compact({
373
+ name: opts.name,
374
+ youtubePost: Object.keys(youtubePost).length ? youtubePost : undefined,
375
+ tiktokPost: opts.tiktokTitle !== undefined ? { title: opts.tiktokTitle } : undefined,
376
+ instagramPost: opts.instagramCaption !== undefined ? { caption: opts.instagramCaption } : undefined,
377
+ xPost: opts.xText !== undefined ? { text: opts.xText } : undefined,
378
+ facebookPost: opts.facebookDescription !== undefined ? { description: opts.facebookDescription } : undefined,
379
+ linkedinPost: opts.linkedinDescription !== undefined ? { description: opts.linkedinDescription } : undefined,
380
+ threadsPost: opts.threadsText !== undefined ? { text: opts.threadsText } : undefined,
381
+ });
382
+ const result = await execute("updateVideo", { pathParams: { id }, body }, cmd);
383
+ print(result, { json: cmd.optsWithGlobals().json });
384
+ });
437
385
 
438
386
  videos
439
- .command("select-thumbnail <id>")
440
- .description(getOp("selectVideoThumbnail").summary)
441
- .requiredOption("--variant <variantId>", "variant id from the thumbnails.variants array on `faceless videos get <id>`")
442
- .action(async (id, opts, cmd) => {
443
- const result = await execute(
444
- "selectVideoThumbnail",
445
- { pathParams: { id }, body: { variantId: opts.variant } },
446
- cmd
447
- );
448
- print(result, { json: cmd.optsWithGlobals().json });
449
- });
450
-
451
- withWaitFlags(
452
- videos.command("status <id>").description(getOp("getVideoStatus").summary),
453
- "video"
454
- ).action(async (id, opts, cmd) => {
455
- const globals = cmd.optsWithGlobals();
456
- const result = opts.wait
457
- ? await waitForVideo(id, opts.timeout, cmd)
458
- : await execute("getVideoStatus", { pathParams: { id } }, cmd);
459
- print(result, { json: globals.json });
387
+ .command("select-thumbnail <id>")
388
+ .description(getOp("selectVideoThumbnail").summary)
389
+ .requiredOption("--variant <variantId>", "variant id from the thumbnails.variants array on `faceless videos get <id>`")
390
+ .action(async (id, opts, cmd) => {
391
+ const result = await execute("selectVideoThumbnail", { pathParams: { id }, body: { variantId: opts.variant } }, cmd);
392
+ print(result, { json: cmd.optsWithGlobals().json });
393
+ });
394
+
395
+ withWaitFlags(videos.command("status <id>").description(getOp("getVideoStatus").summary), "video").action(async (id, opts, cmd) => {
396
+ const globals = cmd.optsWithGlobals();
397
+ const result = opts.wait ? await waitForVideo(id, opts.timeout, cmd) : await execute("getVideoStatus", { pathParams: { id } }, cmd);
398
+ print(result, { json: globals.json });
460
399
  });
461
400
 
462
401
  withWaitFlags(
463
- videos
464
- .command("render <id>")
465
- .description(getOp("renderVideo").summary)
466
- .addOption(new Option("--codec <codec>", "output codec; h264 for MP4").choices(["h264", "vp8"]))
467
- .option("--idempotency-key <k>", "Idempotency-Key header value"),
468
- "render"
402
+ videos
403
+ .command("render <id>")
404
+ .description(getOp("renderVideo").summary)
405
+ .addOption(new Option("--codec <codec>", "output codec; h264 for MP4").choices(["h264", "vp8"]))
406
+ .option("--idempotency-key <k>", "Idempotency-Key header value"),
407
+ "render"
469
408
  ).action(async (id, opts, cmd) => {
470
- const globals = cmd.optsWithGlobals();
471
- const started = await execute(
472
- "renderVideo",
473
- { pathParams: { id }, body: { codec: opts.codec }, idempotencyKey: opts.idempotencyKey },
474
- cmd
475
- );
476
- const renderId = started?.data?.renderId;
477
- if (!opts.wait || !renderId) {
478
- print(started, { json: globals.json });
479
- return;
480
- }
481
- print(await waitForRender(renderId, opts.timeout, cmd), { json: globals.json });
409
+ const globals = cmd.optsWithGlobals();
410
+ const started = await execute("renderVideo", { pathParams: { id }, body: { codec: opts.codec }, idempotencyKey: opts.idempotencyKey }, cmd);
411
+ const renderId = started?.data?.renderId;
412
+ if (!opts.wait || !renderId) {
413
+ print(started, { json: globals.json });
414
+ return;
415
+ }
416
+ print(await waitForRender(renderId, opts.timeout, cmd), { json: globals.json });
482
417
  });
483
418
 
484
419
  videos
485
- .command("delete <id>")
486
- .description(getOp("deleteVideo").summary)
487
- .action(async (id, _opts, cmd) => {
488
- const result = await execute("deleteVideo", { pathParams: { id } }, cmd);
489
- print(result, { json: cmd.optsWithGlobals().json });
490
- });
420
+ .command("delete <id>")
421
+ .description(getOp("deleteVideo").summary)
422
+ .action(async (id, _opts, cmd) => {
423
+ const result = await execute("deleteVideo", { pathParams: { id } }, cmd);
424
+ print(result, { json: cmd.optsWithGlobals().json });
425
+ });
491
426
 
492
427
  const renders = program.command("renders").description("Render progress");
493
428
 
494
- withWaitFlags(renders.command("get <id>").description(getOp("getRender").summary), "render").action(
495
- async (id, opts, cmd) => {
496
- const globals = cmd.optsWithGlobals();
497
- const result = opts.wait
498
- ? await waitForRender(id, opts.timeout, cmd)
499
- : await execute("getRender", { pathParams: { id } }, cmd);
500
- print(result, { json: globals.json });
501
- }
502
- );
429
+ withWaitFlags(renders.command("get <id>").description(getOp("getRender").summary), "render").action(async (id, opts, cmd) => {
430
+ const globals = cmd.optsWithGlobals();
431
+ const result = opts.wait ? await waitForRender(id, opts.timeout, cmd) : await execute("getRender", { pathParams: { id } }, cmd);
432
+ print(result, { json: globals.json });
433
+ });
503
434
 
504
435
  const series = program.command("series").description("Automated video series");
505
436
 
506
437
  withSeriesFlags(
507
- series
508
- .command("create")
509
- .description(getOp("createSeries").summary)
510
- .requiredOption("--name <name>", "series name shown in the dashboard")
511
- .requiredOption(
512
- "--source <source>",
513
- 'content source, e.g. "Facts & stories" (see: faceless options --kind sources)'
514
- )
438
+ series
439
+ .command("create")
440
+ .description(getOp("createSeries").summary)
441
+ .requiredOption("--name <name>", "series name shown in the dashboard")
442
+ .requiredOption("--source <source>", 'content source, e.g. "Facts & stories" (see: faceless options --kind sources)')
515
443
  )
516
- .option("--idempotency-key <k>", "Idempotency-Key header value")
517
- .action(async (opts, cmd) => {
518
- const result = await execute(
519
- "createSeries",
520
- { body: seriesBody(opts), idempotencyKey: opts.idempotencyKey },
521
- cmd
522
- );
523
- print(result, { json: cmd.optsWithGlobals().json });
524
- });
444
+ .option("--idempotency-key <k>", "Idempotency-Key header value")
445
+ .action(async (opts, cmd) => {
446
+ const result = await execute("createSeries", { body: seriesBody(opts), idempotencyKey: opts.idempotencyKey }, cmd);
447
+ print(result, { json: cmd.optsWithGlobals().json });
448
+ });
525
449
 
526
450
  series
527
- .command("list")
528
- .description(getOp("listSeries").summary)
529
- .action(async (_opts, cmd) => {
530
- const result = await execute("listSeries", {}, cmd);
531
- print(result, { json: cmd.optsWithGlobals().json });
532
- });
451
+ .command("list")
452
+ .description(getOp("listSeries").summary)
453
+ .action(async (_opts, cmd) => {
454
+ const result = await execute("listSeries", {}, cmd);
455
+ print(result, { json: cmd.optsWithGlobals().json });
456
+ });
533
457
 
534
458
  series
535
- .command("get <id>")
536
- .description(getOp("getSeries").summary)
537
- .action(async (id, _opts, cmd) => {
538
- const result = await execute("getSeries", { pathParams: { id } }, cmd);
539
- print(result, { json: cmd.optsWithGlobals().json });
540
- });
459
+ .command("get <id>")
460
+ .description(getOp("getSeries").summary)
461
+ .action(async (id, _opts, cmd) => {
462
+ const result = await execute("getSeries", { pathParams: { id } }, cmd);
463
+ print(result, { json: cmd.optsWithGlobals().json });
464
+ });
541
465
 
542
466
  withSeriesFlags(
543
- series
544
- .command("update <id>")
545
- .description(getOp("updateSeries").summary)
546
- .option("--name <name>", "series name")
547
- .option("--source <source>", "content source (see: faceless options --kind sources)")
548
- .option("--paused", "pause automatic episode generation")
549
- .option("--no-paused", "resume automatic episode generation")
467
+ series
468
+ .command("update <id>")
469
+ .description(getOp("updateSeries").summary)
470
+ .option("--name <name>", "series name")
471
+ .option("--source <source>", "content source (see: faceless options --kind sources)")
472
+ .option("--paused", "pause automatic episode generation")
473
+ .option("--no-paused", "resume automatic episode generation")
550
474
  ).action(async (id, opts, cmd) => {
551
- const result = await execute("updateSeries", { pathParams: { id }, body: seriesBody(opts) }, cmd);
552
- print(result, { json: cmd.optsWithGlobals().json });
475
+ const result = await execute("updateSeries", { pathParams: { id }, body: seriesBody(opts) }, cmd);
476
+ print(result, { json: cmd.optsWithGlobals().json });
553
477
  });
554
478
 
555
479
  series
556
- .command("delete <id>")
557
- .description(getOp("deleteSeries").summary)
558
- .action(async (id, _opts, cmd) => {
559
- const result = await execute("deleteSeries", { pathParams: { id } }, cmd);
560
- print(result, { json: cmd.optsWithGlobals().json });
561
- });
480
+ .command("delete <id>")
481
+ .description(getOp("deleteSeries").summary)
482
+ .action(async (id, _opts, cmd) => {
483
+ const result = await execute("deleteSeries", { pathParams: { id } }, cmd);
484
+ print(result, { json: cmd.optsWithGlobals().json });
485
+ });
562
486
 
563
487
  withWaitFlags(
564
- series
565
- .command("generate <id>")
566
- .description(getOp("generateSeriesEpisode").summary)
567
- .option("--idempotency-key <k>", "Idempotency-Key header value"),
568
- "episode's video"
488
+ series.command("generate <id>").description(getOp("generateSeriesEpisode").summary).option("--idempotency-key <k>", "Idempotency-Key header value"),
489
+ "episode's video"
569
490
  ).action(async (id, opts, cmd) => {
570
- const globals = cmd.optsWithGlobals();
571
- noteCost("generateSeriesEpisode");
572
- const started = await execute(
573
- "generateSeriesEpisode",
574
- { pathParams: { id }, idempotencyKey: opts.idempotencyKey },
575
- cmd
576
- );
577
- const projectId = started?.data?.projectId;
578
- if (!opts.wait || !projectId) {
579
- print(started, { json: globals.json });
580
- return;
581
- }
582
- print(await waitForVideo(projectId, opts.timeout, cmd), { json: globals.json });
491
+ const globals = cmd.optsWithGlobals();
492
+ noteCost("generateSeriesEpisode");
493
+ const started = await execute("generateSeriesEpisode", { pathParams: { id }, idempotencyKey: opts.idempotencyKey }, cmd);
494
+ const projectId = started?.data?.projectId;
495
+ if (!opts.wait || !projectId) {
496
+ print(started, { json: globals.json });
497
+ return;
498
+ }
499
+ print(await waitForVideo(projectId, opts.timeout, cmd), { json: globals.json });
583
500
  });
584
501
 
585
502
  series
586
- .command("episodes <id>")
587
- .description(getOp("listSeriesEpisodes").summary)
588
- .option("--page <n>", "page number", toInt)
589
- .option("--limit <n>", "items per page (max 100)", toInt)
590
- .action(async (id, opts, cmd) => {
591
- const result = await execute(
592
- "listSeriesEpisodes",
593
- { pathParams: { id }, query: { page: opts.page, limit: opts.limit } },
594
- cmd
595
- );
596
- print(result, { json: cmd.optsWithGlobals().json });
597
- });
503
+ .command("episodes <id>")
504
+ .description(getOp("listSeriesEpisodes").summary)
505
+ .option("--page <n>", "page number", toInt)
506
+ .option("--limit <n>", "items per page (max 100)", toInt)
507
+ .action(async (id, opts, cmd) => {
508
+ const result = await execute("listSeriesEpisodes", { pathParams: { id }, query: { page: opts.page, limit: opts.limit } }, cmd);
509
+ print(result, { json: cmd.optsWithGlobals().json });
510
+ });
598
511
 
599
512
  const posts = program.command("posts").description("Publishing and scheduling");
600
513
 
601
514
  posts
602
- .command("publish")
603
- .description(getOp("publishPost").summary)
604
- .requiredOption("--video-id <id>", "the video (project) to publish")
605
- .addOption(
606
- new Option("--platform <platform>", "destination platform; must be connected").choices(
607
- PLATFORMS
608
- ).makeOptionMandatory()
609
- )
610
- .option("--title <text>", "post title or caption; falls back to the video's stored post metadata")
611
- .option("--description <text>", "longer description (YouTube, Facebook, LinkedIn)")
612
- .addOption(
613
- new Option("--privacy <p>", "YouTube only; defaults to public").choices([
614
- "public",
615
- "unlisted",
616
- "private",
617
- ])
618
- )
619
- .option("--auth-id <id>", "specific connected account id (see: faceless accounts)")
620
- .option("--idempotency-key <k>", "Idempotency-Key header value")
621
- .action(async (opts, cmd) => {
622
- const result = await execute(
623
- "publishPost",
624
- {
625
- body: {
626
- videoId: opts.videoId,
627
- platform: opts.platform,
628
- title: opts.title,
629
- description: opts.description,
630
- privacyStatus: opts.privacy,
631
- authId: opts.authId,
632
- },
633
- idempotencyKey: opts.idempotencyKey,
634
- },
635
- cmd
636
- );
637
- print(result, { json: cmd.optsWithGlobals().json });
638
- });
515
+ .command("publish")
516
+ .description(getOp("publishPost").summary)
517
+ .requiredOption("--video-id <id>", "the video (project) to publish")
518
+ .addOption(new Option("--platform <platform>", "destination platform; must be connected").choices(PLATFORMS).makeOptionMandatory())
519
+ .option("--title <text>", "post title or caption; falls back to the video's stored post metadata")
520
+ .option("--description <text>", "longer description (YouTube, Facebook, LinkedIn)")
521
+ .addOption(new Option("--privacy <p>", "YouTube only; defaults to public").choices(["public", "unlisted", "private"]))
522
+ .option("--auth-id <id>", "specific connected account id (see: faceless accounts)")
523
+ .option("--idempotency-key <k>", "Idempotency-Key header value")
524
+ .action(async (opts, cmd) => {
525
+ const result = await execute(
526
+ "publishPost",
527
+ {
528
+ body: {
529
+ videoId: opts.videoId,
530
+ platform: opts.platform,
531
+ title: opts.title,
532
+ description: opts.description,
533
+ privacyStatus: opts.privacy,
534
+ authId: opts.authId,
535
+ },
536
+ idempotencyKey: opts.idempotencyKey,
537
+ },
538
+ cmd
539
+ );
540
+ print(result, { json: cmd.optsWithGlobals().json });
541
+ });
639
542
 
640
543
  posts
641
- .command("schedule")
642
- .description(getOp("schedulePost").summary)
643
- .requiredOption("--video-id <id>", "the video (project) to schedule")
644
- .requiredOption(
645
- "--platforms <platform[,platform...]>",
646
- "platforms to post to; each needs its post metadata set on the video first (faceless videos update)"
647
- )
648
- .requiredOption("--scheduled-time <ISO>", "when to post (ISO 8601, future)")
649
- .option("--idempotency-key <k>", "Idempotency-Key header value")
650
- .action(async (opts, cmd) => {
651
- const result = await execute(
652
- "schedulePost",
653
- {
654
- body: {
655
- videoId: opts.videoId,
656
- platforms: splitList(opts.platforms),
657
- scheduledTime: opts.scheduledTime,
658
- },
659
- idempotencyKey: opts.idempotencyKey,
660
- },
661
- cmd
662
- );
663
- print(result, { json: cmd.optsWithGlobals().json });
664
- });
544
+ .command("schedule")
545
+ .description(getOp("schedulePost").summary)
546
+ .requiredOption("--video-id <id>", "the video (project) to schedule")
547
+ .requiredOption(
548
+ "--platforms <platform[,platform...]>",
549
+ "platforms to post to; each needs its post metadata set on the video first (faceless videos update)"
550
+ )
551
+ .requiredOption("--scheduled-time <ISO>", "when to post (ISO 8601, future)")
552
+ .option("--idempotency-key <k>", "Idempotency-Key header value")
553
+ .action(async (opts, cmd) => {
554
+ const result = await execute(
555
+ "schedulePost",
556
+ {
557
+ body: {
558
+ videoId: opts.videoId,
559
+ platforms: splitList(opts.platforms),
560
+ scheduledTime: opts.scheduledTime,
561
+ },
562
+ idempotencyKey: opts.idempotencyKey,
563
+ },
564
+ cmd
565
+ );
566
+ print(result, { json: cmd.optsWithGlobals().json });
567
+ });
665
568
 
666
569
  posts
667
- .command("cancel <videoId>")
668
- .description(getOp("cancelPost").summary)
669
- .action(async (videoId, _opts, cmd) => {
670
- const result = await execute("cancelPost", { pathParams: { id: videoId } }, cmd);
671
- print(result, { json: cmd.optsWithGlobals().json });
672
- });
570
+ .command("cancel <videoId>")
571
+ .description(getOp("cancelPost").summary)
572
+ .action(async (videoId, _opts, cmd) => {
573
+ const result = await execute("cancelPost", { pathParams: { id: videoId } }, cmd);
574
+ print(result, { json: cmd.optsWithGlobals().json });
575
+ });
673
576
 
674
577
  program
675
- .command("calendar")
676
- .description(getOp("getCalendar").summary)
677
- .requiredOption("--start-date <date>", "range start (ISO 8601, inclusive)")
678
- .requiredOption("--end-date <date>", "range end (ISO 8601, inclusive)")
679
- .addOption(new Option("--platform <platform>", "filter to one platform").choices(PLATFORMS))
680
- .option("--status <status>", "filter by post status, e.g. scheduled, posted, failed")
681
- .action(async (opts, cmd) => {
682
- const result = await execute(
683
- "getCalendar",
684
- {
685
- query: {
686
- startDate: opts.startDate,
687
- endDate: opts.endDate,
688
- platform: opts.platform,
689
- status: opts.status,
690
- },
691
- },
692
- cmd
693
- );
694
- print(result, { json: cmd.optsWithGlobals().json });
695
- });
578
+ .command("calendar")
579
+ .description(getOp("getCalendar").summary)
580
+ .requiredOption("--start-date <date>", "range start (ISO 8601, inclusive)")
581
+ .requiredOption("--end-date <date>", "range end (ISO 8601, inclusive)")
582
+ .addOption(new Option("--platform <platform>", "filter to one platform").choices(PLATFORMS))
583
+ .option("--status <status>", "filter by post status, e.g. scheduled, posted, failed")
584
+ .action(async (opts, cmd) => {
585
+ const result = await execute(
586
+ "getCalendar",
587
+ {
588
+ query: {
589
+ startDate: opts.startDate,
590
+ endDate: opts.endDate,
591
+ platform: opts.platform,
592
+ status: opts.status,
593
+ },
594
+ },
595
+ cmd
596
+ );
597
+ print(result, { json: cmd.optsWithGlobals().json });
598
+ });
696
599
 
697
600
  program
698
- .command("accounts")
699
- .description(getOp("listAccounts").summary)
700
- .action(async (_opts, cmd) => {
701
- const result = await execute("listAccounts", {}, cmd);
702
- print(result, { json: cmd.optsWithGlobals().json });
703
- });
601
+ .command("accounts")
602
+ .description(getOp("listAccounts").summary)
603
+ .action(async (_opts, cmd) => {
604
+ const result = await execute("listAccounts", {}, cmd);
605
+ print(result, { json: cmd.optsWithGlobals().json });
606
+ });
704
607
 
705
608
  program
706
- .command("voices")
707
- .description(getOp("listVoices").summary)
708
- .action(async (_opts, cmd) => {
709
- const result = await execute("listVoices", {}, cmd);
710
- print(result, { json: cmd.optsWithGlobals().json });
711
- });
609
+ .command("voices")
610
+ .description(getOp("listVoices").summary)
611
+ .action(async (_opts, cmd) => {
612
+ const result = await execute("listVoices", {}, cmd);
613
+ print(result, { json: cmd.optsWithGlobals().json });
614
+ });
712
615
 
713
616
  program
714
- .command("options")
715
- .description(getOp("listOptions").summary)
716
- .addOption(
717
- new Option("--kind <kind>", "which catalog to return; omit to list available kinds").choices(
718
- OPTION_KINDS
719
- )
720
- )
721
- .action(async (opts, cmd) => {
722
- const result = await execute("listOptions", { query: { kind: opts.kind } }, cmd);
723
- print(result, { json: cmd.optsWithGlobals().json });
724
- });
617
+ .command("options")
618
+ .description(getOp("listOptions").summary)
619
+ .addOption(new Option("--kind <kind>", "which catalog to return; omit to list available kinds").choices(OPTION_KINDS))
620
+ .action(async (opts, cmd) => {
621
+ const result = await execute("listOptions", { query: { kind: opts.kind } }, cmd);
622
+ print(result, { json: cmd.optsWithGlobals().json });
623
+ });
725
624
 
726
625
  const assets = program.command("assets").description("Media asset library");
727
626
 
728
627
  assets
729
- .command("create")
730
- .description(getOp("createAsset").summary)
731
- .requiredOption("--url <url>", "public URL of the media file")
732
- .option("--name <name>", "display name for the asset")
733
- .option("--file-type <mime>", "MIME type hint, e.g. video/mp4")
734
- .option("--idempotency-key <k>", "Idempotency-Key header value")
735
- .action(async (opts, cmd) => {
736
- const result = await execute(
737
- "createAsset",
738
- {
739
- body: { url: opts.url, name: opts.name, fileType: opts.fileType },
740
- idempotencyKey: opts.idempotencyKey,
741
- },
742
- cmd
743
- );
744
- print(result, { json: cmd.optsWithGlobals().json });
745
- });
628
+ .command("create")
629
+ .description(getOp("createAsset").summary)
630
+ .requiredOption("--url <url>", "public URL of the media file")
631
+ .option("--name <name>", "display name for the asset")
632
+ .option("--file-type <mime>", "MIME type hint, e.g. video/mp4")
633
+ .option("--idempotency-key <k>", "Idempotency-Key header value")
634
+ .action(async (opts, cmd) => {
635
+ const result = await execute(
636
+ "createAsset",
637
+ {
638
+ body: { url: opts.url, name: opts.name, fileType: opts.fileType },
639
+ idempotencyKey: opts.idempotencyKey,
640
+ },
641
+ cmd
642
+ );
643
+ print(result, { json: cmd.optsWithGlobals().json });
644
+ });
746
645
 
747
646
  program
748
- .command("analytics")
749
- .description(getOp("getAnalytics").summary)
750
- .addOption(
751
- new Option("--platform <platform>", "filter to one platform").choices([
752
- "youtube",
753
- "tiktok",
754
- "instagram",
755
- "facebook",
756
- ])
757
- )
758
- .option("--auth-id <id>", "filter to one connected account (see: faceless accounts)")
759
- .option("--range <days>", "trailing window in days (default 30, max 365)", toInt)
760
- .action(async (opts, cmd) => {
761
- const result = await execute(
762
- "getAnalytics",
763
- { query: { platform: opts.platform, authId: opts.authId, range: opts.range } },
764
- cmd
765
- );
766
- print(result, { json: cmd.optsWithGlobals().json });
767
- });
647
+ .command("analytics")
648
+ .description(getOp("getAnalytics").summary)
649
+ .addOption(new Option("--platform <platform>", "filter to one platform").choices(["youtube", "tiktok", "instagram", "facebook"]))
650
+ .option("--auth-id <id>", "filter to one connected account (see: faceless accounts)")
651
+ .option("--range <days>", "trailing window in days (default 30, max 365)", toInt)
652
+ .action(async (opts, cmd) => {
653
+ const result = await execute("getAnalytics", { query: { platform: opts.platform, authId: opts.authId, range: opts.range } }, cmd);
654
+ print(result, { json: cmd.optsWithGlobals().json });
655
+ });
768
656
 
769
657
  program
770
- .command("mcp")
771
- .description("Start the local Faceless MCP server over stdio")
772
- .action(async () => {
773
- const { runMcpServer } = await import("./mcp/stdio.mjs");
774
- await runMcpServer();
775
- });
658
+ .command("mcp")
659
+ .description("Start the local Faceless MCP server over stdio")
660
+ .action(async () => {
661
+ const { runMcpServer } = await import("./mcp/stdio.mjs");
662
+ await runMcpServer();
663
+ });
776
664
 
777
665
  program.parseAsync(process.argv).catch((err) => {
778
- printError(err);
779
- process.exit(exitCodeFor(err));
666
+ printError(err);
667
+ process.exit(exitCodeFor(err));
780
668
  });