@flatkey-ai/cli 0.1.25 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api.js +55 -4
- package/src/artifacts.js +3 -1
- package/src/cli.js +97 -18
- package/src/help.js +1 -0
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -29,12 +29,16 @@ export async function uploadTempMediaImage(options) {
|
|
|
29
29
|
export function planImageRequest(options) {
|
|
30
30
|
const model = options.model ?? "nano-banana-pro-preview";
|
|
31
31
|
if (model.startsWith("gpt")) {
|
|
32
|
+
const responseFormat = options.response_format ?? options.responseFormat ?? "url";
|
|
33
|
+
const tempUrl = options.temp_url ?? options.tempUrl;
|
|
32
34
|
return planJsonPost(options, "/v1/images/generations", cleanObject({
|
|
33
35
|
model,
|
|
34
36
|
prompt: options.prompt,
|
|
35
37
|
size: options.size,
|
|
36
38
|
n: parseOptionalInteger(options.n),
|
|
37
39
|
quality: options.quality,
|
|
40
|
+
response_format: responseFormat,
|
|
41
|
+
temp_url: tempUrl ?? true,
|
|
38
42
|
}));
|
|
39
43
|
}
|
|
40
44
|
|
|
@@ -47,6 +51,7 @@ export function planImageRequest(options) {
|
|
|
47
51
|
},
|
|
48
52
|
body: {
|
|
49
53
|
contents: [{ parts: [{ text: options.prompt }] }],
|
|
54
|
+
temp_url: true,
|
|
50
55
|
},
|
|
51
56
|
});
|
|
52
57
|
}
|
|
@@ -88,6 +93,7 @@ export function planVideoRequest(options) {
|
|
|
88
93
|
resolution,
|
|
89
94
|
quality: resolution,
|
|
90
95
|
fps: parseOptionalInteger(options.fps),
|
|
96
|
+
temp_url: true,
|
|
91
97
|
images: !isSeedanceModel(model) && imageUrls.length > 0 ? imageUrls : undefined,
|
|
92
98
|
});
|
|
93
99
|
const seedanceContent = buildSeedanceContent(options);
|
|
@@ -232,7 +238,7 @@ function planRequest(options, path, init = {}) {
|
|
|
232
238
|
|
|
233
239
|
async function requestJsonFromPlan(options, plan) {
|
|
234
240
|
const fetchImpl = options.fetch ?? fetch;
|
|
235
|
-
const
|
|
241
|
+
const request = {
|
|
236
242
|
method: plan.method,
|
|
237
243
|
headers: plan.headers,
|
|
238
244
|
body: plan.body === undefined
|
|
@@ -240,8 +246,12 @@ async function requestJsonFromPlan(options, plan) {
|
|
|
240
246
|
: isFormData(plan.body)
|
|
241
247
|
? plan.body
|
|
242
248
|
: JSON.stringify(plan.body),
|
|
243
|
-
}
|
|
249
|
+
};
|
|
250
|
+
logRequest(options, plan.url, request);
|
|
251
|
+
const response = await fetchImpl(plan.url, request);
|
|
252
|
+
logResponse(options, response);
|
|
244
253
|
const body = await readJson(response);
|
|
254
|
+
logResponseBody(options, body);
|
|
245
255
|
if (!response.ok || body?.success === false) {
|
|
246
256
|
throw new FlatkeyError(extractErrorMessage(body, response.status), {
|
|
247
257
|
status: response.status,
|
|
@@ -256,13 +266,17 @@ function isFormData(value) {
|
|
|
256
266
|
|
|
257
267
|
async function requestBinaryArtifactFromPlan(options, plan) {
|
|
258
268
|
const fetchImpl = options.fetch ?? fetch;
|
|
259
|
-
const
|
|
269
|
+
const request = {
|
|
260
270
|
method: plan.method,
|
|
261
271
|
headers: plan.headers,
|
|
262
272
|
body: plan.body === undefined ? undefined : JSON.stringify(plan.body),
|
|
263
|
-
}
|
|
273
|
+
};
|
|
274
|
+
logRequest(options, plan.url, request);
|
|
275
|
+
const response = await fetchImpl(plan.url, request);
|
|
276
|
+
logResponse(options, response);
|
|
264
277
|
if (!response.ok) {
|
|
265
278
|
const body = await readJson(response);
|
|
279
|
+
logResponseBody(options, body);
|
|
266
280
|
throw new FlatkeyError(extractErrorMessage(body, response.status), {
|
|
267
281
|
status: response.status,
|
|
268
282
|
});
|
|
@@ -272,6 +286,43 @@ async function requestBinaryArtifactFromPlan(options, plan) {
|
|
|
272
286
|
};
|
|
273
287
|
}
|
|
274
288
|
|
|
289
|
+
function logRequest(options, url, request) {
|
|
290
|
+
if (!options?.verbose) return;
|
|
291
|
+
const safeHeaders = redactHeaders(request.headers);
|
|
292
|
+
const safeBody = request.body === undefined
|
|
293
|
+
? undefined
|
|
294
|
+
: isFormData(request.body)
|
|
295
|
+
? "<form-data>"
|
|
296
|
+
: request.body;
|
|
297
|
+
options.verboseLog?.(`-> ${request.method} ${url}`);
|
|
298
|
+
options.verboseLog?.(`headers: ${JSON.stringify(safeHeaders)}`);
|
|
299
|
+
if (safeBody !== undefined) {
|
|
300
|
+
options.verboseLog?.(`body: ${safeBody}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function logResponse(options, response) {
|
|
305
|
+
if (!options?.verbose) return;
|
|
306
|
+
options.verboseLog?.(`<- ${response.status} ${response.statusText || ""}`.trim());
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function logResponseBody(options, body) {
|
|
310
|
+
if (!options?.verbose) return;
|
|
311
|
+
options.verboseLog?.(`response: ${truncateLogValue(JSON.stringify(body))}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function redactHeaders(headers) {
|
|
315
|
+
return Object.fromEntries(Object.entries(headers ?? {}).map(([key, value]) => [
|
|
316
|
+
key,
|
|
317
|
+
key.toLowerCase() === "authorization" ? "Bearer <redacted>" : `${value}`,
|
|
318
|
+
]));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function truncateLogValue(value) {
|
|
322
|
+
if (value === undefined) return "";
|
|
323
|
+
return value.length > 1200 ? `${value.slice(0, 1200)}…` : value;
|
|
324
|
+
}
|
|
325
|
+
|
|
275
326
|
async function requestJson(options, path, init = {}) {
|
|
276
327
|
return requestJsonFromPlan(options, planRequest(options, path, {
|
|
277
328
|
method: init.method ?? "GET",
|
package/src/artifacts.js
CHANGED
|
@@ -47,7 +47,7 @@ function expandHomePath(path) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
async function persistItem({ kind, item, outDir, output, index, fetchImpl }) {
|
|
50
|
-
const dataUrl = getString(item, ["url", "video_url", "data_url", "dataUrl"]);
|
|
50
|
+
const dataUrl = getString(item, ["url", "video_url", "data_url", "dataUrl", "temp_url"]);
|
|
51
51
|
if (dataUrl?.startsWith("data:")) {
|
|
52
52
|
const parsed = parseDataUrl(dataUrl);
|
|
53
53
|
const path = artifactPath({ kind, outDir, output, index, extension: parsed.extension });
|
|
@@ -94,6 +94,8 @@ function extractItems(response) {
|
|
|
94
94
|
if (Array.isArray(response?.candidates)) {
|
|
95
95
|
return response.candidates.flatMap((candidate) => candidate.content?.parts ?? []);
|
|
96
96
|
}
|
|
97
|
+
if (typeof response?.temp_url === "string") return [{ url: response.temp_url }];
|
|
98
|
+
if (typeof response?.metadata?.temp_url === "string") return [{ url: response.metadata.temp_url }];
|
|
97
99
|
if (typeof response?.metadata?.url === "string") return [{ url: response.metadata.url }];
|
|
98
100
|
if (typeof response?.url === "string") return [response];
|
|
99
101
|
if (typeof response?.video_url?.url === "string") return [{ url: response.video_url.url }];
|
package/src/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ const COMMANDS = new Set([
|
|
|
15
15
|
]);
|
|
16
16
|
|
|
17
17
|
const GROUP_ACTIONS = new Set(["audio", "auth", "image", "text", "video"]);
|
|
18
|
-
const GLOBAL_OPTIONS = new Set(["api_key", "base_url", "console_url", "dry_run", "help", "json", "output", "out"]);
|
|
18
|
+
const GLOBAL_OPTIONS = new Set(["api_key", "base_url", "console_url", "dry_run", "help", "json", "output", "out", "verbose"]);
|
|
19
19
|
const REPEATABLE_OPTIONS = new Set(["image", "image_url", "video_url"]);
|
|
20
20
|
const COMMAND_OPTIONS = {
|
|
21
21
|
"audio generate": new Set(["model", "prompt", "similarity_boost", "stability", "style", "voice_id"]),
|
|
@@ -146,26 +146,30 @@ function validateCommandOptions(command) {
|
|
|
146
146
|
return command;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
export async function main(argv) {
|
|
149
|
+
export async function main(argv, deps = {}) {
|
|
150
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
151
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
150
152
|
const command = parseArgv(argv);
|
|
151
153
|
if (command.options.help) {
|
|
152
|
-
const result = await runCommand(command);
|
|
153
|
-
|
|
154
|
+
const result = await runCommand(command, { ...deps, stdout, stderr });
|
|
155
|
+
stdout.write(`${formatHuman(result)}\n`);
|
|
154
156
|
return;
|
|
155
157
|
}
|
|
156
158
|
if (command.group === "onboard") {
|
|
157
159
|
const { writeConfig } = await import("./config.js");
|
|
158
|
-
const configPath = await writeConfig({ apiKey: command.options.api_key });
|
|
159
|
-
|
|
160
|
+
const configPath = await writeConfig({ apiKey: command.options.api_key, configDir: deps.configDir });
|
|
161
|
+
stdout.write(`Saved Flatkey config: ${configPath}\n`);
|
|
162
|
+
await maybeWriteUpdateNotice(command, { ...deps, stderr });
|
|
160
163
|
return;
|
|
161
164
|
}
|
|
162
165
|
|
|
163
|
-
const result = await runCommand(command);
|
|
166
|
+
const result = await runCommand(command, { ...deps, stdout, stderr });
|
|
164
167
|
if (result !== undefined) {
|
|
165
|
-
|
|
168
|
+
stdout.write(
|
|
166
169
|
command.options.json ? `${JSON.stringify(result)}\n` : `${formatHuman(result)}\n`,
|
|
167
170
|
);
|
|
168
171
|
}
|
|
172
|
+
await maybeWriteUpdateNotice(command, { ...deps, stderr });
|
|
169
173
|
}
|
|
170
174
|
|
|
171
175
|
export async function runCommand(command, deps = {}) {
|
|
@@ -187,7 +191,7 @@ export async function runCommand(command, deps = {}) {
|
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
if (command.group === "models") {
|
|
190
|
-
return handleModels(command, deps);
|
|
194
|
+
return handleModels(command, { ...deps, stdout, stderr });
|
|
191
195
|
}
|
|
192
196
|
|
|
193
197
|
if (command.group === "login") {
|
|
@@ -206,12 +210,12 @@ export async function runCommand(command, deps = {}) {
|
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
if (command.group === "credits" || command.group === "status") {
|
|
209
|
-
return handleUtility(command, deps);
|
|
213
|
+
return handleUtility(command, { ...deps, stdout, stderr });
|
|
210
214
|
}
|
|
211
215
|
|
|
212
216
|
if (["image", "video", "audio", "text"].includes(command.group)) {
|
|
213
217
|
if (command.group === "audio" && command.action === "voices") {
|
|
214
|
-
return handleVoices(command, deps);
|
|
218
|
+
return handleVoices(command, { ...deps, stdout, stderr });
|
|
215
219
|
}
|
|
216
220
|
const validAction = command.group === "audio"
|
|
217
221
|
? ["generate", "sfx", "music"].includes(command.action)
|
|
@@ -222,7 +226,7 @@ export async function runCommand(command, deps = {}) {
|
|
|
222
226
|
throw new Error(`Unknown action for ${command.group}: ${command.action}`);
|
|
223
227
|
}
|
|
224
228
|
if (command.group === "image" && command.action === "upload") {
|
|
225
|
-
return handleImageUpload(command, deps);
|
|
229
|
+
return handleImageUpload(command, { ...deps, stdout, stderr });
|
|
226
230
|
}
|
|
227
231
|
return handleGenerate(command, { ...deps, stdout, stderr });
|
|
228
232
|
}
|
|
@@ -236,12 +240,17 @@ async function handleLogin(command, deps) {
|
|
|
236
240
|
const deviceId = await ensureDeviceId({ configDir: deps.configDir });
|
|
237
241
|
const version = await readPackageVersion();
|
|
238
242
|
const consoleOrigin = firstNonEmpty(command.options.console_url, DEFAULT_CONSOLE_URL);
|
|
239
|
-
const
|
|
243
|
+
const requestOptions = {
|
|
240
244
|
consoleUrl: consoleOrigin,
|
|
245
|
+
fetch: deps.fetch,
|
|
246
|
+
verbose: command.options.verbose,
|
|
247
|
+
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
248
|
+
};
|
|
249
|
+
const authorization = await createDeviceAuthorization({
|
|
241
250
|
deviceId,
|
|
242
251
|
clientName: "flatkey-cli",
|
|
243
252
|
clientVersion: version,
|
|
244
|
-
|
|
253
|
+
...requestOptions,
|
|
245
254
|
});
|
|
246
255
|
const data = authorization?.data ?? authorization;
|
|
247
256
|
if (!data?.device_code || !data?.verification_uri_complete) {
|
|
@@ -261,9 +270,8 @@ async function handleLogin(command, deps) {
|
|
|
261
270
|
while (Date.now() < deadline) {
|
|
262
271
|
await delay(nextLoginPollDelay(startedAt, initialIntervalMs), deps);
|
|
263
272
|
const poll = await pollDeviceAuthorization({
|
|
264
|
-
consoleUrl: consoleOrigin,
|
|
265
273
|
deviceCode: data.device_code,
|
|
266
|
-
|
|
274
|
+
...requestOptions,
|
|
267
275
|
});
|
|
268
276
|
const pollData = poll?.data ?? poll;
|
|
269
277
|
if (pollData?.status === "approved") {
|
|
@@ -432,6 +440,8 @@ async function handleImageUpload(command, deps) {
|
|
|
432
440
|
filename: basename(file),
|
|
433
441
|
contentType: imageContentTypeFromPath(file),
|
|
434
442
|
fetch: deps.fetch,
|
|
443
|
+
verbose: command.options.verbose,
|
|
444
|
+
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
435
445
|
});
|
|
436
446
|
const data = response?.data ?? response;
|
|
437
447
|
const signedUrl = extractUploadedImageUrl(response);
|
|
@@ -494,6 +504,7 @@ async function handleGenerate(command, deps) {
|
|
|
494
504
|
tempMediaBaseUrl: tempMediaBaseUrl ?? DEFAULT_CONSOLE_URL,
|
|
495
505
|
env: deps.env ?? process.env,
|
|
496
506
|
fetch: deps.fetch,
|
|
507
|
+
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
497
508
|
};
|
|
498
509
|
if (command.group === "video" && !command.options.dry_run) {
|
|
499
510
|
await uploadVideoLocalImages(options, { uploadTempMediaImage });
|
|
@@ -581,6 +592,8 @@ async function uploadLocalImage(file, options, deps) {
|
|
|
581
592
|
filename: basename(file),
|
|
582
593
|
contentType: imageContentTypeFromPath(file),
|
|
583
594
|
fetch: options.fetch,
|
|
595
|
+
verbose: options.verbose,
|
|
596
|
+
verboseLog: options.verboseLog,
|
|
584
597
|
});
|
|
585
598
|
const url = extractUploadedImageUrl(response);
|
|
586
599
|
if (!url) throw new Error(`Image upload failed: missing signed_url for ${file}`);
|
|
@@ -632,10 +645,12 @@ async function waitForVideoResult(response, options) {
|
|
|
632
645
|
function hasArtifact(response) {
|
|
633
646
|
return Boolean(
|
|
634
647
|
response?.metadata?.url
|
|
648
|
+
|| response?.metadata?.temp_url
|
|
635
649
|
|| response?.url
|
|
650
|
+
|| response?.temp_url
|
|
636
651
|
|| response?.video_url?.url
|
|
637
|
-
|| response?.data?.some?.((item) => item?.url || item?.b64_json || item?.base64 || item?.data)
|
|
638
|
-
|| response?.artifacts?.some?.((item) => item?.url || item?.path)
|
|
652
|
+
|| response?.data?.some?.((item) => item?.url || item?.temp_url || item?.b64_json || item?.base64 || item?.data)
|
|
653
|
+
|| response?.artifacts?.some?.((item) => item?.url || item?.temp_url || item?.path)
|
|
639
654
|
|| response?.content?.some?.((item) => item?.url || item?.video_url?.url || item?.data_url || item?.b64_json || item?.base64 || item?.data),
|
|
640
655
|
);
|
|
641
656
|
}
|
|
@@ -696,6 +711,8 @@ async function handleVoices(command, deps) {
|
|
|
696
711
|
apiKey,
|
|
697
712
|
baseUrl: routerOrigin,
|
|
698
713
|
fetch: deps.fetch,
|
|
714
|
+
verbose: command.options.verbose,
|
|
715
|
+
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
699
716
|
});
|
|
700
717
|
}
|
|
701
718
|
|
|
@@ -755,6 +772,8 @@ async function handleUtility(command, deps) {
|
|
|
755
772
|
apiKey,
|
|
756
773
|
baseUrl: consoleOrigin,
|
|
757
774
|
fetch: deps.fetch,
|
|
775
|
+
verbose: command.options.verbose,
|
|
776
|
+
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
758
777
|
};
|
|
759
778
|
if (command.group === "credits") return getCredits(options);
|
|
760
779
|
|
|
@@ -841,10 +860,21 @@ async function handleModels(command, deps) {
|
|
|
841
860
|
apiKey,
|
|
842
861
|
baseUrl: consoleOrigin,
|
|
843
862
|
fetch: deps.fetch,
|
|
863
|
+
verbose: command.options.verbose,
|
|
864
|
+
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
844
865
|
});
|
|
845
866
|
return { models: normalizeModels(response, command.options.type) };
|
|
846
867
|
}
|
|
847
868
|
|
|
869
|
+
function verboseLogger(stream, enabled) {
|
|
870
|
+
if (!enabled) return () => {};
|
|
871
|
+
return (line) => {
|
|
872
|
+
if (typeof line === "string") {
|
|
873
|
+
stream?.write?.(`${line}\n`);
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
848
878
|
function formatHuman(result) {
|
|
849
879
|
if (typeof result === "string") return result;
|
|
850
880
|
if (!result || typeof result !== "object") return String(result);
|
|
@@ -1001,3 +1031,52 @@ async function readPackageVersion() {
|
|
|
1001
1031
|
const packageUrl = new URL("../package.json", import.meta.url);
|
|
1002
1032
|
return JSON.parse(await readFile(packageUrl, "utf8")).version;
|
|
1003
1033
|
}
|
|
1034
|
+
|
|
1035
|
+
async function maybeWriteUpdateNotice(command, deps = {}) {
|
|
1036
|
+
if (command.options.json || command.options.help || command.group === "help" || command.group === "version") {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
try {
|
|
1040
|
+
const current = await readPackageVersion();
|
|
1041
|
+
const latest = await fetchLatestPackageVersion(deps);
|
|
1042
|
+
if (!latest || compareVersions(latest, current) <= 0) return;
|
|
1043
|
+
deps.stderr?.write?.([
|
|
1044
|
+
`\nFlatkey CLI update available: ${current} -> ${latest}`,
|
|
1045
|
+
"Upgrade: npm install -g @flatkey-ai/cli",
|
|
1046
|
+
"",
|
|
1047
|
+
].join("\n"));
|
|
1048
|
+
} catch {
|
|
1049
|
+
// Update checks must never break normal CLI commands.
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
async function fetchLatestPackageVersion(deps = {}) {
|
|
1054
|
+
const fetchImpl = deps.fetch ?? globalThis.fetch;
|
|
1055
|
+
if (typeof fetchImpl !== "function") return undefined;
|
|
1056
|
+
const response = await fetchImpl("https://registry.npmjs.org/@flatkey-ai%2fcli/latest", {
|
|
1057
|
+
signal: typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function"
|
|
1058
|
+
? AbortSignal.timeout(750)
|
|
1059
|
+
: undefined,
|
|
1060
|
+
});
|
|
1061
|
+
if (!response?.ok) return undefined;
|
|
1062
|
+
const body = await response.json();
|
|
1063
|
+
return typeof body?.version === "string" ? body.version : undefined;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function compareVersions(left, right) {
|
|
1067
|
+
const leftParts = parseVersionParts(left);
|
|
1068
|
+
const rightParts = parseVersionParts(right);
|
|
1069
|
+
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
|
|
1070
|
+
const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
|
1071
|
+
if (difference !== 0) return difference;
|
|
1072
|
+
}
|
|
1073
|
+
return 0;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function parseVersionParts(version) {
|
|
1077
|
+
return String(version)
|
|
1078
|
+
.split("-", 1)[0]
|
|
1079
|
+
.split(".")
|
|
1080
|
+
.map((part) => Number.parseInt(part, 10))
|
|
1081
|
+
.map((part) => Number.isFinite(part) ? part : 0);
|
|
1082
|
+
}
|
package/src/help.js
CHANGED