image-skill 0.1.13 → 0.1.15
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 +20 -0
- package/LICENSE +21 -0
- package/PROVENANCE.md +12 -0
- package/README.md +45 -30
- package/bin/image-skill.mjs +1240 -41
- package/cli.md +162 -57
- package/llms.txt +39 -34
- package/package.json +5 -4
- package/skill.md +114 -67
- package/skills/image-skill/SKILL.md +114 -67
- package/skills/image-skill/references/cli.md +162 -57
- package/skills/image-skill/references/llms.txt +39 -34
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.15";
|
|
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,10 @@ 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.";
|
|
30
|
+
const PUBLIC_NPX_COMMAND_PREFIX = "npx -y image-skill@latest";
|
|
26
31
|
const PAYMENT_CREDENTIAL_FLAGS = new Set([
|
|
27
32
|
"payment-token",
|
|
28
33
|
"payment-secret",
|
|
@@ -60,11 +65,12 @@ async function main(rawArgv) {
|
|
|
60
65
|
) {
|
|
61
66
|
return success("image-skill help", {
|
|
62
67
|
usage:
|
|
63
|
-
"image-skill <doctor|signup|auth|whoami|usage|quota|credits|models|capabilities|create|upload|edit|assets|jobs|activity|feedback> --json",
|
|
68
|
+
"image-skill <doctor|trust|signup|auth|whoami|usage|quota|credits|models|capabilities|create|upload|edit|assets|jobs|activity|feedback> --json",
|
|
64
69
|
docs_url: "https://image-skill.com/cli.md",
|
|
65
70
|
commands: [
|
|
66
71
|
"doctor",
|
|
67
|
-
"
|
|
72
|
+
"trust",
|
|
73
|
+
"signup --agent --agent-contact",
|
|
68
74
|
"auth status",
|
|
69
75
|
"auth save",
|
|
70
76
|
"auth logout",
|
|
@@ -77,6 +83,7 @@ async function main(rawArgv) {
|
|
|
77
83
|
"credits status",
|
|
78
84
|
"models list",
|
|
79
85
|
"models show",
|
|
86
|
+
"create --guide",
|
|
80
87
|
"capabilities list",
|
|
81
88
|
"capabilities show",
|
|
82
89
|
"create",
|
|
@@ -105,6 +112,8 @@ async function main(rawArgv) {
|
|
|
105
112
|
switch (command) {
|
|
106
113
|
case "doctor":
|
|
107
114
|
return await doctor(rest);
|
|
115
|
+
case "trust":
|
|
116
|
+
return await trust(rest);
|
|
108
117
|
case "signup":
|
|
109
118
|
return await signup(rest);
|
|
110
119
|
case "auth":
|
|
@@ -193,6 +202,106 @@ async function doctor(argv) {
|
|
|
193
202
|
});
|
|
194
203
|
}
|
|
195
204
|
|
|
205
|
+
async function trust(argv) {
|
|
206
|
+
const args = parseArgs(argv);
|
|
207
|
+
const unsupportedFlags = [...args.flags.keys()].filter(
|
|
208
|
+
(flag) => !["json", "api-base-url"].includes(flag),
|
|
209
|
+
);
|
|
210
|
+
if (args.positionals.length > 0 || unsupportedFlags.length > 0) {
|
|
211
|
+
return invalid(
|
|
212
|
+
"image-skill trust",
|
|
213
|
+
unsupportedFlags.length > 0
|
|
214
|
+
? `unsupported flags for trust: ${unsupportedFlags.map((flag) => `--${flag}`).join(", ")}`
|
|
215
|
+
: "trust does not accept positional arguments",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const checkedAt = new Date().toISOString();
|
|
220
|
+
const apiBaseUrl = apiBase(args);
|
|
221
|
+
const docsBaseUrl = docsBaseForApiBaseUrl(apiBaseUrl);
|
|
222
|
+
const npmRegistryBaseUrl = npmRegistryBaseForApiBaseUrl(apiBaseUrl);
|
|
223
|
+
|
|
224
|
+
const [npmPackage, hostedContracts, health, models] = await Promise.all([
|
|
225
|
+
inspectNpmPackage({
|
|
226
|
+
checkedAt,
|
|
227
|
+
registryBaseUrl: npmRegistryBaseUrl,
|
|
228
|
+
}),
|
|
229
|
+
inspectHostedContracts({
|
|
230
|
+
checkedAt,
|
|
231
|
+
docsBaseUrl,
|
|
232
|
+
}),
|
|
233
|
+
apiRequest({
|
|
234
|
+
command: "image-skill trust",
|
|
235
|
+
method: "GET",
|
|
236
|
+
apiBaseUrl,
|
|
237
|
+
path: "/healthz",
|
|
238
|
+
}),
|
|
239
|
+
apiRequest({
|
|
240
|
+
command: "image-skill trust",
|
|
241
|
+
method: "GET",
|
|
242
|
+
apiBaseUrl,
|
|
243
|
+
path: "/v1/models",
|
|
244
|
+
}),
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
const hostedApi = trustHostedApi(health, apiBaseUrl, checkedAt);
|
|
248
|
+
const modelRegistry = trustModelRegistry(models, apiBaseUrl, checkedAt);
|
|
249
|
+
const publicRepo = trustPublicRepo(npmPackage);
|
|
250
|
+
const proofUrls = trustProofUrls({
|
|
251
|
+
npmPackage,
|
|
252
|
+
hostedContracts,
|
|
253
|
+
publicRepo,
|
|
254
|
+
});
|
|
255
|
+
const warnings = trustWarnings({
|
|
256
|
+
npmPackage,
|
|
257
|
+
hostedContracts,
|
|
258
|
+
hostedApi,
|
|
259
|
+
modelRegistry,
|
|
260
|
+
proofUrls,
|
|
261
|
+
});
|
|
262
|
+
const summary = trustSummary({
|
|
263
|
+
warnings,
|
|
264
|
+
npmPackage,
|
|
265
|
+
hostedContracts,
|
|
266
|
+
hostedApi,
|
|
267
|
+
modelRegistry,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return success(
|
|
271
|
+
"image-skill trust",
|
|
272
|
+
{
|
|
273
|
+
schema: "image-skill.trust-packet.v0",
|
|
274
|
+
checked_at: checkedAt,
|
|
275
|
+
subject: {
|
|
276
|
+
package: PACKAGE_NAME,
|
|
277
|
+
cli_version: VERSION,
|
|
278
|
+
mode: "public_hosted_cli",
|
|
279
|
+
api_base_url: apiBaseUrl,
|
|
280
|
+
docs_base_url: docsBaseUrl,
|
|
281
|
+
npm_registry_url: npmRegistryBaseUrl,
|
|
282
|
+
},
|
|
283
|
+
summary,
|
|
284
|
+
npm_package: npmPackage,
|
|
285
|
+
public_repo: publicRepo,
|
|
286
|
+
hosted_contracts: hostedContracts,
|
|
287
|
+
hosted_api: hostedApi,
|
|
288
|
+
model_registry: modelRegistry,
|
|
289
|
+
safe_commands: trustSafeCommands(),
|
|
290
|
+
proof_urls: proofUrls,
|
|
291
|
+
redaction: {
|
|
292
|
+
secrets_included: false,
|
|
293
|
+
private_paths_included: false,
|
|
294
|
+
config_file_read: false,
|
|
295
|
+
token_sources_read: false,
|
|
296
|
+
credential_values_read: false,
|
|
297
|
+
forbidden_material:
|
|
298
|
+
"tokens, API keys, private repo paths, provider credentials, payment credentials, card data, wallet secrets, and private package metadata",
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
warnings,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
196
305
|
async function signup(argv) {
|
|
197
306
|
const args = parseArgs(argv);
|
|
198
307
|
if (!flagBool(args, "agent")) {
|
|
@@ -232,7 +341,7 @@ async function signup(argv) {
|
|
|
232
341
|
},
|
|
233
342
|
);
|
|
234
343
|
}
|
|
235
|
-
const save =
|
|
344
|
+
const save = shouldSaveSignupAuth(args);
|
|
236
345
|
const showToken = flagBool(args, "show-token");
|
|
237
346
|
if (save) {
|
|
238
347
|
const configReady = await assertConfigWritable("image-skill signup");
|
|
@@ -246,12 +355,14 @@ async function signup(argv) {
|
|
|
246
355
|
apiBaseUrl: apiBase(args),
|
|
247
356
|
path: "/v1/agent-signups",
|
|
248
357
|
body: {
|
|
249
|
-
|
|
358
|
+
agent_contact: contact.value,
|
|
250
359
|
agent_name: agentName,
|
|
251
360
|
runtime,
|
|
252
361
|
return_token: save || showToken,
|
|
253
362
|
},
|
|
254
363
|
});
|
|
364
|
+
result.envelope.command = "image-skill signup";
|
|
365
|
+
rewriteSignupContactFailure(result);
|
|
255
366
|
|
|
256
367
|
const token = result.envelope.data?.token;
|
|
257
368
|
const warnings = [...result.envelope.warnings];
|
|
@@ -261,7 +372,7 @@ async function signup(argv) {
|
|
|
261
372
|
"image-skill signup",
|
|
262
373
|
3,
|
|
263
374
|
"SIGNUP_TOKEN_NOT_RETURNED",
|
|
264
|
-
"signup
|
|
375
|
+
"signup default auth persistence requires a returned hosted token",
|
|
265
376
|
true,
|
|
266
377
|
{
|
|
267
378
|
suggested_command: SIGNUP_SUGGESTED_COMMAND,
|
|
@@ -282,22 +393,19 @@ async function signup(argv) {
|
|
|
282
393
|
warnings.push(`saved hosted token to ${configPath()}`);
|
|
283
394
|
}
|
|
284
395
|
|
|
285
|
-
if (
|
|
286
|
-
|
|
287
|
-
result.envelope.data &&
|
|
288
|
-
typeof result.envelope.data === "object"
|
|
289
|
-
) {
|
|
396
|
+
if (result.envelope.data && typeof result.envelope.data === "object") {
|
|
397
|
+
const publicData = publicSignupData(result.envelope.data);
|
|
290
398
|
result.envelope.data = {
|
|
291
|
-
...
|
|
292
|
-
token: null,
|
|
293
|
-
token_presented:
|
|
399
|
+
...publicData,
|
|
400
|
+
token: showToken ? (token ?? publicData.token ?? null) : null,
|
|
401
|
+
token_presented: showToken,
|
|
294
402
|
storage: {
|
|
295
|
-
...(
|
|
403
|
+
...(publicData.storage ?? {}),
|
|
296
404
|
saved: save,
|
|
297
405
|
config_path: save ? configPath() : null,
|
|
298
406
|
reason: save
|
|
299
407
|
? "public CLI saved token locally with 0600 permissions"
|
|
300
|
-
: "token
|
|
408
|
+
: "token not saved; later hosted commands need saved auth, IMAGE_SKILL_TOKEN, or --token-stdin",
|
|
301
409
|
},
|
|
302
410
|
};
|
|
303
411
|
}
|
|
@@ -305,6 +413,39 @@ async function signup(argv) {
|
|
|
305
413
|
return result;
|
|
306
414
|
}
|
|
307
415
|
|
|
416
|
+
function rewriteSignupContactFailure(result) {
|
|
417
|
+
const error = result.envelope.error;
|
|
418
|
+
if (
|
|
419
|
+
error !== null &&
|
|
420
|
+
typeof error === "object" &&
|
|
421
|
+
(error.message === "human_email must be a valid email address" ||
|
|
422
|
+
error.message ===
|
|
423
|
+
"agent_contact must be an email-shaped durable contact inbox" ||
|
|
424
|
+
error.message ===
|
|
425
|
+
"human_email is a legacy alias for agent_contact and must be an email-shaped durable contact inbox")
|
|
426
|
+
) {
|
|
427
|
+
error.message =
|
|
428
|
+
"preview signup currently requires --agent-contact to be an email-shaped durable contact inbox; it does not need to belong to an individual human";
|
|
429
|
+
error.recovery = {
|
|
430
|
+
...(error.recovery ?? {}),
|
|
431
|
+
suggested_command: SIGNUP_SUGGESTED_COMMAND,
|
|
432
|
+
docs_url: "https://image-skill.com/cli.md#image-skill-signup-agent",
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function publicSignupData(data) {
|
|
438
|
+
const { human_email: humanEmail, ...rest } = data;
|
|
439
|
+
const agentContact =
|
|
440
|
+
typeof rest.agent_contact === "string" ? rest.agent_contact : humanEmail;
|
|
441
|
+
return {
|
|
442
|
+
...rest,
|
|
443
|
+
...(typeof agentContact === "string"
|
|
444
|
+
? { agent_contact: agentContact }
|
|
445
|
+
: {}),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
308
449
|
async function auth(argv) {
|
|
309
450
|
const [subcommand, ...rest] = argv;
|
|
310
451
|
const args = parseArgs(rest);
|
|
@@ -384,25 +525,31 @@ async function whoami(argv) {
|
|
|
384
525
|
|
|
385
526
|
async function usage(argv) {
|
|
386
527
|
const [subcommand, ...rest] = argv;
|
|
528
|
+
if (subcommand === undefined || subcommand.startsWith("-")) {
|
|
529
|
+
return await quota(argv, "image-skill usage quota");
|
|
530
|
+
}
|
|
387
531
|
if (subcommand !== "quota") {
|
|
388
532
|
return invalid("image-skill usage", "usage requires the quota subcommand");
|
|
389
533
|
}
|
|
390
|
-
return quota(rest);
|
|
534
|
+
return quota(rest, "image-skill usage quota");
|
|
391
535
|
}
|
|
392
536
|
|
|
393
|
-
async function quota(argv) {
|
|
537
|
+
async function quota(argv, command = "image-skill quota") {
|
|
394
538
|
const args = parseArgs(argv);
|
|
395
539
|
const token = await resolveToken(args);
|
|
396
540
|
if (!token.ok) {
|
|
397
|
-
return token.result;
|
|
541
|
+
return withCommand(token.result, command);
|
|
398
542
|
}
|
|
399
|
-
return
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
543
|
+
return withCommand(
|
|
544
|
+
await apiRequest({
|
|
545
|
+
command,
|
|
546
|
+
method: "GET",
|
|
547
|
+
apiBaseUrl: apiBase(args),
|
|
548
|
+
path: "/v1/quota",
|
|
549
|
+
token: token.token,
|
|
550
|
+
}),
|
|
551
|
+
command,
|
|
552
|
+
);
|
|
406
553
|
}
|
|
407
554
|
|
|
408
555
|
async function credits(argv) {
|
|
@@ -601,15 +748,58 @@ async function models(argv) {
|
|
|
601
748
|
) {
|
|
602
749
|
return invalid("image-skill models", "models supports list or show");
|
|
603
750
|
}
|
|
751
|
+
const query = modelListQuery(args);
|
|
752
|
+
if (!query.ok) {
|
|
753
|
+
return invalid(
|
|
754
|
+
subcommand === "list" ? "image-skill models list" : "image-skill models",
|
|
755
|
+
query.message,
|
|
756
|
+
);
|
|
757
|
+
}
|
|
604
758
|
return apiRequest({
|
|
605
759
|
command:
|
|
606
760
|
subcommand === "list" ? "image-skill models list" : "image-skill models",
|
|
607
761
|
method: "GET",
|
|
608
762
|
apiBaseUrl: apiBase(args),
|
|
609
|
-
path:
|
|
763
|
+
path: query.path,
|
|
610
764
|
});
|
|
611
765
|
}
|
|
612
766
|
|
|
767
|
+
function modelListQuery(args) {
|
|
768
|
+
const available = flagBool(args, "available");
|
|
769
|
+
const executable = flagBool(args, "executable");
|
|
770
|
+
const catalogOnly = flagBool(args, "catalog-only");
|
|
771
|
+
if (catalogOnly && (available || executable)) {
|
|
772
|
+
return {
|
|
773
|
+
ok: false,
|
|
774
|
+
message:
|
|
775
|
+
"models list --catalog-only cannot be combined with --available or --executable",
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
const params = new URLSearchParams();
|
|
779
|
+
if (available) {
|
|
780
|
+
params.set("available", "true");
|
|
781
|
+
}
|
|
782
|
+
if (executable) {
|
|
783
|
+
params.set("executable", "true");
|
|
784
|
+
}
|
|
785
|
+
if (catalogOnly) {
|
|
786
|
+
params.set("catalog_only", "true");
|
|
787
|
+
}
|
|
788
|
+
addQueryValue(params, "operation", flagString(args, "operation"));
|
|
789
|
+
addQueryValue(params, "provider", flagString(args, "provider"));
|
|
790
|
+
const query = params.toString();
|
|
791
|
+
return {
|
|
792
|
+
ok: true,
|
|
793
|
+
path: query.length === 0 ? "/v1/models" : `/v1/models?${query}`,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function addQueryValue(params, name, value) {
|
|
798
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
799
|
+
params.set(name, value.trim());
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
613
803
|
async function capabilities(argv) {
|
|
614
804
|
const [subcommand, ...rest] = argv;
|
|
615
805
|
const args = parseArgs(
|
|
@@ -651,20 +841,468 @@ async function capabilities(argv) {
|
|
|
651
841
|
});
|
|
652
842
|
}
|
|
653
843
|
|
|
844
|
+
async function createGuide(args) {
|
|
845
|
+
if (flagBool(args, "dry-run")) {
|
|
846
|
+
return invalid(
|
|
847
|
+
"image-skill create --guide",
|
|
848
|
+
"create --guide cannot be combined with --dry-run; the guide returns the dry-run escape hatch separately",
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
if (hasReferenceFlags(args)) {
|
|
852
|
+
return invalid(
|
|
853
|
+
"image-skill create --guide",
|
|
854
|
+
"create --guide does not upload or resolve reference images; inspect the model with models show, then run create --dry-run before live referenced creates",
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
const modelParameters = jsonObjectFlag(args, "model-parameters-json");
|
|
858
|
+
if (!modelParameters.ok) {
|
|
859
|
+
return modelParameters.result;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const apiBaseUrl = apiBase(args);
|
|
863
|
+
const prompt = flagString(args, "prompt") ?? args.positionals[0] ?? "";
|
|
864
|
+
const trimmedPrompt = prompt.trim();
|
|
865
|
+
const requestedModelId = flagString(args, "model");
|
|
866
|
+
const requestedProviderId = flagString(args, "provider");
|
|
867
|
+
const requestedIntent = flagString(args, "intent") ?? "explore";
|
|
868
|
+
const health = await apiRequest({
|
|
869
|
+
command: "image-skill create --guide",
|
|
870
|
+
method: "GET",
|
|
871
|
+
apiBaseUrl,
|
|
872
|
+
path: "/healthz",
|
|
873
|
+
});
|
|
874
|
+
const models = await apiRequest({
|
|
875
|
+
command: "image-skill create --guide",
|
|
876
|
+
method: "GET",
|
|
877
|
+
apiBaseUrl,
|
|
878
|
+
path: "/v1/models",
|
|
879
|
+
});
|
|
880
|
+
const payments = await apiRequest({
|
|
881
|
+
command: "image-skill create --guide",
|
|
882
|
+
method: "GET",
|
|
883
|
+
apiBaseUrl,
|
|
884
|
+
path: "/v1/payment-methods",
|
|
885
|
+
});
|
|
886
|
+
const token = await resolveToken(args, { allowMissing: true });
|
|
887
|
+
if (!token.ok) {
|
|
888
|
+
return token.result;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const selected =
|
|
892
|
+
models.envelope.ok && models.envelope.data?.models
|
|
893
|
+
? selectCreateGuideModel(models.envelope.data.models, requestedModelId)
|
|
894
|
+
: null;
|
|
895
|
+
const pricing = selected?.economics?.credit_pricing ?? null;
|
|
896
|
+
const estimatedCredits = pricing?.credits_required ?? null;
|
|
897
|
+
const estimatedUsdPerImage =
|
|
898
|
+
selected?.economics?.estimated_usd_per_image ??
|
|
899
|
+
(pricing === null ? null : pricing.estimated_revenue_usd);
|
|
900
|
+
const budgetGuard =
|
|
901
|
+
flagNumber(args, "max-estimated-usd-per-image") ??
|
|
902
|
+
estimatedUsdPerImage ??
|
|
903
|
+
(estimatedCredits === null ? 0.07 : estimatedCredits / 100);
|
|
904
|
+
const quota =
|
|
905
|
+
token.token === null
|
|
906
|
+
? null
|
|
907
|
+
: await apiRequest({
|
|
908
|
+
command: "image-skill create --guide",
|
|
909
|
+
method: "GET",
|
|
910
|
+
apiBaseUrl,
|
|
911
|
+
path: "/v1/quota",
|
|
912
|
+
token: token.token,
|
|
913
|
+
});
|
|
914
|
+
const paymentSummary = createGuidePaymentSummary(payments.envelope.data);
|
|
915
|
+
const stage = createGuideStage({
|
|
916
|
+
prompt: trimmedPrompt,
|
|
917
|
+
health,
|
|
918
|
+
models,
|
|
919
|
+
selected,
|
|
920
|
+
token,
|
|
921
|
+
quota,
|
|
922
|
+
estimatedCredits,
|
|
923
|
+
});
|
|
924
|
+
const blocker = createGuideBlocker(stage, {
|
|
925
|
+
requestedModelId,
|
|
926
|
+
quota,
|
|
927
|
+
estimatedCredits,
|
|
928
|
+
});
|
|
929
|
+
const nextCommand = createGuideNextCommand(stage, {
|
|
930
|
+
prompt: trimmedPrompt,
|
|
931
|
+
selected,
|
|
932
|
+
requestedProviderId,
|
|
933
|
+
requestedIntent,
|
|
934
|
+
budgetGuard,
|
|
935
|
+
apiBaseUrl: explicitApiBaseUrl(args),
|
|
936
|
+
paymentSummary,
|
|
937
|
+
commandPrefix: PUBLIC_NPX_COMMAND_PREFIX,
|
|
938
|
+
});
|
|
939
|
+
const afterNext =
|
|
940
|
+
stage === "auth_required" || stage === "quota_required"
|
|
941
|
+
? renderGuideCommand(
|
|
942
|
+
trimmedPrompt,
|
|
943
|
+
explicitApiBaseUrl(args),
|
|
944
|
+
PUBLIC_NPX_COMMAND_PREFIX,
|
|
945
|
+
)
|
|
946
|
+
: null;
|
|
947
|
+
return success("image-skill create --guide", {
|
|
948
|
+
schema: "image-skill.create-guide.v1",
|
|
949
|
+
ready: stage === "ready_to_create",
|
|
950
|
+
stage,
|
|
951
|
+
checks: {
|
|
952
|
+
hosted_api: {
|
|
953
|
+
reachable: health.envelope.ok,
|
|
954
|
+
status: health.envelope.data?.status ?? null,
|
|
955
|
+
api_base_url: apiBaseUrl,
|
|
956
|
+
error_code: health.envelope.error?.code ?? null,
|
|
957
|
+
},
|
|
958
|
+
auth: {
|
|
959
|
+
source: token.source === "anonymous" ? "none" : token.source,
|
|
960
|
+
authenticated: quota?.envelope.data?.authenticated === true,
|
|
961
|
+
claim_state: quota?.envelope.data?.claim_state ?? null,
|
|
962
|
+
token_status: quota?.envelope.data?.token_status ?? null,
|
|
963
|
+
saved_config_path: configPath(),
|
|
964
|
+
},
|
|
965
|
+
models: {
|
|
966
|
+
reachable: models.envelope.ok,
|
|
967
|
+
executable_count: models.envelope.data?.summary?.executable ?? 0,
|
|
968
|
+
cataloged_not_wired_count:
|
|
969
|
+
models.envelope.data?.summary?.cataloged_not_wired ?? 0,
|
|
970
|
+
error_code: models.envelope.error?.code ?? null,
|
|
971
|
+
},
|
|
972
|
+
quota: {
|
|
973
|
+
checked: quota !== null,
|
|
974
|
+
authenticated: quota?.envelope.data?.authenticated === true,
|
|
975
|
+
remaining_credits: quotaRemainingCredits(quota?.envelope.data ?? null),
|
|
976
|
+
required_credits: estimatedCredits,
|
|
977
|
+
daily_jobs_remaining:
|
|
978
|
+
quota?.envelope.data?.daily_jobs?.remaining ?? null,
|
|
979
|
+
reason:
|
|
980
|
+
quota === null
|
|
981
|
+
? "auth_required"
|
|
982
|
+
: quota.envelope.ok
|
|
983
|
+
? null
|
|
984
|
+
: (quota.envelope.error?.code ?? "quota_unavailable"),
|
|
985
|
+
},
|
|
986
|
+
payments: paymentSummary,
|
|
987
|
+
},
|
|
988
|
+
selection:
|
|
989
|
+
selected === null
|
|
990
|
+
? null
|
|
991
|
+
: {
|
|
992
|
+
operation: "create",
|
|
993
|
+
model_id: selected.id,
|
|
994
|
+
model_status: selected.status,
|
|
995
|
+
model_execution_status: selected.execution.model_execution_status,
|
|
996
|
+
reason:
|
|
997
|
+
requestedModelId === null
|
|
998
|
+
? "default executable create model for first image"
|
|
999
|
+
: "requested executable create model",
|
|
1000
|
+
},
|
|
1001
|
+
cost: {
|
|
1002
|
+
estimated_credits: estimatedCredits,
|
|
1003
|
+
estimated_usd_per_image: estimatedUsdPerImage,
|
|
1004
|
+
pricing_confidence: pricing?.pricing_confidence ?? null,
|
|
1005
|
+
},
|
|
1006
|
+
blocker,
|
|
1007
|
+
next_command: nextCommand,
|
|
1008
|
+
after_next: afterNext,
|
|
1009
|
+
escape_hatches: {
|
|
1010
|
+
doctor: renderGuidePrefixedCommand(
|
|
1011
|
+
PUBLIC_NPX_COMMAND_PREFIX,
|
|
1012
|
+
"doctor --json",
|
|
1013
|
+
),
|
|
1014
|
+
model_inspection:
|
|
1015
|
+
selected === null
|
|
1016
|
+
? renderGuidePrefixedCommand(
|
|
1017
|
+
PUBLIC_NPX_COMMAND_PREFIX,
|
|
1018
|
+
"models list --json",
|
|
1019
|
+
)
|
|
1020
|
+
: renderGuidePrefixedCommand(
|
|
1021
|
+
PUBLIC_NPX_COMMAND_PREFIX,
|
|
1022
|
+
`models show ${shellQuote(selected.id)} --json`,
|
|
1023
|
+
),
|
|
1024
|
+
payment_methods: renderGuidePrefixedCommand(
|
|
1025
|
+
PUBLIC_NPX_COMMAND_PREFIX,
|
|
1026
|
+
"credits methods --json",
|
|
1027
|
+
),
|
|
1028
|
+
quota: renderGuidePrefixedCommand(
|
|
1029
|
+
PUBLIC_NPX_COMMAND_PREFIX,
|
|
1030
|
+
"usage quota --json",
|
|
1031
|
+
),
|
|
1032
|
+
dry_run:
|
|
1033
|
+
selected === null || trimmedPrompt.length === 0
|
|
1034
|
+
? renderGuidePrefixedCommand(
|
|
1035
|
+
PUBLIC_NPX_COMMAND_PREFIX,
|
|
1036
|
+
"create --dry-run --prompt PROMPT --json",
|
|
1037
|
+
)
|
|
1038
|
+
: renderCreateCommand({
|
|
1039
|
+
prompt: trimmedPrompt,
|
|
1040
|
+
modelId: selected.id,
|
|
1041
|
+
providerId: requestedProviderId,
|
|
1042
|
+
intent: requestedIntent,
|
|
1043
|
+
budgetGuard,
|
|
1044
|
+
dryRun: true,
|
|
1045
|
+
apiBaseUrl: explicitApiBaseUrl(args),
|
|
1046
|
+
commandPrefix: PUBLIC_NPX_COMMAND_PREFIX,
|
|
1047
|
+
}),
|
|
1048
|
+
},
|
|
1049
|
+
mutation: {
|
|
1050
|
+
provider_call: false,
|
|
1051
|
+
hosted_create: false,
|
|
1052
|
+
hosted_signup: false,
|
|
1053
|
+
payment_object: false,
|
|
1054
|
+
credit_debit: false,
|
|
1055
|
+
media_write: false,
|
|
1056
|
+
},
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function selectCreateGuideModel(models, requestedModelId) {
|
|
1061
|
+
const isExecutableCreate = (model) =>
|
|
1062
|
+
model?.status === "available" &&
|
|
1063
|
+
model?.execution?.model_execution_status === "executable" &&
|
|
1064
|
+
Array.isArray(model?.supports) &&
|
|
1065
|
+
model.supports.includes("create");
|
|
1066
|
+
if (requestedModelId !== null) {
|
|
1067
|
+
const requested = models.find((model) => model.id === requestedModelId);
|
|
1068
|
+
return requested !== undefined && isExecutableCreate(requested)
|
|
1069
|
+
? requested
|
|
1070
|
+
: null;
|
|
1071
|
+
}
|
|
1072
|
+
return models.find(isExecutableCreate) ?? null;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function createGuidePaymentSummary(data) {
|
|
1076
|
+
const methods = Array.isArray(data?.methods)
|
|
1077
|
+
? data.methods.filter((method) => method.live_money)
|
|
1078
|
+
: [];
|
|
1079
|
+
return {
|
|
1080
|
+
checked: data !== null && typeof data === "object",
|
|
1081
|
+
live_money_methods: methods
|
|
1082
|
+
.filter((method) => method.available)
|
|
1083
|
+
.map((method) => method.method_id),
|
|
1084
|
+
requires_browser: methods.some((method) => method.requires_browser),
|
|
1085
|
+
buyer_modes: [
|
|
1086
|
+
...new Set(methods.flatMap((method) => method.buyer_modes ?? [])),
|
|
1087
|
+
],
|
|
1088
|
+
suggested_commands: [
|
|
1089
|
+
"image-skill credits methods --json",
|
|
1090
|
+
"image-skill credits packs list --json",
|
|
1091
|
+
methods[0]?.recovery?.quote_command ??
|
|
1092
|
+
"image-skill credits quote --pack starter-500 --payment-method stripe_checkout --idempotency-key KEY --json",
|
|
1093
|
+
methods[0]?.recovery?.purchase_command ??
|
|
1094
|
+
"image-skill credits buy --provider stripe --quote-id QUOTE_ID --idempotency-key KEY --json",
|
|
1095
|
+
methods[0]?.recovery?.status_command ??
|
|
1096
|
+
"image-skill credits status --payment-attempt-id PAYMENT_ATTEMPT_ID --json",
|
|
1097
|
+
],
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function createGuideStage(input) {
|
|
1102
|
+
if (input.prompt.length === 0) {
|
|
1103
|
+
return "prompt_required";
|
|
1104
|
+
}
|
|
1105
|
+
if (!input.health.envelope.ok || !input.models.envelope.ok) {
|
|
1106
|
+
return "service_unreachable";
|
|
1107
|
+
}
|
|
1108
|
+
if (input.selected === null) {
|
|
1109
|
+
return "no_executable_model";
|
|
1110
|
+
}
|
|
1111
|
+
if (input.token.token === null) {
|
|
1112
|
+
return "auth_required";
|
|
1113
|
+
}
|
|
1114
|
+
if (input.quota === null || !input.quota.envelope.ok) {
|
|
1115
|
+
return input.quota?.envelope.error?.code === "AUTH_REQUIRED"
|
|
1116
|
+
? "auth_required"
|
|
1117
|
+
: "service_unreachable";
|
|
1118
|
+
}
|
|
1119
|
+
const remaining = quotaRemainingCredits(input.quota.envelope.data);
|
|
1120
|
+
if (
|
|
1121
|
+
input.estimatedCredits !== null &&
|
|
1122
|
+
remaining !== null &&
|
|
1123
|
+
remaining < input.estimatedCredits
|
|
1124
|
+
) {
|
|
1125
|
+
return "quota_required";
|
|
1126
|
+
}
|
|
1127
|
+
if (
|
|
1128
|
+
input.quota.envelope.data?.daily_jobs !== undefined &&
|
|
1129
|
+
input.quota.envelope.data.daily_jobs.remaining <= 0
|
|
1130
|
+
) {
|
|
1131
|
+
return "quota_required";
|
|
1132
|
+
}
|
|
1133
|
+
return "ready_to_create";
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function createGuideBlocker(stage, input) {
|
|
1137
|
+
if (stage === "ready_to_create") {
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
if (stage === "prompt_required") {
|
|
1141
|
+
return {
|
|
1142
|
+
code: "prompt_required",
|
|
1143
|
+
message: "Add --prompt so the guide can return the exact create command.",
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
if (stage === "no_executable_model") {
|
|
1147
|
+
return {
|
|
1148
|
+
code: "no_executable_model",
|
|
1149
|
+
message:
|
|
1150
|
+
input.requestedModelId === null
|
|
1151
|
+
? "No available executable create model was found in the public registry."
|
|
1152
|
+
: `Requested model is not currently an available executable create model: ${input.requestedModelId}`,
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
if (stage === "auth_required") {
|
|
1156
|
+
return {
|
|
1157
|
+
code: "auth_required",
|
|
1158
|
+
message:
|
|
1159
|
+
"Sign up once with a durable agent contact before creating hosted media.",
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
if (stage === "quota_required") {
|
|
1163
|
+
const remaining = quotaRemainingCredits(input.quota?.envelope.data ?? null);
|
|
1164
|
+
return {
|
|
1165
|
+
code: "quota_required",
|
|
1166
|
+
message: `Selected first image requires ${input.estimatedCredits ?? "unknown"} credits; current remaining credits are ${remaining ?? "unknown"}.`,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
return {
|
|
1170
|
+
code: "service_unreachable",
|
|
1171
|
+
message:
|
|
1172
|
+
input.quota?.envelope.error?.message ??
|
|
1173
|
+
"Guide could not complete read-only hosted or registry checks.",
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function createGuideNextCommand(stage, input) {
|
|
1178
|
+
if (stage === "prompt_required") {
|
|
1179
|
+
return renderGuideCommand("PROMPT", input.apiBaseUrl, input.commandPrefix);
|
|
1180
|
+
}
|
|
1181
|
+
if (stage === "no_executable_model" || stage === "service_unreachable") {
|
|
1182
|
+
return renderGuidePrefixedCommand(
|
|
1183
|
+
input.commandPrefix,
|
|
1184
|
+
"models list --json",
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
if (stage === "auth_required") {
|
|
1188
|
+
return renderGuidePrefixedCommand(
|
|
1189
|
+
input.commandPrefix,
|
|
1190
|
+
"signup --agent --agent-contact AGENT_OR_OPERATOR_INBOX --agent-name AGENT_NAME --runtime RUNTIME_NAME --json",
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
if (stage === "quota_required") {
|
|
1194
|
+
return renderGuidePrefixedCommand(
|
|
1195
|
+
input.commandPrefix,
|
|
1196
|
+
stripImageSkillCommandPrefix(input.paymentSummary.suggested_commands[0]),
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
return renderCreateCommand({
|
|
1200
|
+
prompt: input.prompt,
|
|
1201
|
+
modelId: input.selected.id,
|
|
1202
|
+
providerId: input.requestedProviderId,
|
|
1203
|
+
intent: input.requestedIntent,
|
|
1204
|
+
budgetGuard: input.budgetGuard,
|
|
1205
|
+
dryRun: false,
|
|
1206
|
+
apiBaseUrl: input.apiBaseUrl,
|
|
1207
|
+
commandPrefix: input.commandPrefix,
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function renderGuideCommand(prompt, apiBaseUrl, commandPrefix = "image-skill") {
|
|
1212
|
+
return [
|
|
1213
|
+
commandPrefix,
|
|
1214
|
+
"create --guide --prompt",
|
|
1215
|
+
shellQuote(prompt),
|
|
1216
|
+
...(apiBaseUrl === null ? [] : ["--api-base-url", shellQuote(apiBaseUrl)]),
|
|
1217
|
+
"--json",
|
|
1218
|
+
].join(" ");
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function renderCreateCommand(input) {
|
|
1222
|
+
return [
|
|
1223
|
+
input.commandPrefix ?? "image-skill",
|
|
1224
|
+
"create",
|
|
1225
|
+
...(input.dryRun ? ["--dry-run"] : []),
|
|
1226
|
+
...(input.providerId === null
|
|
1227
|
+
? []
|
|
1228
|
+
: ["--provider", shellQuote(input.providerId)]),
|
|
1229
|
+
"--model",
|
|
1230
|
+
shellQuote(input.modelId),
|
|
1231
|
+
"--prompt",
|
|
1232
|
+
shellQuote(input.prompt),
|
|
1233
|
+
"--intent",
|
|
1234
|
+
shellQuote(input.intent),
|
|
1235
|
+
"--max-estimated-usd-per-image",
|
|
1236
|
+
shellQuote(formatUsd(input.budgetGuard)),
|
|
1237
|
+
...(input.apiBaseUrl === null
|
|
1238
|
+
? []
|
|
1239
|
+
: ["--api-base-url", shellQuote(input.apiBaseUrl)]),
|
|
1240
|
+
"--json",
|
|
1241
|
+
].join(" ");
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function renderGuidePrefixedCommand(commandPrefix, command) {
|
|
1245
|
+
return `${commandPrefix} ${stripImageSkillCommandPrefix(command)}`;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function stripImageSkillCommandPrefix(command) {
|
|
1249
|
+
return String(command ?? "").replace(/^image-skill\s+/, "");
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function explicitApiBaseUrl(args) {
|
|
1253
|
+
return flagString(args, "api-base-url");
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function formatUsd(value) {
|
|
1257
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function shellQuote(value) {
|
|
1261
|
+
return JSON.stringify(value);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function quotaRemainingCredits(data) {
|
|
1265
|
+
if (data === null || data === undefined) {
|
|
1266
|
+
return null;
|
|
1267
|
+
}
|
|
1268
|
+
const limits = data.limits ?? {};
|
|
1269
|
+
const freeCredits =
|
|
1270
|
+
typeof limits.remaining_credits === "number" ? limits.remaining_credits : 0;
|
|
1271
|
+
const paidCredits =
|
|
1272
|
+
typeof limits.payment_backed_remaining_credits === "number"
|
|
1273
|
+
? limits.payment_backed_remaining_credits
|
|
1274
|
+
: 0;
|
|
1275
|
+
return freeCredits + paidCredits;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
654
1278
|
async function create(argv) {
|
|
655
1279
|
const args = parseArgs(argv);
|
|
1280
|
+
if (flagBool(args, "guide")) {
|
|
1281
|
+
return createGuide(args);
|
|
1282
|
+
}
|
|
656
1283
|
const prompt = await promptValue(args);
|
|
657
1284
|
if (!prompt.ok) {
|
|
658
1285
|
return prompt.result;
|
|
659
1286
|
}
|
|
660
|
-
|
|
661
|
-
if (
|
|
662
|
-
|
|
1287
|
+
let referenceToken = null;
|
|
1288
|
+
if (flagBool(args, "dry-run") && hasReferenceFlags(args)) {
|
|
1289
|
+
referenceToken = await resolveToken(args);
|
|
1290
|
+
if (!referenceToken.ok) {
|
|
1291
|
+
return referenceToken.result;
|
|
1292
|
+
}
|
|
663
1293
|
}
|
|
664
1294
|
const referencePlan = parseReferencePlan(args, "image-skill create");
|
|
665
1295
|
if (!referencePlan.ok) {
|
|
666
1296
|
return referencePlan.result;
|
|
667
1297
|
}
|
|
1298
|
+
const anonymousDryRun =
|
|
1299
|
+
flagBool(args, "dry-run") && referencePlan.referencePlans.length === 0;
|
|
1300
|
+
const token =
|
|
1301
|
+
referenceToken ??
|
|
1302
|
+
(await resolveToken(args, { allowMissing: anonymousDryRun }));
|
|
1303
|
+
if (!token.ok) {
|
|
1304
|
+
return token.result;
|
|
1305
|
+
}
|
|
668
1306
|
const modelParameters = jsonObjectFlag(args, "model-parameters-json");
|
|
669
1307
|
if (!modelParameters.ok) {
|
|
670
1308
|
return modelParameters.result;
|
|
@@ -675,11 +1313,14 @@ async function create(argv) {
|
|
|
675
1313
|
if (!outputCount.ok) {
|
|
676
1314
|
return outputCount.result;
|
|
677
1315
|
}
|
|
678
|
-
const references =
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
1316
|
+
const references =
|
|
1317
|
+
token.token === null
|
|
1318
|
+
? { ok: true, references: [] }
|
|
1319
|
+
: await resolveReferences(
|
|
1320
|
+
referencePlan.referencePlans,
|
|
1321
|
+
args,
|
|
1322
|
+
token.token,
|
|
1323
|
+
);
|
|
683
1324
|
if (!references.ok) {
|
|
684
1325
|
return references.result;
|
|
685
1326
|
}
|
|
@@ -688,7 +1329,7 @@ async function create(argv) {
|
|
|
688
1329
|
method: "POST",
|
|
689
1330
|
apiBaseUrl: apiBase(args),
|
|
690
1331
|
path: "/v1/create",
|
|
691
|
-
token: token.token,
|
|
1332
|
+
...(token.token === null ? {} : { token: token.token }),
|
|
692
1333
|
body: {
|
|
693
1334
|
prompt: prompt.value,
|
|
694
1335
|
...(flagString(args, "provider") === null
|
|
@@ -869,7 +1510,7 @@ async function assets(argv) {
|
|
|
869
1510
|
}
|
|
870
1511
|
const asset = shown.envelope.data?.asset ?? shown.envelope.data;
|
|
871
1512
|
const output =
|
|
872
|
-
flagString(args, "output") ??
|
|
1513
|
+
flagString(args, "output") ?? deriveAssetGetOutputPath(asset);
|
|
873
1514
|
const downloaded = await downloadUrl(asset.url, output, {
|
|
874
1515
|
overwrite: flagBool(args, "overwrite"),
|
|
875
1516
|
});
|
|
@@ -1173,6 +1814,14 @@ function parseReferencePlan(args, command) {
|
|
|
1173
1814
|
return { ok: true, referencePlans };
|
|
1174
1815
|
}
|
|
1175
1816
|
|
|
1817
|
+
function hasReferenceFlags(args) {
|
|
1818
|
+
return (
|
|
1819
|
+
args.flags.has("element-frontal") ||
|
|
1820
|
+
args.flags.has("element-reference") ||
|
|
1821
|
+
args.flags.has("reference-image")
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1176
1825
|
async function resolveReferences(referencePlans, args, token) {
|
|
1177
1826
|
const references = [];
|
|
1178
1827
|
for (const plan of referencePlans) {
|
|
@@ -1461,6 +2110,441 @@ async function uploadPayload(input) {
|
|
|
1461
2110
|
};
|
|
1462
2111
|
}
|
|
1463
2112
|
|
|
2113
|
+
async function inspectNpmPackage(input) {
|
|
2114
|
+
const registryUrl = new URL(
|
|
2115
|
+
`${encodeURIComponent(PACKAGE_NAME)}/${encodeURIComponent(VERSION)}`,
|
|
2116
|
+
ensureTrailingSlash(input.registryBaseUrl),
|
|
2117
|
+
).toString();
|
|
2118
|
+
const fetched = await fetchPublicJson(registryUrl, {
|
|
2119
|
+
accept: "application/vnd.npm.install-v1+json, application/json",
|
|
2120
|
+
});
|
|
2121
|
+
if (!fetched.ok) {
|
|
2122
|
+
return {
|
|
2123
|
+
status: fetched.statusCode === 404 ? "not_available_yet" : "unreachable",
|
|
2124
|
+
checked_at: input.checkedAt,
|
|
2125
|
+
package: PACKAGE_NAME,
|
|
2126
|
+
version: VERSION,
|
|
2127
|
+
registry_url: registryUrl,
|
|
2128
|
+
dist_integrity: null,
|
|
2129
|
+
tarball: null,
|
|
2130
|
+
git_head: null,
|
|
2131
|
+
repository_url: null,
|
|
2132
|
+
attestation: {
|
|
2133
|
+
status: "not_available_yet",
|
|
2134
|
+
url: null,
|
|
2135
|
+
},
|
|
2136
|
+
error: fetched.error,
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
const parsed = isRecord(fetched.json) ? fetched.json : {};
|
|
2141
|
+
const dist = isRecord(parsed.dist) ? parsed.dist : {};
|
|
2142
|
+
const repository = isRecord(parsed.repository) ? parsed.repository : {};
|
|
2143
|
+
const attestationUrl =
|
|
2144
|
+
isRecord(dist.attestations) && typeof dist.attestations.url === "string"
|
|
2145
|
+
? dist.attestations.url
|
|
2146
|
+
: null;
|
|
2147
|
+
const version = typeof parsed.version === "string" ? parsed.version : VERSION;
|
|
2148
|
+
return {
|
|
2149
|
+
status: version === VERSION ? "verified" : "mismatched",
|
|
2150
|
+
checked_at: input.checkedAt,
|
|
2151
|
+
package: PACKAGE_NAME,
|
|
2152
|
+
version,
|
|
2153
|
+
expected_version: VERSION,
|
|
2154
|
+
registry_url: registryUrl,
|
|
2155
|
+
dist_integrity: typeof dist.integrity === "string" ? dist.integrity : null,
|
|
2156
|
+
tarball: typeof dist.tarball === "string" ? dist.tarball : null,
|
|
2157
|
+
git_head: typeof parsed.gitHead === "string" ? parsed.gitHead : null,
|
|
2158
|
+
repository_url:
|
|
2159
|
+
typeof repository.url === "string" ? repository.url : PUBLIC_REPO_URL,
|
|
2160
|
+
attestation: {
|
|
2161
|
+
status: attestationUrl === null ? "not_available_yet" : "available",
|
|
2162
|
+
url: attestationUrl,
|
|
2163
|
+
},
|
|
2164
|
+
error: null,
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
async function inspectHostedContracts(input) {
|
|
2169
|
+
const contracts = [
|
|
2170
|
+
{ key: "skill", path: "/skill.md" },
|
|
2171
|
+
{ key: "llms", path: "/llms.txt" },
|
|
2172
|
+
{ key: "cli", path: "/cli.md" },
|
|
2173
|
+
];
|
|
2174
|
+
const entries = [];
|
|
2175
|
+
for (const contract of contracts) {
|
|
2176
|
+
const url = new URL(
|
|
2177
|
+
contract.path,
|
|
2178
|
+
ensureTrailingSlash(input.docsBaseUrl),
|
|
2179
|
+
).toString();
|
|
2180
|
+
const fetched = await fetchPublicText(url, {
|
|
2181
|
+
accept: "text/markdown, text/plain, */*",
|
|
2182
|
+
});
|
|
2183
|
+
entries.push({
|
|
2184
|
+
key: contract.key,
|
|
2185
|
+
url,
|
|
2186
|
+
status: fetched.ok ? "verified" : "unreachable",
|
|
2187
|
+
http_status: fetched.statusCode,
|
|
2188
|
+
content_sha256:
|
|
2189
|
+
fetched.text === null
|
|
2190
|
+
? null
|
|
2191
|
+
: `sha256:${sha256Hex(Buffer.from(fetched.text, "utf8"))}`,
|
|
2192
|
+
bytes:
|
|
2193
|
+
fetched.text === null ? null : Buffer.byteLength(fetched.text, "utf8"),
|
|
2194
|
+
error: fetched.error,
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
const verified = entries.filter((entry) => entry.status === "verified");
|
|
2198
|
+
return {
|
|
2199
|
+
status: verified.length === entries.length ? "verified" : "unreachable",
|
|
2200
|
+
checked_at: input.checkedAt,
|
|
2201
|
+
contracts: entries,
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
function trustHostedApi(health, apiBaseUrl, checkedAt) {
|
|
2206
|
+
return {
|
|
2207
|
+
status: health.envelope.ok ? "reachable" : "unreachable",
|
|
2208
|
+
checked_at: checkedAt,
|
|
2209
|
+
url: new URL("/healthz", ensureTrailingSlash(apiBaseUrl)).toString(),
|
|
2210
|
+
reachable: health.envelope.ok,
|
|
2211
|
+
api_status: health.envelope.data?.status ?? null,
|
|
2212
|
+
api_version: health.envelope.data?.api_version ?? null,
|
|
2213
|
+
error: health.envelope.error,
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
function trustModelRegistry(models, apiBaseUrl, checkedAt) {
|
|
2218
|
+
const data = isRecord(models.envelope.data) ? models.envelope.data : {};
|
|
2219
|
+
const modelList = Array.isArray(data.models) ? data.models : [];
|
|
2220
|
+
const counted = countModelAvailability(modelList);
|
|
2221
|
+
const summary = isRecord(data.summary) ? data.summary : {};
|
|
2222
|
+
const executable = numberOrFallback(summary.executable, counted.executable);
|
|
2223
|
+
const catalogedNotWired = numberOrFallback(
|
|
2224
|
+
summary.cataloged_not_wired,
|
|
2225
|
+
counted.cataloged_not_wired,
|
|
2226
|
+
);
|
|
2227
|
+
const unavailable = numberOrFallback(
|
|
2228
|
+
summary.unavailable,
|
|
2229
|
+
counted.unavailable,
|
|
2230
|
+
);
|
|
2231
|
+
return {
|
|
2232
|
+
status: models.envelope.ok ? "available" : "unreachable",
|
|
2233
|
+
checked_at: checkedAt,
|
|
2234
|
+
url: new URL("/v1/models", ensureTrailingSlash(apiBaseUrl)).toString(),
|
|
2235
|
+
freshness: {
|
|
2236
|
+
source: "hosted /v1/models",
|
|
2237
|
+
checked_at: checkedAt,
|
|
2238
|
+
},
|
|
2239
|
+
availability_summary: {
|
|
2240
|
+
total: numberOrFallback(summary.total, modelList.length),
|
|
2241
|
+
returned: numberOrFallback(summary.returned, modelList.length),
|
|
2242
|
+
executable,
|
|
2243
|
+
cataloged_not_wired: catalogedNotWired,
|
|
2244
|
+
unavailable,
|
|
2245
|
+
providers: counted.providers,
|
|
2246
|
+
status_counts: counted.status_counts,
|
|
2247
|
+
},
|
|
2248
|
+
rules: [
|
|
2249
|
+
"Prefer executable models for create/edit.",
|
|
2250
|
+
"Treat cataloged_not_wired as inspect-only evidence, not spend-ready capability.",
|
|
2251
|
+
"Run models show MODEL_ID before using provider-native model parameters.",
|
|
2252
|
+
],
|
|
2253
|
+
error: models.envelope.error,
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
function trustPublicRepo(npmPackage) {
|
|
2258
|
+
const repoUrl = publicRepoUrlFromNpm(npmPackage.repository_url);
|
|
2259
|
+
return {
|
|
2260
|
+
status: repoUrl === null ? "unknown" : "checked",
|
|
2261
|
+
url: repoUrl,
|
|
2262
|
+
git_head: npmPackage.git_head,
|
|
2263
|
+
package_registry_url: npmPackage.registry_url,
|
|
2264
|
+
main_may_be_newer_than_package: true,
|
|
2265
|
+
note: "npm gitHead is the package-source commit when present; public main can move ahead between releases.",
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
function trustProofUrls(input) {
|
|
2270
|
+
return {
|
|
2271
|
+
npm_package: {
|
|
2272
|
+
status: input.npmPackage.status,
|
|
2273
|
+
url: input.npmPackage.registry_url,
|
|
2274
|
+
},
|
|
2275
|
+
npm_attestation: input.npmPackage.attestation,
|
|
2276
|
+
public_repo: {
|
|
2277
|
+
status: input.publicRepo.status,
|
|
2278
|
+
url: input.publicRepo.url,
|
|
2279
|
+
git_head: input.publicRepo.git_head,
|
|
2280
|
+
},
|
|
2281
|
+
hosted_contracts: {
|
|
2282
|
+
status: input.hostedContracts.status,
|
|
2283
|
+
urls: input.hostedContracts.contracts.map((contract) => contract.url),
|
|
2284
|
+
},
|
|
2285
|
+
real_agent_studies: {
|
|
2286
|
+
status: "not_available_yet",
|
|
2287
|
+
url: null,
|
|
2288
|
+
},
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
function trustWarnings(input) {
|
|
2293
|
+
const warnings = [];
|
|
2294
|
+
if (input.npmPackage.status !== "verified") {
|
|
2295
|
+
warnings.push(`npm package metadata is ${input.npmPackage.status}`);
|
|
2296
|
+
}
|
|
2297
|
+
if (input.npmPackage.git_head === null) {
|
|
2298
|
+
warnings.push("npm package gitHead is not available");
|
|
2299
|
+
}
|
|
2300
|
+
if (input.npmPackage.attestation.status !== "available") {
|
|
2301
|
+
warnings.push("npm provenance attestation URL is not available yet");
|
|
2302
|
+
}
|
|
2303
|
+
if (input.hostedContracts.status !== "verified") {
|
|
2304
|
+
warnings.push("one or more hosted contract documents could not be hashed");
|
|
2305
|
+
}
|
|
2306
|
+
if (input.hostedApi.status !== "reachable") {
|
|
2307
|
+
warnings.push("hosted API health is unreachable");
|
|
2308
|
+
}
|
|
2309
|
+
if (input.modelRegistry.status !== "available") {
|
|
2310
|
+
warnings.push("hosted model registry is unreachable");
|
|
2311
|
+
}
|
|
2312
|
+
const availability = input.modelRegistry.availability_summary;
|
|
2313
|
+
if (availability.executable === 0) {
|
|
2314
|
+
warnings.push("hosted model registry reports zero executable models");
|
|
2315
|
+
}
|
|
2316
|
+
if (availability.cataloged_not_wired > 0) {
|
|
2317
|
+
warnings.push(
|
|
2318
|
+
`hosted model registry reports ${availability.cataloged_not_wired} cataloged_not_wired model(s)`,
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
if (input.proofUrls.real_agent_studies.status === "not_available_yet") {
|
|
2322
|
+
warnings.push("real-agent study proof URL is not available yet");
|
|
2323
|
+
}
|
|
2324
|
+
return warnings;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
function trustSummary(input) {
|
|
2328
|
+
const hasMismatch =
|
|
2329
|
+
input.npmPackage.status === "mismatched" ||
|
|
2330
|
+
input.hostedContracts.status === "mismatched";
|
|
2331
|
+
const availability = input.modelRegistry.availability_summary;
|
|
2332
|
+
let status;
|
|
2333
|
+
if (input.hostedApi.status !== "reachable") {
|
|
2334
|
+
status = "unavailable";
|
|
2335
|
+
} else if (hasMismatch) {
|
|
2336
|
+
status = "stale_or_mismatched";
|
|
2337
|
+
} else if (
|
|
2338
|
+
input.modelRegistry.status !== "available" ||
|
|
2339
|
+
availability.executable === 0
|
|
2340
|
+
) {
|
|
2341
|
+
status = "inspect_only";
|
|
2342
|
+
} else if (input.warnings.length > 0) {
|
|
2343
|
+
status = "usable_with_warnings";
|
|
2344
|
+
} else {
|
|
2345
|
+
status = "usable";
|
|
2346
|
+
}
|
|
2347
|
+
return {
|
|
2348
|
+
status,
|
|
2349
|
+
selection_hint: trustSelectionHint(status),
|
|
2350
|
+
top_warnings: input.warnings.slice(0, 5),
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
function trustSelectionHint(status) {
|
|
2355
|
+
if (status === "usable" || status === "usable_with_warnings") {
|
|
2356
|
+
return "Use create --guide as the first creative command; use trust output as selection evidence, not as a required setup step.";
|
|
2357
|
+
}
|
|
2358
|
+
if (status === "inspect_only") {
|
|
2359
|
+
return "Inspect docs and model metadata, but do not assume spend-ready image generation until executable models and health are available.";
|
|
2360
|
+
}
|
|
2361
|
+
if (status === "stale_or_mismatched") {
|
|
2362
|
+
return "Do not rely on this package/contract combination until the mismatch is resolved or a newer package is selected.";
|
|
2363
|
+
}
|
|
2364
|
+
return "Do not select Image Skill for new work until hosted health and model registry checks recover.";
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
function trustSafeCommands() {
|
|
2368
|
+
return [
|
|
2369
|
+
{
|
|
2370
|
+
purpose: "trust_packet",
|
|
2371
|
+
command: "npx -y image-skill@latest trust --json",
|
|
2372
|
+
mutation: false,
|
|
2373
|
+
spend: false,
|
|
2374
|
+
},
|
|
2375
|
+
{
|
|
2376
|
+
purpose: "first_image_guide",
|
|
2377
|
+
command:
|
|
2378
|
+
'npx -y image-skill@latest create --guide --prompt "a compact field camera on a stainless workbench" --json',
|
|
2379
|
+
mutation: false,
|
|
2380
|
+
spend: false,
|
|
2381
|
+
},
|
|
2382
|
+
{
|
|
2383
|
+
purpose: "model_inspection",
|
|
2384
|
+
command: "npx -y image-skill@latest models list --json",
|
|
2385
|
+
mutation: false,
|
|
2386
|
+
spend: false,
|
|
2387
|
+
},
|
|
2388
|
+
];
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
function countModelAvailability(models) {
|
|
2392
|
+
const statusCounts = {};
|
|
2393
|
+
const providers = new Set();
|
|
2394
|
+
let executable = 0;
|
|
2395
|
+
let catalogedNotWired = 0;
|
|
2396
|
+
let unavailable = 0;
|
|
2397
|
+
for (const model of models) {
|
|
2398
|
+
if (!isRecord(model)) {
|
|
2399
|
+
continue;
|
|
2400
|
+
}
|
|
2401
|
+
const providerId = modelProviderId(model);
|
|
2402
|
+
if (providerId !== null) {
|
|
2403
|
+
providers.add(providerId);
|
|
2404
|
+
}
|
|
2405
|
+
const status = modelAvailabilityStatus(model);
|
|
2406
|
+
statusCounts[status] = (statusCounts[status] ?? 0) + 1;
|
|
2407
|
+
if (status === "executable" || status === "available") {
|
|
2408
|
+
executable += 1;
|
|
2409
|
+
}
|
|
2410
|
+
if (status === "cataloged_not_wired") {
|
|
2411
|
+
catalogedNotWired += 1;
|
|
2412
|
+
}
|
|
2413
|
+
if (model.status === "unavailable" || status === "unavailable") {
|
|
2414
|
+
unavailable += 1;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
return {
|
|
2418
|
+
executable,
|
|
2419
|
+
cataloged_not_wired: catalogedNotWired,
|
|
2420
|
+
unavailable,
|
|
2421
|
+
providers: [...providers].sort(),
|
|
2422
|
+
status_counts: statusCounts,
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
function modelProviderId(model) {
|
|
2427
|
+
if (typeof model.provider_id === "string") {
|
|
2428
|
+
return model.provider_id;
|
|
2429
|
+
}
|
|
2430
|
+
if (isRecord(model.provider) && typeof model.provider.id === "string") {
|
|
2431
|
+
return model.provider.id;
|
|
2432
|
+
}
|
|
2433
|
+
if (typeof model.id === "string" && model.id.includes(".")) {
|
|
2434
|
+
return model.id.split(".")[0];
|
|
2435
|
+
}
|
|
2436
|
+
return null;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
function modelAvailabilityStatus(model) {
|
|
2440
|
+
if (
|
|
2441
|
+
isRecord(model.execution) &&
|
|
2442
|
+
typeof model.execution.model_execution_status === "string"
|
|
2443
|
+
) {
|
|
2444
|
+
return model.execution.model_execution_status;
|
|
2445
|
+
}
|
|
2446
|
+
if (typeof model.availability_reason === "string") {
|
|
2447
|
+
return model.availability_reason;
|
|
2448
|
+
}
|
|
2449
|
+
if (typeof model.status === "string") {
|
|
2450
|
+
return model.status;
|
|
2451
|
+
}
|
|
2452
|
+
return "unknown";
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
function numberOrFallback(value, fallback) {
|
|
2456
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
function publicRepoUrlFromNpm(repositoryUrl) {
|
|
2460
|
+
if (typeof repositoryUrl !== "string" || repositoryUrl.trim().length === 0) {
|
|
2461
|
+
return PUBLIC_REPO_URL;
|
|
2462
|
+
}
|
|
2463
|
+
return repositoryUrl
|
|
2464
|
+
.replace(/^git\+/, "")
|
|
2465
|
+
.replace(/\.git$/, "")
|
|
2466
|
+
.replace(/^ssh:\/\/git@github.com\//, "https://github.com/");
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
function docsBaseForApiBaseUrl(apiBaseUrl) {
|
|
2470
|
+
return sameBaseUrl(apiBaseUrl, DEFAULT_API_BASE_URL)
|
|
2471
|
+
? DEFAULT_DOCS_BASE_URL
|
|
2472
|
+
: apiBaseUrl;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
function npmRegistryBaseForApiBaseUrl(apiBaseUrl) {
|
|
2476
|
+
return sameBaseUrl(apiBaseUrl, DEFAULT_API_BASE_URL)
|
|
2477
|
+
? DEFAULT_NPM_REGISTRY_BASE_URL
|
|
2478
|
+
: apiBaseUrl;
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
function sameBaseUrl(left, right) {
|
|
2482
|
+
return stripTrailingSlash(left) === stripTrailingSlash(right);
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
function stripTrailingSlash(value) {
|
|
2486
|
+
return value.replace(/\/+$/, "");
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
async function fetchPublicJson(url, options = {}) {
|
|
2490
|
+
const fetched = await fetchPublicText(url, options);
|
|
2491
|
+
if (!fetched.ok || fetched.text === null) {
|
|
2492
|
+
return { ...fetched, json: null };
|
|
2493
|
+
}
|
|
2494
|
+
try {
|
|
2495
|
+
return { ...fetched, json: JSON.parse(fetched.text) };
|
|
2496
|
+
} catch {
|
|
2497
|
+
return {
|
|
2498
|
+
...fetched,
|
|
2499
|
+
ok: false,
|
|
2500
|
+
json: null,
|
|
2501
|
+
error: {
|
|
2502
|
+
code: "PUBLIC_JSON_PARSE_FAILED",
|
|
2503
|
+
message: "public metadata endpoint returned non-JSON content",
|
|
2504
|
+
retryable: true,
|
|
2505
|
+
},
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
async function fetchPublicText(url, options = {}) {
|
|
2511
|
+
try {
|
|
2512
|
+
const response = await fetch(url, {
|
|
2513
|
+
method: "GET",
|
|
2514
|
+
headers: {
|
|
2515
|
+
accept: options.accept ?? "*/*",
|
|
2516
|
+
},
|
|
2517
|
+
});
|
|
2518
|
+
const text = await response.text();
|
|
2519
|
+
return {
|
|
2520
|
+
ok: response.ok,
|
|
2521
|
+
statusCode: response.status,
|
|
2522
|
+
url: response.url,
|
|
2523
|
+
text,
|
|
2524
|
+
error: response.ok
|
|
2525
|
+
? null
|
|
2526
|
+
: {
|
|
2527
|
+
code: "PUBLIC_FETCH_FAILED",
|
|
2528
|
+
message: `public HTTP GET returned ${response.status}`,
|
|
2529
|
+
retryable: response.status >= 500,
|
|
2530
|
+
},
|
|
2531
|
+
};
|
|
2532
|
+
} catch (error) {
|
|
2533
|
+
return {
|
|
2534
|
+
ok: false,
|
|
2535
|
+
statusCode: null,
|
|
2536
|
+
url,
|
|
2537
|
+
text: null,
|
|
2538
|
+
error: {
|
|
2539
|
+
code: "PUBLIC_FETCH_FAILED",
|
|
2540
|
+
message:
|
|
2541
|
+
error instanceof Error ? error.message : "public HTTP GET failed",
|
|
2542
|
+
retryable: true,
|
|
2543
|
+
},
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
1464
2548
|
async function apiRequest(input) {
|
|
1465
2549
|
const url = new URL(input.path, ensureTrailingSlash(input.apiBaseUrl));
|
|
1466
2550
|
try {
|
|
@@ -1771,13 +2855,16 @@ async function resolveToken(args, options = {}) {
|
|
|
1771
2855
|
return { ok: true, token: config.token.trim(), source: "config" };
|
|
1772
2856
|
}
|
|
1773
2857
|
}
|
|
2858
|
+
if (options.allowMissing === true) {
|
|
2859
|
+
return { ok: true, token: null, source: "anonymous" };
|
|
2860
|
+
}
|
|
1774
2861
|
return {
|
|
1775
2862
|
ok: false,
|
|
1776
2863
|
result: failure(
|
|
1777
2864
|
commandLabel(process.argv.slice(2)),
|
|
1778
2865
|
3,
|
|
1779
2866
|
"AUTH_REQUIRED",
|
|
1780
|
-
"hosted command requires auth; run signup
|
|
2867
|
+
"hosted command requires auth; run signup, set IMAGE_SKILL_TOKEN, or pass --token-stdin",
|
|
1781
2868
|
false,
|
|
1782
2869
|
{
|
|
1783
2870
|
suggested_command: SIGNUP_SUGGESTED_COMMAND,
|
|
@@ -1840,12 +2927,16 @@ function configWriteFailure(command, error) {
|
|
|
1840
2927
|
true,
|
|
1841
2928
|
{
|
|
1842
2929
|
suggested_command:
|
|
1843
|
-
'IMAGE_SKILL_CONFIG_PATH="$PWD/.image-skill/config.json" image-skill signup --agent --agent-contact
|
|
2930
|
+
'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
2931
|
docs_url: "https://image-skill.com/cli.md#local-config-and-install",
|
|
1845
2932
|
},
|
|
1846
2933
|
);
|
|
1847
2934
|
}
|
|
1848
2935
|
|
|
2936
|
+
function shouldSaveSignupAuth(args) {
|
|
2937
|
+
return !flagBool(args, "no-save");
|
|
2938
|
+
}
|
|
2939
|
+
|
|
1849
2940
|
function parseArgs(argv) {
|
|
1850
2941
|
const flags = new Map();
|
|
1851
2942
|
const positionals = [];
|
|
@@ -2086,6 +3177,16 @@ function invalid(command, message) {
|
|
|
2086
3177
|
});
|
|
2087
3178
|
}
|
|
2088
3179
|
|
|
3180
|
+
function withCommand(result, command) {
|
|
3181
|
+
return {
|
|
3182
|
+
...result,
|
|
3183
|
+
envelope: {
|
|
3184
|
+
...result.envelope,
|
|
3185
|
+
command,
|
|
3186
|
+
},
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
|
|
2089
3190
|
function failure(command, exitCode, code, message, retryable, recovery) {
|
|
2090
3191
|
return {
|
|
2091
3192
|
exitCode,
|
|
@@ -2151,6 +3252,13 @@ function assetIdFromReference(reference) {
|
|
|
2151
3252
|
}
|
|
2152
3253
|
try {
|
|
2153
3254
|
const url = new URL(reference);
|
|
3255
|
+
if (
|
|
3256
|
+
url.protocol !== "https:" ||
|
|
3257
|
+
url.hostname !== "media.image-skill.com" ||
|
|
3258
|
+
!url.pathname.startsWith("/a/")
|
|
3259
|
+
) {
|
|
3260
|
+
return null;
|
|
3261
|
+
}
|
|
2154
3262
|
const candidate = basename(url.pathname).replace(/\.[a-z0-9]+$/i, "");
|
|
2155
3263
|
return isAssetId(candidate) ? candidate : null;
|
|
2156
3264
|
} catch {
|
|
@@ -2164,6 +3272,97 @@ function isAssetId(value) {
|
|
|
2164
3272
|
);
|
|
2165
3273
|
}
|
|
2166
3274
|
|
|
3275
|
+
function deriveAssetGetOutputPath(asset) {
|
|
3276
|
+
const urlBasename = safeUsefulUrlBasename(asset.url);
|
|
3277
|
+
if (urlBasename !== null) {
|
|
3278
|
+
return urlBasename;
|
|
3279
|
+
}
|
|
3280
|
+
const assetId =
|
|
3281
|
+
typeof asset.asset_id === "string" &&
|
|
3282
|
+
isSafeDerivedAssetFilename(asset.asset_id)
|
|
3283
|
+
? asset.asset_id
|
|
3284
|
+
: (assetIdFromReference(asset.url) ?? "asset");
|
|
3285
|
+
return `${assetId}${assetOutputExtension(asset)}`;
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
function safeUsefulUrlBasename(value) {
|
|
3289
|
+
let url;
|
|
3290
|
+
try {
|
|
3291
|
+
url = new URL(value);
|
|
3292
|
+
} catch {
|
|
3293
|
+
return null;
|
|
3294
|
+
}
|
|
3295
|
+
const rawBasename = basename(url.pathname);
|
|
3296
|
+
if (rawBasename.length === 0) {
|
|
3297
|
+
return null;
|
|
3298
|
+
}
|
|
3299
|
+
let decoded;
|
|
3300
|
+
try {
|
|
3301
|
+
decoded = decodeURIComponent(rawBasename);
|
|
3302
|
+
} catch {
|
|
3303
|
+
return null;
|
|
3304
|
+
}
|
|
3305
|
+
if (!isSafeDerivedAssetFilename(decoded)) {
|
|
3306
|
+
return null;
|
|
3307
|
+
}
|
|
3308
|
+
return extname(decoded).length > 0 ? decoded : null;
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
function isSafeDerivedAssetFilename(value) {
|
|
3312
|
+
return (
|
|
3313
|
+
value.length > 0 &&
|
|
3314
|
+
value.length <= 220 &&
|
|
3315
|
+
value !== "." &&
|
|
3316
|
+
value !== ".." &&
|
|
3317
|
+
!value.startsWith(".") &&
|
|
3318
|
+
/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value)
|
|
3319
|
+
);
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
function assetOutputExtension(asset) {
|
|
3323
|
+
const mimeType =
|
|
3324
|
+
typeof asset.mime_type === "string"
|
|
3325
|
+
? asset.mime_type.split(";")[0].trim().toLowerCase()
|
|
3326
|
+
: null;
|
|
3327
|
+
if (mimeType === "image/png") {
|
|
3328
|
+
return ".png";
|
|
3329
|
+
}
|
|
3330
|
+
if (mimeType === "image/jpeg") {
|
|
3331
|
+
return ".jpg";
|
|
3332
|
+
}
|
|
3333
|
+
if (mimeType === "image/webp") {
|
|
3334
|
+
return ".webp";
|
|
3335
|
+
}
|
|
3336
|
+
if (mimeType === "image/gif") {
|
|
3337
|
+
return ".gif";
|
|
3338
|
+
}
|
|
3339
|
+
if (mimeType === "image/avif") {
|
|
3340
|
+
return ".avif";
|
|
3341
|
+
}
|
|
3342
|
+
return safeUrlExtension(asset.url) ?? "";
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
function safeUrlExtension(value) {
|
|
3346
|
+
let url;
|
|
3347
|
+
try {
|
|
3348
|
+
url = new URL(value);
|
|
3349
|
+
} catch {
|
|
3350
|
+
return null;
|
|
3351
|
+
}
|
|
3352
|
+
const rawBasename = basename(url.pathname);
|
|
3353
|
+
if (rawBasename.length === 0) {
|
|
3354
|
+
return null;
|
|
3355
|
+
}
|
|
3356
|
+
let decoded;
|
|
3357
|
+
try {
|
|
3358
|
+
decoded = decodeURIComponent(rawBasename);
|
|
3359
|
+
} catch {
|
|
3360
|
+
return null;
|
|
3361
|
+
}
|
|
3362
|
+
const extension = extname(decoded).toLowerCase();
|
|
3363
|
+
return /^\.[a-z0-9]{1,10}$/.test(extension) ? extension : "";
|
|
3364
|
+
}
|
|
3365
|
+
|
|
2167
3366
|
async function downloadUrl(url, outputPath, options) {
|
|
2168
3367
|
if (!options.overwrite && (await fileExists(outputPath))) {
|
|
2169
3368
|
return {
|