image-skill 0.1.12 → 0.1.14
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/CHANGELOG.md +17 -5
- package/PROVENANCE.md +12 -0
- package/README.md +30 -19
- package/bin/image-skill.mjs +1186 -40
- package/cli.md +146 -55
- package/llms.txt +42 -27
- package/package.json +1 -1
- package/skill.md +99 -58
- package/skills/image-skill/SKILL.md +99 -58
- package/skills/image-skill/references/cli.md +146 -55
- package/skills/image-skill/references/llms.txt +42 -27
package/bin/image-skill.mjs
CHANGED
|
@@ -7,8 +7,12 @@ import { Readable } from "node:stream";
|
|
|
7
7
|
import { pipeline } from "node:stream/promises";
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
|
|
10
|
-
const VERSION = "0.1.
|
|
10
|
+
const VERSION = "0.1.14";
|
|
11
|
+
const PACKAGE_NAME = "image-skill";
|
|
11
12
|
const DEFAULT_API_BASE_URL = "https://api.image-skill.com";
|
|
13
|
+
const DEFAULT_DOCS_BASE_URL = "https://image-skill.com";
|
|
14
|
+
const DEFAULT_NPM_REGISTRY_BASE_URL = "https://registry.npmjs.org";
|
|
15
|
+
const PUBLIC_REPO_URL = "https://github.com/danielgwilson/image-skill-cli";
|
|
12
16
|
const PROMPTLESS_EDIT_MODEL_IDS = new Set([
|
|
13
17
|
"fal.flux-dev-redux",
|
|
14
18
|
"fal.flux-krea-redux",
|
|
@@ -20,9 +24,9 @@ const DEFAULT_CONFIG_PATH = join(
|
|
|
20
24
|
"config.json",
|
|
21
25
|
);
|
|
22
26
|
const SIGNUP_SUGGESTED_COMMAND =
|
|
23
|
-
"image-skill signup --agent --agent-contact
|
|
27
|
+
"image-skill signup --agent --agent-contact AGENT_OR_OPERATOR_INBOX --agent-name NAME --runtime RUNTIME --json";
|
|
24
28
|
const SIGNUP_CONTACT_GUIDANCE =
|
|
25
|
-
"
|
|
29
|
+
"Preview signup currently requires an email-shaped durable contact inbox, not an individual human email. Use an agent-owned inbox when available; otherwise use an operator, team, or sponsor inbox that can receive future claim, billing, or abuse notices. Do not block waiting for a person, invent a person, or use a throwaway inbox. --human-email remains a compatibility alias.";
|
|
26
30
|
const PAYMENT_CREDENTIAL_FLAGS = new Set([
|
|
27
31
|
"payment-token",
|
|
28
32
|
"payment-secret",
|
|
@@ -60,11 +64,12 @@ async function main(rawArgv) {
|
|
|
60
64
|
) {
|
|
61
65
|
return success("image-skill help", {
|
|
62
66
|
usage:
|
|
63
|
-
"image-skill <doctor|signup|auth|whoami|usage|quota|credits|models|capabilities|create|upload|edit|assets|jobs|activity|feedback> --json",
|
|
67
|
+
"image-skill <doctor|trust|signup|auth|whoami|usage|quota|credits|models|capabilities|create|upload|edit|assets|jobs|activity|feedback> --json",
|
|
64
68
|
docs_url: "https://image-skill.com/cli.md",
|
|
65
69
|
commands: [
|
|
66
70
|
"doctor",
|
|
67
|
-
"
|
|
71
|
+
"trust",
|
|
72
|
+
"signup --agent --agent-contact",
|
|
68
73
|
"auth status",
|
|
69
74
|
"auth save",
|
|
70
75
|
"auth logout",
|
|
@@ -77,6 +82,7 @@ async function main(rawArgv) {
|
|
|
77
82
|
"credits status",
|
|
78
83
|
"models list",
|
|
79
84
|
"models show",
|
|
85
|
+
"create --guide",
|
|
80
86
|
"capabilities list",
|
|
81
87
|
"capabilities show",
|
|
82
88
|
"create",
|
|
@@ -105,6 +111,8 @@ async function main(rawArgv) {
|
|
|
105
111
|
switch (command) {
|
|
106
112
|
case "doctor":
|
|
107
113
|
return await doctor(rest);
|
|
114
|
+
case "trust":
|
|
115
|
+
return await trust(rest);
|
|
108
116
|
case "signup":
|
|
109
117
|
return await signup(rest);
|
|
110
118
|
case "auth":
|
|
@@ -193,6 +201,106 @@ async function doctor(argv) {
|
|
|
193
201
|
});
|
|
194
202
|
}
|
|
195
203
|
|
|
204
|
+
async function trust(argv) {
|
|
205
|
+
const args = parseArgs(argv);
|
|
206
|
+
const unsupportedFlags = [...args.flags.keys()].filter(
|
|
207
|
+
(flag) => !["json", "api-base-url"].includes(flag),
|
|
208
|
+
);
|
|
209
|
+
if (args.positionals.length > 0 || unsupportedFlags.length > 0) {
|
|
210
|
+
return invalid(
|
|
211
|
+
"image-skill trust",
|
|
212
|
+
unsupportedFlags.length > 0
|
|
213
|
+
? `unsupported flags for trust: ${unsupportedFlags.map((flag) => `--${flag}`).join(", ")}`
|
|
214
|
+
: "trust does not accept positional arguments",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const checkedAt = new Date().toISOString();
|
|
219
|
+
const apiBaseUrl = apiBase(args);
|
|
220
|
+
const docsBaseUrl = docsBaseForApiBaseUrl(apiBaseUrl);
|
|
221
|
+
const npmRegistryBaseUrl = npmRegistryBaseForApiBaseUrl(apiBaseUrl);
|
|
222
|
+
|
|
223
|
+
const [npmPackage, hostedContracts, health, models] = await Promise.all([
|
|
224
|
+
inspectNpmPackage({
|
|
225
|
+
checkedAt,
|
|
226
|
+
registryBaseUrl: npmRegistryBaseUrl,
|
|
227
|
+
}),
|
|
228
|
+
inspectHostedContracts({
|
|
229
|
+
checkedAt,
|
|
230
|
+
docsBaseUrl,
|
|
231
|
+
}),
|
|
232
|
+
apiRequest({
|
|
233
|
+
command: "image-skill trust",
|
|
234
|
+
method: "GET",
|
|
235
|
+
apiBaseUrl,
|
|
236
|
+
path: "/healthz",
|
|
237
|
+
}),
|
|
238
|
+
apiRequest({
|
|
239
|
+
command: "image-skill trust",
|
|
240
|
+
method: "GET",
|
|
241
|
+
apiBaseUrl,
|
|
242
|
+
path: "/v1/models",
|
|
243
|
+
}),
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
const hostedApi = trustHostedApi(health, apiBaseUrl, checkedAt);
|
|
247
|
+
const modelRegistry = trustModelRegistry(models, apiBaseUrl, checkedAt);
|
|
248
|
+
const publicRepo = trustPublicRepo(npmPackage);
|
|
249
|
+
const proofUrls = trustProofUrls({
|
|
250
|
+
npmPackage,
|
|
251
|
+
hostedContracts,
|
|
252
|
+
publicRepo,
|
|
253
|
+
});
|
|
254
|
+
const warnings = trustWarnings({
|
|
255
|
+
npmPackage,
|
|
256
|
+
hostedContracts,
|
|
257
|
+
hostedApi,
|
|
258
|
+
modelRegistry,
|
|
259
|
+
proofUrls,
|
|
260
|
+
});
|
|
261
|
+
const summary = trustSummary({
|
|
262
|
+
warnings,
|
|
263
|
+
npmPackage,
|
|
264
|
+
hostedContracts,
|
|
265
|
+
hostedApi,
|
|
266
|
+
modelRegistry,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return success(
|
|
270
|
+
"image-skill trust",
|
|
271
|
+
{
|
|
272
|
+
schema: "image-skill.trust-packet.v0",
|
|
273
|
+
checked_at: checkedAt,
|
|
274
|
+
subject: {
|
|
275
|
+
package: PACKAGE_NAME,
|
|
276
|
+
cli_version: VERSION,
|
|
277
|
+
mode: "public_hosted_cli",
|
|
278
|
+
api_base_url: apiBaseUrl,
|
|
279
|
+
docs_base_url: docsBaseUrl,
|
|
280
|
+
npm_registry_url: npmRegistryBaseUrl,
|
|
281
|
+
},
|
|
282
|
+
summary,
|
|
283
|
+
npm_package: npmPackage,
|
|
284
|
+
public_repo: publicRepo,
|
|
285
|
+
hosted_contracts: hostedContracts,
|
|
286
|
+
hosted_api: hostedApi,
|
|
287
|
+
model_registry: modelRegistry,
|
|
288
|
+
safe_commands: trustSafeCommands(),
|
|
289
|
+
proof_urls: proofUrls,
|
|
290
|
+
redaction: {
|
|
291
|
+
secrets_included: false,
|
|
292
|
+
private_paths_included: false,
|
|
293
|
+
config_file_read: false,
|
|
294
|
+
token_sources_read: false,
|
|
295
|
+
credential_values_read: false,
|
|
296
|
+
forbidden_material:
|
|
297
|
+
"tokens, API keys, private repo paths, provider credentials, payment credentials, card data, wallet secrets, and private package metadata",
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
warnings,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
196
304
|
async function signup(argv) {
|
|
197
305
|
const args = parseArgs(argv);
|
|
198
306
|
if (!flagBool(args, "agent")) {
|
|
@@ -232,7 +340,7 @@ async function signup(argv) {
|
|
|
232
340
|
},
|
|
233
341
|
);
|
|
234
342
|
}
|
|
235
|
-
const save =
|
|
343
|
+
const save = shouldSaveSignupAuth(args);
|
|
236
344
|
const showToken = flagBool(args, "show-token");
|
|
237
345
|
if (save) {
|
|
238
346
|
const configReady = await assertConfigWritable("image-skill signup");
|
|
@@ -252,6 +360,8 @@ async function signup(argv) {
|
|
|
252
360
|
return_token: save || showToken,
|
|
253
361
|
},
|
|
254
362
|
});
|
|
363
|
+
result.envelope.command = "image-skill signup";
|
|
364
|
+
rewriteSignupContactFailure(result);
|
|
255
365
|
|
|
256
366
|
const token = result.envelope.data?.token;
|
|
257
367
|
const warnings = [...result.envelope.warnings];
|
|
@@ -261,7 +371,7 @@ async function signup(argv) {
|
|
|
261
371
|
"image-skill signup",
|
|
262
372
|
3,
|
|
263
373
|
"SIGNUP_TOKEN_NOT_RETURNED",
|
|
264
|
-
"signup
|
|
374
|
+
"signup default auth persistence requires a returned hosted token",
|
|
265
375
|
true,
|
|
266
376
|
{
|
|
267
377
|
suggested_command: SIGNUP_SUGGESTED_COMMAND,
|
|
@@ -282,22 +392,19 @@ async function signup(argv) {
|
|
|
282
392
|
warnings.push(`saved hosted token to ${configPath()}`);
|
|
283
393
|
}
|
|
284
394
|
|
|
285
|
-
if (
|
|
286
|
-
|
|
287
|
-
result.envelope.data &&
|
|
288
|
-
typeof result.envelope.data === "object"
|
|
289
|
-
) {
|
|
395
|
+
if (result.envelope.data && typeof result.envelope.data === "object") {
|
|
396
|
+
const publicData = publicSignupData(result.envelope.data);
|
|
290
397
|
result.envelope.data = {
|
|
291
|
-
...
|
|
292
|
-
token: null,
|
|
293
|
-
token_presented:
|
|
398
|
+
...publicData,
|
|
399
|
+
token: showToken ? (token ?? publicData.token ?? null) : null,
|
|
400
|
+
token_presented: showToken,
|
|
294
401
|
storage: {
|
|
295
|
-
...(
|
|
402
|
+
...(publicData.storage ?? {}),
|
|
296
403
|
saved: save,
|
|
297
404
|
config_path: save ? configPath() : null,
|
|
298
405
|
reason: save
|
|
299
406
|
? "public CLI saved token locally with 0600 permissions"
|
|
300
|
-
: "token
|
|
407
|
+
: "token not saved; later hosted commands need saved auth, IMAGE_SKILL_TOKEN, or --token-stdin",
|
|
301
408
|
},
|
|
302
409
|
};
|
|
303
410
|
}
|
|
@@ -305,6 +412,31 @@ async function signup(argv) {
|
|
|
305
412
|
return result;
|
|
306
413
|
}
|
|
307
414
|
|
|
415
|
+
function rewriteSignupContactFailure(result) {
|
|
416
|
+
const error = result.envelope.error;
|
|
417
|
+
if (
|
|
418
|
+
error !== null &&
|
|
419
|
+
typeof error === "object" &&
|
|
420
|
+
error.message === "human_email must be a valid email address"
|
|
421
|
+
) {
|
|
422
|
+
error.message =
|
|
423
|
+
"preview signup currently requires --agent-contact to be an email-shaped durable contact inbox; it does not need to belong to an individual human";
|
|
424
|
+
error.recovery = {
|
|
425
|
+
...(error.recovery ?? {}),
|
|
426
|
+
suggested_command: SIGNUP_SUGGESTED_COMMAND,
|
|
427
|
+
docs_url: "https://image-skill.com/cli.md#image-skill-signup-agent",
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function publicSignupData(data) {
|
|
433
|
+
const { human_email: humanEmail, ...rest } = data;
|
|
434
|
+
return {
|
|
435
|
+
...rest,
|
|
436
|
+
...(typeof humanEmail === "string" ? { agent_contact: humanEmail } : {}),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
308
440
|
async function auth(argv) {
|
|
309
441
|
const [subcommand, ...rest] = argv;
|
|
310
442
|
const args = parseArgs(rest);
|
|
@@ -384,25 +516,31 @@ async function whoami(argv) {
|
|
|
384
516
|
|
|
385
517
|
async function usage(argv) {
|
|
386
518
|
const [subcommand, ...rest] = argv;
|
|
519
|
+
if (subcommand === undefined || subcommand.startsWith("-")) {
|
|
520
|
+
return await quota(argv, "image-skill usage quota");
|
|
521
|
+
}
|
|
387
522
|
if (subcommand !== "quota") {
|
|
388
523
|
return invalid("image-skill usage", "usage requires the quota subcommand");
|
|
389
524
|
}
|
|
390
|
-
return quota(rest);
|
|
525
|
+
return quota(rest, "image-skill usage quota");
|
|
391
526
|
}
|
|
392
527
|
|
|
393
|
-
async function quota(argv) {
|
|
528
|
+
async function quota(argv, command = "image-skill quota") {
|
|
394
529
|
const args = parseArgs(argv);
|
|
395
530
|
const token = await resolveToken(args);
|
|
396
531
|
if (!token.ok) {
|
|
397
|
-
return token.result;
|
|
532
|
+
return withCommand(token.result, command);
|
|
398
533
|
}
|
|
399
|
-
return
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
534
|
+
return withCommand(
|
|
535
|
+
await apiRequest({
|
|
536
|
+
command,
|
|
537
|
+
method: "GET",
|
|
538
|
+
apiBaseUrl: apiBase(args),
|
|
539
|
+
path: "/v1/quota",
|
|
540
|
+
token: token.token,
|
|
541
|
+
}),
|
|
542
|
+
command,
|
|
543
|
+
);
|
|
406
544
|
}
|
|
407
545
|
|
|
408
546
|
async function credits(argv) {
|
|
@@ -601,15 +739,58 @@ async function models(argv) {
|
|
|
601
739
|
) {
|
|
602
740
|
return invalid("image-skill models", "models supports list or show");
|
|
603
741
|
}
|
|
742
|
+
const query = modelListQuery(args);
|
|
743
|
+
if (!query.ok) {
|
|
744
|
+
return invalid(
|
|
745
|
+
subcommand === "list" ? "image-skill models list" : "image-skill models",
|
|
746
|
+
query.message,
|
|
747
|
+
);
|
|
748
|
+
}
|
|
604
749
|
return apiRequest({
|
|
605
750
|
command:
|
|
606
751
|
subcommand === "list" ? "image-skill models list" : "image-skill models",
|
|
607
752
|
method: "GET",
|
|
608
753
|
apiBaseUrl: apiBase(args),
|
|
609
|
-
path:
|
|
754
|
+
path: query.path,
|
|
610
755
|
});
|
|
611
756
|
}
|
|
612
757
|
|
|
758
|
+
function modelListQuery(args) {
|
|
759
|
+
const available = flagBool(args, "available");
|
|
760
|
+
const executable = flagBool(args, "executable");
|
|
761
|
+
const catalogOnly = flagBool(args, "catalog-only");
|
|
762
|
+
if (catalogOnly && (available || executable)) {
|
|
763
|
+
return {
|
|
764
|
+
ok: false,
|
|
765
|
+
message:
|
|
766
|
+
"models list --catalog-only cannot be combined with --available or --executable",
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
const params = new URLSearchParams();
|
|
770
|
+
if (available) {
|
|
771
|
+
params.set("available", "true");
|
|
772
|
+
}
|
|
773
|
+
if (executable) {
|
|
774
|
+
params.set("executable", "true");
|
|
775
|
+
}
|
|
776
|
+
if (catalogOnly) {
|
|
777
|
+
params.set("catalog_only", "true");
|
|
778
|
+
}
|
|
779
|
+
addQueryValue(params, "operation", flagString(args, "operation"));
|
|
780
|
+
addQueryValue(params, "provider", flagString(args, "provider"));
|
|
781
|
+
const query = params.toString();
|
|
782
|
+
return {
|
|
783
|
+
ok: true,
|
|
784
|
+
path: query.length === 0 ? "/v1/models" : `/v1/models?${query}`,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function addQueryValue(params, name, value) {
|
|
789
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
790
|
+
params.set(name, value.trim());
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
613
794
|
async function capabilities(argv) {
|
|
614
795
|
const [subcommand, ...rest] = argv;
|
|
615
796
|
const args = parseArgs(
|
|
@@ -651,20 +832,424 @@ async function capabilities(argv) {
|
|
|
651
832
|
});
|
|
652
833
|
}
|
|
653
834
|
|
|
835
|
+
async function createGuide(args) {
|
|
836
|
+
if (flagBool(args, "dry-run")) {
|
|
837
|
+
return invalid(
|
|
838
|
+
"image-skill create --guide",
|
|
839
|
+
"create --guide cannot be combined with --dry-run; the guide returns the dry-run escape hatch separately",
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
if (hasReferenceFlags(args)) {
|
|
843
|
+
return invalid(
|
|
844
|
+
"image-skill create --guide",
|
|
845
|
+
"create --guide does not upload or resolve reference images; inspect the model with models show, then run create --dry-run before live referenced creates",
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
const modelParameters = jsonObjectFlag(args, "model-parameters-json");
|
|
849
|
+
if (!modelParameters.ok) {
|
|
850
|
+
return modelParameters.result;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const apiBaseUrl = apiBase(args);
|
|
854
|
+
const prompt = flagString(args, "prompt") ?? args.positionals[0] ?? "";
|
|
855
|
+
const trimmedPrompt = prompt.trim();
|
|
856
|
+
const requestedModelId = flagString(args, "model");
|
|
857
|
+
const requestedProviderId = flagString(args, "provider");
|
|
858
|
+
const requestedIntent = flagString(args, "intent") ?? "explore";
|
|
859
|
+
const health = await apiRequest({
|
|
860
|
+
command: "image-skill create --guide",
|
|
861
|
+
method: "GET",
|
|
862
|
+
apiBaseUrl,
|
|
863
|
+
path: "/healthz",
|
|
864
|
+
});
|
|
865
|
+
const models = await apiRequest({
|
|
866
|
+
command: "image-skill create --guide",
|
|
867
|
+
method: "GET",
|
|
868
|
+
apiBaseUrl,
|
|
869
|
+
path: "/v1/models",
|
|
870
|
+
});
|
|
871
|
+
const payments = await apiRequest({
|
|
872
|
+
command: "image-skill create --guide",
|
|
873
|
+
method: "GET",
|
|
874
|
+
apiBaseUrl,
|
|
875
|
+
path: "/v1/payment-methods",
|
|
876
|
+
});
|
|
877
|
+
const token = await resolveToken(args, { allowMissing: true });
|
|
878
|
+
if (!token.ok) {
|
|
879
|
+
return token.result;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const selected =
|
|
883
|
+
models.envelope.ok && models.envelope.data?.models
|
|
884
|
+
? selectCreateGuideModel(models.envelope.data.models, requestedModelId)
|
|
885
|
+
: null;
|
|
886
|
+
const pricing = selected?.economics?.credit_pricing ?? null;
|
|
887
|
+
const estimatedCredits = pricing?.credits_required ?? null;
|
|
888
|
+
const estimatedUsdPerImage =
|
|
889
|
+
selected?.economics?.estimated_usd_per_image ??
|
|
890
|
+
(pricing === null ? null : pricing.estimated_revenue_usd);
|
|
891
|
+
const budgetGuard =
|
|
892
|
+
flagNumber(args, "max-estimated-usd-per-image") ??
|
|
893
|
+
estimatedUsdPerImage ??
|
|
894
|
+
(estimatedCredits === null ? 0.07 : estimatedCredits / 100);
|
|
895
|
+
const quota =
|
|
896
|
+
token.token === null
|
|
897
|
+
? null
|
|
898
|
+
: await apiRequest({
|
|
899
|
+
command: "image-skill create --guide",
|
|
900
|
+
method: "GET",
|
|
901
|
+
apiBaseUrl,
|
|
902
|
+
path: "/v1/quota",
|
|
903
|
+
token: token.token,
|
|
904
|
+
});
|
|
905
|
+
const paymentSummary = createGuidePaymentSummary(payments.envelope.data);
|
|
906
|
+
const stage = createGuideStage({
|
|
907
|
+
prompt: trimmedPrompt,
|
|
908
|
+
health,
|
|
909
|
+
models,
|
|
910
|
+
selected,
|
|
911
|
+
token,
|
|
912
|
+
quota,
|
|
913
|
+
estimatedCredits,
|
|
914
|
+
});
|
|
915
|
+
const blocker = createGuideBlocker(stage, {
|
|
916
|
+
requestedModelId,
|
|
917
|
+
quota,
|
|
918
|
+
estimatedCredits,
|
|
919
|
+
});
|
|
920
|
+
const nextCommand = createGuideNextCommand(stage, {
|
|
921
|
+
prompt: trimmedPrompt,
|
|
922
|
+
selected,
|
|
923
|
+
requestedProviderId,
|
|
924
|
+
requestedIntent,
|
|
925
|
+
budgetGuard,
|
|
926
|
+
apiBaseUrl: explicitApiBaseUrl(args),
|
|
927
|
+
paymentSummary,
|
|
928
|
+
});
|
|
929
|
+
const afterNext =
|
|
930
|
+
stage === "auth_required" || stage === "quota_required"
|
|
931
|
+
? renderGuideCommand(trimmedPrompt, explicitApiBaseUrl(args))
|
|
932
|
+
: null;
|
|
933
|
+
return success("image-skill create --guide", {
|
|
934
|
+
schema: "image-skill.create-guide.v1",
|
|
935
|
+
ready: stage === "ready_to_create",
|
|
936
|
+
stage,
|
|
937
|
+
checks: {
|
|
938
|
+
hosted_api: {
|
|
939
|
+
reachable: health.envelope.ok,
|
|
940
|
+
status: health.envelope.data?.status ?? null,
|
|
941
|
+
api_base_url: apiBaseUrl,
|
|
942
|
+
error_code: health.envelope.error?.code ?? null,
|
|
943
|
+
},
|
|
944
|
+
auth: {
|
|
945
|
+
source: token.source === "anonymous" ? "none" : token.source,
|
|
946
|
+
authenticated: quota?.envelope.data?.authenticated === true,
|
|
947
|
+
claim_state: quota?.envelope.data?.claim_state ?? null,
|
|
948
|
+
token_status: quota?.envelope.data?.token_status ?? null,
|
|
949
|
+
saved_config_path: configPath(),
|
|
950
|
+
},
|
|
951
|
+
models: {
|
|
952
|
+
reachable: models.envelope.ok,
|
|
953
|
+
executable_count: models.envelope.data?.summary?.executable ?? 0,
|
|
954
|
+
cataloged_not_wired_count:
|
|
955
|
+
models.envelope.data?.summary?.cataloged_not_wired ?? 0,
|
|
956
|
+
error_code: models.envelope.error?.code ?? null,
|
|
957
|
+
},
|
|
958
|
+
quota: {
|
|
959
|
+
checked: quota !== null,
|
|
960
|
+
authenticated: quota?.envelope.data?.authenticated === true,
|
|
961
|
+
remaining_credits: quotaRemainingCredits(quota?.envelope.data ?? null),
|
|
962
|
+
required_credits: estimatedCredits,
|
|
963
|
+
daily_jobs_remaining:
|
|
964
|
+
quota?.envelope.data?.daily_jobs?.remaining ?? null,
|
|
965
|
+
reason:
|
|
966
|
+
quota === null
|
|
967
|
+
? "auth_required"
|
|
968
|
+
: quota.envelope.ok
|
|
969
|
+
? null
|
|
970
|
+
: (quota.envelope.error?.code ?? "quota_unavailable"),
|
|
971
|
+
},
|
|
972
|
+
payments: paymentSummary,
|
|
973
|
+
},
|
|
974
|
+
selection:
|
|
975
|
+
selected === null
|
|
976
|
+
? null
|
|
977
|
+
: {
|
|
978
|
+
operation: "create",
|
|
979
|
+
model_id: selected.id,
|
|
980
|
+
model_status: selected.status,
|
|
981
|
+
model_execution_status: selected.execution.model_execution_status,
|
|
982
|
+
reason:
|
|
983
|
+
requestedModelId === null
|
|
984
|
+
? "default executable create model for first image"
|
|
985
|
+
: "requested executable create model",
|
|
986
|
+
},
|
|
987
|
+
cost: {
|
|
988
|
+
estimated_credits: estimatedCredits,
|
|
989
|
+
estimated_usd_per_image: estimatedUsdPerImage,
|
|
990
|
+
pricing_confidence: pricing?.pricing_confidence ?? null,
|
|
991
|
+
},
|
|
992
|
+
blocker,
|
|
993
|
+
next_command: nextCommand,
|
|
994
|
+
after_next: afterNext,
|
|
995
|
+
escape_hatches: {
|
|
996
|
+
doctor: "image-skill doctor --json",
|
|
997
|
+
model_inspection:
|
|
998
|
+
selected === null
|
|
999
|
+
? "image-skill models list --json"
|
|
1000
|
+
: `image-skill models show ${shellQuote(selected.id)} --json`,
|
|
1001
|
+
payment_methods: "image-skill credits methods --json",
|
|
1002
|
+
quota: "image-skill usage quota --json",
|
|
1003
|
+
dry_run:
|
|
1004
|
+
selected === null || trimmedPrompt.length === 0
|
|
1005
|
+
? "image-skill create --dry-run --prompt PROMPT --json"
|
|
1006
|
+
: renderCreateCommand({
|
|
1007
|
+
prompt: trimmedPrompt,
|
|
1008
|
+
modelId: selected.id,
|
|
1009
|
+
providerId: requestedProviderId,
|
|
1010
|
+
intent: requestedIntent,
|
|
1011
|
+
budgetGuard,
|
|
1012
|
+
dryRun: true,
|
|
1013
|
+
apiBaseUrl: explicitApiBaseUrl(args),
|
|
1014
|
+
}),
|
|
1015
|
+
},
|
|
1016
|
+
mutation: {
|
|
1017
|
+
provider_call: false,
|
|
1018
|
+
hosted_create: false,
|
|
1019
|
+
hosted_signup: false,
|
|
1020
|
+
payment_object: false,
|
|
1021
|
+
credit_debit: false,
|
|
1022
|
+
media_write: false,
|
|
1023
|
+
},
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function selectCreateGuideModel(models, requestedModelId) {
|
|
1028
|
+
const isExecutableCreate = (model) =>
|
|
1029
|
+
model?.status === "available" &&
|
|
1030
|
+
model?.execution?.model_execution_status === "executable" &&
|
|
1031
|
+
Array.isArray(model?.supports) &&
|
|
1032
|
+
model.supports.includes("create");
|
|
1033
|
+
if (requestedModelId !== null) {
|
|
1034
|
+
const requested = models.find((model) => model.id === requestedModelId);
|
|
1035
|
+
return requested !== undefined && isExecutableCreate(requested)
|
|
1036
|
+
? requested
|
|
1037
|
+
: null;
|
|
1038
|
+
}
|
|
1039
|
+
return models.find(isExecutableCreate) ?? null;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function createGuidePaymentSummary(data) {
|
|
1043
|
+
const methods = Array.isArray(data?.methods)
|
|
1044
|
+
? data.methods.filter((method) => method.live_money)
|
|
1045
|
+
: [];
|
|
1046
|
+
return {
|
|
1047
|
+
checked: data !== null && typeof data === "object",
|
|
1048
|
+
live_money_methods: methods
|
|
1049
|
+
.filter((method) => method.available)
|
|
1050
|
+
.map((method) => method.method_id),
|
|
1051
|
+
requires_browser: methods.some((method) => method.requires_browser),
|
|
1052
|
+
buyer_modes: [
|
|
1053
|
+
...new Set(methods.flatMap((method) => method.buyer_modes ?? [])),
|
|
1054
|
+
],
|
|
1055
|
+
suggested_commands: [
|
|
1056
|
+
"image-skill credits methods --json",
|
|
1057
|
+
"image-skill credits packs list --json",
|
|
1058
|
+
methods[0]?.recovery?.quote_command ??
|
|
1059
|
+
"image-skill credits quote --pack starter-500 --payment-method stripe_checkout --idempotency-key KEY --json",
|
|
1060
|
+
methods[0]?.recovery?.purchase_command ??
|
|
1061
|
+
"image-skill credits buy --provider stripe --quote-id QUOTE_ID --idempotency-key KEY --json",
|
|
1062
|
+
methods[0]?.recovery?.status_command ??
|
|
1063
|
+
"image-skill credits status --payment-attempt-id PAYMENT_ATTEMPT_ID --json",
|
|
1064
|
+
],
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function createGuideStage(input) {
|
|
1069
|
+
if (input.prompt.length === 0) {
|
|
1070
|
+
return "prompt_required";
|
|
1071
|
+
}
|
|
1072
|
+
if (!input.health.envelope.ok || !input.models.envelope.ok) {
|
|
1073
|
+
return "service_unreachable";
|
|
1074
|
+
}
|
|
1075
|
+
if (input.selected === null) {
|
|
1076
|
+
return "no_executable_model";
|
|
1077
|
+
}
|
|
1078
|
+
if (input.token.token === null) {
|
|
1079
|
+
return "auth_required";
|
|
1080
|
+
}
|
|
1081
|
+
if (input.quota === null || !input.quota.envelope.ok) {
|
|
1082
|
+
return input.quota?.envelope.error?.code === "AUTH_REQUIRED"
|
|
1083
|
+
? "auth_required"
|
|
1084
|
+
: "service_unreachable";
|
|
1085
|
+
}
|
|
1086
|
+
const remaining = quotaRemainingCredits(input.quota.envelope.data);
|
|
1087
|
+
if (
|
|
1088
|
+
input.estimatedCredits !== null &&
|
|
1089
|
+
remaining !== null &&
|
|
1090
|
+
remaining < input.estimatedCredits
|
|
1091
|
+
) {
|
|
1092
|
+
return "quota_required";
|
|
1093
|
+
}
|
|
1094
|
+
if (
|
|
1095
|
+
input.quota.envelope.data?.daily_jobs !== undefined &&
|
|
1096
|
+
input.quota.envelope.data.daily_jobs.remaining <= 0
|
|
1097
|
+
) {
|
|
1098
|
+
return "quota_required";
|
|
1099
|
+
}
|
|
1100
|
+
return "ready_to_create";
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function createGuideBlocker(stage, input) {
|
|
1104
|
+
if (stage === "ready_to_create") {
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
if (stage === "prompt_required") {
|
|
1108
|
+
return {
|
|
1109
|
+
code: "prompt_required",
|
|
1110
|
+
message: "Add --prompt so the guide can return the exact create command.",
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
if (stage === "no_executable_model") {
|
|
1114
|
+
return {
|
|
1115
|
+
code: "no_executable_model",
|
|
1116
|
+
message:
|
|
1117
|
+
input.requestedModelId === null
|
|
1118
|
+
? "No available executable create model was found in the public registry."
|
|
1119
|
+
: `Requested model is not currently an available executable create model: ${input.requestedModelId}`,
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
if (stage === "auth_required") {
|
|
1123
|
+
return {
|
|
1124
|
+
code: "auth_required",
|
|
1125
|
+
message:
|
|
1126
|
+
"Sign up once with a durable agent contact before creating hosted media.",
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
if (stage === "quota_required") {
|
|
1130
|
+
const remaining = quotaRemainingCredits(input.quota?.envelope.data ?? null);
|
|
1131
|
+
return {
|
|
1132
|
+
code: "quota_required",
|
|
1133
|
+
message: `Selected first image requires ${input.estimatedCredits ?? "unknown"} credits; current remaining credits are ${remaining ?? "unknown"}.`,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
return {
|
|
1137
|
+
code: "service_unreachable",
|
|
1138
|
+
message:
|
|
1139
|
+
input.quota?.envelope.error?.message ??
|
|
1140
|
+
"Guide could not complete read-only hosted or registry checks.",
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function createGuideNextCommand(stage, input) {
|
|
1145
|
+
if (stage === "prompt_required") {
|
|
1146
|
+
return renderGuideCommand("PROMPT", input.apiBaseUrl);
|
|
1147
|
+
}
|
|
1148
|
+
if (stage === "no_executable_model" || stage === "service_unreachable") {
|
|
1149
|
+
return "image-skill models list --json";
|
|
1150
|
+
}
|
|
1151
|
+
if (stage === "auth_required") {
|
|
1152
|
+
return "image-skill signup --agent --agent-contact AGENT_OR_OPERATOR_INBOX --agent-name AGENT_NAME --runtime RUNTIME_NAME --json";
|
|
1153
|
+
}
|
|
1154
|
+
if (stage === "quota_required") {
|
|
1155
|
+
return input.paymentSummary.suggested_commands[0];
|
|
1156
|
+
}
|
|
1157
|
+
return renderCreateCommand({
|
|
1158
|
+
prompt: input.prompt,
|
|
1159
|
+
modelId: input.selected.id,
|
|
1160
|
+
providerId: input.requestedProviderId,
|
|
1161
|
+
intent: input.requestedIntent,
|
|
1162
|
+
budgetGuard: input.budgetGuard,
|
|
1163
|
+
dryRun: false,
|
|
1164
|
+
apiBaseUrl: input.apiBaseUrl,
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function renderGuideCommand(prompt, apiBaseUrl) {
|
|
1169
|
+
return [
|
|
1170
|
+
"image-skill create --guide --prompt",
|
|
1171
|
+
shellQuote(prompt),
|
|
1172
|
+
...(apiBaseUrl === null ? [] : ["--api-base-url", shellQuote(apiBaseUrl)]),
|
|
1173
|
+
"--json",
|
|
1174
|
+
].join(" ");
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function renderCreateCommand(input) {
|
|
1178
|
+
return [
|
|
1179
|
+
"image-skill create",
|
|
1180
|
+
...(input.dryRun ? ["--dry-run"] : []),
|
|
1181
|
+
...(input.providerId === null
|
|
1182
|
+
? []
|
|
1183
|
+
: ["--provider", shellQuote(input.providerId)]),
|
|
1184
|
+
"--model",
|
|
1185
|
+
shellQuote(input.modelId),
|
|
1186
|
+
"--prompt",
|
|
1187
|
+
shellQuote(input.prompt),
|
|
1188
|
+
"--intent",
|
|
1189
|
+
shellQuote(input.intent),
|
|
1190
|
+
"--max-estimated-usd-per-image",
|
|
1191
|
+
shellQuote(formatUsd(input.budgetGuard)),
|
|
1192
|
+
...(input.apiBaseUrl === null
|
|
1193
|
+
? []
|
|
1194
|
+
: ["--api-base-url", shellQuote(input.apiBaseUrl)]),
|
|
1195
|
+
"--json",
|
|
1196
|
+
].join(" ");
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function explicitApiBaseUrl(args) {
|
|
1200
|
+
return flagString(args, "api-base-url");
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function formatUsd(value) {
|
|
1204
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function shellQuote(value) {
|
|
1208
|
+
return JSON.stringify(value);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function quotaRemainingCredits(data) {
|
|
1212
|
+
if (data === null || data === undefined) {
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
const limits = data.limits ?? {};
|
|
1216
|
+
const freeCredits =
|
|
1217
|
+
typeof limits.remaining_credits === "number" ? limits.remaining_credits : 0;
|
|
1218
|
+
const paidCredits =
|
|
1219
|
+
typeof limits.payment_backed_remaining_credits === "number"
|
|
1220
|
+
? limits.payment_backed_remaining_credits
|
|
1221
|
+
: 0;
|
|
1222
|
+
return freeCredits + paidCredits;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
654
1225
|
async function create(argv) {
|
|
655
1226
|
const args = parseArgs(argv);
|
|
1227
|
+
if (flagBool(args, "guide")) {
|
|
1228
|
+
return createGuide(args);
|
|
1229
|
+
}
|
|
656
1230
|
const prompt = await promptValue(args);
|
|
657
1231
|
if (!prompt.ok) {
|
|
658
1232
|
return prompt.result;
|
|
659
1233
|
}
|
|
660
|
-
|
|
661
|
-
if (
|
|
662
|
-
|
|
1234
|
+
let referenceToken = null;
|
|
1235
|
+
if (flagBool(args, "dry-run") && hasReferenceFlags(args)) {
|
|
1236
|
+
referenceToken = await resolveToken(args);
|
|
1237
|
+
if (!referenceToken.ok) {
|
|
1238
|
+
return referenceToken.result;
|
|
1239
|
+
}
|
|
663
1240
|
}
|
|
664
1241
|
const referencePlan = parseReferencePlan(args, "image-skill create");
|
|
665
1242
|
if (!referencePlan.ok) {
|
|
666
1243
|
return referencePlan.result;
|
|
667
1244
|
}
|
|
1245
|
+
const anonymousDryRun =
|
|
1246
|
+
flagBool(args, "dry-run") && referencePlan.referencePlans.length === 0;
|
|
1247
|
+
const token =
|
|
1248
|
+
referenceToken ??
|
|
1249
|
+
(await resolveToken(args, { allowMissing: anonymousDryRun }));
|
|
1250
|
+
if (!token.ok) {
|
|
1251
|
+
return token.result;
|
|
1252
|
+
}
|
|
668
1253
|
const modelParameters = jsonObjectFlag(args, "model-parameters-json");
|
|
669
1254
|
if (!modelParameters.ok) {
|
|
670
1255
|
return modelParameters.result;
|
|
@@ -675,11 +1260,14 @@ async function create(argv) {
|
|
|
675
1260
|
if (!outputCount.ok) {
|
|
676
1261
|
return outputCount.result;
|
|
677
1262
|
}
|
|
678
|
-
const references =
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
1263
|
+
const references =
|
|
1264
|
+
token.token === null
|
|
1265
|
+
? { ok: true, references: [] }
|
|
1266
|
+
: await resolveReferences(
|
|
1267
|
+
referencePlan.referencePlans,
|
|
1268
|
+
args,
|
|
1269
|
+
token.token,
|
|
1270
|
+
);
|
|
683
1271
|
if (!references.ok) {
|
|
684
1272
|
return references.result;
|
|
685
1273
|
}
|
|
@@ -688,7 +1276,7 @@ async function create(argv) {
|
|
|
688
1276
|
method: "POST",
|
|
689
1277
|
apiBaseUrl: apiBase(args),
|
|
690
1278
|
path: "/v1/create",
|
|
691
|
-
token: token.token,
|
|
1279
|
+
...(token.token === null ? {} : { token: token.token }),
|
|
692
1280
|
body: {
|
|
693
1281
|
prompt: prompt.value,
|
|
694
1282
|
...(flagString(args, "provider") === null
|
|
@@ -869,7 +1457,7 @@ async function assets(argv) {
|
|
|
869
1457
|
}
|
|
870
1458
|
const asset = shown.envelope.data?.asset ?? shown.envelope.data;
|
|
871
1459
|
const output =
|
|
872
|
-
flagString(args, "output") ??
|
|
1460
|
+
flagString(args, "output") ?? deriveAssetGetOutputPath(asset);
|
|
873
1461
|
const downloaded = await downloadUrl(asset.url, output, {
|
|
874
1462
|
overwrite: flagBool(args, "overwrite"),
|
|
875
1463
|
});
|
|
@@ -1173,6 +1761,14 @@ function parseReferencePlan(args, command) {
|
|
|
1173
1761
|
return { ok: true, referencePlans };
|
|
1174
1762
|
}
|
|
1175
1763
|
|
|
1764
|
+
function hasReferenceFlags(args) {
|
|
1765
|
+
return (
|
|
1766
|
+
args.flags.has("element-frontal") ||
|
|
1767
|
+
args.flags.has("element-reference") ||
|
|
1768
|
+
args.flags.has("reference-image")
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1176
1772
|
async function resolveReferences(referencePlans, args, token) {
|
|
1177
1773
|
const references = [];
|
|
1178
1774
|
for (const plan of referencePlans) {
|
|
@@ -1461,6 +2057,441 @@ async function uploadPayload(input) {
|
|
|
1461
2057
|
};
|
|
1462
2058
|
}
|
|
1463
2059
|
|
|
2060
|
+
async function inspectNpmPackage(input) {
|
|
2061
|
+
const registryUrl = new URL(
|
|
2062
|
+
`${encodeURIComponent(PACKAGE_NAME)}/${encodeURIComponent(VERSION)}`,
|
|
2063
|
+
ensureTrailingSlash(input.registryBaseUrl),
|
|
2064
|
+
).toString();
|
|
2065
|
+
const fetched = await fetchPublicJson(registryUrl, {
|
|
2066
|
+
accept: "application/vnd.npm.install-v1+json, application/json",
|
|
2067
|
+
});
|
|
2068
|
+
if (!fetched.ok) {
|
|
2069
|
+
return {
|
|
2070
|
+
status: fetched.statusCode === 404 ? "not_available_yet" : "unreachable",
|
|
2071
|
+
checked_at: input.checkedAt,
|
|
2072
|
+
package: PACKAGE_NAME,
|
|
2073
|
+
version: VERSION,
|
|
2074
|
+
registry_url: registryUrl,
|
|
2075
|
+
dist_integrity: null,
|
|
2076
|
+
tarball: null,
|
|
2077
|
+
git_head: null,
|
|
2078
|
+
repository_url: null,
|
|
2079
|
+
attestation: {
|
|
2080
|
+
status: "not_available_yet",
|
|
2081
|
+
url: null,
|
|
2082
|
+
},
|
|
2083
|
+
error: fetched.error,
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
const parsed = isRecord(fetched.json) ? fetched.json : {};
|
|
2088
|
+
const dist = isRecord(parsed.dist) ? parsed.dist : {};
|
|
2089
|
+
const repository = isRecord(parsed.repository) ? parsed.repository : {};
|
|
2090
|
+
const attestationUrl =
|
|
2091
|
+
isRecord(dist.attestations) && typeof dist.attestations.url === "string"
|
|
2092
|
+
? dist.attestations.url
|
|
2093
|
+
: null;
|
|
2094
|
+
const version = typeof parsed.version === "string" ? parsed.version : VERSION;
|
|
2095
|
+
return {
|
|
2096
|
+
status: version === VERSION ? "verified" : "mismatched",
|
|
2097
|
+
checked_at: input.checkedAt,
|
|
2098
|
+
package: PACKAGE_NAME,
|
|
2099
|
+
version,
|
|
2100
|
+
expected_version: VERSION,
|
|
2101
|
+
registry_url: registryUrl,
|
|
2102
|
+
dist_integrity: typeof dist.integrity === "string" ? dist.integrity : null,
|
|
2103
|
+
tarball: typeof dist.tarball === "string" ? dist.tarball : null,
|
|
2104
|
+
git_head: typeof parsed.gitHead === "string" ? parsed.gitHead : null,
|
|
2105
|
+
repository_url:
|
|
2106
|
+
typeof repository.url === "string" ? repository.url : PUBLIC_REPO_URL,
|
|
2107
|
+
attestation: {
|
|
2108
|
+
status: attestationUrl === null ? "not_available_yet" : "available",
|
|
2109
|
+
url: attestationUrl,
|
|
2110
|
+
},
|
|
2111
|
+
error: null,
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
async function inspectHostedContracts(input) {
|
|
2116
|
+
const contracts = [
|
|
2117
|
+
{ key: "skill", path: "/skill.md" },
|
|
2118
|
+
{ key: "llms", path: "/llms.txt" },
|
|
2119
|
+
{ key: "cli", path: "/cli.md" },
|
|
2120
|
+
];
|
|
2121
|
+
const entries = [];
|
|
2122
|
+
for (const contract of contracts) {
|
|
2123
|
+
const url = new URL(
|
|
2124
|
+
contract.path,
|
|
2125
|
+
ensureTrailingSlash(input.docsBaseUrl),
|
|
2126
|
+
).toString();
|
|
2127
|
+
const fetched = await fetchPublicText(url, {
|
|
2128
|
+
accept: "text/markdown, text/plain, */*",
|
|
2129
|
+
});
|
|
2130
|
+
entries.push({
|
|
2131
|
+
key: contract.key,
|
|
2132
|
+
url,
|
|
2133
|
+
status: fetched.ok ? "verified" : "unreachable",
|
|
2134
|
+
http_status: fetched.statusCode,
|
|
2135
|
+
content_sha256:
|
|
2136
|
+
fetched.text === null
|
|
2137
|
+
? null
|
|
2138
|
+
: `sha256:${sha256Hex(Buffer.from(fetched.text, "utf8"))}`,
|
|
2139
|
+
bytes:
|
|
2140
|
+
fetched.text === null ? null : Buffer.byteLength(fetched.text, "utf8"),
|
|
2141
|
+
error: fetched.error,
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
const verified = entries.filter((entry) => entry.status === "verified");
|
|
2145
|
+
return {
|
|
2146
|
+
status: verified.length === entries.length ? "verified" : "unreachable",
|
|
2147
|
+
checked_at: input.checkedAt,
|
|
2148
|
+
contracts: entries,
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
function trustHostedApi(health, apiBaseUrl, checkedAt) {
|
|
2153
|
+
return {
|
|
2154
|
+
status: health.envelope.ok ? "reachable" : "unreachable",
|
|
2155
|
+
checked_at: checkedAt,
|
|
2156
|
+
url: new URL("/healthz", ensureTrailingSlash(apiBaseUrl)).toString(),
|
|
2157
|
+
reachable: health.envelope.ok,
|
|
2158
|
+
api_status: health.envelope.data?.status ?? null,
|
|
2159
|
+
api_version: health.envelope.data?.api_version ?? null,
|
|
2160
|
+
error: health.envelope.error,
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
function trustModelRegistry(models, apiBaseUrl, checkedAt) {
|
|
2165
|
+
const data = isRecord(models.envelope.data) ? models.envelope.data : {};
|
|
2166
|
+
const modelList = Array.isArray(data.models) ? data.models : [];
|
|
2167
|
+
const counted = countModelAvailability(modelList);
|
|
2168
|
+
const summary = isRecord(data.summary) ? data.summary : {};
|
|
2169
|
+
const executable = numberOrFallback(summary.executable, counted.executable);
|
|
2170
|
+
const catalogedNotWired = numberOrFallback(
|
|
2171
|
+
summary.cataloged_not_wired,
|
|
2172
|
+
counted.cataloged_not_wired,
|
|
2173
|
+
);
|
|
2174
|
+
const unavailable = numberOrFallback(
|
|
2175
|
+
summary.unavailable,
|
|
2176
|
+
counted.unavailable,
|
|
2177
|
+
);
|
|
2178
|
+
return {
|
|
2179
|
+
status: models.envelope.ok ? "available" : "unreachable",
|
|
2180
|
+
checked_at: checkedAt,
|
|
2181
|
+
url: new URL("/v1/models", ensureTrailingSlash(apiBaseUrl)).toString(),
|
|
2182
|
+
freshness: {
|
|
2183
|
+
source: "hosted /v1/models",
|
|
2184
|
+
checked_at: checkedAt,
|
|
2185
|
+
},
|
|
2186
|
+
availability_summary: {
|
|
2187
|
+
total: numberOrFallback(summary.total, modelList.length),
|
|
2188
|
+
returned: numberOrFallback(summary.returned, modelList.length),
|
|
2189
|
+
executable,
|
|
2190
|
+
cataloged_not_wired: catalogedNotWired,
|
|
2191
|
+
unavailable,
|
|
2192
|
+
providers: counted.providers,
|
|
2193
|
+
status_counts: counted.status_counts,
|
|
2194
|
+
},
|
|
2195
|
+
rules: [
|
|
2196
|
+
"Prefer executable models for create/edit.",
|
|
2197
|
+
"Treat cataloged_not_wired as inspect-only evidence, not spend-ready capability.",
|
|
2198
|
+
"Run models show MODEL_ID before using provider-native model parameters.",
|
|
2199
|
+
],
|
|
2200
|
+
error: models.envelope.error,
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
function trustPublicRepo(npmPackage) {
|
|
2205
|
+
const repoUrl = publicRepoUrlFromNpm(npmPackage.repository_url);
|
|
2206
|
+
return {
|
|
2207
|
+
status: repoUrl === null ? "unknown" : "checked",
|
|
2208
|
+
url: repoUrl,
|
|
2209
|
+
git_head: npmPackage.git_head,
|
|
2210
|
+
package_registry_url: npmPackage.registry_url,
|
|
2211
|
+
main_may_be_newer_than_package: true,
|
|
2212
|
+
note: "npm gitHead is the package-source commit when present; public main can move ahead between releases.",
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
function trustProofUrls(input) {
|
|
2217
|
+
return {
|
|
2218
|
+
npm_package: {
|
|
2219
|
+
status: input.npmPackage.status,
|
|
2220
|
+
url: input.npmPackage.registry_url,
|
|
2221
|
+
},
|
|
2222
|
+
npm_attestation: input.npmPackage.attestation,
|
|
2223
|
+
public_repo: {
|
|
2224
|
+
status: input.publicRepo.status,
|
|
2225
|
+
url: input.publicRepo.url,
|
|
2226
|
+
git_head: input.publicRepo.git_head,
|
|
2227
|
+
},
|
|
2228
|
+
hosted_contracts: {
|
|
2229
|
+
status: input.hostedContracts.status,
|
|
2230
|
+
urls: input.hostedContracts.contracts.map((contract) => contract.url),
|
|
2231
|
+
},
|
|
2232
|
+
real_agent_studies: {
|
|
2233
|
+
status: "not_available_yet",
|
|
2234
|
+
url: null,
|
|
2235
|
+
},
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
function trustWarnings(input) {
|
|
2240
|
+
const warnings = [];
|
|
2241
|
+
if (input.npmPackage.status !== "verified") {
|
|
2242
|
+
warnings.push(`npm package metadata is ${input.npmPackage.status}`);
|
|
2243
|
+
}
|
|
2244
|
+
if (input.npmPackage.git_head === null) {
|
|
2245
|
+
warnings.push("npm package gitHead is not available");
|
|
2246
|
+
}
|
|
2247
|
+
if (input.npmPackage.attestation.status !== "available") {
|
|
2248
|
+
warnings.push("npm provenance attestation URL is not available yet");
|
|
2249
|
+
}
|
|
2250
|
+
if (input.hostedContracts.status !== "verified") {
|
|
2251
|
+
warnings.push("one or more hosted contract documents could not be hashed");
|
|
2252
|
+
}
|
|
2253
|
+
if (input.hostedApi.status !== "reachable") {
|
|
2254
|
+
warnings.push("hosted API health is unreachable");
|
|
2255
|
+
}
|
|
2256
|
+
if (input.modelRegistry.status !== "available") {
|
|
2257
|
+
warnings.push("hosted model registry is unreachable");
|
|
2258
|
+
}
|
|
2259
|
+
const availability = input.modelRegistry.availability_summary;
|
|
2260
|
+
if (availability.executable === 0) {
|
|
2261
|
+
warnings.push("hosted model registry reports zero executable models");
|
|
2262
|
+
}
|
|
2263
|
+
if (availability.cataloged_not_wired > 0) {
|
|
2264
|
+
warnings.push(
|
|
2265
|
+
`hosted model registry reports ${availability.cataloged_not_wired} cataloged_not_wired model(s)`,
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
if (input.proofUrls.real_agent_studies.status === "not_available_yet") {
|
|
2269
|
+
warnings.push("real-agent study proof URL is not available yet");
|
|
2270
|
+
}
|
|
2271
|
+
return warnings;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
function trustSummary(input) {
|
|
2275
|
+
const hasMismatch =
|
|
2276
|
+
input.npmPackage.status === "mismatched" ||
|
|
2277
|
+
input.hostedContracts.status === "mismatched";
|
|
2278
|
+
const availability = input.modelRegistry.availability_summary;
|
|
2279
|
+
let status;
|
|
2280
|
+
if (input.hostedApi.status !== "reachable") {
|
|
2281
|
+
status = "unavailable";
|
|
2282
|
+
} else if (hasMismatch) {
|
|
2283
|
+
status = "stale_or_mismatched";
|
|
2284
|
+
} else if (
|
|
2285
|
+
input.modelRegistry.status !== "available" ||
|
|
2286
|
+
availability.executable === 0
|
|
2287
|
+
) {
|
|
2288
|
+
status = "inspect_only";
|
|
2289
|
+
} else if (input.warnings.length > 0) {
|
|
2290
|
+
status = "usable_with_warnings";
|
|
2291
|
+
} else {
|
|
2292
|
+
status = "usable";
|
|
2293
|
+
}
|
|
2294
|
+
return {
|
|
2295
|
+
status,
|
|
2296
|
+
selection_hint: trustSelectionHint(status),
|
|
2297
|
+
top_warnings: input.warnings.slice(0, 5),
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
function trustSelectionHint(status) {
|
|
2302
|
+
if (status === "usable" || status === "usable_with_warnings") {
|
|
2303
|
+
return "Use create --guide as the first creative command; use trust output as selection evidence, not as a required setup step.";
|
|
2304
|
+
}
|
|
2305
|
+
if (status === "inspect_only") {
|
|
2306
|
+
return "Inspect docs and model metadata, but do not assume spend-ready image generation until executable models and health are available.";
|
|
2307
|
+
}
|
|
2308
|
+
if (status === "stale_or_mismatched") {
|
|
2309
|
+
return "Do not rely on this package/contract combination until the mismatch is resolved or a newer package is selected.";
|
|
2310
|
+
}
|
|
2311
|
+
return "Do not select Image Skill for new work until hosted health and model registry checks recover.";
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
function trustSafeCommands() {
|
|
2315
|
+
return [
|
|
2316
|
+
{
|
|
2317
|
+
purpose: "trust_packet",
|
|
2318
|
+
command: "npx -y image-skill@latest trust --json",
|
|
2319
|
+
mutation: false,
|
|
2320
|
+
spend: false,
|
|
2321
|
+
},
|
|
2322
|
+
{
|
|
2323
|
+
purpose: "first_image_guide",
|
|
2324
|
+
command:
|
|
2325
|
+
'npx -y image-skill@latest create --guide --prompt "a compact field camera on a stainless workbench" --json',
|
|
2326
|
+
mutation: false,
|
|
2327
|
+
spend: false,
|
|
2328
|
+
},
|
|
2329
|
+
{
|
|
2330
|
+
purpose: "model_inspection",
|
|
2331
|
+
command: "npx -y image-skill@latest models list --json",
|
|
2332
|
+
mutation: false,
|
|
2333
|
+
spend: false,
|
|
2334
|
+
},
|
|
2335
|
+
];
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
function countModelAvailability(models) {
|
|
2339
|
+
const statusCounts = {};
|
|
2340
|
+
const providers = new Set();
|
|
2341
|
+
let executable = 0;
|
|
2342
|
+
let catalogedNotWired = 0;
|
|
2343
|
+
let unavailable = 0;
|
|
2344
|
+
for (const model of models) {
|
|
2345
|
+
if (!isRecord(model)) {
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
const providerId = modelProviderId(model);
|
|
2349
|
+
if (providerId !== null) {
|
|
2350
|
+
providers.add(providerId);
|
|
2351
|
+
}
|
|
2352
|
+
const status = modelAvailabilityStatus(model);
|
|
2353
|
+
statusCounts[status] = (statusCounts[status] ?? 0) + 1;
|
|
2354
|
+
if (status === "executable" || status === "available") {
|
|
2355
|
+
executable += 1;
|
|
2356
|
+
}
|
|
2357
|
+
if (status === "cataloged_not_wired") {
|
|
2358
|
+
catalogedNotWired += 1;
|
|
2359
|
+
}
|
|
2360
|
+
if (model.status === "unavailable" || status === "unavailable") {
|
|
2361
|
+
unavailable += 1;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
return {
|
|
2365
|
+
executable,
|
|
2366
|
+
cataloged_not_wired: catalogedNotWired,
|
|
2367
|
+
unavailable,
|
|
2368
|
+
providers: [...providers].sort(),
|
|
2369
|
+
status_counts: statusCounts,
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
function modelProviderId(model) {
|
|
2374
|
+
if (typeof model.provider_id === "string") {
|
|
2375
|
+
return model.provider_id;
|
|
2376
|
+
}
|
|
2377
|
+
if (isRecord(model.provider) && typeof model.provider.id === "string") {
|
|
2378
|
+
return model.provider.id;
|
|
2379
|
+
}
|
|
2380
|
+
if (typeof model.id === "string" && model.id.includes(".")) {
|
|
2381
|
+
return model.id.split(".")[0];
|
|
2382
|
+
}
|
|
2383
|
+
return null;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
function modelAvailabilityStatus(model) {
|
|
2387
|
+
if (
|
|
2388
|
+
isRecord(model.execution) &&
|
|
2389
|
+
typeof model.execution.model_execution_status === "string"
|
|
2390
|
+
) {
|
|
2391
|
+
return model.execution.model_execution_status;
|
|
2392
|
+
}
|
|
2393
|
+
if (typeof model.availability_reason === "string") {
|
|
2394
|
+
return model.availability_reason;
|
|
2395
|
+
}
|
|
2396
|
+
if (typeof model.status === "string") {
|
|
2397
|
+
return model.status;
|
|
2398
|
+
}
|
|
2399
|
+
return "unknown";
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
function numberOrFallback(value, fallback) {
|
|
2403
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
function publicRepoUrlFromNpm(repositoryUrl) {
|
|
2407
|
+
if (typeof repositoryUrl !== "string" || repositoryUrl.trim().length === 0) {
|
|
2408
|
+
return PUBLIC_REPO_URL;
|
|
2409
|
+
}
|
|
2410
|
+
return repositoryUrl
|
|
2411
|
+
.replace(/^git\+/, "")
|
|
2412
|
+
.replace(/\.git$/, "")
|
|
2413
|
+
.replace(/^ssh:\/\/git@github.com\//, "https://github.com/");
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
function docsBaseForApiBaseUrl(apiBaseUrl) {
|
|
2417
|
+
return sameBaseUrl(apiBaseUrl, DEFAULT_API_BASE_URL)
|
|
2418
|
+
? DEFAULT_DOCS_BASE_URL
|
|
2419
|
+
: apiBaseUrl;
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
function npmRegistryBaseForApiBaseUrl(apiBaseUrl) {
|
|
2423
|
+
return sameBaseUrl(apiBaseUrl, DEFAULT_API_BASE_URL)
|
|
2424
|
+
? DEFAULT_NPM_REGISTRY_BASE_URL
|
|
2425
|
+
: apiBaseUrl;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
function sameBaseUrl(left, right) {
|
|
2429
|
+
return stripTrailingSlash(left) === stripTrailingSlash(right);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
function stripTrailingSlash(value) {
|
|
2433
|
+
return value.replace(/\/+$/, "");
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
async function fetchPublicJson(url, options = {}) {
|
|
2437
|
+
const fetched = await fetchPublicText(url, options);
|
|
2438
|
+
if (!fetched.ok || fetched.text === null) {
|
|
2439
|
+
return { ...fetched, json: null };
|
|
2440
|
+
}
|
|
2441
|
+
try {
|
|
2442
|
+
return { ...fetched, json: JSON.parse(fetched.text) };
|
|
2443
|
+
} catch {
|
|
2444
|
+
return {
|
|
2445
|
+
...fetched,
|
|
2446
|
+
ok: false,
|
|
2447
|
+
json: null,
|
|
2448
|
+
error: {
|
|
2449
|
+
code: "PUBLIC_JSON_PARSE_FAILED",
|
|
2450
|
+
message: "public metadata endpoint returned non-JSON content",
|
|
2451
|
+
retryable: true,
|
|
2452
|
+
},
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
async function fetchPublicText(url, options = {}) {
|
|
2458
|
+
try {
|
|
2459
|
+
const response = await fetch(url, {
|
|
2460
|
+
method: "GET",
|
|
2461
|
+
headers: {
|
|
2462
|
+
accept: options.accept ?? "*/*",
|
|
2463
|
+
},
|
|
2464
|
+
});
|
|
2465
|
+
const text = await response.text();
|
|
2466
|
+
return {
|
|
2467
|
+
ok: response.ok,
|
|
2468
|
+
statusCode: response.status,
|
|
2469
|
+
url: response.url,
|
|
2470
|
+
text,
|
|
2471
|
+
error: response.ok
|
|
2472
|
+
? null
|
|
2473
|
+
: {
|
|
2474
|
+
code: "PUBLIC_FETCH_FAILED",
|
|
2475
|
+
message: `public HTTP GET returned ${response.status}`,
|
|
2476
|
+
retryable: response.status >= 500,
|
|
2477
|
+
},
|
|
2478
|
+
};
|
|
2479
|
+
} catch (error) {
|
|
2480
|
+
return {
|
|
2481
|
+
ok: false,
|
|
2482
|
+
statusCode: null,
|
|
2483
|
+
url,
|
|
2484
|
+
text: null,
|
|
2485
|
+
error: {
|
|
2486
|
+
code: "PUBLIC_FETCH_FAILED",
|
|
2487
|
+
message:
|
|
2488
|
+
error instanceof Error ? error.message : "public HTTP GET failed",
|
|
2489
|
+
retryable: true,
|
|
2490
|
+
},
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
|
|
1464
2495
|
async function apiRequest(input) {
|
|
1465
2496
|
const url = new URL(input.path, ensureTrailingSlash(input.apiBaseUrl));
|
|
1466
2497
|
try {
|
|
@@ -1771,13 +2802,16 @@ async function resolveToken(args, options = {}) {
|
|
|
1771
2802
|
return { ok: true, token: config.token.trim(), source: "config" };
|
|
1772
2803
|
}
|
|
1773
2804
|
}
|
|
2805
|
+
if (options.allowMissing === true) {
|
|
2806
|
+
return { ok: true, token: null, source: "anonymous" };
|
|
2807
|
+
}
|
|
1774
2808
|
return {
|
|
1775
2809
|
ok: false,
|
|
1776
2810
|
result: failure(
|
|
1777
2811
|
commandLabel(process.argv.slice(2)),
|
|
1778
2812
|
3,
|
|
1779
2813
|
"AUTH_REQUIRED",
|
|
1780
|
-
"hosted command requires auth; run signup
|
|
2814
|
+
"hosted command requires auth; run signup, set IMAGE_SKILL_TOKEN, or pass --token-stdin",
|
|
1781
2815
|
false,
|
|
1782
2816
|
{
|
|
1783
2817
|
suggested_command: SIGNUP_SUGGESTED_COMMAND,
|
|
@@ -1840,12 +2874,16 @@ function configWriteFailure(command, error) {
|
|
|
1840
2874
|
true,
|
|
1841
2875
|
{
|
|
1842
2876
|
suggested_command:
|
|
1843
|
-
'IMAGE_SKILL_CONFIG_PATH="$PWD/.image-skill/config.json" image-skill signup --agent --agent-contact
|
|
2877
|
+
'IMAGE_SKILL_CONFIG_PATH="$PWD/.image-skill/config.json" image-skill signup --agent --agent-contact AGENT_OR_OPERATOR_INBOX --agent-name NAME --runtime RUNTIME --json',
|
|
1844
2878
|
docs_url: "https://image-skill.com/cli.md#local-config-and-install",
|
|
1845
2879
|
},
|
|
1846
2880
|
);
|
|
1847
2881
|
}
|
|
1848
2882
|
|
|
2883
|
+
function shouldSaveSignupAuth(args) {
|
|
2884
|
+
return !flagBool(args, "no-save");
|
|
2885
|
+
}
|
|
2886
|
+
|
|
1849
2887
|
function parseArgs(argv) {
|
|
1850
2888
|
const flags = new Map();
|
|
1851
2889
|
const positionals = [];
|
|
@@ -2086,6 +3124,16 @@ function invalid(command, message) {
|
|
|
2086
3124
|
});
|
|
2087
3125
|
}
|
|
2088
3126
|
|
|
3127
|
+
function withCommand(result, command) {
|
|
3128
|
+
return {
|
|
3129
|
+
...result,
|
|
3130
|
+
envelope: {
|
|
3131
|
+
...result.envelope,
|
|
3132
|
+
command,
|
|
3133
|
+
},
|
|
3134
|
+
};
|
|
3135
|
+
}
|
|
3136
|
+
|
|
2089
3137
|
function failure(command, exitCode, code, message, retryable, recovery) {
|
|
2090
3138
|
return {
|
|
2091
3139
|
exitCode,
|
|
@@ -2151,6 +3199,13 @@ function assetIdFromReference(reference) {
|
|
|
2151
3199
|
}
|
|
2152
3200
|
try {
|
|
2153
3201
|
const url = new URL(reference);
|
|
3202
|
+
if (
|
|
3203
|
+
url.protocol !== "https:" ||
|
|
3204
|
+
url.hostname !== "media.image-skill.com" ||
|
|
3205
|
+
!url.pathname.startsWith("/a/")
|
|
3206
|
+
) {
|
|
3207
|
+
return null;
|
|
3208
|
+
}
|
|
2154
3209
|
const candidate = basename(url.pathname).replace(/\.[a-z0-9]+$/i, "");
|
|
2155
3210
|
return isAssetId(candidate) ? candidate : null;
|
|
2156
3211
|
} catch {
|
|
@@ -2164,6 +3219,97 @@ function isAssetId(value) {
|
|
|
2164
3219
|
);
|
|
2165
3220
|
}
|
|
2166
3221
|
|
|
3222
|
+
function deriveAssetGetOutputPath(asset) {
|
|
3223
|
+
const urlBasename = safeUsefulUrlBasename(asset.url);
|
|
3224
|
+
if (urlBasename !== null) {
|
|
3225
|
+
return urlBasename;
|
|
3226
|
+
}
|
|
3227
|
+
const assetId =
|
|
3228
|
+
typeof asset.asset_id === "string" &&
|
|
3229
|
+
isSafeDerivedAssetFilename(asset.asset_id)
|
|
3230
|
+
? asset.asset_id
|
|
3231
|
+
: (assetIdFromReference(asset.url) ?? "asset");
|
|
3232
|
+
return `${assetId}${assetOutputExtension(asset)}`;
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
function safeUsefulUrlBasename(value) {
|
|
3236
|
+
let url;
|
|
3237
|
+
try {
|
|
3238
|
+
url = new URL(value);
|
|
3239
|
+
} catch {
|
|
3240
|
+
return null;
|
|
3241
|
+
}
|
|
3242
|
+
const rawBasename = basename(url.pathname);
|
|
3243
|
+
if (rawBasename.length === 0) {
|
|
3244
|
+
return null;
|
|
3245
|
+
}
|
|
3246
|
+
let decoded;
|
|
3247
|
+
try {
|
|
3248
|
+
decoded = decodeURIComponent(rawBasename);
|
|
3249
|
+
} catch {
|
|
3250
|
+
return null;
|
|
3251
|
+
}
|
|
3252
|
+
if (!isSafeDerivedAssetFilename(decoded)) {
|
|
3253
|
+
return null;
|
|
3254
|
+
}
|
|
3255
|
+
return extname(decoded).length > 0 ? decoded : null;
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
function isSafeDerivedAssetFilename(value) {
|
|
3259
|
+
return (
|
|
3260
|
+
value.length > 0 &&
|
|
3261
|
+
value.length <= 220 &&
|
|
3262
|
+
value !== "." &&
|
|
3263
|
+
value !== ".." &&
|
|
3264
|
+
!value.startsWith(".") &&
|
|
3265
|
+
/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value)
|
|
3266
|
+
);
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
function assetOutputExtension(asset) {
|
|
3270
|
+
const mimeType =
|
|
3271
|
+
typeof asset.mime_type === "string"
|
|
3272
|
+
? asset.mime_type.split(";")[0].trim().toLowerCase()
|
|
3273
|
+
: null;
|
|
3274
|
+
if (mimeType === "image/png") {
|
|
3275
|
+
return ".png";
|
|
3276
|
+
}
|
|
3277
|
+
if (mimeType === "image/jpeg") {
|
|
3278
|
+
return ".jpg";
|
|
3279
|
+
}
|
|
3280
|
+
if (mimeType === "image/webp") {
|
|
3281
|
+
return ".webp";
|
|
3282
|
+
}
|
|
3283
|
+
if (mimeType === "image/gif") {
|
|
3284
|
+
return ".gif";
|
|
3285
|
+
}
|
|
3286
|
+
if (mimeType === "image/avif") {
|
|
3287
|
+
return ".avif";
|
|
3288
|
+
}
|
|
3289
|
+
return safeUrlExtension(asset.url) ?? "";
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
function safeUrlExtension(value) {
|
|
3293
|
+
let url;
|
|
3294
|
+
try {
|
|
3295
|
+
url = new URL(value);
|
|
3296
|
+
} catch {
|
|
3297
|
+
return null;
|
|
3298
|
+
}
|
|
3299
|
+
const rawBasename = basename(url.pathname);
|
|
3300
|
+
if (rawBasename.length === 0) {
|
|
3301
|
+
return null;
|
|
3302
|
+
}
|
|
3303
|
+
let decoded;
|
|
3304
|
+
try {
|
|
3305
|
+
decoded = decodeURIComponent(rawBasename);
|
|
3306
|
+
} catch {
|
|
3307
|
+
return null;
|
|
3308
|
+
}
|
|
3309
|
+
const extension = extname(decoded).toLowerCase();
|
|
3310
|
+
return /^\.[a-z0-9]{1,10}$/.test(extension) ? extension : "";
|
|
3311
|
+
}
|
|
3312
|
+
|
|
2167
3313
|
async function downloadUrl(url, outputPath, options) {
|
|
2168
3314
|
if (!options.overwrite && (await fileExists(outputPath))) {
|
|
2169
3315
|
return {
|