image-skill 0.1.12 → 0.1.14

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