image-skill 0.1.6 → 0.1.8

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,13 @@ 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.6";
10
+ const VERSION = "0.1.8";
11
11
  const DEFAULT_API_BASE_URL = "https://api.image-skill.com";
12
+ const PROMPTLESS_EDIT_MODEL_IDS = new Set([
13
+ "fal.flux-dev-redux",
14
+ "fal.flux-krea-redux",
15
+ "fal.flux-schnell-redux",
16
+ ]);
12
17
  const DEFAULT_CONFIG_PATH = join(
13
18
  process.env.XDG_CONFIG_HOME ?? join(os.homedir(), ".config"),
14
19
  "image-skill",
@@ -79,37 +84,37 @@ async function main(rawArgv) {
79
84
  try {
80
85
  switch (command) {
81
86
  case "doctor":
82
- return doctor(rest);
87
+ return await doctor(rest);
83
88
  case "signup":
84
- return signup(rest);
89
+ return await signup(rest);
85
90
  case "auth":
86
- return auth(rest);
91
+ return await auth(rest);
87
92
  case "whoami":
88
- return whoami(rest);
93
+ return await whoami(rest);
89
94
  case "usage":
90
- return usage(rest);
95
+ return await usage(rest);
91
96
  case "quota":
92
- return quota(rest);
97
+ return await quota(rest);
93
98
  case "credits":
94
- return credits(rest);
99
+ return await credits(rest);
95
100
  case "models":
96
- return models(rest);
101
+ return await models(rest);
97
102
  case "capabilities":
98
- return capabilities(rest);
103
+ return await capabilities(rest);
99
104
  case "create":
100
- return create(rest);
105
+ return await create(rest);
101
106
  case "upload":
102
- return upload(rest);
107
+ return await upload(rest);
103
108
  case "edit":
104
- return edit(rest);
109
+ return await edit(rest);
105
110
  case "assets":
106
- return assets(rest);
111
+ return await assets(rest);
107
112
  case "jobs":
108
- return jobs(rest);
113
+ return await jobs(rest);
109
114
  case "activity":
110
- return activity(rest);
115
+ return await activity(rest);
111
116
  case "feedback":
112
- return feedback(rest);
117
+ return await feedback(rest);
113
118
  default:
114
119
  return failure(
115
120
  `image-skill ${command}`,
@@ -209,6 +214,12 @@ async function signup(argv) {
209
214
  }
210
215
  const save = flagBool(args, "save");
211
216
  const showToken = flagBool(args, "show-token");
217
+ if (save) {
218
+ const configReady = await assertConfigWritable("image-skill signup");
219
+ if (!configReady.ok) {
220
+ return configReady.result;
221
+ }
222
+ }
212
223
  const result = await apiRequest({
213
224
  command: "image-skill signup",
214
225
  method: "POST",
@@ -238,12 +249,16 @@ async function signup(argv) {
238
249
  },
239
250
  );
240
251
  }
241
- await saveConfig({
242
- api_base_url: apiBase(args),
243
- token,
244
- saved_at: new Date().toISOString(),
245
- actor: result.envelope.actor ?? result.envelope.data?.actor ?? null,
246
- });
252
+ try {
253
+ await saveConfig({
254
+ api_base_url: apiBase(args),
255
+ token,
256
+ saved_at: new Date().toISOString(),
257
+ actor: result.envelope.actor ?? result.envelope.data?.actor ?? null,
258
+ });
259
+ } catch (error) {
260
+ return configWriteFailure("image-skill signup", error);
261
+ }
247
262
  warnings.push(`saved hosted token to ${configPath()}`);
248
263
  }
249
264
 
@@ -302,12 +317,20 @@ async function auth(argv) {
302
317
  if (!token.ok) {
303
318
  return token.result;
304
319
  }
305
- await saveConfig({
306
- api_base_url: apiBase(args),
307
- token: token.token,
308
- saved_at: new Date().toISOString(),
309
- actor: null,
310
- });
320
+ const configReady = await assertConfigWritable("image-skill auth save");
321
+ if (!configReady.ok) {
322
+ return configReady.result;
323
+ }
324
+ try {
325
+ await saveConfig({
326
+ api_base_url: apiBase(args),
327
+ token: token.token,
328
+ saved_at: new Date().toISOString(),
329
+ actor: null,
330
+ });
331
+ } catch (error) {
332
+ return configWriteFailure("image-skill auth save", error);
333
+ }
311
334
  return success("image-skill auth save", {
312
335
  saved: true,
313
336
  config_path: configPath(),
@@ -481,7 +504,7 @@ async function credits(argv) {
481
504
  idempotency_key: idempotency.value,
482
505
  },
483
506
  });
484
- return result;
507
+ return withStripeCheckoutCopyFallback(result);
485
508
  }
486
509
  if (subcommand === "fake-purchase") {
487
510
  const args = parseArgs(rest);
@@ -528,13 +551,14 @@ async function credits(argv) {
528
551
  addQueryFlag(query, args, "payment-attempt-id", "payment_attempt_id");
529
552
  addQueryFlag(query, args, "checkout-session-id", "checkout_session_id");
530
553
  addQueryFlag(query, args, "receipt-id", "receipt_id");
531
- return apiRequest({
554
+ const result = await apiRequest({
532
555
  command: "image-skill credits status",
533
556
  method: "GET",
534
557
  apiBaseUrl: apiBase(args),
535
558
  path: `/v1/credit-purchases/status?${query.toString()}`,
536
559
  token: token.token,
537
560
  });
561
+ return withStripeCheckoutCopyFallback(result);
538
562
  }
539
563
  return invalid(
540
564
  "image-skill credits",
@@ -629,10 +653,28 @@ async function create(argv) {
629
653
  if (!token.ok) {
630
654
  return token.result;
631
655
  }
656
+ const referencePlan = parseReferencePlan(args, "image-skill create");
657
+ if (!referencePlan.ok) {
658
+ return referencePlan.result;
659
+ }
632
660
  const modelParameters = jsonObjectFlag(args, "model-parameters-json");
633
661
  if (!modelParameters.ok) {
634
662
  return modelParameters.result;
635
663
  }
664
+ const outputCount = positiveIntegerFlag(args, "output-count", {
665
+ command: "image-skill create",
666
+ });
667
+ if (!outputCount.ok) {
668
+ return outputCount.result;
669
+ }
670
+ const references = await resolveReferences(
671
+ referencePlan.referencePlans,
672
+ args,
673
+ token.token,
674
+ );
675
+ if (!references.ok) {
676
+ return references.result;
677
+ }
636
678
  return apiRequest({
637
679
  command: "image-skill create",
638
680
  method: "POST",
@@ -651,6 +693,12 @@ async function create(argv) {
651
693
  ? {}
652
694
  : { intent: flagString(args, "intent") }),
653
695
  aspect_ratio: flagString(args, "aspect-ratio") ?? "1:1",
696
+ ...(references.references.length === 0
697
+ ? {}
698
+ : { references: references.references }),
699
+ ...(outputCount.value === null
700
+ ? {}
701
+ : { output_count: outputCount.value }),
654
702
  ...(flagNumber(args, "max-estimated-usd-per-image") === null
655
703
  ? {}
656
704
  : {
@@ -695,13 +743,14 @@ async function upload(argv) {
695
743
  async function edit(argv) {
696
744
  const args = parseArgs(argv);
697
745
  const input = flagString(args, "input") ?? args.positionals[0];
746
+ const modelId = flagString(args, "model");
698
747
  if (input === undefined) {
699
748
  return invalid(
700
749
  "image-skill edit",
701
750
  "edit requires --input ASSET_ID_OR_PATH_OR_URL",
702
751
  );
703
752
  }
704
- const prompt = await promptValue(args);
753
+ const prompt = await editPromptValue(args, modelId);
705
754
  if (!prompt.ok) {
706
755
  return prompt.result;
707
756
  }
@@ -709,10 +758,28 @@ async function edit(argv) {
709
758
  if (!token.ok) {
710
759
  return token.result;
711
760
  }
761
+ const referencePlan = parseReferencePlan(args, "image-skill edit");
762
+ if (!referencePlan.ok) {
763
+ return referencePlan.result;
764
+ }
712
765
  const assetId = await resolveInputAssetId(input, args, token.token);
713
766
  if (!assetId.ok) {
714
767
  return assetId.result;
715
768
  }
769
+ const mask = flagString(args, "mask");
770
+ const maskAssetId =
771
+ mask === null ? null : await resolveInputAssetId(mask, args, token.token);
772
+ if (maskAssetId !== null && !maskAssetId.ok) {
773
+ return maskAssetId.result;
774
+ }
775
+ const references = await resolveReferences(
776
+ referencePlan.referencePlans,
777
+ args,
778
+ token.token,
779
+ );
780
+ if (!references.ok) {
781
+ return references.result;
782
+ }
716
783
  const modelParameters = jsonObjectFlag(args, "model-parameters-json");
717
784
  if (!modelParameters.ok) {
718
785
  return modelParameters.result;
@@ -725,13 +792,15 @@ async function edit(argv) {
725
792
  token: token.token,
726
793
  body: {
727
794
  input_asset_id: assetId.assetId,
728
- prompt: prompt.value,
795
+ ...(maskAssetId === null ? {} : { mask_asset_id: maskAssetId.assetId }),
796
+ ...(references.references.length === 0
797
+ ? {}
798
+ : { references: references.references }),
799
+ ...(prompt.value.length === 0 ? {} : { prompt: prompt.value }),
729
800
  ...(flagString(args, "provider") === null
730
801
  ? {}
731
802
  : { provider: flagString(args, "provider") }),
732
- ...(flagString(args, "model") === null
733
- ? {}
734
- : { model: flagString(args, "model") }),
803
+ ...(modelId === null ? {} : { model: modelId }),
735
804
  ...(flagString(args, "intent") === null
736
805
  ? {}
737
806
  : { intent: flagString(args, "intent") }),
@@ -1022,6 +1091,308 @@ async function resolveInputAssetId(input, args, token) {
1022
1091
  return { ok: true, assetId };
1023
1092
  }
1024
1093
 
1094
+ function parseReferencePlan(args, command) {
1095
+ for (const flag of [
1096
+ "element-frontal",
1097
+ "element-reference",
1098
+ "reference-image",
1099
+ ]) {
1100
+ if (
1101
+ args.flags.has(flag) &&
1102
+ args.flags.get(flag)?.some((value) => typeof value !== "string")
1103
+ ) {
1104
+ return {
1105
+ ok: false,
1106
+ result: invalid(command, `--${flag} requires an image`),
1107
+ };
1108
+ }
1109
+ }
1110
+ const referencePlans = [];
1111
+ for (const value of flagStrings(args, "element-frontal")) {
1112
+ const parsed = parseElementReferenceFlag(value, {
1113
+ flag: "--element-frontal",
1114
+ allowReferenceIndex: false,
1115
+ command,
1116
+ });
1117
+ if (!parsed.ok) {
1118
+ return parsed;
1119
+ }
1120
+ referencePlans.push({
1121
+ input: parsed.input,
1122
+ role: "element_frontal",
1123
+ index: parsed.index,
1124
+ referenceIndex: null,
1125
+ referenceTask: null,
1126
+ });
1127
+ }
1128
+ for (const value of flagStrings(args, "element-reference")) {
1129
+ const parsed = parseElementReferenceFlag(value, {
1130
+ flag: "--element-reference",
1131
+ allowReferenceIndex: true,
1132
+ command,
1133
+ });
1134
+ if (!parsed.ok) {
1135
+ return parsed;
1136
+ }
1137
+ referencePlans.push({
1138
+ input: parsed.input,
1139
+ role: "element_reference",
1140
+ index: parsed.index,
1141
+ referenceIndex: parsed.referenceIndex,
1142
+ referenceTask: null,
1143
+ });
1144
+ }
1145
+ for (const value of flagStrings(args, "reference-image")) {
1146
+ const parsed = parseReferenceImageFlag(value, {
1147
+ flag: "--reference-image",
1148
+ command,
1149
+ });
1150
+ if (!parsed.ok) {
1151
+ return parsed;
1152
+ }
1153
+ referencePlans.push({
1154
+ input: parsed.input,
1155
+ role: "reference_image",
1156
+ index: parsed.index,
1157
+ referenceIndex: null,
1158
+ referenceTask: parsed.referenceTask,
1159
+ });
1160
+ }
1161
+ const planValidation = validateElementReferencePlan(referencePlans, command);
1162
+ if (!planValidation.ok) {
1163
+ return planValidation;
1164
+ }
1165
+ return { ok: true, referencePlans };
1166
+ }
1167
+
1168
+ async function resolveReferences(referencePlans, args, token) {
1169
+ const references = [];
1170
+ for (const plan of referencePlans) {
1171
+ const assetId = await resolveInputAssetId(plan.input, args, token);
1172
+ if (!assetId.ok) {
1173
+ return assetId;
1174
+ }
1175
+ if (plan.role === "element_frontal") {
1176
+ references.push({
1177
+ asset_id: assetId.assetId,
1178
+ role: "element_frontal",
1179
+ index: plan.index,
1180
+ });
1181
+ continue;
1182
+ }
1183
+ if (plan.role === "reference_image") {
1184
+ references.push({
1185
+ asset_id: assetId.assetId,
1186
+ role: "reference_image",
1187
+ index: plan.index,
1188
+ ...(plan.referenceTask === null
1189
+ ? {}
1190
+ : { reference_task: plan.referenceTask }),
1191
+ });
1192
+ continue;
1193
+ }
1194
+ references.push({
1195
+ asset_id: assetId.assetId,
1196
+ role: "element_reference",
1197
+ index: plan.index,
1198
+ ...(plan.referenceIndex === null
1199
+ ? {}
1200
+ : { reference_index: plan.referenceIndex }),
1201
+ });
1202
+ }
1203
+ return { ok: true, references };
1204
+ }
1205
+
1206
+ function validateElementReferencePlan(referencePlans, command) {
1207
+ if (referencePlans.length === 0) {
1208
+ return { ok: true };
1209
+ }
1210
+ const frontals = new Set();
1211
+ const referencesByElement = new Map();
1212
+ const elementIndexes = new Set();
1213
+ for (const plan of referencePlans) {
1214
+ if (plan.role === "reference_image") {
1215
+ continue;
1216
+ }
1217
+ elementIndexes.add(plan.index);
1218
+ if (plan.role === "element_frontal") {
1219
+ if (frontals.has(plan.index)) {
1220
+ return {
1221
+ ok: false,
1222
+ result: invalid(
1223
+ command,
1224
+ `only one --element-frontal is allowed for element ${plan.index}`,
1225
+ ),
1226
+ };
1227
+ }
1228
+ frontals.add(plan.index);
1229
+ } else {
1230
+ const count = referencesByElement.get(plan.index) ?? 0;
1231
+ referencesByElement.set(plan.index, count + 1);
1232
+ }
1233
+ }
1234
+
1235
+ const sortedIndexes = [...elementIndexes].sort((left, right) => left - right);
1236
+ for (let expected = 0; expected < sortedIndexes.length; expected += 1) {
1237
+ if (sortedIndexes[expected] !== expected) {
1238
+ return {
1239
+ ok: false,
1240
+ result: invalid(
1241
+ command,
1242
+ "element indexes must be contiguous starting at 0",
1243
+ ),
1244
+ };
1245
+ }
1246
+ }
1247
+ for (const [index, count] of referencesByElement.entries()) {
1248
+ if (!frontals.has(index)) {
1249
+ return {
1250
+ ok: false,
1251
+ result: invalid(
1252
+ command,
1253
+ `--element-reference for element ${index} requires --element-frontal for the same element`,
1254
+ ),
1255
+ };
1256
+ }
1257
+ if (count > 3) {
1258
+ return {
1259
+ ok: false,
1260
+ result: invalid(
1261
+ command,
1262
+ `element ${index} accepts at most 3 --element-reference images`,
1263
+ ),
1264
+ };
1265
+ }
1266
+ }
1267
+ const referenceImageIndexes = new Set();
1268
+ for (const plan of referencePlans) {
1269
+ if (plan.role !== "reference_image") {
1270
+ continue;
1271
+ }
1272
+ if (referenceImageIndexes.has(plan.index)) {
1273
+ return {
1274
+ ok: false,
1275
+ result: invalid(
1276
+ command,
1277
+ `only one --reference-image is allowed for index ${plan.index}`,
1278
+ ),
1279
+ };
1280
+ }
1281
+ referenceImageIndexes.add(plan.index);
1282
+ }
1283
+ const sortedReferenceImageIndexes = [...referenceImageIndexes].sort(
1284
+ (left, right) => left - right,
1285
+ );
1286
+ for (
1287
+ let expected = 0;
1288
+ expected < sortedReferenceImageIndexes.length;
1289
+ expected += 1
1290
+ ) {
1291
+ if (sortedReferenceImageIndexes[expected] !== expected) {
1292
+ return {
1293
+ ok: false,
1294
+ result: invalid(
1295
+ command,
1296
+ "reference image indexes must be contiguous starting at 0",
1297
+ ),
1298
+ };
1299
+ }
1300
+ }
1301
+ return { ok: true };
1302
+ }
1303
+
1304
+ function parseReferenceImageFlag(value, options) {
1305
+ const parsed = parseReferenceImageSuffix(value);
1306
+ if (parsed.input.length === 0) {
1307
+ return {
1308
+ ok: false,
1309
+ result: invalid(options.command, `${options.flag} requires an image`),
1310
+ };
1311
+ }
1312
+ if (parsed.index > 9) {
1313
+ return {
1314
+ ok: false,
1315
+ result: invalid(
1316
+ options.command,
1317
+ `${options.flag} index must be between 0 and 9`,
1318
+ ),
1319
+ };
1320
+ }
1321
+ return { ok: true, ...parsed };
1322
+ }
1323
+
1324
+ function parseReferenceImageSuffix(value) {
1325
+ const atIndex = value.lastIndexOf("@");
1326
+ if (atIndex === -1) {
1327
+ return { input: value, index: 0, referenceTask: null };
1328
+ }
1329
+ const suffix = value.slice(atIndex + 1);
1330
+ if (!/^\d+(?::(?:ip|id|style))?$/.test(suffix)) {
1331
+ return { input: value, index: 0, referenceTask: null };
1332
+ }
1333
+ const [index, referenceTask] = suffix.split(":");
1334
+ return {
1335
+ input: value.slice(0, atIndex),
1336
+ index: Number(index),
1337
+ referenceTask: referenceTask ?? null,
1338
+ };
1339
+ }
1340
+
1341
+ function parseElementReferenceFlag(value, options) {
1342
+ const parsed = parseElementReferenceSuffix(value);
1343
+ if (parsed.input.length === 0) {
1344
+ return {
1345
+ ok: false,
1346
+ result: invalid(options.command, `${options.flag} requires an image`),
1347
+ };
1348
+ }
1349
+ if (!options.allowReferenceIndex && parsed.referenceIndex !== null) {
1350
+ return {
1351
+ ok: false,
1352
+ result: invalid(
1353
+ options.command,
1354
+ `${options.flag} accepts IMAGE[@ELEMENT_INDEX], not a reference index`,
1355
+ ),
1356
+ };
1357
+ }
1358
+ if (parsed.index > 9) {
1359
+ return {
1360
+ ok: false,
1361
+ result: invalid(
1362
+ options.command,
1363
+ `${options.flag} element index must be between 0 and 9`,
1364
+ ),
1365
+ };
1366
+ }
1367
+ if (parsed.referenceIndex !== null && parsed.referenceIndex > 2) {
1368
+ return {
1369
+ ok: false,
1370
+ result: invalid(
1371
+ options.command,
1372
+ `${options.flag} reference index must be between 0 and 2`,
1373
+ ),
1374
+ };
1375
+ }
1376
+ return { ok: true, ...parsed };
1377
+ }
1378
+
1379
+ function parseElementReferenceSuffix(value) {
1380
+ const atIndex = value.lastIndexOf("@");
1381
+ if (atIndex === -1) {
1382
+ return { input: value, index: 0, referenceIndex: null };
1383
+ }
1384
+ const suffix = value.slice(atIndex + 1);
1385
+ if (!/^\d+(?::\d+)?$/.test(suffix)) {
1386
+ return { input: value, index: 0, referenceIndex: null };
1387
+ }
1388
+ const [index, referenceIndex] = suffix.split(":").map((part) => Number(part));
1389
+ return {
1390
+ input: value.slice(0, atIndex),
1391
+ index,
1392
+ referenceIndex: referenceIndex ?? null,
1393
+ };
1394
+ }
1395
+
1025
1396
  async function uploadPayload(input) {
1026
1397
  const isRemote = /^https?:\/\//i.test(input);
1027
1398
  let bytes;
@@ -1150,6 +1521,125 @@ function parseEnvelope(text, command, statusCode) {
1150
1521
  };
1151
1522
  }
1152
1523
 
1524
+ function withStripeCheckoutCopyFallback(result) {
1525
+ const data = result.envelope.data;
1526
+ if (!isRecord(data)) {
1527
+ return result;
1528
+ }
1529
+
1530
+ const updated = stripeCheckoutCopyFallbackData(data);
1531
+ if (updated === data) {
1532
+ return result;
1533
+ }
1534
+
1535
+ return {
1536
+ ...result,
1537
+ envelope: {
1538
+ ...result.envelope,
1539
+ data: updated,
1540
+ },
1541
+ };
1542
+ }
1543
+
1544
+ function stripeCheckoutCopyFallbackData(data) {
1545
+ let changed = false;
1546
+ const updated = { ...data };
1547
+
1548
+ if (addCheckoutCompactUrl(updated)) {
1549
+ changed = true;
1550
+ }
1551
+
1552
+ if (isRecord(updated.next)) {
1553
+ const next = { ...updated.next };
1554
+ let nextChanged = addCheckoutCompactUrl(next);
1555
+ if (
1556
+ typeof updated.checkout_compact_url === "string" &&
1557
+ typeof next.checkout_compact_url !== "string"
1558
+ ) {
1559
+ next.checkout_compact_url = updated.checkout_compact_url;
1560
+ nextChanged = true;
1561
+ }
1562
+ if (nextChanged) {
1563
+ updated.next = next;
1564
+ changed = true;
1565
+ }
1566
+ }
1567
+
1568
+ if (isRecord(updated.payment_attempt)) {
1569
+ const paymentAttempt = { ...updated.payment_attempt };
1570
+ if (addCheckoutCompactUrl(paymentAttempt)) {
1571
+ updated.payment_attempt = paymentAttempt;
1572
+ changed = true;
1573
+ }
1574
+ if (isRecord(updated.next)) {
1575
+ const next = { ...updated.next };
1576
+ if (
1577
+ next.human_action === "open_checkout_url" &&
1578
+ typeof paymentAttempt.checkout_compact_url === "string" &&
1579
+ typeof next.checkout_compact_url !== "string"
1580
+ ) {
1581
+ next.checkout_compact_url = paymentAttempt.checkout_compact_url;
1582
+ updated.next = next;
1583
+ changed = true;
1584
+ }
1585
+ }
1586
+ }
1587
+
1588
+ return changed ? updated : data;
1589
+ }
1590
+
1591
+ function addCheckoutCompactUrl(record) {
1592
+ const raw =
1593
+ typeof record.checkout_url === "string"
1594
+ ? record.checkout_url
1595
+ : typeof record.fallback_checkout_url === "string"
1596
+ ? record.fallback_checkout_url
1597
+ : null;
1598
+ if (raw === null || raw.length === 0) {
1599
+ return false;
1600
+ }
1601
+ const compact = stripeCheckoutCompactUrl(raw);
1602
+ let changed = false;
1603
+ if (record.checkout_compact_url !== compact) {
1604
+ record.checkout_compact_url = compact;
1605
+ changed = true;
1606
+ }
1607
+ if (
1608
+ typeof record.checkout_url === "string" &&
1609
+ record.checkout_url !== compact
1610
+ ) {
1611
+ record.checkout_url = compact;
1612
+ changed = true;
1613
+ }
1614
+ if (
1615
+ typeof record.fallback_checkout_url === "string" &&
1616
+ record.fallback_checkout_url !== compact
1617
+ ) {
1618
+ record.fallback_checkout_url = compact;
1619
+ changed = true;
1620
+ }
1621
+ return changed;
1622
+ }
1623
+
1624
+ function stripeCheckoutCompactUrl(checkoutUrl) {
1625
+ const trimmed = checkoutUrl.trim();
1626
+ if (trimmed.length === 0) {
1627
+ return checkoutUrl;
1628
+ }
1629
+ try {
1630
+ const parsed = new URL(trimmed);
1631
+ parsed.hash = "";
1632
+ return parsed.toString();
1633
+ } catch {
1634
+ const hashIndex = trimmed.indexOf("#");
1635
+ return hashIndex === -1 ? trimmed : trimmed.slice(0, hashIndex);
1636
+ }
1637
+ }
1638
+
1639
+ function isRecord(value) {
1640
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1641
+ }
1642
+
1153
1643
  async function promptValue(args) {
1154
1644
  const prompt = flagString(args, "prompt");
1155
1645
  const promptFile = flagString(args, "prompt-file");
@@ -1174,6 +1664,69 @@ async function promptValue(args) {
1174
1664
  };
1175
1665
  }
1176
1666
 
1667
+ async function editPromptValue(args, modelId) {
1668
+ if (args.flags.has("prompt") && flagString(args, "prompt") === null) {
1669
+ return {
1670
+ ok: false,
1671
+ result: invalid("image-skill edit", "--prompt requires a value"),
1672
+ };
1673
+ }
1674
+ if (
1675
+ args.flags.has("prompt-file") &&
1676
+ flagString(args, "prompt-file") === null
1677
+ ) {
1678
+ return {
1679
+ ok: false,
1680
+ result: invalid("image-skill edit", "--prompt-file requires a value"),
1681
+ };
1682
+ }
1683
+ const prompt = flagString(args, "prompt");
1684
+ const promptFile = flagString(args, "prompt-file");
1685
+ if (prompt !== null && promptFile !== null) {
1686
+ return {
1687
+ ok: false,
1688
+ result: invalid(
1689
+ "image-skill edit",
1690
+ "provide either --prompt or --prompt-file, not both",
1691
+ ),
1692
+ };
1693
+ }
1694
+ const isPromptlessModel =
1695
+ modelId !== null && PROMPTLESS_EDIT_MODEL_IDS.has(modelId);
1696
+ let value = null;
1697
+ if (prompt !== null) {
1698
+ value = prompt;
1699
+ } else if (promptFile !== null) {
1700
+ value = await readFile(promptFile, "utf8");
1701
+ }
1702
+ const trimmed = value?.trim() ?? "";
1703
+ if (isPromptlessModel) {
1704
+ if (trimmed.length > 0) {
1705
+ return {
1706
+ ok: false,
1707
+ result: invalid(
1708
+ "image-skill edit",
1709
+ `model ${modelId} does not accept --prompt`,
1710
+ ),
1711
+ };
1712
+ }
1713
+ return { ok: true, value: "" };
1714
+ }
1715
+ if (value === null) {
1716
+ return {
1717
+ ok: false,
1718
+ result: invalid("image-skill edit", "edit requires --prompt"),
1719
+ };
1720
+ }
1721
+ if (trimmed.length === 0) {
1722
+ return {
1723
+ ok: false,
1724
+ result: invalid("image-skill edit", "edit prompt cannot be empty"),
1725
+ };
1726
+ }
1727
+ return { ok: true, value: trimmed };
1728
+ }
1729
+
1177
1730
  async function resolveToken(args, options = {}) {
1178
1731
  if (flagBool(args, "token-stdin")) {
1179
1732
  if (process.stdin.isTTY) {
@@ -1256,6 +1809,43 @@ async function saveConfig(value) {
1256
1809
  await chmod(path, 0o600);
1257
1810
  }
1258
1811
 
1812
+ async function assertConfigWritable(command) {
1813
+ const path = configPath();
1814
+ const probePath = `${path}.write-test-${process.pid}-${randomBytes(4).toString("hex")}`;
1815
+ try {
1816
+ await mkdir(dirname(path), { recursive: true });
1817
+ await writeFile(probePath, "", { mode: 0o600 });
1818
+ await chmod(probePath, 0o600);
1819
+ await rm(probePath, { force: true });
1820
+ return { ok: true };
1821
+ } catch (error) {
1822
+ await rm(probePath, { force: true }).catch(() => {});
1823
+ return {
1824
+ ok: false,
1825
+ result: configWriteFailure(command, error),
1826
+ };
1827
+ }
1828
+ }
1829
+
1830
+ function configWriteFailure(command, error) {
1831
+ const message =
1832
+ error instanceof Error
1833
+ ? error.message
1834
+ : "public CLI could not write its local auth config";
1835
+ return failure(
1836
+ command,
1837
+ 9,
1838
+ "PUBLIC_CLI_CONFIG_WRITE_FAILED",
1839
+ `public CLI could not write auth config at ${configPath()}: ${message}`,
1840
+ true,
1841
+ {
1842
+ 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',
1844
+ docs_url: "https://image-skill.com/cli.md#local-config-and-install",
1845
+ },
1846
+ );
1847
+ }
1848
+
1259
1849
  function parseArgs(argv) {
1260
1850
  const flags = new Map();
1261
1851
  const positionals = [];
@@ -1293,6 +1883,12 @@ function flagString(args, name) {
1293
1883
  return typeof value === "string" ? value : null;
1294
1884
  }
1295
1885
 
1886
+ function flagStrings(args, name) {
1887
+ return (args.flags.get(name) ?? []).filter(
1888
+ (value) => typeof value === "string",
1889
+ );
1890
+ }
1891
+
1296
1892
  function flagBool(args, name) {
1297
1893
  return args.flags.has(name) && args.flags.get(name)?.at(-1) !== "false";
1298
1894
  }
@@ -1349,6 +1945,21 @@ function flagNumber(args, name) {
1349
1945
  return Number.isFinite(number) ? number : null;
1350
1946
  }
1351
1947
 
1948
+ function positiveIntegerFlag(args, name, input) {
1949
+ const value = flagString(args, name);
1950
+ if (value === null) {
1951
+ return { ok: true, value: null };
1952
+ }
1953
+ const number = Number(value);
1954
+ if (!Number.isInteger(number) || number <= 0) {
1955
+ return {
1956
+ ok: false,
1957
+ result: invalid(input.command, `${name} must be a positive integer`),
1958
+ };
1959
+ }
1960
+ return { ok: true, value: number };
1961
+ }
1962
+
1352
1963
  function jsonObjectFlag(args, name) {
1353
1964
  const raw = flagString(args, name);
1354
1965
  if (raw === null) {