@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flatkey-ai/cli",
3
- "version": "0.1.25",
3
+ "version": "0.2.1",
4
4
  "description": "Flatkey media generation CLI for image, video, audio, credits, status, and models.",
5
5
  "type": "module",
6
6
  "bin": {
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 response = await fetchImpl(plan.url, {
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 response = await fetchImpl(plan.url, {
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
- process.stdout.write(`${formatHuman(result)}\n`);
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
- process.stdout.write(`Saved Flatkey config: ${configPath}\n`);
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
- process.stdout.write(
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 authorization = await createDeviceAuthorization({
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
- fetch: deps.fetch,
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
- fetch: deps.fetch,
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
@@ -67,6 +67,7 @@ Commands:
67
67
  Global options:
68
68
  --json Print machine-readable JSON
69
69
  --output, -o <file> Write generated output to a local file
70
+ --verbose Print request/response logs to stderr
70
71
  --base-url <url> Override Flatkey router URL
71
72
 
72
73
  Environment: