@flatkey-ai/cli 0.1.23 → 0.2.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/package.json +1 -1
- package/src/api.js +55 -4
- package/src/artifacts.js +3 -1
- package/src/cli.js +83 -23
- 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"]),
|
|
@@ -187,7 +187,7 @@ export async function runCommand(command, deps = {}) {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
if (command.group === "models") {
|
|
190
|
-
return handleModels(command, deps);
|
|
190
|
+
return handleModels(command, { ...deps, stdout, stderr });
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
if (command.group === "login") {
|
|
@@ -206,12 +206,12 @@ export async function runCommand(command, deps = {}) {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
if (command.group === "credits" || command.group === "status") {
|
|
209
|
-
return handleUtility(command, deps);
|
|
209
|
+
return handleUtility(command, { ...deps, stdout, stderr });
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
if (["image", "video", "audio", "text"].includes(command.group)) {
|
|
213
213
|
if (command.group === "audio" && command.action === "voices") {
|
|
214
|
-
return handleVoices(command, deps);
|
|
214
|
+
return handleVoices(command, { ...deps, stdout, stderr });
|
|
215
215
|
}
|
|
216
216
|
const validAction = command.group === "audio"
|
|
217
217
|
? ["generate", "sfx", "music"].includes(command.action)
|
|
@@ -222,7 +222,7 @@ export async function runCommand(command, deps = {}) {
|
|
|
222
222
|
throw new Error(`Unknown action for ${command.group}: ${command.action}`);
|
|
223
223
|
}
|
|
224
224
|
if (command.group === "image" && command.action === "upload") {
|
|
225
|
-
return handleImageUpload(command, deps);
|
|
225
|
+
return handleImageUpload(command, { ...deps, stdout, stderr });
|
|
226
226
|
}
|
|
227
227
|
return handleGenerate(command, { ...deps, stdout, stderr });
|
|
228
228
|
}
|
|
@@ -236,12 +236,17 @@ async function handleLogin(command, deps) {
|
|
|
236
236
|
const deviceId = await ensureDeviceId({ configDir: deps.configDir });
|
|
237
237
|
const version = await readPackageVersion();
|
|
238
238
|
const consoleOrigin = firstNonEmpty(command.options.console_url, DEFAULT_CONSOLE_URL);
|
|
239
|
-
const
|
|
239
|
+
const requestOptions = {
|
|
240
240
|
consoleUrl: consoleOrigin,
|
|
241
|
+
fetch: deps.fetch,
|
|
242
|
+
verbose: command.options.verbose,
|
|
243
|
+
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
244
|
+
};
|
|
245
|
+
const authorization = await createDeviceAuthorization({
|
|
241
246
|
deviceId,
|
|
242
247
|
clientName: "flatkey-cli",
|
|
243
248
|
clientVersion: version,
|
|
244
|
-
|
|
249
|
+
...requestOptions,
|
|
245
250
|
});
|
|
246
251
|
const data = authorization?.data ?? authorization;
|
|
247
252
|
if (!data?.device_code || !data?.verification_uri_complete) {
|
|
@@ -261,12 +266,12 @@ async function handleLogin(command, deps) {
|
|
|
261
266
|
while (Date.now() < deadline) {
|
|
262
267
|
await delay(nextLoginPollDelay(startedAt, initialIntervalMs), deps);
|
|
263
268
|
const poll = await pollDeviceAuthorization({
|
|
264
|
-
consoleUrl: consoleOrigin,
|
|
265
269
|
deviceCode: data.device_code,
|
|
266
|
-
|
|
270
|
+
...requestOptions,
|
|
267
271
|
});
|
|
268
272
|
const pollData = poll?.data ?? poll;
|
|
269
273
|
if (pollData?.status === "approved") {
|
|
274
|
+
const account = accountFromAuthData(pollData);
|
|
270
275
|
if (!pollData.api_key) {
|
|
271
276
|
const saved = await readConfig(deps.configDir);
|
|
272
277
|
if (typeof saved?.apiKey === "string" && saved.apiKey.trim() !== "") {
|
|
@@ -276,7 +281,7 @@ async function handleLogin(command, deps) {
|
|
|
276
281
|
deviceId,
|
|
277
282
|
userId: pollData.user_id ?? saved.auth?.userId,
|
|
278
283
|
tokenId: pollData.token_id ?? saved.auth?.tokenId,
|
|
279
|
-
account:
|
|
284
|
+
account: account ?? saved.auth?.account,
|
|
280
285
|
loginAt: Math.floor(Date.now() / 1000),
|
|
281
286
|
},
|
|
282
287
|
configDir: deps.configDir,
|
|
@@ -285,9 +290,7 @@ async function handleLogin(command, deps) {
|
|
|
285
290
|
? {
|
|
286
291
|
success: true,
|
|
287
292
|
configPath,
|
|
288
|
-
|
|
289
|
-
userId: pollData.user_id ?? saved.auth?.userId,
|
|
290
|
-
account: accountFromAuthData(pollData) ?? saved.auth?.account,
|
|
293
|
+
account: stripDisplayOnlyIds(account ?? saved.auth?.account),
|
|
291
294
|
reusedConfig: true,
|
|
292
295
|
}
|
|
293
296
|
: `Flatkey CLI already authorized. Saved config: ${configPath}`;
|
|
@@ -300,7 +303,7 @@ async function handleLogin(command, deps) {
|
|
|
300
303
|
deviceId,
|
|
301
304
|
userId: pollData.user_id,
|
|
302
305
|
tokenId: pollData.token_id,
|
|
303
|
-
account
|
|
306
|
+
account,
|
|
304
307
|
loginAt: Math.floor(Date.now() / 1000),
|
|
305
308
|
},
|
|
306
309
|
configDir: deps.configDir,
|
|
@@ -309,9 +312,7 @@ async function handleLogin(command, deps) {
|
|
|
309
312
|
? {
|
|
310
313
|
success: true,
|
|
311
314
|
configPath,
|
|
312
|
-
|
|
313
|
-
userId: pollData.user_id,
|
|
314
|
-
account: accountFromAuthData(pollData),
|
|
315
|
+
account: stripDisplayOnlyIds(account),
|
|
315
316
|
}
|
|
316
317
|
: `Flatkey CLI authorized. Saved config: ${configPath}`;
|
|
317
318
|
}
|
|
@@ -435,6 +436,8 @@ async function handleImageUpload(command, deps) {
|
|
|
435
436
|
filename: basename(file),
|
|
436
437
|
contentType: imageContentTypeFromPath(file),
|
|
437
438
|
fetch: deps.fetch,
|
|
439
|
+
verbose: command.options.verbose,
|
|
440
|
+
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
438
441
|
});
|
|
439
442
|
const data = response?.data ?? response;
|
|
440
443
|
const signedUrl = extractUploadedImageUrl(response);
|
|
@@ -497,6 +500,7 @@ async function handleGenerate(command, deps) {
|
|
|
497
500
|
tempMediaBaseUrl: tempMediaBaseUrl ?? DEFAULT_CONSOLE_URL,
|
|
498
501
|
env: deps.env ?? process.env,
|
|
499
502
|
fetch: deps.fetch,
|
|
503
|
+
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
500
504
|
};
|
|
501
505
|
if (command.group === "video" && !command.options.dry_run) {
|
|
502
506
|
await uploadVideoLocalImages(options, { uploadTempMediaImage });
|
|
@@ -584,6 +588,8 @@ async function uploadLocalImage(file, options, deps) {
|
|
|
584
588
|
filename: basename(file),
|
|
585
589
|
contentType: imageContentTypeFromPath(file),
|
|
586
590
|
fetch: options.fetch,
|
|
591
|
+
verbose: options.verbose,
|
|
592
|
+
verboseLog: options.verboseLog,
|
|
587
593
|
});
|
|
588
594
|
const url = extractUploadedImageUrl(response);
|
|
589
595
|
if (!url) throw new Error(`Image upload failed: missing signed_url for ${file}`);
|
|
@@ -635,10 +641,12 @@ async function waitForVideoResult(response, options) {
|
|
|
635
641
|
function hasArtifact(response) {
|
|
636
642
|
return Boolean(
|
|
637
643
|
response?.metadata?.url
|
|
644
|
+
|| response?.metadata?.temp_url
|
|
638
645
|
|| response?.url
|
|
646
|
+
|| response?.temp_url
|
|
639
647
|
|| response?.video_url?.url
|
|
640
|
-
|| response?.data?.some?.((item) => item?.url || item?.b64_json || item?.base64 || item?.data)
|
|
641
|
-
|| response?.artifacts?.some?.((item) => item?.url || item?.path)
|
|
648
|
+
|| response?.data?.some?.((item) => item?.url || item?.temp_url || item?.b64_json || item?.base64 || item?.data)
|
|
649
|
+
|| response?.artifacts?.some?.((item) => item?.url || item?.temp_url || item?.path)
|
|
642
650
|
|| response?.content?.some?.((item) => item?.url || item?.video_url?.url || item?.data_url || item?.b64_json || item?.base64 || item?.data),
|
|
643
651
|
);
|
|
644
652
|
}
|
|
@@ -699,6 +707,8 @@ async function handleVoices(command, deps) {
|
|
|
699
707
|
apiKey,
|
|
700
708
|
baseUrl: routerOrigin,
|
|
701
709
|
fetch: deps.fetch,
|
|
710
|
+
verbose: command.options.verbose,
|
|
711
|
+
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
702
712
|
});
|
|
703
713
|
}
|
|
704
714
|
|
|
@@ -758,15 +768,18 @@ async function handleUtility(command, deps) {
|
|
|
758
768
|
apiKey,
|
|
759
769
|
baseUrl: consoleOrigin,
|
|
760
770
|
fetch: deps.fetch,
|
|
771
|
+
verbose: command.options.verbose,
|
|
772
|
+
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
761
773
|
};
|
|
762
774
|
if (command.group === "credits") return getCredits(options);
|
|
763
775
|
|
|
764
|
-
const status = await getStatus(options);
|
|
776
|
+
const status = sanitizeStatusResponse(await getStatus(options));
|
|
765
777
|
const account = effectiveAccountForStatus({ command, saved, apiKey });
|
|
766
778
|
if (!account || !status || typeof status !== "object" || Array.isArray(status) || status.account !== undefined) {
|
|
767
|
-
return status;
|
|
779
|
+
return command.options.json ? status : extractStatusDisplayFields(status);
|
|
768
780
|
}
|
|
769
|
-
|
|
781
|
+
const mergedStatus = sanitizeStatusResponse({ ...status, account });
|
|
782
|
+
return command.options.json ? mergedStatus : extractStatusDisplayFields(mergedStatus);
|
|
770
783
|
}
|
|
771
784
|
|
|
772
785
|
function effectiveAccountForStatus({ command, saved, apiKey }) {
|
|
@@ -778,16 +791,52 @@ function effectiveAccountForStatus({ command, saved, apiKey }) {
|
|
|
778
791
|
function accountFromAuthData(data) {
|
|
779
792
|
if (!data || typeof data !== "object") return null;
|
|
780
793
|
const email = data.account?.email ?? data.email ?? data.userEmail ?? data.user_email;
|
|
794
|
+
const name = data.account?.name ?? data.name;
|
|
781
795
|
const userId = data.account?.userId ?? data.account?.user_id ?? data.userId ?? data.user_id;
|
|
782
|
-
if (email === undefined && userId === undefined) return null;
|
|
796
|
+
if (email === undefined && name === undefined && userId === undefined) return null;
|
|
783
797
|
return Object.fromEntries(
|
|
784
798
|
Object.entries({
|
|
785
799
|
email,
|
|
800
|
+
name,
|
|
786
801
|
userId,
|
|
787
802
|
}).filter(([, value]) => value !== undefined && value !== null),
|
|
788
803
|
);
|
|
789
804
|
}
|
|
790
805
|
|
|
806
|
+
function sanitizeStatusResponse(status) {
|
|
807
|
+
if (!status || typeof status !== "object") return status;
|
|
808
|
+
if (Array.isArray(status)) {
|
|
809
|
+
return status.map((entry) => sanitizeStatusResponse(entry));
|
|
810
|
+
}
|
|
811
|
+
return Object.fromEntries(
|
|
812
|
+
Object.entries(status)
|
|
813
|
+
.filter(([key]) => !["userId", "user_id"].includes(key))
|
|
814
|
+
.map(([key, value]) => [key, sanitizeStatusResponse(value)]),
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function stripDisplayOnlyIds(value) {
|
|
819
|
+
if (!value || typeof value !== "object") return value;
|
|
820
|
+
if (Array.isArray(value)) {
|
|
821
|
+
return value.map((entry) => stripDisplayOnlyIds(entry));
|
|
822
|
+
}
|
|
823
|
+
return Object.fromEntries(
|
|
824
|
+
Object.entries(value)
|
|
825
|
+
.filter(([key]) => !["userId", "user_id", "tokenId", "token_id"].includes(key))
|
|
826
|
+
.map(([key, nested]) => [key, stripDisplayOnlyIds(nested)]),
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function extractStatusDisplayFields(status) {
|
|
831
|
+
if (!status || typeof status !== "object") return status;
|
|
832
|
+
const email = status.email ?? status.account?.email ?? status.user?.email;
|
|
833
|
+
const name = status.name ?? status.account?.name ?? status.user?.name;
|
|
834
|
+
const result = {};
|
|
835
|
+
if (email !== undefined) result.email = email;
|
|
836
|
+
if (name !== undefined) result.name = name;
|
|
837
|
+
return result;
|
|
838
|
+
}
|
|
839
|
+
|
|
791
840
|
async function handleModels(command, deps) {
|
|
792
841
|
const { resolveApiKey, resolveOrigins } = await import("./config.js");
|
|
793
842
|
const { getModels } = await import("./api.js");
|
|
@@ -807,10 +856,21 @@ async function handleModels(command, deps) {
|
|
|
807
856
|
apiKey,
|
|
808
857
|
baseUrl: consoleOrigin,
|
|
809
858
|
fetch: deps.fetch,
|
|
859
|
+
verbose: command.options.verbose,
|
|
860
|
+
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
810
861
|
});
|
|
811
862
|
return { models: normalizeModels(response, command.options.type) };
|
|
812
863
|
}
|
|
813
864
|
|
|
865
|
+
function verboseLogger(stream, enabled) {
|
|
866
|
+
if (!enabled) return () => {};
|
|
867
|
+
return (line) => {
|
|
868
|
+
if (typeof line === "string") {
|
|
869
|
+
stream?.write?.(`${line}\n`);
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
814
874
|
function formatHuman(result) {
|
|
815
875
|
if (typeof result === "string") return result;
|
|
816
876
|
if (!result || typeof result !== "object") return String(result);
|
package/src/help.js
CHANGED