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.
@@ -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.13";
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 CONTACT_OR_SPONSOR_INBOX --agent-name NAME --runtime RUNTIME --save --json";
27
+ "image-skill signup --agent --agent-contact AGENT_OR_OPERATOR_INBOX --agent-name NAME --runtime RUNTIME --json";
24
28
  const SIGNUP_CONTACT_GUIDANCE =
25
- "Use --agent-contact for the accountable contact, sponsor, operator, or agent inbox for this restricted agent identity. If no individual human is in the loop, use a durable operator/team/agent inbox that can receive future claim, billing, or abuse notices; do not invent a person or use a throwaway inbox. --human-email remains a compatibility alias.";
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
- "signup --agent --agent-contact --save",
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 = flagBool(args, "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
- human_email: contact.value,
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 --save requires a returned hosted token",
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
- !showToken &&
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
- ...result.envelope.data,
292
- token: null,
293
- token_presented: false,
399
+ ...publicData,
400
+ token: showToken ? (token ?? publicData.token ?? null) : null,
401
+ token_presented: showToken,
294
402
  storage: {
295
- ...(result.envelope.data.storage ?? {}),
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 redacted; rerun with --show-token or --save at signup time",
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 apiRequest({
400
- command: "image-skill quota",
401
- method: "GET",
402
- apiBaseUrl: apiBase(args),
403
- path: "/v1/quota",
404
- token: token.token,
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: "/v1/models",
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
- const token = await resolveToken(args);
661
- if (!token.ok) {
662
- return token.result;
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 = await resolveReferences(
679
- referencePlan.referencePlans,
680
- args,
681
- token.token,
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") ?? basename(new URL(asset.url).pathname);
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 --save, set IMAGE_SKILL_TOKEN, or pass --token-stdin",
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 CONTACT_OR_SPONSOR_INBOX --agent-name NAME --runtime RUNTIME --save --json',
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 {