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 +118 -2
- package/dist/client.d.ts +363 -0
- package/dist/client.js +870 -0
- package/dist/index.js +2742 -0
- package/package.json +63 -4
- package/skills/videodraft/SKILL.md +95 -0
- package/skills/videodraft/references/examples.md +84 -0
- package/skills/videodraft/references/models.md +43 -0
- package/skills/videodraft/references/pipeline.md +59 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var EXIT = {
|
|
3
|
+
OK: 0,
|
|
4
|
+
ERROR: 1,
|
|
5
|
+
USAGE: 2,
|
|
6
|
+
AUTH: 3,
|
|
7
|
+
CREDITS: 4
|
|
8
|
+
};
|
|
9
|
+
var CliError = class extends Error {
|
|
10
|
+
exitCode;
|
|
11
|
+
/** One-line remediation shown under the error (human mode only). */
|
|
12
|
+
hint;
|
|
13
|
+
constructor(message, exitCode = EXIT.ERROR, hint) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "CliError";
|
|
16
|
+
this.exitCode = exitCode;
|
|
17
|
+
this.hint = hint;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var AuthError = class extends CliError {
|
|
21
|
+
constructor(message = "Not authenticated.") {
|
|
22
|
+
super(
|
|
23
|
+
message,
|
|
24
|
+
EXIT.AUTH,
|
|
25
|
+
"Run `videodraft login` (or set VIDEODRAFT_API_KEY to a vd_mcp_... token from /mcp-keys)."
|
|
26
|
+
);
|
|
27
|
+
this.name = "AuthError";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var RpcError = class extends CliError {
|
|
31
|
+
code;
|
|
32
|
+
data;
|
|
33
|
+
constructor(code, message, data) {
|
|
34
|
+
super(`RPC error ${code}: ${message}`);
|
|
35
|
+
this.name = "RpcError";
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.data = data;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var CREDIT_ERROR_RE = /insufficient credits|not enough credits|credit balance/i;
|
|
41
|
+
var ToolError = class extends CliError {
|
|
42
|
+
toolName;
|
|
43
|
+
constructor(toolName, message) {
|
|
44
|
+
const isCredits = CREDIT_ERROR_RE.test(message);
|
|
45
|
+
super(
|
|
46
|
+
message,
|
|
47
|
+
isCredits ? EXIT.CREDITS : EXIT.ERROR,
|
|
48
|
+
isCredits ? "Check your balance with `videodraft credits` or top up at https://app.videodraft.ai/pricing" : void 0
|
|
49
|
+
);
|
|
50
|
+
this.name = "ToolError";
|
|
51
|
+
this.toolName = toolName;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var TimeoutError = class extends CliError {
|
|
55
|
+
constructor(message) {
|
|
56
|
+
super(message, EXIT.ERROR);
|
|
57
|
+
this.name = "TimeoutError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/core/config.ts
|
|
62
|
+
import fs from "fs";
|
|
63
|
+
import path from "path";
|
|
64
|
+
import os from "os";
|
|
65
|
+
import crypto from "crypto";
|
|
66
|
+
var DEFAULT_BASE_URL = "https://app.videodraft.ai";
|
|
67
|
+
function configDir(env = process.env) {
|
|
68
|
+
if (env.VIDEODRAFT_CONFIG_DIR) return env.VIDEODRAFT_CONFIG_DIR;
|
|
69
|
+
const xdg = env.XDG_CONFIG_HOME;
|
|
70
|
+
return path.join(xdg && xdg.trim() !== "" ? xdg : path.join(os.homedir(), ".config"), "videodraft");
|
|
71
|
+
}
|
|
72
|
+
function configPath(env = process.env) {
|
|
73
|
+
return path.join(configDir(env), "config.json");
|
|
74
|
+
}
|
|
75
|
+
function emptyConfig() {
|
|
76
|
+
return { version: 1, active_profile: "default", profiles: {} };
|
|
77
|
+
}
|
|
78
|
+
function readConfig(env = process.env) {
|
|
79
|
+
try {
|
|
80
|
+
const raw = fs.readFileSync(configPath(env), "utf8");
|
|
81
|
+
const parsed = JSON.parse(raw);
|
|
82
|
+
if (!parsed || typeof parsed !== "object" || !parsed.profiles) return emptyConfig();
|
|
83
|
+
return parsed;
|
|
84
|
+
} catch {
|
|
85
|
+
return emptyConfig();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function writeConfig(config, env = process.env) {
|
|
89
|
+
const dir = configDir(env);
|
|
90
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
91
|
+
const file = configPath(env);
|
|
92
|
+
const tmp = path.join(dir, `.config.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`);
|
|
93
|
+
fs.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
|
|
94
|
+
`, { mode: 384 });
|
|
95
|
+
fs.renameSync(tmp, file);
|
|
96
|
+
try {
|
|
97
|
+
fs.chmodSync(file, 384);
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function updateConfig(mutate, env = process.env) {
|
|
102
|
+
const config = readConfig(env);
|
|
103
|
+
mutate(config);
|
|
104
|
+
writeConfig(config, env);
|
|
105
|
+
return config;
|
|
106
|
+
}
|
|
107
|
+
function getProfile(name, env = process.env) {
|
|
108
|
+
const config = readConfig(env);
|
|
109
|
+
const profileName = name ?? config.active_profile ?? "default";
|
|
110
|
+
return { name: profileName, profile: config.profiles[profileName], config };
|
|
111
|
+
}
|
|
112
|
+
var LOCK_STALE_MS = 3e4;
|
|
113
|
+
async function withLock(lockName, fn, env = process.env) {
|
|
114
|
+
const dir = configDir(env);
|
|
115
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
116
|
+
const lockPath = path.join(dir, `${lockName}.lock`);
|
|
117
|
+
const deadline = Date.now() + 15e3;
|
|
118
|
+
for (; ; ) {
|
|
119
|
+
try {
|
|
120
|
+
const fd = fs.openSync(lockPath, "wx");
|
|
121
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
|
|
122
|
+
fs.closeSync(fd);
|
|
123
|
+
break;
|
|
124
|
+
} catch {
|
|
125
|
+
try {
|
|
126
|
+
const stat = fs.statSync(lockPath);
|
|
127
|
+
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
128
|
+
fs.rmSync(lockPath, { force: true });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (Date.now() > deadline) {
|
|
135
|
+
throw new Error(`Timed out waiting for lock ${lockName} (${lockPath})`);
|
|
136
|
+
}
|
|
137
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
return await fn();
|
|
142
|
+
} finally {
|
|
143
|
+
fs.rmSync(lockPath, { force: true });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/core/rpc.ts
|
|
148
|
+
var VideoDraftClient = class {
|
|
149
|
+
baseUrl;
|
|
150
|
+
tokenProvider;
|
|
151
|
+
fetchImpl;
|
|
152
|
+
userAgent;
|
|
153
|
+
requestTimeoutMs;
|
|
154
|
+
nextId = 1;
|
|
155
|
+
constructor(options) {
|
|
156
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
157
|
+
this.tokenProvider = options.tokenProvider;
|
|
158
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
159
|
+
this.userAgent = options.userAgent ?? "videodraft-cli";
|
|
160
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 3e5;
|
|
161
|
+
}
|
|
162
|
+
get endpoint() {
|
|
163
|
+
return `${this.baseUrl}/api/mcp`;
|
|
164
|
+
}
|
|
165
|
+
async rpc(method, params) {
|
|
166
|
+
const token = await this.tokenProvider.getAccessToken();
|
|
167
|
+
let response = await this.post(method, params, token);
|
|
168
|
+
if (response.status === 401 && this.tokenProvider.onUnauthorized) {
|
|
169
|
+
const fresh = await this.tokenProvider.onUnauthorized();
|
|
170
|
+
if (fresh) response = await this.post(method, params, fresh);
|
|
171
|
+
}
|
|
172
|
+
if (response.status === 401) {
|
|
173
|
+
throw new AuthError("Token is invalid, expired, or revoked.");
|
|
174
|
+
}
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
let detail = "";
|
|
177
|
+
try {
|
|
178
|
+
const body2 = await response.json();
|
|
179
|
+
detail = body2?.error?.message ?? body2?.error ?? "";
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
throw new RpcError(response.status, detail || `HTTP ${response.status} from ${this.endpoint}`);
|
|
183
|
+
}
|
|
184
|
+
let body;
|
|
185
|
+
try {
|
|
186
|
+
body = await response.json();
|
|
187
|
+
} catch {
|
|
188
|
+
throw new RpcError(0, `Server returned a non-JSON response (HTTP ${response.status}).`);
|
|
189
|
+
}
|
|
190
|
+
if (body?.error) {
|
|
191
|
+
throw new RpcError(body.error.code ?? -1, body.error.message ?? "Unknown RPC error", body.error.data);
|
|
192
|
+
}
|
|
193
|
+
return body?.result;
|
|
194
|
+
}
|
|
195
|
+
post(method, params, token) {
|
|
196
|
+
return this.fetchImpl(this.endpoint, {
|
|
197
|
+
method: "POST",
|
|
198
|
+
headers: {
|
|
199
|
+
"content-type": "application/json",
|
|
200
|
+
authorization: `Bearer ${token}`,
|
|
201
|
+
"user-agent": this.userAgent
|
|
202
|
+
},
|
|
203
|
+
body: JSON.stringify({
|
|
204
|
+
jsonrpc: "2.0",
|
|
205
|
+
id: this.nextId++,
|
|
206
|
+
method,
|
|
207
|
+
...params === void 0 ? {} : { params }
|
|
208
|
+
}),
|
|
209
|
+
signal: AbortSignal.timeout(this.requestTimeoutMs)
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Batch several JSON-RPC requests into ONE HTTP round trip (the server
|
|
214
|
+
* implements JSON-RPC 2.0 batching). With N concurrent jobs this turns N
|
|
215
|
+
* polling requests per tick into 1 — the difference between 50 waiting CLIs
|
|
216
|
+
* generating ~17 req/s and ~0.3 req/s against /api/mcp.
|
|
217
|
+
* Results are matched by id (order-independent per spec).
|
|
218
|
+
*/
|
|
219
|
+
async rpcBatch(calls) {
|
|
220
|
+
if (calls.length === 0) return [];
|
|
221
|
+
const token = await this.tokenProvider.getAccessToken();
|
|
222
|
+
const ids = calls.map(() => this.nextId++);
|
|
223
|
+
const payload = calls.map((c, i) => ({
|
|
224
|
+
jsonrpc: "2.0",
|
|
225
|
+
id: ids[i],
|
|
226
|
+
method: c.method,
|
|
227
|
+
...c.params === void 0 ? {} : { params: c.params }
|
|
228
|
+
}));
|
|
229
|
+
const post = (bearer) => this.fetchImpl(this.endpoint, {
|
|
230
|
+
method: "POST",
|
|
231
|
+
headers: {
|
|
232
|
+
"content-type": "application/json",
|
|
233
|
+
authorization: `Bearer ${bearer}`,
|
|
234
|
+
"user-agent": this.userAgent
|
|
235
|
+
},
|
|
236
|
+
body: JSON.stringify(payload),
|
|
237
|
+
signal: AbortSignal.timeout(this.requestTimeoutMs)
|
|
238
|
+
});
|
|
239
|
+
let response = await post(token);
|
|
240
|
+
if (response.status === 401 && this.tokenProvider.onUnauthorized) {
|
|
241
|
+
const fresh = await this.tokenProvider.onUnauthorized();
|
|
242
|
+
if (fresh) response = await post(fresh);
|
|
243
|
+
}
|
|
244
|
+
if (response.status === 401) throw new AuthError("Token is invalid, expired, or revoked.");
|
|
245
|
+
if (!response.ok) throw new RpcError(response.status, `HTTP ${response.status} from ${this.endpoint}`);
|
|
246
|
+
let body;
|
|
247
|
+
try {
|
|
248
|
+
body = await response.json();
|
|
249
|
+
} catch {
|
|
250
|
+
throw new RpcError(0, `Server returned a non-JSON response (HTTP ${response.status}).`);
|
|
251
|
+
}
|
|
252
|
+
const responses = Array.isArray(body) ? body : [body];
|
|
253
|
+
const byId = new Map(responses.map((r) => [r?.id, r]));
|
|
254
|
+
return ids.map((id) => {
|
|
255
|
+
const r = byId.get(id);
|
|
256
|
+
if (!r) throw new RpcError(-1, `Batch response missing id ${id}`);
|
|
257
|
+
if (r.error) throw new RpcError(r.error.code ?? -1, r.error.message ?? "Unknown RPC error", r.error.data);
|
|
258
|
+
return r.result;
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Call several tools in one batched HTTP request. Per-item failures come
|
|
263
|
+
* back as { ok:false, error } instead of throwing, so one failed job can't
|
|
264
|
+
* abort polling of its siblings.
|
|
265
|
+
*/
|
|
266
|
+
async callToolBatch(calls) {
|
|
267
|
+
const results = await this.rpcBatch(
|
|
268
|
+
calls.map((c) => ({ method: "tools/call", params: { name: c.name, arguments: c.args ?? {} } }))
|
|
269
|
+
);
|
|
270
|
+
return results.map((result) => {
|
|
271
|
+
const text = result?.content?.[0]?.text ?? "";
|
|
272
|
+
if (result?.isError) {
|
|
273
|
+
return { ok: false, error: text.replace(/^Error:\s*/, "") || "tool failed" };
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
return { ok: true, result: JSON.parse(text) };
|
|
277
|
+
} catch {
|
|
278
|
+
return { ok: true, result: text };
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Call an MCP tool and return its parsed result.
|
|
284
|
+
* The server wraps results as { content: [{type:"text", text}], isError } —
|
|
285
|
+
* text is JSON for successes and "Error: ..." for tool failures.
|
|
286
|
+
*/
|
|
287
|
+
async callTool(name, args = {}) {
|
|
288
|
+
const result = await this.rpc(
|
|
289
|
+
"tools/call",
|
|
290
|
+
{ name, arguments: args }
|
|
291
|
+
);
|
|
292
|
+
const text = result?.content?.[0]?.text ?? "";
|
|
293
|
+
if (result?.isError) {
|
|
294
|
+
throw new ToolError(name, text.replace(/^Error:\s*/, "") || `Tool ${name} failed`);
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
return JSON.parse(text);
|
|
298
|
+
} catch {
|
|
299
|
+
return text;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async listTools() {
|
|
303
|
+
const result = await this.rpc("tools/list");
|
|
304
|
+
return result?.tools ?? [];
|
|
305
|
+
}
|
|
306
|
+
async ping() {
|
|
307
|
+
await this.rpc("ping");
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
function staticTokenProvider(token) {
|
|
311
|
+
return {
|
|
312
|
+
async getAccessToken() {
|
|
313
|
+
if (!token) throw new AuthError();
|
|
314
|
+
return token;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/auth/oauth.ts
|
|
320
|
+
import http from "http";
|
|
321
|
+
import crypto2 from "crypto";
|
|
322
|
+
var STATIC_CLI_CLIENT_ID = "vd_client_cli";
|
|
323
|
+
var OAUTH_SCOPE = "mcp";
|
|
324
|
+
function b64url(buf) {
|
|
325
|
+
return buf.toString("base64url");
|
|
326
|
+
}
|
|
327
|
+
function createPkcePair() {
|
|
328
|
+
const verifier = b64url(crypto2.randomBytes(32));
|
|
329
|
+
const challenge = b64url(crypto2.createHash("sha256").update(verifier).digest());
|
|
330
|
+
return { verifier, challenge };
|
|
331
|
+
}
|
|
332
|
+
function buildAuthorizeUrl(params) {
|
|
333
|
+
const url = new URL("/api/oauth/authorize", params.baseUrl);
|
|
334
|
+
url.searchParams.set("response_type", "code");
|
|
335
|
+
url.searchParams.set("client_id", params.clientId);
|
|
336
|
+
url.searchParams.set("redirect_uri", params.redirectUri);
|
|
337
|
+
url.searchParams.set("scope", OAUTH_SCOPE);
|
|
338
|
+
url.searchParams.set("state", params.state);
|
|
339
|
+
url.searchParams.set("code_challenge", params.codeChallenge);
|
|
340
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
341
|
+
url.searchParams.set("resource", `${params.baseUrl}/api/mcp`);
|
|
342
|
+
return url.toString();
|
|
343
|
+
}
|
|
344
|
+
async function probeClientId(baseUrl, clientId, redirectUri, fetchImpl = fetch) {
|
|
345
|
+
const { challenge } = createPkcePair();
|
|
346
|
+
const url = buildAuthorizeUrl({
|
|
347
|
+
baseUrl,
|
|
348
|
+
clientId,
|
|
349
|
+
redirectUri,
|
|
350
|
+
state: "probe",
|
|
351
|
+
codeChallenge: challenge
|
|
352
|
+
});
|
|
353
|
+
const res = await fetchImpl(url, { redirect: "manual", signal: AbortSignal.timeout(15e3) });
|
|
354
|
+
return res.status >= 300 && res.status < 400;
|
|
355
|
+
}
|
|
356
|
+
async function registerCliClient(baseUrl, fetchImpl = fetch) {
|
|
357
|
+
const res = await fetchImpl(new URL("/api/oauth/register", baseUrl), {
|
|
358
|
+
method: "POST",
|
|
359
|
+
headers: { "content-type": "application/json" },
|
|
360
|
+
body: JSON.stringify({
|
|
361
|
+
client_name: "VideoDraft CLI",
|
|
362
|
+
redirect_uris: ["http://127.0.0.1/callback"],
|
|
363
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
364
|
+
response_types: ["code"],
|
|
365
|
+
token_endpoint_auth_method: "none",
|
|
366
|
+
scope: OAUTH_SCOPE
|
|
367
|
+
}),
|
|
368
|
+
signal: AbortSignal.timeout(15e3)
|
|
369
|
+
});
|
|
370
|
+
const body = await res.json().catch(() => ({}));
|
|
371
|
+
if (!res.ok || !body?.client_id) {
|
|
372
|
+
throw new CliError(
|
|
373
|
+
`OAuth client registration failed: ${body?.error_description ?? body?.error ?? res.status}`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
return body.client_id;
|
|
377
|
+
}
|
|
378
|
+
async function authorizeViaLoopback(params) {
|
|
379
|
+
const { verifier, challenge } = createPkcePair();
|
|
380
|
+
const state = b64url(crypto2.randomBytes(16));
|
|
381
|
+
const server = http.createServer();
|
|
382
|
+
await new Promise((resolve, reject) => {
|
|
383
|
+
server.once("error", reject);
|
|
384
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
385
|
+
});
|
|
386
|
+
const port = server.address().port;
|
|
387
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
388
|
+
const codePromise = new Promise((resolve, reject) => {
|
|
389
|
+
const timeout = setTimeout(
|
|
390
|
+
() => reject(new CliError("Timed out waiting for the browser login (5 minutes).")),
|
|
391
|
+
params.timeoutMs ?? 3e5
|
|
392
|
+
);
|
|
393
|
+
timeout.unref();
|
|
394
|
+
server.on("request", (req, res) => {
|
|
395
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
396
|
+
if (url.pathname !== "/callback") {
|
|
397
|
+
res.writeHead(404).end();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const err = url.searchParams.get("error");
|
|
401
|
+
const code = url.searchParams.get("code");
|
|
402
|
+
const gotState = url.searchParams.get("state");
|
|
403
|
+
const page = (title, body) => `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body style="font-family:system-ui,sans-serif;max-width:480px;margin:80px auto;padding:0 20px;text-align:center"><h1 style="font-size:20px">${title}</h1><p style="color:#555">${body}</p></body></html>`;
|
|
404
|
+
if (err) {
|
|
405
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
406
|
+
res.end(page("Login failed", `${err}. You can close this tab and return to the terminal.`));
|
|
407
|
+
clearTimeout(timeout);
|
|
408
|
+
reject(new CliError(`Authorization failed: ${err}`));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (!code || gotState !== state) {
|
|
412
|
+
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
|
|
413
|
+
res.end(page("Login failed", "Missing code or state mismatch. Close this tab and retry."));
|
|
414
|
+
clearTimeout(timeout);
|
|
415
|
+
reject(new CliError("Authorization response missing code or state mismatch."));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
419
|
+
res.end(page("You're logged in to VideoDraft", "You can close this tab and return to the terminal."));
|
|
420
|
+
clearTimeout(timeout);
|
|
421
|
+
resolve(code);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
codePromise.catch(() => {
|
|
425
|
+
});
|
|
426
|
+
try {
|
|
427
|
+
const authorizeUrl = buildAuthorizeUrl({
|
|
428
|
+
baseUrl: params.baseUrl,
|
|
429
|
+
clientId: params.clientId,
|
|
430
|
+
redirectUri,
|
|
431
|
+
state,
|
|
432
|
+
codeChallenge: challenge
|
|
433
|
+
});
|
|
434
|
+
await params.onAuthorizeUrl(authorizeUrl);
|
|
435
|
+
const code = await codePromise;
|
|
436
|
+
return { code, redirectUri, verifier };
|
|
437
|
+
} finally {
|
|
438
|
+
server.close();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async function exchangeCode(params) {
|
|
442
|
+
const fetchImpl = params.fetchImpl ?? fetch;
|
|
443
|
+
const res = await fetchImpl(new URL("/api/oauth/token", params.baseUrl), {
|
|
444
|
+
method: "POST",
|
|
445
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
446
|
+
body: new URLSearchParams({
|
|
447
|
+
grant_type: "authorization_code",
|
|
448
|
+
code: params.code,
|
|
449
|
+
redirect_uri: params.redirectUri,
|
|
450
|
+
client_id: params.clientId,
|
|
451
|
+
code_verifier: params.verifier
|
|
452
|
+
}).toString(),
|
|
453
|
+
signal: AbortSignal.timeout(3e4)
|
|
454
|
+
});
|
|
455
|
+
const body = await res.json().catch(() => ({}));
|
|
456
|
+
if (!res.ok || !body?.access_token) {
|
|
457
|
+
throw new CliError(
|
|
458
|
+
`Token exchange failed: ${body?.error_description ?? body?.error ?? `HTTP ${res.status}`}`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
return body;
|
|
462
|
+
}
|
|
463
|
+
async function refreshAccessToken(params) {
|
|
464
|
+
const fetchImpl = params.fetchImpl ?? fetch;
|
|
465
|
+
const res = await fetchImpl(new URL("/api/oauth/token", params.baseUrl), {
|
|
466
|
+
method: "POST",
|
|
467
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
468
|
+
body: new URLSearchParams({
|
|
469
|
+
grant_type: "refresh_token",
|
|
470
|
+
refresh_token: params.refreshToken,
|
|
471
|
+
client_id: params.clientId
|
|
472
|
+
}).toString(),
|
|
473
|
+
signal: AbortSignal.timeout(3e4)
|
|
474
|
+
});
|
|
475
|
+
if (!res.ok) return null;
|
|
476
|
+
const body = await res.json().catch(() => null);
|
|
477
|
+
return body?.access_token ? body : null;
|
|
478
|
+
}
|
|
479
|
+
async function revokeToken(baseUrl, token, fetchImpl = fetch) {
|
|
480
|
+
try {
|
|
481
|
+
await fetchImpl(new URL("/api/oauth/revoke", baseUrl), {
|
|
482
|
+
method: "POST",
|
|
483
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
484
|
+
body: new URLSearchParams({ token }).toString(),
|
|
485
|
+
signal: AbortSignal.timeout(1e4)
|
|
486
|
+
});
|
|
487
|
+
} catch {
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/auth/token-provider.ts
|
|
492
|
+
var EXPIRY_SKEW_MS = 6e4;
|
|
493
|
+
function isExpiring(profile) {
|
|
494
|
+
if (!profile.expires_at) return false;
|
|
495
|
+
return new Date(profile.expires_at).getTime() - Date.now() < EXPIRY_SKEW_MS;
|
|
496
|
+
}
|
|
497
|
+
function saveRotatedTokens(profileName, tokens, env) {
|
|
498
|
+
updateConfig((config) => {
|
|
499
|
+
const profile = config.profiles[profileName];
|
|
500
|
+
if (!profile) return;
|
|
501
|
+
profile.access_token = tokens.access_token;
|
|
502
|
+
profile.refresh_token = tokens.refresh_token;
|
|
503
|
+
profile.expires_at = new Date(Date.now() + tokens.expires_in * 1e3).toISOString();
|
|
504
|
+
}, env);
|
|
505
|
+
}
|
|
506
|
+
async function refreshUnderLock(profileName, env) {
|
|
507
|
+
return withLock(
|
|
508
|
+
"oauth-refresh",
|
|
509
|
+
async () => {
|
|
510
|
+
const { profile } = getProfile(profileName, env);
|
|
511
|
+
if (!profile) return null;
|
|
512
|
+
if (profile.auth_kind !== "oauth") return profile.access_token;
|
|
513
|
+
if (!isExpiring(profile)) return profile.access_token;
|
|
514
|
+
if (!profile.refresh_token || !profile.client_id) return null;
|
|
515
|
+
const tokens = await refreshAccessToken({
|
|
516
|
+
baseUrl: profile.base_url || DEFAULT_BASE_URL,
|
|
517
|
+
clientId: profile.client_id,
|
|
518
|
+
refreshToken: profile.refresh_token
|
|
519
|
+
});
|
|
520
|
+
if (!tokens) return null;
|
|
521
|
+
saveRotatedTokens(profileName, tokens, env);
|
|
522
|
+
return tokens.access_token;
|
|
523
|
+
},
|
|
524
|
+
env
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
function profileTokenProvider(profileName, env = process.env) {
|
|
528
|
+
return {
|
|
529
|
+
async getAccessToken() {
|
|
530
|
+
const { profile } = getProfile(profileName, env);
|
|
531
|
+
if (!profile?.access_token) throw new AuthError();
|
|
532
|
+
if (profile.auth_kind === "oauth" && isExpiring(profile)) {
|
|
533
|
+
const fresh = await refreshUnderLock(profileName, env);
|
|
534
|
+
if (!fresh) throw new AuthError("Your session expired and could not be refreshed.");
|
|
535
|
+
return fresh;
|
|
536
|
+
}
|
|
537
|
+
return profile.access_token;
|
|
538
|
+
},
|
|
539
|
+
async onUnauthorized() {
|
|
540
|
+
const { profile } = getProfile(profileName, env);
|
|
541
|
+
if (profile?.auth_kind !== "oauth") return null;
|
|
542
|
+
updateConfig((config) => {
|
|
543
|
+
const p = config.profiles[profileName];
|
|
544
|
+
if (p) p.expires_at = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
545
|
+
}, env);
|
|
546
|
+
return refreshUnderLock(profileName, env);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function resolveAuth(options) {
|
|
551
|
+
const env = options.env ?? process.env;
|
|
552
|
+
const envBase = env.VIDEODRAFT_BASE_URL;
|
|
553
|
+
if (options.token) {
|
|
554
|
+
return {
|
|
555
|
+
tokenProvider: { getAccessToken: async () => options.token },
|
|
556
|
+
baseUrl: options.baseUrl ?? envBase ?? DEFAULT_BASE_URL,
|
|
557
|
+
source: "flag"
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
if (env.VIDEODRAFT_API_KEY) {
|
|
561
|
+
return {
|
|
562
|
+
tokenProvider: { getAccessToken: async () => env.VIDEODRAFT_API_KEY },
|
|
563
|
+
baseUrl: options.baseUrl ?? envBase ?? DEFAULT_BASE_URL,
|
|
564
|
+
source: "env"
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const { name, profile } = getProfile(options.profile, env);
|
|
568
|
+
return {
|
|
569
|
+
tokenProvider: profileTokenProvider(name, env),
|
|
570
|
+
baseUrl: options.baseUrl ?? envBase ?? profile?.base_url ?? DEFAULT_BASE_URL,
|
|
571
|
+
source: "profile",
|
|
572
|
+
profileName: name
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/core/poll.ts
|
|
577
|
+
function nextPollDelay(baseMs, elapsedMs, adaptive = true) {
|
|
578
|
+
if (!adaptive) return baseMs;
|
|
579
|
+
const factor = elapsedMs >= 18e4 ? 3 : elapsedMs >= 6e4 ? 2 : 1;
|
|
580
|
+
const scaled = Math.min(baseMs * factor, 15e3);
|
|
581
|
+
const jitter = scaled * 0.1 * (Math.random() * 2 - 1);
|
|
582
|
+
return Math.max(250, Math.round(scaled + jitter));
|
|
583
|
+
}
|
|
584
|
+
var GENERATION_TERMINAL = /* @__PURE__ */ new Set(["completed", "failed"]);
|
|
585
|
+
var EXPORT_TERMINAL = /* @__PURE__ */ new Set(["finished", "failed"]);
|
|
586
|
+
function sleep(ms) {
|
|
587
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
588
|
+
}
|
|
589
|
+
function extractOutputUrls(payload) {
|
|
590
|
+
const urls = [];
|
|
591
|
+
const push = (value) => {
|
|
592
|
+
if (typeof value === "string" && /^https?:\/\//.test(value)) urls.push(value);
|
|
593
|
+
};
|
|
594
|
+
if (Array.isArray(payload?.outputUrls)) payload.outputUrls.forEach(push);
|
|
595
|
+
if (Array.isArray(payload?.output_urls)) payload.output_urls.forEach(push);
|
|
596
|
+
push(payload?.outputUrl);
|
|
597
|
+
push(payload?.output_url);
|
|
598
|
+
push(payload?.video_url);
|
|
599
|
+
push(payload?.videoUrl);
|
|
600
|
+
push(payload?.image_url);
|
|
601
|
+
push(payload?.imageUrl);
|
|
602
|
+
push(payload?.url);
|
|
603
|
+
push(payload?.audio_url);
|
|
604
|
+
push(payload?.audioUrl);
|
|
605
|
+
push(payload?.speech_url);
|
|
606
|
+
push(payload?.public_url);
|
|
607
|
+
return [...new Set(urls)];
|
|
608
|
+
}
|
|
609
|
+
async function pollGeneration(client, jobId, options = {}) {
|
|
610
|
+
const intervalMs = options.intervalMs ?? 3e3;
|
|
611
|
+
const timeoutMs = options.timeoutMs ?? 6e5;
|
|
612
|
+
const startedAt = Date.now();
|
|
613
|
+
const deadline = startedAt + timeoutMs;
|
|
614
|
+
for (; ; ) {
|
|
615
|
+
const payload = await client.callTool("check_generation_status", { job_id: jobId });
|
|
616
|
+
const status = String(payload?.status ?? "unknown");
|
|
617
|
+
options.onTick?.(status, payload);
|
|
618
|
+
if (GENERATION_TERMINAL.has(status)) {
|
|
619
|
+
return { status, outputUrls: extractOutputUrls(payload), payload };
|
|
620
|
+
}
|
|
621
|
+
if (Date.now() > deadline) {
|
|
622
|
+
throw new TimeoutError(
|
|
623
|
+
`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for job ${jobId} (last status: ${status}). It is still running server-side \u2014 check later with: videodraft status ${jobId}`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
await sleep(nextPollDelay(intervalMs, Date.now() - startedAt, options.adaptive));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
async function pollGenerationsBatch(client, jobIds, options = {}) {
|
|
630
|
+
const intervalMs = options.intervalMs ?? 3e3;
|
|
631
|
+
const timeoutMs = options.timeoutMs ?? 6e5;
|
|
632
|
+
const startedAt = Date.now();
|
|
633
|
+
const deadline = startedAt + timeoutMs;
|
|
634
|
+
const done = /* @__PURE__ */ new Map();
|
|
635
|
+
let pending = [...new Set(jobIds)];
|
|
636
|
+
const MAX_BATCH = 25;
|
|
637
|
+
while (pending.length > 0) {
|
|
638
|
+
const replies = [];
|
|
639
|
+
for (let at = 0; at < pending.length; at += MAX_BATCH) {
|
|
640
|
+
const chunk = pending.slice(at, at + MAX_BATCH);
|
|
641
|
+
replies.push(
|
|
642
|
+
...await client.callToolBatch(
|
|
643
|
+
chunk.map((id) => ({ name: "check_generation_status", args: { job_id: id } }))
|
|
644
|
+
)
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
const still = [];
|
|
648
|
+
for (let i = 0; i < pending.length; i++) {
|
|
649
|
+
const jobId = pending[i];
|
|
650
|
+
const reply = replies[i];
|
|
651
|
+
if (!reply.ok) {
|
|
652
|
+
const result = {
|
|
653
|
+
status: "failed",
|
|
654
|
+
outputUrls: [],
|
|
655
|
+
payload: { error: reply.error }
|
|
656
|
+
};
|
|
657
|
+
done.set(jobId, result);
|
|
658
|
+
options.onJobDone?.(jobId, result);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
const payload = reply.result;
|
|
662
|
+
const status = String(payload?.status ?? "unknown");
|
|
663
|
+
if (GENERATION_TERMINAL.has(status)) {
|
|
664
|
+
const result = { status, outputUrls: extractOutputUrls(payload), payload };
|
|
665
|
+
done.set(jobId, result);
|
|
666
|
+
options.onJobDone?.(jobId, result);
|
|
667
|
+
} else {
|
|
668
|
+
still.push(jobId);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
pending = still;
|
|
672
|
+
options.onTick?.(`${done.size}/${jobIds.length} done`, { pending: pending.length });
|
|
673
|
+
if (pending.length === 0) break;
|
|
674
|
+
if (Date.now() > deadline) {
|
|
675
|
+
throw new TimeoutError(
|
|
676
|
+
`Timed out after ${Math.round(timeoutMs / 1e3)}s with ${pending.length} job(s) still running: ${pending.join(", ")}. They continue server-side \u2014 resume with: videodraft wait ${pending.join(" ")}`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
await sleep(nextPollDelay(intervalMs, Date.now() - startedAt, options.adaptive));
|
|
680
|
+
}
|
|
681
|
+
return done;
|
|
682
|
+
}
|
|
683
|
+
async function pollExport(client, ref, options = {}) {
|
|
684
|
+
const intervalMs = options.intervalMs ?? 5e3;
|
|
685
|
+
const timeoutMs = options.timeoutMs ?? 12e5;
|
|
686
|
+
const startedAt = Date.now();
|
|
687
|
+
const deadline = startedAt + timeoutMs;
|
|
688
|
+
for (; ; ) {
|
|
689
|
+
const payload = await client.callTool("check_export_status", {
|
|
690
|
+
...ref.exportId ? { export_id: ref.exportId } : {},
|
|
691
|
+
...ref.projectId ? { project_id: ref.projectId } : {}
|
|
692
|
+
});
|
|
693
|
+
const status = String(payload?.status ?? "unknown");
|
|
694
|
+
options.onTick?.(status, payload);
|
|
695
|
+
if (EXPORT_TERMINAL.has(status)) {
|
|
696
|
+
return { status, videoUrl: payload?.video_url ?? payload?.videoUrl, payload };
|
|
697
|
+
}
|
|
698
|
+
if (Date.now() > deadline) {
|
|
699
|
+
throw new TimeoutError(
|
|
700
|
+
`Timed out waiting for export${ref.exportId ? ` ${ref.exportId}` : ""} (last status: ${status}).`
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
await sleep(nextPollDelay(intervalMs, Date.now() - startedAt, options.adaptive));
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
async function pollGenerations(client, jobIds, options = {}) {
|
|
707
|
+
return Promise.all(jobIds.map((id) => pollGeneration(client, id, options)));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/core/download.ts
|
|
711
|
+
import fs2 from "fs";
|
|
712
|
+
import path2 from "path";
|
|
713
|
+
import { pipeline } from "stream/promises";
|
|
714
|
+
import { Readable } from "stream";
|
|
715
|
+
function extFromUrl(url, fallback = "bin") {
|
|
716
|
+
try {
|
|
717
|
+
const pathname = new URL(url).pathname;
|
|
718
|
+
const ext = path2.extname(pathname).replace(/^\./, "");
|
|
719
|
+
return ext || fallback;
|
|
720
|
+
} catch {
|
|
721
|
+
return fallback;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function renderTemplate(template, vars) {
|
|
725
|
+
const hasPlaceholders = /\{(job_id|index|ext|name)\}/.test(template);
|
|
726
|
+
if (!hasPlaceholders) {
|
|
727
|
+
const looksLikeFile = path2.extname(template) !== "";
|
|
728
|
+
if (looksLikeFile) {
|
|
729
|
+
if (vars.index > 0) {
|
|
730
|
+
const ext = path2.extname(template);
|
|
731
|
+
return `${template.slice(0, -ext.length)}_${vars.index}${ext}`;
|
|
732
|
+
}
|
|
733
|
+
return template;
|
|
734
|
+
}
|
|
735
|
+
return path2.join(template, `${vars.job_id ?? vars.name ?? "output"}_${vars.index}.${vars.ext}`);
|
|
736
|
+
}
|
|
737
|
+
return template.replaceAll("{job_id}", vars.job_id ?? "job").replaceAll("{index}", String(vars.index)).replaceAll("{ext}", vars.ext).replaceAll("{name}", vars.name ?? "output");
|
|
738
|
+
}
|
|
739
|
+
async function downloadUrl(url, destPath, fetchImpl = fetch) {
|
|
740
|
+
const res = await fetchImpl(url, { signal: AbortSignal.timeout(6e5) });
|
|
741
|
+
if (!res.ok || !res.body) {
|
|
742
|
+
throw new CliError(`Download failed (HTTP ${res.status}): ${url}`);
|
|
743
|
+
}
|
|
744
|
+
fs2.mkdirSync(path2.dirname(path2.resolve(destPath)), { recursive: true });
|
|
745
|
+
await pipeline(Readable.fromWeb(res.body), fs2.createWriteStream(destPath));
|
|
746
|
+
const bytes = fs2.statSync(destPath).size;
|
|
747
|
+
return { url, path: destPath, bytes };
|
|
748
|
+
}
|
|
749
|
+
async function downloadOutputs(urls, template, vars, fetchImpl = fetch) {
|
|
750
|
+
const files = [];
|
|
751
|
+
for (let i = 0; i < urls.length; i++) {
|
|
752
|
+
const url = urls[i];
|
|
753
|
+
const dest = renderTemplate(template, {
|
|
754
|
+
job_id: vars.job_id,
|
|
755
|
+
name: vars.name,
|
|
756
|
+
index: i,
|
|
757
|
+
ext: extFromUrl(url)
|
|
758
|
+
});
|
|
759
|
+
files.push(await downloadUrl(url, dest, fetchImpl));
|
|
760
|
+
}
|
|
761
|
+
return files;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/core/upload.ts
|
|
765
|
+
import fs3 from "fs";
|
|
766
|
+
import path3 from "path";
|
|
767
|
+
var MIME_BY_EXT = {
|
|
768
|
+
png: "image/png",
|
|
769
|
+
jpg: "image/jpeg",
|
|
770
|
+
jpeg: "image/jpeg",
|
|
771
|
+
webp: "image/webp",
|
|
772
|
+
gif: "image/gif",
|
|
773
|
+
heic: "image/heic",
|
|
774
|
+
mp4: "video/mp4",
|
|
775
|
+
mov: "video/quicktime",
|
|
776
|
+
webm: "video/webm",
|
|
777
|
+
m4v: "video/x-m4v",
|
|
778
|
+
mp3: "audio/mpeg",
|
|
779
|
+
wav: "audio/wav",
|
|
780
|
+
m4a: "audio/mp4",
|
|
781
|
+
aac: "audio/aac",
|
|
782
|
+
ogg: "audio/ogg",
|
|
783
|
+
flac: "audio/flac"
|
|
784
|
+
};
|
|
785
|
+
function guessContentType(filename) {
|
|
786
|
+
const ext = path3.extname(filename).replace(/^\./, "").toLowerCase();
|
|
787
|
+
return MIME_BY_EXT[ext];
|
|
788
|
+
}
|
|
789
|
+
async function uploadFile(client, localPath, options = {}) {
|
|
790
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
791
|
+
const resolved = path3.resolve(localPath);
|
|
792
|
+
if (!fs3.existsSync(resolved)) {
|
|
793
|
+
throw new CliError(`File not found: ${resolved}`);
|
|
794
|
+
}
|
|
795
|
+
const filename = path3.basename(resolved);
|
|
796
|
+
const contentType = options.contentType ?? guessContentType(filename);
|
|
797
|
+
if (!contentType) {
|
|
798
|
+
throw new CliError(
|
|
799
|
+
`Cannot infer media type for "${filename}". Pass --content-type (image/*, video/* or audio/*).`
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
const created = await client.callTool("create_media_upload", {
|
|
803
|
+
filename,
|
|
804
|
+
content_type: contentType
|
|
805
|
+
});
|
|
806
|
+
const uploadUrl = created?.upload_url;
|
|
807
|
+
const filePath = created?.file_path;
|
|
808
|
+
if (!uploadUrl || !filePath) {
|
|
809
|
+
throw new CliError("create_media_upload did not return upload_url/file_path.");
|
|
810
|
+
}
|
|
811
|
+
const body = fs3.readFileSync(resolved);
|
|
812
|
+
const putRes = await fetchImpl(uploadUrl, {
|
|
813
|
+
method: "PUT",
|
|
814
|
+
headers: { "content-type": contentType },
|
|
815
|
+
body,
|
|
816
|
+
signal: AbortSignal.timeout(6e5)
|
|
817
|
+
});
|
|
818
|
+
if (!putRes.ok) {
|
|
819
|
+
throw new CliError(`Upload PUT failed (HTTP ${putRes.status}). The presigned URL may have expired \u2014 retry.`);
|
|
820
|
+
}
|
|
821
|
+
const finalized = await client.callTool("finalize_media_upload", {
|
|
822
|
+
file_path: filePath,
|
|
823
|
+
original_filename: filename
|
|
824
|
+
});
|
|
825
|
+
const url = finalized?.url ?? finalized?.cdn_url;
|
|
826
|
+
if (!url) {
|
|
827
|
+
throw new CliError("finalize_media_upload did not return a public url.");
|
|
828
|
+
}
|
|
829
|
+
return { ...finalized, url, file_path: filePath };
|
|
830
|
+
}
|
|
831
|
+
export {
|
|
832
|
+
AuthError,
|
|
833
|
+
CliError,
|
|
834
|
+
DEFAULT_BASE_URL,
|
|
835
|
+
EXIT,
|
|
836
|
+
RpcError,
|
|
837
|
+
STATIC_CLI_CLIENT_ID,
|
|
838
|
+
TimeoutError,
|
|
839
|
+
ToolError,
|
|
840
|
+
VideoDraftClient,
|
|
841
|
+
authorizeViaLoopback,
|
|
842
|
+
buildAuthorizeUrl,
|
|
843
|
+
configDir,
|
|
844
|
+
configPath,
|
|
845
|
+
createPkcePair,
|
|
846
|
+
downloadOutputs,
|
|
847
|
+
downloadUrl,
|
|
848
|
+
exchangeCode,
|
|
849
|
+
extFromUrl,
|
|
850
|
+
extractOutputUrls,
|
|
851
|
+
getProfile,
|
|
852
|
+
guessContentType,
|
|
853
|
+
nextPollDelay,
|
|
854
|
+
pollExport,
|
|
855
|
+
pollGeneration,
|
|
856
|
+
pollGenerations,
|
|
857
|
+
pollGenerationsBatch,
|
|
858
|
+
probeClientId,
|
|
859
|
+
profileTokenProvider,
|
|
860
|
+
readConfig,
|
|
861
|
+
refreshAccessToken,
|
|
862
|
+
registerCliClient,
|
|
863
|
+
renderTemplate,
|
|
864
|
+
resolveAuth,
|
|
865
|
+
revokeToken,
|
|
866
|
+
staticTokenProvider,
|
|
867
|
+
updateConfig,
|
|
868
|
+
uploadFile,
|
|
869
|
+
writeConfig
|
|
870
|
+
};
|