image-skill 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1524 @@
1
+ #!/usr/bin/env node
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { createWriteStream } from "node:fs";
4
+ import { chmod, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
5
+ import { basename, dirname, extname, join, resolve } from "node:path";
6
+ import { Readable } from "node:stream";
7
+ import { pipeline } from "node:stream/promises";
8
+ import os from "node:os";
9
+
10
+ const VERSION = "0.1.0";
11
+ const DEFAULT_API_BASE_URL = "https://api.image-skill.com";
12
+ const DEFAULT_CONFIG_PATH = join(
13
+ process.env.XDG_CONFIG_HOME ?? join(os.homedir(), ".config"),
14
+ "image-skill",
15
+ "config.json",
16
+ );
17
+
18
+ const argv = process.argv.slice(2);
19
+ const result = await main(argv);
20
+ process.stdout.write(`${JSON.stringify(result.envelope, null, 2)}\n`);
21
+ process.exitCode = result.exitCode;
22
+
23
+ async function main(rawArgv) {
24
+ const [command, ...rest] = rawArgv;
25
+
26
+ if (
27
+ command === undefined ||
28
+ command === "help" ||
29
+ command === "--help" ||
30
+ command === "-h"
31
+ ) {
32
+ return success("image-skill help", {
33
+ usage:
34
+ "image-skill <doctor|signup|auth|whoami|usage|quota|credits|models|capabilities|create|upload|edit|assets|jobs|activity|feedback> --json",
35
+ docs_url: "https://image-skill.com/cli.md",
36
+ commands: [
37
+ "doctor",
38
+ "signup --agent --save",
39
+ "auth status",
40
+ "auth save",
41
+ "auth logout",
42
+ "whoami",
43
+ "usage quota",
44
+ "credits packs list",
45
+ "credits quote",
46
+ "credits buy",
47
+ "credits status",
48
+ "models list",
49
+ "models show",
50
+ "capabilities list",
51
+ "capabilities show",
52
+ "create",
53
+ "upload",
54
+ "edit",
55
+ "assets show",
56
+ "assets get",
57
+ "jobs show",
58
+ "jobs wait",
59
+ "activity list",
60
+ "activity show",
61
+ "feedback create",
62
+ ],
63
+ });
64
+ }
65
+
66
+ if (command === "version" || command === "--version" || command === "-v") {
67
+ return success("image-skill version", {
68
+ version: VERSION,
69
+ package: "image-skill",
70
+ mode: "public_hosted_cli",
71
+ });
72
+ }
73
+
74
+ try {
75
+ switch (command) {
76
+ case "doctor":
77
+ return doctor(rest);
78
+ case "signup":
79
+ return signup(rest);
80
+ case "auth":
81
+ return auth(rest);
82
+ case "whoami":
83
+ return whoami(rest);
84
+ case "usage":
85
+ return usage(rest);
86
+ case "quota":
87
+ return quota(rest);
88
+ case "credits":
89
+ return credits(rest);
90
+ case "models":
91
+ return models(rest);
92
+ case "capabilities":
93
+ return capabilities(rest);
94
+ case "create":
95
+ return create(rest);
96
+ case "upload":
97
+ return upload(rest);
98
+ case "edit":
99
+ return edit(rest);
100
+ case "assets":
101
+ return assets(rest);
102
+ case "jobs":
103
+ return jobs(rest);
104
+ case "activity":
105
+ return activity(rest);
106
+ case "feedback":
107
+ return feedback(rest);
108
+ default:
109
+ return failure(
110
+ `image-skill ${command}`,
111
+ 2,
112
+ "PUBLIC_CLI_COMMAND_NOT_AVAILABLE",
113
+ `public CLI command is not available: ${command}`,
114
+ false,
115
+ {
116
+ suggested_command: "image-skill help --json",
117
+ docs_url: "https://image-skill.com/cli.md",
118
+ },
119
+ );
120
+ }
121
+ } catch (error) {
122
+ return failure(
123
+ commandLabel(rawArgv),
124
+ 1,
125
+ "PUBLIC_CLI_FAILED",
126
+ error instanceof Error ? error.message : "unknown public CLI failure",
127
+ true,
128
+ );
129
+ }
130
+ }
131
+
132
+ async function doctor(argv) {
133
+ const args = parseArgs(argv);
134
+ const apiBaseUrl = apiBase(args);
135
+ const config = await readConfig(configPath());
136
+ const health = await apiRequest({
137
+ command: "image-skill doctor",
138
+ method: "GET",
139
+ apiBaseUrl,
140
+ path: "/healthz",
141
+ });
142
+ return success("image-skill doctor", {
143
+ cli_version: VERSION,
144
+ package: "image-skill",
145
+ mode: "public_hosted_cli",
146
+ api_base_url: apiBaseUrl,
147
+ hosted_api: {
148
+ reachable: health.envelope.ok,
149
+ status: health.envelope.data?.status ?? null,
150
+ api_version: health.envelope.data?.api_version ?? null,
151
+ error: health.envelope.error,
152
+ },
153
+ auth: {
154
+ config_path: configPath(),
155
+ saved_token: config.tokenPresent,
156
+ env_token: hasEnvToken(),
157
+ },
158
+ docs: {
159
+ skill: "https://image-skill.com/skill.md",
160
+ llms: "https://image-skill.com/llms.txt",
161
+ cli: "https://image-skill.com/cli.md",
162
+ },
163
+ });
164
+ }
165
+
166
+ async function signup(argv) {
167
+ const args = parseArgs(argv);
168
+ if (!flagBool(args, "agent")) {
169
+ return invalid("image-skill signup", "signup currently requires --agent");
170
+ }
171
+ const humanEmail = flagString(args, "human-email");
172
+ const agentName = flagString(args, "agent-name");
173
+ const runtime = flagString(args, "runtime");
174
+ if (humanEmail === null || agentName === null || runtime === null) {
175
+ return invalid(
176
+ "image-skill signup",
177
+ "signup requires --human-email, --agent-name, and --runtime",
178
+ );
179
+ }
180
+ const save = flagBool(args, "save");
181
+ const showToken = flagBool(args, "show-token");
182
+ const result = await apiRequest({
183
+ command: "image-skill signup",
184
+ method: "POST",
185
+ apiBaseUrl: apiBase(args),
186
+ path: "/v1/agent-signups",
187
+ body: {
188
+ human_email: humanEmail,
189
+ agent_name: agentName,
190
+ runtime,
191
+ return_token: save || showToken,
192
+ },
193
+ });
194
+
195
+ const token = result.envelope.data?.token;
196
+ const warnings = [...result.envelope.warnings];
197
+ if (result.envelope.ok && save) {
198
+ if (typeof token !== "string" || token.trim().length === 0) {
199
+ return failure(
200
+ "image-skill signup",
201
+ 3,
202
+ "SIGNUP_TOKEN_NOT_RETURNED",
203
+ "signup --save requires a returned hosted token",
204
+ true,
205
+ {
206
+ suggested_command:
207
+ "image-skill signup --agent --human-email EMAIL --agent-name NAME --runtime RUNTIME --save --json",
208
+ docs_url: "https://image-skill.com/cli.md#image-skill-signup-agent",
209
+ },
210
+ );
211
+ }
212
+ await saveConfig({
213
+ api_base_url: apiBase(args),
214
+ token,
215
+ saved_at: new Date().toISOString(),
216
+ actor: result.envelope.actor ?? result.envelope.data?.actor ?? null,
217
+ });
218
+ warnings.push(`saved hosted token to ${configPath()}`);
219
+ }
220
+
221
+ if (
222
+ !showToken &&
223
+ result.envelope.data &&
224
+ typeof result.envelope.data === "object"
225
+ ) {
226
+ result.envelope.data = {
227
+ ...result.envelope.data,
228
+ token: null,
229
+ token_presented: false,
230
+ storage: {
231
+ ...(result.envelope.data.storage ?? {}),
232
+ saved: save,
233
+ config_path: save ? configPath() : null,
234
+ reason: save
235
+ ? "public CLI saved token locally with 0600 permissions"
236
+ : "token redacted; rerun with --show-token or --save at signup time",
237
+ },
238
+ };
239
+ }
240
+ result.envelope.warnings = warnings;
241
+ return result;
242
+ }
243
+
244
+ async function auth(argv) {
245
+ const [subcommand, ...rest] = argv;
246
+ const args = parseArgs(rest);
247
+ if (subcommand === "status") {
248
+ const token = await resolveToken(args);
249
+ if (!token.ok) {
250
+ return success("image-skill auth status", {
251
+ authenticated: false,
252
+ source: null,
253
+ config_path: configPath(),
254
+ });
255
+ }
256
+ const result = await apiRequest({
257
+ command: "image-skill auth status",
258
+ method: "GET",
259
+ apiBaseUrl: apiBase(args),
260
+ path: "/v1/whoami",
261
+ token: token.token,
262
+ });
263
+ if (result.envelope.data && typeof result.envelope.data === "object") {
264
+ result.envelope.data = {
265
+ ...result.envelope.data,
266
+ auth_source: token.source,
267
+ };
268
+ }
269
+ return result;
270
+ }
271
+ if (subcommand === "save") {
272
+ const token = await resolveToken(args, { allowSaved: false });
273
+ if (!token.ok) {
274
+ return token.result;
275
+ }
276
+ await saveConfig({
277
+ api_base_url: apiBase(args),
278
+ token: token.token,
279
+ saved_at: new Date().toISOString(),
280
+ actor: null,
281
+ });
282
+ return success("image-skill auth save", {
283
+ saved: true,
284
+ config_path: configPath(),
285
+ token_source: token.source,
286
+ });
287
+ }
288
+ if (subcommand === "logout") {
289
+ await rm(configPath(), { force: true });
290
+ return success("image-skill auth logout", {
291
+ saved: false,
292
+ config_path: configPath(),
293
+ });
294
+ }
295
+ return invalid("image-skill auth", "auth requires status, save, or logout");
296
+ }
297
+
298
+ async function whoami(argv) {
299
+ const args = parseArgs(argv);
300
+ const token = await resolveToken(args);
301
+ if (!token.ok) {
302
+ return token.result;
303
+ }
304
+ return apiRequest({
305
+ command: "image-skill whoami",
306
+ method: "GET",
307
+ apiBaseUrl: apiBase(args),
308
+ path: "/v1/whoami",
309
+ token: token.token,
310
+ });
311
+ }
312
+
313
+ async function usage(argv) {
314
+ const [subcommand, ...rest] = argv;
315
+ if (subcommand !== "quota") {
316
+ return invalid("image-skill usage", "usage requires the quota subcommand");
317
+ }
318
+ return quota(rest);
319
+ }
320
+
321
+ async function quota(argv) {
322
+ const args = parseArgs(argv);
323
+ const token = await resolveToken(args);
324
+ if (!token.ok) {
325
+ return token.result;
326
+ }
327
+ return apiRequest({
328
+ command: "image-skill quota",
329
+ method: "GET",
330
+ apiBaseUrl: apiBase(args),
331
+ path: "/v1/quota",
332
+ token: token.token,
333
+ });
334
+ }
335
+
336
+ async function credits(argv) {
337
+ const [subcommand, ...rest] = argv;
338
+ if (subcommand === "packs") {
339
+ const [packsSubcommand, ...packsRest] = rest;
340
+ if (packsSubcommand !== "list") {
341
+ return invalid(
342
+ "image-skill credits packs",
343
+ "credits packs requires the list subcommand",
344
+ );
345
+ }
346
+ const args = parseArgs(packsRest);
347
+ return apiRequest({
348
+ command: "image-skill credits packs list",
349
+ method: "GET",
350
+ apiBaseUrl: apiBase(args),
351
+ path: "/v1/credit-packs",
352
+ });
353
+ }
354
+ if (subcommand === "quote") {
355
+ const args = parseArgs(rest);
356
+ const token = await resolveToken(args);
357
+ if (!token.ok) {
358
+ return token.result;
359
+ }
360
+ const creditsValue = flagNumber(args, "credits");
361
+ const pack = flagString(args, "pack");
362
+ if (creditsValue === null && pack === null) {
363
+ return invalid(
364
+ "image-skill credits quote",
365
+ "credits quote requires --pack PACK_ID or --credits N",
366
+ );
367
+ }
368
+ const idempotency = optionalIdempotencyKey(args, "quote");
369
+ const body = {
370
+ ...(creditsValue === null ? {} : { credits: creditsValue }),
371
+ ...(pack === null ? {} : { pack_id: pack }),
372
+ payment_method: flagString(args, "payment-method") ?? "fake",
373
+ idempotency_key: idempotency.value,
374
+ };
375
+ const result = await apiRequest({
376
+ command: "image-skill credits quote",
377
+ method: "POST",
378
+ apiBaseUrl: apiBase(args),
379
+ path: "/v1/credit-quotes",
380
+ token: token.token,
381
+ body,
382
+ });
383
+ if (idempotency.generated) {
384
+ result.envelope.warnings.push(
385
+ `generated idempotency key ${idempotency.value}; pass --idempotency-key for stable retries`,
386
+ );
387
+ }
388
+ return result;
389
+ }
390
+ if (subcommand === "buy") {
391
+ const args = parseArgs(rest);
392
+ const provider = flagString(args, "provider");
393
+ if (provider !== "stripe") {
394
+ return invalid(
395
+ "image-skill credits buy",
396
+ "credits buy currently requires --provider stripe",
397
+ );
398
+ }
399
+ const quoteId = flagString(args, "quote-id");
400
+ if (quoteId === null) {
401
+ return invalid(
402
+ "image-skill credits buy",
403
+ "credits buy requires --quote-id",
404
+ );
405
+ }
406
+ const token = await resolveToken(args);
407
+ if (!token.ok) {
408
+ return token.result;
409
+ }
410
+ const idempotency = requiredIdempotencyKey(
411
+ args,
412
+ "image-skill credits buy",
413
+ "credits buy creates or replays a Stripe Checkout attempt and requires --idempotency-key for retry-safe payment mutation",
414
+ );
415
+ if (!idempotency.ok) {
416
+ return idempotency.result;
417
+ }
418
+ const result = await apiRequest({
419
+ command: "image-skill credits buy",
420
+ method: "POST",
421
+ apiBaseUrl: apiBase(args),
422
+ path: "/v1/credit-purchases/stripe-checkout-sessions",
423
+ token: token.token,
424
+ body: {
425
+ quote_id: quoteId,
426
+ idempotency_key: idempotency.value,
427
+ },
428
+ });
429
+ return result;
430
+ }
431
+ if (subcommand === "fake-purchase") {
432
+ const args = parseArgs(rest);
433
+ const quoteId = flagString(args, "quote-id");
434
+ if (quoteId === null) {
435
+ return invalid(
436
+ "image-skill credits fake-purchase",
437
+ "credits fake-purchase requires --quote-id",
438
+ );
439
+ }
440
+ const token = await resolveToken(args);
441
+ if (!token.ok) {
442
+ return token.result;
443
+ }
444
+ const idempotency = requiredIdempotencyKey(
445
+ args,
446
+ "image-skill credits fake-purchase",
447
+ "credits fake-purchase creates or replays a credit grant and requires --idempotency-key for retry-safe payment mutation",
448
+ );
449
+ if (!idempotency.ok) {
450
+ return idempotency.result;
451
+ }
452
+ const result = await apiRequest({
453
+ command: "image-skill credits fake-purchase",
454
+ method: "POST",
455
+ apiBaseUrl: apiBase(args),
456
+ path: "/v1/credit-purchases",
457
+ token: token.token,
458
+ body: {
459
+ quote_id: quoteId,
460
+ idempotency_key: idempotency.value,
461
+ },
462
+ });
463
+ return result;
464
+ }
465
+ if (subcommand === "status") {
466
+ const args = parseArgs(rest);
467
+ const token = await resolveToken(args);
468
+ if (!token.ok) {
469
+ return token.result;
470
+ }
471
+ const query = new URLSearchParams();
472
+ addQueryFlag(query, args, "quote-id", "quote_id");
473
+ addQueryFlag(query, args, "payment-attempt-id", "payment_attempt_id");
474
+ addQueryFlag(query, args, "checkout-session-id", "checkout_session_id");
475
+ addQueryFlag(query, args, "receipt-id", "receipt_id");
476
+ return apiRequest({
477
+ command: "image-skill credits status",
478
+ method: "GET",
479
+ apiBaseUrl: apiBase(args),
480
+ path: `/v1/credit-purchases/status?${query.toString()}`,
481
+ token: token.token,
482
+ });
483
+ }
484
+ return invalid(
485
+ "image-skill credits",
486
+ "credits requires packs, quote, buy, status, or fake-purchase",
487
+ );
488
+ }
489
+
490
+ async function models(argv) {
491
+ const [subcommand, ...rest] = argv;
492
+ const args = parseArgs(
493
+ subcommand === "list" || subcommand === "show" ? rest : argv,
494
+ );
495
+ if (subcommand === "show") {
496
+ const modelId = args.positionals[0];
497
+ if (modelId === undefined) {
498
+ return invalid(
499
+ "image-skill models show",
500
+ "models show requires MODEL_ID",
501
+ );
502
+ }
503
+ return apiRequest({
504
+ command: "image-skill models show",
505
+ method: "GET",
506
+ apiBaseUrl: apiBase(args),
507
+ path: `/v1/models/${encodeURIComponent(modelId)}`,
508
+ });
509
+ }
510
+ if (
511
+ subcommand !== undefined &&
512
+ subcommand !== "list" &&
513
+ !subcommand.startsWith("--")
514
+ ) {
515
+ return invalid("image-skill models", "models supports list or show");
516
+ }
517
+ return apiRequest({
518
+ command:
519
+ subcommand === "list" ? "image-skill models list" : "image-skill models",
520
+ method: "GET",
521
+ apiBaseUrl: apiBase(args),
522
+ path: "/v1/models",
523
+ });
524
+ }
525
+
526
+ async function capabilities(argv) {
527
+ const [subcommand, ...rest] = argv;
528
+ const args = parseArgs(
529
+ subcommand === "list" || subcommand === "show" ? rest : argv,
530
+ );
531
+ if (subcommand === "show") {
532
+ const capabilityId = args.positionals[0];
533
+ if (capabilityId === undefined) {
534
+ return invalid(
535
+ "image-skill capabilities show",
536
+ "capabilities show requires CAPABILITY_ID",
537
+ );
538
+ }
539
+ return apiRequest({
540
+ command: "image-skill capabilities show",
541
+ method: "GET",
542
+ apiBaseUrl: apiBase(args),
543
+ path: `/v1/capabilities/${encodeURIComponent(capabilityId)}`,
544
+ });
545
+ }
546
+ if (
547
+ subcommand !== undefined &&
548
+ subcommand !== "list" &&
549
+ !subcommand.startsWith("--")
550
+ ) {
551
+ return invalid(
552
+ "image-skill capabilities",
553
+ "capabilities supports list or show",
554
+ );
555
+ }
556
+ return apiRequest({
557
+ command:
558
+ subcommand === "list"
559
+ ? "image-skill capabilities list"
560
+ : "image-skill capabilities",
561
+ method: "GET",
562
+ apiBaseUrl: apiBase(args),
563
+ path: "/v1/capabilities",
564
+ });
565
+ }
566
+
567
+ async function create(argv) {
568
+ const args = parseArgs(argv);
569
+ const prompt = await promptValue(args);
570
+ if (!prompt.ok) {
571
+ return prompt.result;
572
+ }
573
+ const token = await resolveToken(args);
574
+ if (!token.ok) {
575
+ return token.result;
576
+ }
577
+ const modelParameters = jsonObjectFlag(args, "model-parameters-json");
578
+ if (!modelParameters.ok) {
579
+ return modelParameters.result;
580
+ }
581
+ return apiRequest({
582
+ command: "image-skill create",
583
+ method: "POST",
584
+ apiBaseUrl: apiBase(args),
585
+ path: "/v1/create",
586
+ token: token.token,
587
+ body: {
588
+ prompt: prompt.value,
589
+ ...(flagString(args, "provider") === null
590
+ ? {}
591
+ : { provider: flagString(args, "provider") }),
592
+ ...(flagString(args, "model") === null
593
+ ? {}
594
+ : { model: flagString(args, "model") }),
595
+ ...(flagString(args, "intent") === null
596
+ ? {}
597
+ : { intent: flagString(args, "intent") }),
598
+ aspect_ratio: flagString(args, "aspect-ratio") ?? "1:1",
599
+ ...(flagNumber(args, "max-estimated-usd-per-image") === null
600
+ ? {}
601
+ : {
602
+ max_estimated_usd_per_image: flagNumber(
603
+ args,
604
+ "max-estimated-usd-per-image",
605
+ ),
606
+ }),
607
+ ...(modelParameters.value === null
608
+ ? {}
609
+ : { model_parameters: modelParameters.value }),
610
+ dry_run: flagBool(args, "dry-run"),
611
+ accept_unknown_cost: flagBool(args, "accept-unknown-cost"),
612
+ },
613
+ });
614
+ }
615
+
616
+ async function upload(argv) {
617
+ const args = parseArgs(argv);
618
+ const input = flagString(args, "input") ?? args.positionals[0];
619
+ if (input === undefined) {
620
+ return invalid("image-skill upload", "upload requires PATH_OR_URL");
621
+ }
622
+ const token = await resolveToken(args);
623
+ if (!token.ok) {
624
+ return token.result;
625
+ }
626
+ const uploadBody = await uploadPayload(input);
627
+ if (!uploadBody.ok) {
628
+ return uploadBody.result;
629
+ }
630
+ return apiRequest({
631
+ command: "image-skill upload",
632
+ method: "POST",
633
+ apiBaseUrl: apiBase(args),
634
+ path: "/v1/upload",
635
+ token: token.token,
636
+ body: uploadBody.body,
637
+ });
638
+ }
639
+
640
+ async function edit(argv) {
641
+ const args = parseArgs(argv);
642
+ const input = flagString(args, "input") ?? args.positionals[0];
643
+ if (input === undefined) {
644
+ return invalid(
645
+ "image-skill edit",
646
+ "edit requires --input ASSET_ID_OR_PATH_OR_URL",
647
+ );
648
+ }
649
+ const prompt = await promptValue(args);
650
+ if (!prompt.ok) {
651
+ return prompt.result;
652
+ }
653
+ const token = await resolveToken(args);
654
+ if (!token.ok) {
655
+ return token.result;
656
+ }
657
+ const assetId = await resolveInputAssetId(input, args, token.token);
658
+ if (!assetId.ok) {
659
+ return assetId.result;
660
+ }
661
+ const modelParameters = jsonObjectFlag(args, "model-parameters-json");
662
+ if (!modelParameters.ok) {
663
+ return modelParameters.result;
664
+ }
665
+ return apiRequest({
666
+ command: "image-skill edit",
667
+ method: "POST",
668
+ apiBaseUrl: apiBase(args),
669
+ path: "/v1/edit",
670
+ token: token.token,
671
+ body: {
672
+ input_asset_id: assetId.assetId,
673
+ prompt: prompt.value,
674
+ ...(flagString(args, "provider") === null
675
+ ? {}
676
+ : { provider: flagString(args, "provider") }),
677
+ ...(flagString(args, "model") === null
678
+ ? {}
679
+ : { model: flagString(args, "model") }),
680
+ ...(flagString(args, "intent") === null
681
+ ? {}
682
+ : { intent: flagString(args, "intent") }),
683
+ aspect_ratio: flagString(args, "aspect-ratio") ?? "auto",
684
+ ...(flagNumber(args, "max-estimated-usd-per-image") === null
685
+ ? {}
686
+ : {
687
+ max_estimated_usd_per_image: flagNumber(
688
+ args,
689
+ "max-estimated-usd-per-image",
690
+ ),
691
+ }),
692
+ ...(modelParameters.value === null
693
+ ? {}
694
+ : { model_parameters: modelParameters.value }),
695
+ accept_unknown_cost: flagBool(args, "accept-unknown-cost"),
696
+ },
697
+ });
698
+ }
699
+
700
+ async function assets(argv) {
701
+ const [subcommand, ...rest] = argv;
702
+ const args = parseArgs(rest);
703
+ const reference = args.positionals[0] ?? flagString(args, "id");
704
+ if (reference === undefined || reference === null) {
705
+ return invalid("image-skill assets", "assets requires an asset id or URL");
706
+ }
707
+ const assetId = assetIdFromReference(reference);
708
+ if (assetId === null) {
709
+ return invalid(
710
+ "image-skill assets",
711
+ "assets currently supports Image Skill asset ids and media.image-skill.com URLs",
712
+ );
713
+ }
714
+ const token = await resolveToken(args);
715
+ if (!token.ok) {
716
+ return token.result;
717
+ }
718
+ if (subcommand === "show") {
719
+ return apiRequest({
720
+ command: "image-skill assets show",
721
+ method: "GET",
722
+ apiBaseUrl: apiBase(args),
723
+ path: `/v1/assets/${encodeURIComponent(assetId)}`,
724
+ token: token.token,
725
+ });
726
+ }
727
+ if (subcommand === "get") {
728
+ const shown = await apiRequest({
729
+ command: "image-skill assets get",
730
+ method: "GET",
731
+ apiBaseUrl: apiBase(args),
732
+ path: `/v1/assets/${encodeURIComponent(assetId)}`,
733
+ token: token.token,
734
+ });
735
+ if (!shown.envelope.ok) {
736
+ return shown;
737
+ }
738
+ const asset = shown.envelope.data?.asset ?? shown.envelope.data;
739
+ const output =
740
+ flagString(args, "output") ?? basename(new URL(asset.url).pathname);
741
+ const downloaded = await downloadUrl(asset.url, output, {
742
+ overwrite: flagBool(args, "overwrite"),
743
+ });
744
+ if (!downloaded.ok) {
745
+ return downloaded.result;
746
+ }
747
+ shown.envelope.data = {
748
+ request: {
749
+ reference,
750
+ reference_type: reference === assetId ? "asset_id" : "url",
751
+ },
752
+ asset,
753
+ download: downloaded.data,
754
+ };
755
+ return shown;
756
+ }
757
+ return invalid("image-skill assets", "assets requires show or get");
758
+ }
759
+
760
+ async function jobs(argv) {
761
+ const [subcommand, ...rest] = argv;
762
+ const args = parseArgs(rest);
763
+ const jobId = args.positionals[0] ?? flagString(args, "job-id");
764
+ if (jobId === undefined || jobId === null) {
765
+ return invalid("image-skill jobs", "jobs requires JOB_ID");
766
+ }
767
+ const token = await resolveToken(args);
768
+ if (!token.ok) {
769
+ return token.result;
770
+ }
771
+ if (subcommand === "show") {
772
+ return apiRequest({
773
+ command: "image-skill jobs show",
774
+ method: "GET",
775
+ apiBaseUrl: apiBase(args),
776
+ path: `/v1/jobs/${encodeURIComponent(jobId)}`,
777
+ token: token.token,
778
+ });
779
+ }
780
+ if (subcommand === "wait") {
781
+ const timeoutMs = flagNumber(args, "timeout-ms") ?? 30_000;
782
+ const pollIntervalMs = flagNumber(args, "poll-interval-ms") ?? 1_000;
783
+ const started = Date.now();
784
+ while (Date.now() - started <= timeoutMs) {
785
+ const current = await apiRequest({
786
+ command: "image-skill jobs wait",
787
+ method: "GET",
788
+ apiBaseUrl: apiBase(args),
789
+ path: `/v1/jobs/${encodeURIComponent(jobId)}`,
790
+ token: token.token,
791
+ });
792
+ if (!current.envelope.ok) {
793
+ return current;
794
+ }
795
+ const status = current.envelope.data?.job?.status;
796
+ if (
797
+ status === "completed" ||
798
+ status === "failed" ||
799
+ status === "canceled"
800
+ ) {
801
+ current.envelope.data.request = {
802
+ ...(current.envelope.data.request ?? {}),
803
+ timeout_ms: timeoutMs,
804
+ poll_interval_ms: pollIntervalMs,
805
+ };
806
+ return current;
807
+ }
808
+ await sleep(pollIntervalMs);
809
+ }
810
+ return failure(
811
+ "image-skill jobs wait",
812
+ 8,
813
+ "TIMEOUT",
814
+ `job ${jobId} did not reach a terminal state within ${timeoutMs}ms`,
815
+ true,
816
+ { retry_after_seconds: Math.ceil(pollIntervalMs / 1000) },
817
+ );
818
+ }
819
+ return invalid("image-skill jobs", "jobs requires show or wait");
820
+ }
821
+
822
+ async function activity(argv) {
823
+ const [subcommand, ...rest] = argv;
824
+ const args = parseArgs(rest);
825
+ const token = await resolveToken(args);
826
+ if (!token.ok) {
827
+ return token.result;
828
+ }
829
+ if (subcommand === "show") {
830
+ const reference = args.positionals[0] ?? flagString(args, "reference");
831
+ if (reference === undefined || reference === null) {
832
+ return invalid(
833
+ "image-skill activity show",
834
+ "activity show requires REFERENCE",
835
+ );
836
+ }
837
+ return apiRequest({
838
+ command: "image-skill activity show",
839
+ method: "GET",
840
+ apiBaseUrl: apiBase(args),
841
+ path: `/v1/activity/${encodeURIComponent(reference)}`,
842
+ token: token.token,
843
+ });
844
+ }
845
+ if (subcommand === "list") {
846
+ const query = new URLSearchParams();
847
+ const limit = flagNumber(args, "limit");
848
+ if (limit !== null) {
849
+ query.set("limit", String(limit));
850
+ }
851
+ const subject = flagString(args, "subject");
852
+ if (subject !== null) {
853
+ query.set("subject", subject);
854
+ }
855
+ return apiRequest({
856
+ command: "image-skill activity list",
857
+ method: "GET",
858
+ apiBaseUrl: apiBase(args),
859
+ path: `/v1/activity${query.size > 0 ? `?${query.toString()}` : ""}`,
860
+ token: token.token,
861
+ });
862
+ }
863
+ return invalid("image-skill activity", "activity requires list or show");
864
+ }
865
+
866
+ async function feedback(argv) {
867
+ const [subcommand, ...rest] = argv;
868
+ if (subcommand !== "create") {
869
+ return invalid("image-skill feedback", "feedback requires create");
870
+ }
871
+ const args = parseArgs(rest);
872
+ const token = await resolveToken(args);
873
+ if (!token.ok) {
874
+ return token.result;
875
+ }
876
+ const title = flagString(args, "title");
877
+ const body = flagString(args, "body");
878
+ if (title === null && body === null) {
879
+ return invalid(
880
+ "image-skill feedback create",
881
+ "feedback create requires --title or --body",
882
+ );
883
+ }
884
+ return apiRequest({
885
+ command: "image-skill feedback create",
886
+ method: "POST",
887
+ apiBaseUrl: apiBase(args),
888
+ path: "/v1/feedback",
889
+ token: token.token,
890
+ body: {
891
+ type: flagString(args, "type") ?? "user_feedback",
892
+ ...(title === null ? {} : { title }),
893
+ ...(body === null ? {} : { body }),
894
+ severity: flagString(args, "severity") ?? "medium",
895
+ confidence: flagString(args, "confidence") ?? "medium",
896
+ next_state: flagString(args, "next-state") ?? "watch",
897
+ ...(flagString(args, "command") === null
898
+ ? {}
899
+ : { command: flagString(args, "command") }),
900
+ ...(flagString(args, "expected") === null
901
+ ? {}
902
+ : { expected: flagString(args, "expected") }),
903
+ ...(flagString(args, "actual") === null
904
+ ? {}
905
+ : { actual: flagString(args, "actual") }),
906
+ ...(flagString(args, "friction") === null
907
+ ? {}
908
+ : { friction: flagString(args, "friction") }),
909
+ ...(flagString(args, "proof-needed") === null
910
+ ? {}
911
+ : { proof_needed: flagString(args, "proof-needed") }),
912
+ ...(flagString(args, "strategic-reason") === null
913
+ ? {}
914
+ : { strategic_reason: flagString(args, "strategic-reason") }),
915
+ ...(flagString(args, "dedupe-key") === null
916
+ ? {}
917
+ : { dedupe_key: flagString(args, "dedupe-key") }),
918
+ ...(flagString(args, "source") === null
919
+ ? {}
920
+ : { source: flagString(args, "source") }),
921
+ ...(flagString(args, "trace-id") === null
922
+ ? {}
923
+ : { trace_id: flagString(args, "trace-id") }),
924
+ surface: csvFlag(args, "surface", ["cli"]),
925
+ evidence: csvFlag(args, "evidence", []),
926
+ ...(flagBool(args, "allow-weak-signal")
927
+ ? { allow_weak_signal: true }
928
+ : {}),
929
+ },
930
+ });
931
+ }
932
+
933
+ async function resolveInputAssetId(input, args, token) {
934
+ const direct = assetIdFromReference(input);
935
+ if (direct !== null) {
936
+ return { ok: true, assetId: direct };
937
+ }
938
+ const payload = await uploadPayload(input);
939
+ if (!payload.ok) {
940
+ return payload;
941
+ }
942
+ const uploaded = await apiRequest({
943
+ command: "image-skill upload",
944
+ method: "POST",
945
+ apiBaseUrl: apiBase(args),
946
+ path: "/v1/upload",
947
+ token,
948
+ body: payload.body,
949
+ });
950
+ if (!uploaded.envelope.ok) {
951
+ return { ok: false, result: uploaded };
952
+ }
953
+ const assetId = uploaded.envelope.data?.asset?.asset_id;
954
+ if (typeof assetId !== "string") {
955
+ return {
956
+ ok: false,
957
+ result: failure(
958
+ "image-skill upload",
959
+ 9,
960
+ "UPLOAD_ASSET_ID_MISSING",
961
+ "hosted upload did not return an asset id",
962
+ true,
963
+ ),
964
+ };
965
+ }
966
+ return { ok: true, assetId };
967
+ }
968
+
969
+ async function uploadPayload(input) {
970
+ const isRemote = /^https?:\/\//i.test(input);
971
+ let bytes;
972
+ let filename;
973
+ let remoteOrigin = null;
974
+ let mimeType;
975
+ if (isRemote) {
976
+ const url = new URL(input);
977
+ const response = await fetch(url);
978
+ if (!response.ok) {
979
+ return {
980
+ ok: false,
981
+ result: failure(
982
+ "image-skill upload",
983
+ 7,
984
+ "REMOTE_UPLOAD_FETCH_FAILED",
985
+ `could not fetch remote upload URL: HTTP ${response.status}`,
986
+ true,
987
+ ),
988
+ };
989
+ }
990
+ const arrayBuffer = await response.arrayBuffer();
991
+ bytes = Buffer.from(arrayBuffer);
992
+ filename = basename(url.pathname) || "remote-upload";
993
+ remoteOrigin = url.origin;
994
+ mimeType =
995
+ response.headers.get("content-type")?.split(";")[0]?.toLowerCase() ??
996
+ mimeFromFilename(filename);
997
+ } else {
998
+ const filePath = resolve(input);
999
+ bytes = await readFile(filePath);
1000
+ filename = basename(filePath);
1001
+ mimeType = mimeFromFilename(filename);
1002
+ }
1003
+ if (mimeType === null) {
1004
+ return {
1005
+ ok: false,
1006
+ result: failure(
1007
+ "image-skill upload",
1008
+ 2,
1009
+ "UPLOAD_MIME_TYPE_UNKNOWN",
1010
+ "could not infer MIME type; use png, jpg, jpeg, webp, gif, or avif",
1011
+ false,
1012
+ ),
1013
+ };
1014
+ }
1015
+ return {
1016
+ ok: true,
1017
+ body: {
1018
+ source_kind: isRemote ? "remote_url" : "local_path",
1019
+ filename,
1020
+ remote_origin: remoteOrigin,
1021
+ mime_type: mimeType,
1022
+ content_length: bytes.byteLength,
1023
+ sha256: sha256Hex(bytes),
1024
+ bytes_base64: bytes.toString("base64"),
1025
+ },
1026
+ };
1027
+ }
1028
+
1029
+ async function apiRequest(input) {
1030
+ const url = new URL(input.path, ensureTrailingSlash(input.apiBaseUrl));
1031
+ try {
1032
+ const response = await fetch(url, {
1033
+ method: input.method,
1034
+ headers: {
1035
+ accept: "application/json",
1036
+ ...(input.body === undefined
1037
+ ? {}
1038
+ : { "content-type": "application/json" }),
1039
+ ...(input.token === undefined
1040
+ ? {}
1041
+ : { authorization: `Bearer ${input.token}` }),
1042
+ },
1043
+ body: input.body === undefined ? undefined : JSON.stringify(input.body),
1044
+ });
1045
+ const text = await response.text();
1046
+ const envelope = parseEnvelope(text, input.command, response.status);
1047
+ const exitCodeHeader = response.headers.get("x-image-skill-exit-code");
1048
+ return {
1049
+ exitCode:
1050
+ exitCodeHeader === null
1051
+ ? envelope.ok
1052
+ ? 0
1053
+ : exitCodeForStatus(response.status)
1054
+ : Number(exitCodeHeader),
1055
+ envelope,
1056
+ };
1057
+ } catch (error) {
1058
+ return failure(
1059
+ input.command,
1060
+ 7,
1061
+ "HOSTED_API_REQUEST_FAILED",
1062
+ error instanceof Error ? error.message : "hosted API request failed",
1063
+ true,
1064
+ {
1065
+ suggested_command: "image-skill doctor --json",
1066
+ docs_url: "https://image-skill.com/cli.md",
1067
+ retry_after_seconds: 30,
1068
+ },
1069
+ );
1070
+ }
1071
+ }
1072
+
1073
+ function parseEnvelope(text, command, statusCode) {
1074
+ try {
1075
+ const parsed = JSON.parse(text);
1076
+ if (parsed && typeof parsed === "object" && "ok" in parsed) {
1077
+ return parsed;
1078
+ }
1079
+ } catch {
1080
+ // Fall through to normalized public error.
1081
+ }
1082
+ return {
1083
+ ok: false,
1084
+ command,
1085
+ trace_id: traceId(),
1086
+ actor: null,
1087
+ data: null,
1088
+ warnings: [],
1089
+ error: {
1090
+ code: "HOSTED_API_NON_JSON_RESPONSE",
1091
+ message: `hosted API returned HTTP ${statusCode} without a JSON envelope`,
1092
+ retryable: statusCode >= 500,
1093
+ },
1094
+ };
1095
+ }
1096
+
1097
+ async function promptValue(args) {
1098
+ const prompt = flagString(args, "prompt");
1099
+ const promptFile = flagString(args, "prompt-file");
1100
+ if (prompt !== null && promptFile !== null) {
1101
+ return {
1102
+ ok: false,
1103
+ result: invalid(
1104
+ "image-skill create",
1105
+ "provide either --prompt or --prompt-file, not both",
1106
+ ),
1107
+ };
1108
+ }
1109
+ if (prompt !== null) {
1110
+ return { ok: true, value: prompt };
1111
+ }
1112
+ if (promptFile !== null) {
1113
+ return { ok: true, value: await readFile(promptFile, "utf8") };
1114
+ }
1115
+ return {
1116
+ ok: false,
1117
+ result: invalid("image-skill create", "create/edit requires --prompt"),
1118
+ };
1119
+ }
1120
+
1121
+ async function resolveToken(args, options = {}) {
1122
+ if (flagBool(args, "token-stdin")) {
1123
+ if (process.stdin.isTTY) {
1124
+ return {
1125
+ ok: false,
1126
+ result: failure(
1127
+ commandLabel(process.argv.slice(2)),
1128
+ 2,
1129
+ "INVALID_ARGUMENTS",
1130
+ "--token-stdin requires a token piped on stdin",
1131
+ false,
1132
+ ),
1133
+ };
1134
+ }
1135
+ const token = (await readStdin()).trim();
1136
+ if (token.length === 0) {
1137
+ return {
1138
+ ok: false,
1139
+ result: failure(
1140
+ commandLabel(process.argv.slice(2)),
1141
+ 3,
1142
+ "AUTH_REQUIRED",
1143
+ "--token-stdin received empty stdin",
1144
+ false,
1145
+ ),
1146
+ };
1147
+ }
1148
+ return { ok: true, token, source: "stdin" };
1149
+ }
1150
+ const flagToken = flagString(args, "token");
1151
+ if (flagToken !== null) {
1152
+ return { ok: true, token: flagToken, source: "flag" };
1153
+ }
1154
+ const envToken =
1155
+ process.env.IMAGE_SKILL_TOKEN ?? process.env.IMAGE_SKILL_HOSTED_TOKEN;
1156
+ if (envToken !== undefined && envToken.trim().length > 0) {
1157
+ return { ok: true, token: envToken.trim(), source: "env" };
1158
+ }
1159
+ if (options.allowSaved !== false) {
1160
+ const config = await readConfig(configPath());
1161
+ if (typeof config.token === "string" && config.token.trim().length > 0) {
1162
+ return { ok: true, token: config.token.trim(), source: "config" };
1163
+ }
1164
+ }
1165
+ return {
1166
+ ok: false,
1167
+ result: failure(
1168
+ commandLabel(process.argv.slice(2)),
1169
+ 3,
1170
+ "AUTH_REQUIRED",
1171
+ "hosted command requires auth; run signup --save, set IMAGE_SKILL_TOKEN, or pass --token-stdin",
1172
+ false,
1173
+ {
1174
+ suggested_command:
1175
+ "image-skill signup --agent --human-email EMAIL --agent-name NAME --runtime RUNTIME --save --json",
1176
+ docs_url: "https://image-skill.com/cli.md#image-skill-signup-agent",
1177
+ },
1178
+ ),
1179
+ };
1180
+ }
1181
+
1182
+ async function readConfig(path) {
1183
+ try {
1184
+ const value = JSON.parse(await readFile(path, "utf8"));
1185
+ return {
1186
+ ...value,
1187
+ tokenPresent:
1188
+ typeof value.token === "string" && value.token.trim().length > 0,
1189
+ };
1190
+ } catch {
1191
+ return { token: null, tokenPresent: false };
1192
+ }
1193
+ }
1194
+
1195
+ async function saveConfig(value) {
1196
+ const path = configPath();
1197
+ await mkdir(dirname(path), { recursive: true });
1198
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, {
1199
+ mode: 0o600,
1200
+ });
1201
+ await chmod(path, 0o600);
1202
+ }
1203
+
1204
+ function parseArgs(argv) {
1205
+ const flags = new Map();
1206
+ const positionals = [];
1207
+ for (let index = 0; index < argv.length; index += 1) {
1208
+ const item = argv[index];
1209
+ if (item?.startsWith("--")) {
1210
+ const raw = item.slice(2);
1211
+ const equalIndex = raw.indexOf("=");
1212
+ if (equalIndex !== -1) {
1213
+ pushFlag(flags, raw.slice(0, equalIndex), raw.slice(equalIndex + 1));
1214
+ continue;
1215
+ }
1216
+ const next = argv[index + 1];
1217
+ if (next !== undefined && !next.startsWith("--")) {
1218
+ pushFlag(flags, raw, next);
1219
+ index += 1;
1220
+ } else {
1221
+ pushFlag(flags, raw, true);
1222
+ }
1223
+ } else if (item !== undefined) {
1224
+ positionals.push(item);
1225
+ }
1226
+ }
1227
+ return { flags, positionals };
1228
+ }
1229
+
1230
+ function pushFlag(flags, name, value) {
1231
+ const values = flags.get(name) ?? [];
1232
+ values.push(value);
1233
+ flags.set(name, values);
1234
+ }
1235
+
1236
+ function flagString(args, name) {
1237
+ const value = args.flags.get(name)?.at(-1);
1238
+ return typeof value === "string" ? value : null;
1239
+ }
1240
+
1241
+ function flagBool(args, name) {
1242
+ return args.flags.has(name) && args.flags.get(name)?.at(-1) !== "false";
1243
+ }
1244
+
1245
+ function flagNumber(args, name) {
1246
+ const value = flagString(args, name);
1247
+ if (value === null) {
1248
+ return null;
1249
+ }
1250
+ const number = Number(value);
1251
+ return Number.isFinite(number) ? number : null;
1252
+ }
1253
+
1254
+ function jsonObjectFlag(args, name) {
1255
+ const raw = flagString(args, name);
1256
+ if (raw === null) {
1257
+ return { ok: true, value: null };
1258
+ }
1259
+ try {
1260
+ const parsed = JSON.parse(raw);
1261
+ if (
1262
+ parsed !== null &&
1263
+ typeof parsed === "object" &&
1264
+ !Array.isArray(parsed)
1265
+ ) {
1266
+ return { ok: true, value: parsed };
1267
+ }
1268
+ } catch {
1269
+ // Fall through to normalized public error.
1270
+ }
1271
+ return {
1272
+ ok: false,
1273
+ result: invalid(
1274
+ commandLabel(process.argv.slice(2)),
1275
+ `--${name} must be a JSON object`,
1276
+ ),
1277
+ };
1278
+ }
1279
+
1280
+ function csvFlag(args, name, fallback) {
1281
+ const value = flagString(args, name);
1282
+ return value === null
1283
+ ? fallback
1284
+ : value
1285
+ .split(",")
1286
+ .map((item) => item.trim())
1287
+ .filter(Boolean);
1288
+ }
1289
+
1290
+ function optionalIdempotencyKey(args, prefix) {
1291
+ const value = flagString(args, "idempotency-key");
1292
+ if (value !== null) {
1293
+ return { value, generated: false };
1294
+ }
1295
+ return {
1296
+ value: `${prefix}-${Date.now()}-${randomBytes(4).toString("hex")}`,
1297
+ generated: true,
1298
+ };
1299
+ }
1300
+
1301
+ function requiredIdempotencyKey(args, command, message) {
1302
+ const value = flagString(args, "idempotency-key");
1303
+ if (value !== null) {
1304
+ return { ok: true, value };
1305
+ }
1306
+ return {
1307
+ ok: false,
1308
+ result: failure(command, 2, "INVALID_ARGUMENTS", message, false, {
1309
+ required_flag: "--idempotency-key",
1310
+ suggested_command: `${command} --idempotency-key KEY --json`,
1311
+ docs_url: "https://image-skill.com/cli.md#image-skill-credits",
1312
+ }),
1313
+ };
1314
+ }
1315
+
1316
+ function addQueryFlag(query, args, flag, key) {
1317
+ const value = flagString(args, flag);
1318
+ if (value !== null) {
1319
+ query.set(key, value);
1320
+ }
1321
+ }
1322
+
1323
+ function apiBase(args) {
1324
+ return (
1325
+ flagString(args, "api-base-url") ??
1326
+ process.env.IMAGE_SKILL_API_BASE_URL ??
1327
+ DEFAULT_API_BASE_URL
1328
+ );
1329
+ }
1330
+
1331
+ function configPath() {
1332
+ return process.env.IMAGE_SKILL_CONFIG_PATH ?? DEFAULT_CONFIG_PATH;
1333
+ }
1334
+
1335
+ function hasEnvToken() {
1336
+ return Boolean(
1337
+ process.env.IMAGE_SKILL_TOKEN ?? process.env.IMAGE_SKILL_HOSTED_TOKEN,
1338
+ );
1339
+ }
1340
+
1341
+ function success(command, data, warnings = []) {
1342
+ return {
1343
+ exitCode: 0,
1344
+ envelope: {
1345
+ ok: true,
1346
+ command,
1347
+ trace_id: traceId(),
1348
+ actor: null,
1349
+ data,
1350
+ warnings,
1351
+ error: null,
1352
+ },
1353
+ };
1354
+ }
1355
+
1356
+ function invalid(command, message) {
1357
+ return failure(command, 2, "INVALID_ARGUMENTS", message, false, {
1358
+ docs_url: "https://image-skill.com/cli.md",
1359
+ });
1360
+ }
1361
+
1362
+ function failure(command, exitCode, code, message, retryable, recovery) {
1363
+ return {
1364
+ exitCode,
1365
+ envelope: {
1366
+ ok: false,
1367
+ command,
1368
+ trace_id: traceId(),
1369
+ actor: null,
1370
+ data: null,
1371
+ warnings: [],
1372
+ error: {
1373
+ code,
1374
+ message,
1375
+ retryable,
1376
+ ...(recovery === undefined ? {} : { recovery }),
1377
+ },
1378
+ },
1379
+ };
1380
+ }
1381
+
1382
+ function commandLabel(commandArgv) {
1383
+ return commandArgv.length === 0
1384
+ ? "image-skill"
1385
+ : `image-skill ${commandArgv[0]}`;
1386
+ }
1387
+
1388
+ function traceId() {
1389
+ return `trace_${randomBytes(8).toString("hex")}`;
1390
+ }
1391
+
1392
+ function exitCodeForStatus(status) {
1393
+ if (status === 401 || status === 403) {
1394
+ return 3;
1395
+ }
1396
+ if (status === 402 || status === 429) {
1397
+ return 5;
1398
+ }
1399
+ if (status >= 500) {
1400
+ return 7;
1401
+ }
1402
+ return 1;
1403
+ }
1404
+
1405
+ function ensureTrailingSlash(value) {
1406
+ return value.endsWith("/") ? value : `${value}/`;
1407
+ }
1408
+
1409
+ function readStdin() {
1410
+ return new Promise((resolvePromise, reject) => {
1411
+ let value = "";
1412
+ process.stdin.setEncoding("utf8");
1413
+ process.stdin.on("data", (chunk) => {
1414
+ value += chunk;
1415
+ });
1416
+ process.stdin.on("error", reject);
1417
+ process.stdin.on("end", () => resolvePromise(value));
1418
+ });
1419
+ }
1420
+
1421
+ function assetIdFromReference(reference) {
1422
+ if (isAssetId(reference)) {
1423
+ return reference;
1424
+ }
1425
+ try {
1426
+ const url = new URL(reference);
1427
+ const candidate = basename(url.pathname).replace(/\.[a-z0-9]+$/i, "");
1428
+ return isAssetId(candidate) ? candidate : null;
1429
+ } catch {
1430
+ return null;
1431
+ }
1432
+ }
1433
+
1434
+ function isAssetId(value) {
1435
+ return /^(?:asset|image|video|audio|mask|thumb|file)_[a-zA-Z0-9._-]{1,128}$/.test(
1436
+ value,
1437
+ );
1438
+ }
1439
+
1440
+ async function downloadUrl(url, outputPath, options) {
1441
+ if (!options.overwrite && (await fileExists(outputPath))) {
1442
+ return {
1443
+ ok: false,
1444
+ result: failure(
1445
+ "image-skill assets get",
1446
+ 9,
1447
+ "OUTPUT_EXISTS",
1448
+ `output path already exists: ${outputPath}`,
1449
+ false,
1450
+ ),
1451
+ };
1452
+ }
1453
+ const response = await fetch(url);
1454
+ if (!response.ok || response.body === null) {
1455
+ return {
1456
+ ok: false,
1457
+ result: failure(
1458
+ "image-skill assets get",
1459
+ 7,
1460
+ "ASSET_DOWNLOAD_FAILED",
1461
+ `asset download failed: HTTP ${response.status}`,
1462
+ true,
1463
+ ),
1464
+ };
1465
+ }
1466
+ await mkdir(dirname(resolve(outputPath)), { recursive: true });
1467
+ await pipeline(
1468
+ Readable.fromWeb(response.body),
1469
+ createWriteStream(outputPath),
1470
+ );
1471
+ const file = await stat(outputPath);
1472
+ return {
1473
+ ok: true,
1474
+ data: {
1475
+ output_path: outputPath,
1476
+ bytes: file.size,
1477
+ content_type: response.headers.get("content-type"),
1478
+ content_length_header:
1479
+ response.headers.get("content-length") === null
1480
+ ? null
1481
+ : Number(response.headers.get("content-length")),
1482
+ overwritten: options.overwrite,
1483
+ },
1484
+ };
1485
+ }
1486
+
1487
+ async function fileExists(path) {
1488
+ try {
1489
+ await stat(path);
1490
+ return true;
1491
+ } catch {
1492
+ return false;
1493
+ }
1494
+ }
1495
+
1496
+ function mimeFromFilename(filename) {
1497
+ const ext = extname(filename).toLowerCase();
1498
+ if (ext === ".png") {
1499
+ return "image/png";
1500
+ }
1501
+ if (ext === ".jpg" || ext === ".jpeg") {
1502
+ return "image/jpeg";
1503
+ }
1504
+ if (ext === ".webp") {
1505
+ return "image/webp";
1506
+ }
1507
+ if (ext === ".gif") {
1508
+ return "image/gif";
1509
+ }
1510
+ if (ext === ".avif") {
1511
+ return "image/avif";
1512
+ }
1513
+ return null;
1514
+ }
1515
+
1516
+ function sha256Hex(bytes) {
1517
+ return createHash("sha256").update(bytes).digest("hex");
1518
+ }
1519
+
1520
+ function sleep(ms) {
1521
+ return new Promise((resolvePromise) => {
1522
+ setTimeout(resolvePromise, ms);
1523
+ });
1524
+ }