faceless-cli 1.0.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/LICENSE +21 -0
- package/README.md +160 -0
- package/bin/faceless.js +2 -0
- package/package.json +37 -0
- package/src/client.mjs +129 -0
- package/src/config.mjs +47 -0
- package/src/generated/.gitkeep +0 -0
- package/src/generated/operations.json +1759 -0
- package/src/index.mjs +767 -0
- package/src/mcp/stdio.mjs +113 -0
- package/src/output.mjs +109 -0
package/src/index.mjs
ADDED
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import readline from "node:readline/promises";
|
|
3
|
+
import { Command, Option } from "commander";
|
|
4
|
+
import { CONFIG_PATH, loadConfig, resolveApiKey, resolveBaseUrl, saveConfig } from "./config.mjs";
|
|
5
|
+
import { CliError, exitCodeFor, request, sleep } from "./client.mjs";
|
|
6
|
+
import { print, printError } from "./output.mjs";
|
|
7
|
+
|
|
8
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
9
|
+
let spec;
|
|
10
|
+
try {
|
|
11
|
+
spec = JSON.parse(
|
|
12
|
+
fs.readFileSync(new URL("./generated/operations.json", import.meta.url), "utf8")
|
|
13
|
+
);
|
|
14
|
+
} 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);
|
|
19
|
+
}
|
|
20
|
+
const operations = spec.operations;
|
|
21
|
+
|
|
22
|
+
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;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
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;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function splitList(value) {
|
|
37
|
+
return value
|
|
38
|
+
.split(",")
|
|
39
|
+
.map((v) => v.trim())
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
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;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const MODELS = ["storyboard", "motion_lite", "motion_pro"];
|
|
52
|
+
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
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
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
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Polls GET /videos/{id}/status until completed or failed.
|
|
111
|
+
function waitForVideo(id, timeoutSec, cmd) {
|
|
112
|
+
return waitForTerminal({ operationId: "getVideoStatus", label: "video", id, timeoutSec, cmd });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Polls GET /renders/{id} until done or error.
|
|
116
|
+
function waitForRender(id, timeoutSec, cmd) {
|
|
117
|
+
return waitForTerminal({ operationId: "getRender", label: "render", id, timeoutSec, cmd });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function noteCost(operationId) {
|
|
121
|
+
const cost = getOp(operationId).creditCost;
|
|
122
|
+
if (cost && cost !== "none") process.stderr.write(`This costs ${cost}.\n`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Async create commands share one shape: state the cost, fire the request,
|
|
126
|
+
// then optionally poll the created video's status until it is terminal.
|
|
127
|
+
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 });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
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);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Series settings shared by "series create" and "series update". Field names
|
|
146
|
+
// mirror the createSeries request schema.
|
|
147
|
+
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
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
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
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const program = new Command();
|
|
228
|
+
|
|
229
|
+
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)");
|
|
238
|
+
|
|
239
|
+
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
|
+
});
|
|
271
|
+
|
|
272
|
+
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
|
+
});
|
|
285
|
+
|
|
286
|
+
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
|
+
});
|
|
293
|
+
|
|
294
|
+
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
|
+
});
|
|
303
|
+
|
|
304
|
+
const videos = program.command("videos").description("Faceless video projects");
|
|
305
|
+
|
|
306
|
+
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"
|
|
326
|
+
).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
|
+
);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
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"
|
|
355
|
+
).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
|
+
);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
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
|
+
});
|
|
383
|
+
|
|
384
|
+
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
|
+
});
|
|
391
|
+
|
|
392
|
+
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
|
+
});
|
|
437
|
+
|
|
438
|
+
withWaitFlags(
|
|
439
|
+
videos.command("status <id>").description(getOp("getVideoStatus").summary),
|
|
440
|
+
"video"
|
|
441
|
+
).action(async (id, opts, cmd) => {
|
|
442
|
+
const globals = cmd.optsWithGlobals();
|
|
443
|
+
const result = opts.wait
|
|
444
|
+
? await waitForVideo(id, opts.timeout, cmd)
|
|
445
|
+
: await execute("getVideoStatus", { pathParams: { id } }, cmd);
|
|
446
|
+
print(result, { json: globals.json });
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
withWaitFlags(
|
|
450
|
+
videos
|
|
451
|
+
.command("render <id>")
|
|
452
|
+
.description(getOp("renderVideo").summary)
|
|
453
|
+
.addOption(new Option("--codec <codec>", "output codec; h264 for MP4").choices(["h264", "vp8"]))
|
|
454
|
+
.option("--idempotency-key <k>", "Idempotency-Key header value"),
|
|
455
|
+
"render"
|
|
456
|
+
).action(async (id, opts, cmd) => {
|
|
457
|
+
const globals = cmd.optsWithGlobals();
|
|
458
|
+
const started = await execute(
|
|
459
|
+
"renderVideo",
|
|
460
|
+
{ pathParams: { id }, body: { codec: opts.codec }, idempotencyKey: opts.idempotencyKey },
|
|
461
|
+
cmd
|
|
462
|
+
);
|
|
463
|
+
const renderId = started?.data?.renderId;
|
|
464
|
+
if (!opts.wait || !renderId) {
|
|
465
|
+
print(started, { json: globals.json });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
print(await waitForRender(renderId, opts.timeout, cmd), { json: globals.json });
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
videos
|
|
472
|
+
.command("delete <id>")
|
|
473
|
+
.description(getOp("deleteVideo").summary)
|
|
474
|
+
.action(async (id, _opts, cmd) => {
|
|
475
|
+
const result = await execute("deleteVideo", { pathParams: { id } }, cmd);
|
|
476
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const renders = program.command("renders").description("Render progress");
|
|
480
|
+
|
|
481
|
+
withWaitFlags(renders.command("get <id>").description(getOp("getRender").summary), "render").action(
|
|
482
|
+
async (id, opts, cmd) => {
|
|
483
|
+
const globals = cmd.optsWithGlobals();
|
|
484
|
+
const result = opts.wait
|
|
485
|
+
? await waitForRender(id, opts.timeout, cmd)
|
|
486
|
+
: await execute("getRender", { pathParams: { id } }, cmd);
|
|
487
|
+
print(result, { json: globals.json });
|
|
488
|
+
}
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
const series = program.command("series").description("Automated video series");
|
|
492
|
+
|
|
493
|
+
withSeriesFlags(
|
|
494
|
+
series
|
|
495
|
+
.command("create")
|
|
496
|
+
.description(getOp("createSeries").summary)
|
|
497
|
+
.requiredOption("--name <name>", "series name shown in the dashboard")
|
|
498
|
+
.requiredOption(
|
|
499
|
+
"--source <source>",
|
|
500
|
+
'content source, e.g. "Facts & stories" (see: faceless options --kind sources)'
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
.option("--idempotency-key <k>", "Idempotency-Key header value")
|
|
504
|
+
.action(async (opts, cmd) => {
|
|
505
|
+
const result = await execute(
|
|
506
|
+
"createSeries",
|
|
507
|
+
{ body: seriesBody(opts), idempotencyKey: opts.idempotencyKey },
|
|
508
|
+
cmd
|
|
509
|
+
);
|
|
510
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
series
|
|
514
|
+
.command("list")
|
|
515
|
+
.description(getOp("listSeries").summary)
|
|
516
|
+
.action(async (_opts, cmd) => {
|
|
517
|
+
const result = await execute("listSeries", {}, cmd);
|
|
518
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
series
|
|
522
|
+
.command("get <id>")
|
|
523
|
+
.description(getOp("getSeries").summary)
|
|
524
|
+
.action(async (id, _opts, cmd) => {
|
|
525
|
+
const result = await execute("getSeries", { pathParams: { id } }, cmd);
|
|
526
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
withSeriesFlags(
|
|
530
|
+
series
|
|
531
|
+
.command("update <id>")
|
|
532
|
+
.description(getOp("updateSeries").summary)
|
|
533
|
+
.option("--name <name>", "series name")
|
|
534
|
+
.option("--source <source>", "content source (see: faceless options --kind sources)")
|
|
535
|
+
.option("--paused", "pause automatic episode generation")
|
|
536
|
+
.option("--no-paused", "resume automatic episode generation")
|
|
537
|
+
).action(async (id, opts, cmd) => {
|
|
538
|
+
const result = await execute("updateSeries", { pathParams: { id }, body: seriesBody(opts) }, cmd);
|
|
539
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
series
|
|
543
|
+
.command("delete <id>")
|
|
544
|
+
.description(getOp("deleteSeries").summary)
|
|
545
|
+
.action(async (id, _opts, cmd) => {
|
|
546
|
+
const result = await execute("deleteSeries", { pathParams: { id } }, cmd);
|
|
547
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
withWaitFlags(
|
|
551
|
+
series
|
|
552
|
+
.command("generate <id>")
|
|
553
|
+
.description(getOp("generateSeriesEpisode").summary)
|
|
554
|
+
.option("--idempotency-key <k>", "Idempotency-Key header value"),
|
|
555
|
+
"episode's video"
|
|
556
|
+
).action(async (id, opts, cmd) => {
|
|
557
|
+
const globals = cmd.optsWithGlobals();
|
|
558
|
+
noteCost("generateSeriesEpisode");
|
|
559
|
+
const started = await execute(
|
|
560
|
+
"generateSeriesEpisode",
|
|
561
|
+
{ pathParams: { id }, idempotencyKey: opts.idempotencyKey },
|
|
562
|
+
cmd
|
|
563
|
+
);
|
|
564
|
+
const projectId = started?.data?.projectId;
|
|
565
|
+
if (!opts.wait || !projectId) {
|
|
566
|
+
print(started, { json: globals.json });
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
print(await waitForVideo(projectId, opts.timeout, cmd), { json: globals.json });
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
series
|
|
573
|
+
.command("episodes <id>")
|
|
574
|
+
.description(getOp("listSeriesEpisodes").summary)
|
|
575
|
+
.option("--page <n>", "page number", toInt)
|
|
576
|
+
.option("--limit <n>", "items per page (max 100)", toInt)
|
|
577
|
+
.action(async (id, opts, cmd) => {
|
|
578
|
+
const result = await execute(
|
|
579
|
+
"listSeriesEpisodes",
|
|
580
|
+
{ pathParams: { id }, query: { page: opts.page, limit: opts.limit } },
|
|
581
|
+
cmd
|
|
582
|
+
);
|
|
583
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const posts = program.command("posts").description("Publishing and scheduling");
|
|
587
|
+
|
|
588
|
+
posts
|
|
589
|
+
.command("publish")
|
|
590
|
+
.description(getOp("publishPost").summary)
|
|
591
|
+
.requiredOption("--video-id <id>", "the video (project) to publish")
|
|
592
|
+
.addOption(
|
|
593
|
+
new Option("--platform <platform>", "destination platform; must be connected").choices(
|
|
594
|
+
PLATFORMS
|
|
595
|
+
).makeOptionMandatory()
|
|
596
|
+
)
|
|
597
|
+
.option("--title <text>", "post title or caption; falls back to the video's stored post metadata")
|
|
598
|
+
.option("--description <text>", "longer description (YouTube, Facebook, LinkedIn)")
|
|
599
|
+
.addOption(
|
|
600
|
+
new Option("--privacy <p>", "YouTube only; defaults to public").choices([
|
|
601
|
+
"public",
|
|
602
|
+
"unlisted",
|
|
603
|
+
"private",
|
|
604
|
+
])
|
|
605
|
+
)
|
|
606
|
+
.option("--auth-id <id>", "specific connected account id (see: faceless accounts)")
|
|
607
|
+
.option("--idempotency-key <k>", "Idempotency-Key header value")
|
|
608
|
+
.action(async (opts, cmd) => {
|
|
609
|
+
const result = await execute(
|
|
610
|
+
"publishPost",
|
|
611
|
+
{
|
|
612
|
+
body: {
|
|
613
|
+
videoId: opts.videoId,
|
|
614
|
+
platform: opts.platform,
|
|
615
|
+
title: opts.title,
|
|
616
|
+
description: opts.description,
|
|
617
|
+
privacyStatus: opts.privacy,
|
|
618
|
+
authId: opts.authId,
|
|
619
|
+
},
|
|
620
|
+
idempotencyKey: opts.idempotencyKey,
|
|
621
|
+
},
|
|
622
|
+
cmd
|
|
623
|
+
);
|
|
624
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
posts
|
|
628
|
+
.command("schedule")
|
|
629
|
+
.description(getOp("schedulePost").summary)
|
|
630
|
+
.requiredOption("--video-id <id>", "the video (project) to schedule")
|
|
631
|
+
.requiredOption(
|
|
632
|
+
"--platforms <platform[,platform...]>",
|
|
633
|
+
"platforms to post to; each needs its post metadata set on the video first (faceless videos update)"
|
|
634
|
+
)
|
|
635
|
+
.requiredOption("--scheduled-time <ISO>", "when to post (ISO 8601, future)")
|
|
636
|
+
.option("--idempotency-key <k>", "Idempotency-Key header value")
|
|
637
|
+
.action(async (opts, cmd) => {
|
|
638
|
+
const result = await execute(
|
|
639
|
+
"schedulePost",
|
|
640
|
+
{
|
|
641
|
+
body: {
|
|
642
|
+
videoId: opts.videoId,
|
|
643
|
+
platforms: splitList(opts.platforms),
|
|
644
|
+
scheduledTime: opts.scheduledTime,
|
|
645
|
+
},
|
|
646
|
+
idempotencyKey: opts.idempotencyKey,
|
|
647
|
+
},
|
|
648
|
+
cmd
|
|
649
|
+
);
|
|
650
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
posts
|
|
654
|
+
.command("cancel <videoId>")
|
|
655
|
+
.description(getOp("cancelPost").summary)
|
|
656
|
+
.action(async (videoId, _opts, cmd) => {
|
|
657
|
+
const result = await execute("cancelPost", { pathParams: { id: videoId } }, cmd);
|
|
658
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
program
|
|
662
|
+
.command("calendar")
|
|
663
|
+
.description(getOp("getCalendar").summary)
|
|
664
|
+
.requiredOption("--start-date <date>", "range start (ISO 8601, inclusive)")
|
|
665
|
+
.requiredOption("--end-date <date>", "range end (ISO 8601, inclusive)")
|
|
666
|
+
.addOption(new Option("--platform <platform>", "filter to one platform").choices(PLATFORMS))
|
|
667
|
+
.option("--status <status>", "filter by post status, e.g. scheduled, posted, failed")
|
|
668
|
+
.action(async (opts, cmd) => {
|
|
669
|
+
const result = await execute(
|
|
670
|
+
"getCalendar",
|
|
671
|
+
{
|
|
672
|
+
query: {
|
|
673
|
+
startDate: opts.startDate,
|
|
674
|
+
endDate: opts.endDate,
|
|
675
|
+
platform: opts.platform,
|
|
676
|
+
status: opts.status,
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
cmd
|
|
680
|
+
);
|
|
681
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
program
|
|
685
|
+
.command("accounts")
|
|
686
|
+
.description(getOp("listAccounts").summary)
|
|
687
|
+
.action(async (_opts, cmd) => {
|
|
688
|
+
const result = await execute("listAccounts", {}, cmd);
|
|
689
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
program
|
|
693
|
+
.command("voices")
|
|
694
|
+
.description(getOp("listVoices").summary)
|
|
695
|
+
.action(async (_opts, cmd) => {
|
|
696
|
+
const result = await execute("listVoices", {}, cmd);
|
|
697
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
program
|
|
701
|
+
.command("options")
|
|
702
|
+
.description(getOp("listOptions").summary)
|
|
703
|
+
.addOption(
|
|
704
|
+
new Option("--kind <kind>", "which catalog to return; omit to list available kinds").choices(
|
|
705
|
+
OPTION_KINDS
|
|
706
|
+
)
|
|
707
|
+
)
|
|
708
|
+
.action(async (opts, cmd) => {
|
|
709
|
+
const result = await execute("listOptions", { query: { kind: opts.kind } }, cmd);
|
|
710
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
const assets = program.command("assets").description("Media asset library");
|
|
714
|
+
|
|
715
|
+
assets
|
|
716
|
+
.command("create")
|
|
717
|
+
.description(getOp("createAsset").summary)
|
|
718
|
+
.requiredOption("--url <url>", "public URL of the media file")
|
|
719
|
+
.option("--name <name>", "display name for the asset")
|
|
720
|
+
.option("--file-type <mime>", "MIME type hint, e.g. video/mp4")
|
|
721
|
+
.option("--idempotency-key <k>", "Idempotency-Key header value")
|
|
722
|
+
.action(async (opts, cmd) => {
|
|
723
|
+
const result = await execute(
|
|
724
|
+
"createAsset",
|
|
725
|
+
{
|
|
726
|
+
body: { url: opts.url, name: opts.name, fileType: opts.fileType },
|
|
727
|
+
idempotencyKey: opts.idempotencyKey,
|
|
728
|
+
},
|
|
729
|
+
cmd
|
|
730
|
+
);
|
|
731
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
program
|
|
735
|
+
.command("analytics")
|
|
736
|
+
.description(getOp("getAnalytics").summary)
|
|
737
|
+
.addOption(
|
|
738
|
+
new Option("--platform <platform>", "filter to one platform").choices([
|
|
739
|
+
"youtube",
|
|
740
|
+
"tiktok",
|
|
741
|
+
"instagram",
|
|
742
|
+
"facebook",
|
|
743
|
+
])
|
|
744
|
+
)
|
|
745
|
+
.option("--auth-id <id>", "filter to one connected account (see: faceless accounts)")
|
|
746
|
+
.option("--range <days>", "trailing window in days (default 30, max 365)", toInt)
|
|
747
|
+
.action(async (opts, cmd) => {
|
|
748
|
+
const result = await execute(
|
|
749
|
+
"getAnalytics",
|
|
750
|
+
{ query: { platform: opts.platform, authId: opts.authId, range: opts.range } },
|
|
751
|
+
cmd
|
|
752
|
+
);
|
|
753
|
+
print(result, { json: cmd.optsWithGlobals().json });
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
program
|
|
757
|
+
.command("mcp")
|
|
758
|
+
.description("Start the local Faceless MCP server over stdio")
|
|
759
|
+
.action(async () => {
|
|
760
|
+
const { runMcpServer } = await import("./mcp/stdio.mjs");
|
|
761
|
+
await runMcpServer();
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
765
|
+
printError(err);
|
|
766
|
+
process.exit(exitCodeFor(err));
|
|
767
|
+
});
|