image-skill 0.1.7 → 0.1.9

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.7";
10
+ const VERSION = "0.1.9";
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",
@@ -499,7 +504,7 @@ async function credits(argv) {
499
504
  idempotency_key: idempotency.value,
500
505
  },
501
506
  });
502
- return result;
507
+ return withStripeCheckoutCopyFallback(result);
503
508
  }
504
509
  if (subcommand === "fake-purchase") {
505
510
  const args = parseArgs(rest);
@@ -546,13 +551,14 @@ async function credits(argv) {
546
551
  addQueryFlag(query, args, "payment-attempt-id", "payment_attempt_id");
547
552
  addQueryFlag(query, args, "checkout-session-id", "checkout_session_id");
548
553
  addQueryFlag(query, args, "receipt-id", "receipt_id");
549
- return apiRequest({
554
+ const result = await apiRequest({
550
555
  command: "image-skill credits status",
551
556
  method: "GET",
552
557
  apiBaseUrl: apiBase(args),
553
558
  path: `/v1/credit-purchases/status?${query.toString()}`,
554
559
  token: token.token,
555
560
  });
561
+ return withStripeCheckoutCopyFallback(result);
556
562
  }
557
563
  return invalid(
558
564
  "image-skill credits",
@@ -647,10 +653,28 @@ async function create(argv) {
647
653
  if (!token.ok) {
648
654
  return token.result;
649
655
  }
656
+ const referencePlan = parseReferencePlan(args, "image-skill create");
657
+ if (!referencePlan.ok) {
658
+ return referencePlan.result;
659
+ }
650
660
  const modelParameters = jsonObjectFlag(args, "model-parameters-json");
651
661
  if (!modelParameters.ok) {
652
662
  return modelParameters.result;
653
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
+ }
654
678
  return apiRequest({
655
679
  command: "image-skill create",
656
680
  method: "POST",
@@ -669,6 +693,12 @@ async function create(argv) {
669
693
  ? {}
670
694
  : { intent: flagString(args, "intent") }),
671
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 }),
672
702
  ...(flagNumber(args, "max-estimated-usd-per-image") === null
673
703
  ? {}
674
704
  : {
@@ -713,13 +743,14 @@ async function upload(argv) {
713
743
  async function edit(argv) {
714
744
  const args = parseArgs(argv);
715
745
  const input = flagString(args, "input") ?? args.positionals[0];
746
+ const modelId = flagString(args, "model");
716
747
  if (input === undefined) {
717
748
  return invalid(
718
749
  "image-skill edit",
719
750
  "edit requires --input ASSET_ID_OR_PATH_OR_URL",
720
751
  );
721
752
  }
722
- const prompt = await promptValue(args);
753
+ const prompt = await editPromptValue(args, modelId);
723
754
  if (!prompt.ok) {
724
755
  return prompt.result;
725
756
  }
@@ -727,10 +758,28 @@ async function edit(argv) {
727
758
  if (!token.ok) {
728
759
  return token.result;
729
760
  }
761
+ const referencePlan = parseReferencePlan(args, "image-skill edit");
762
+ if (!referencePlan.ok) {
763
+ return referencePlan.result;
764
+ }
730
765
  const assetId = await resolveInputAssetId(input, args, token.token);
731
766
  if (!assetId.ok) {
732
767
  return assetId.result;
733
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
+ }
734
783
  const modelParameters = jsonObjectFlag(args, "model-parameters-json");
735
784
  if (!modelParameters.ok) {
736
785
  return modelParameters.result;
@@ -743,13 +792,15 @@ async function edit(argv) {
743
792
  token: token.token,
744
793
  body: {
745
794
  input_asset_id: assetId.assetId,
746
- 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 }),
747
800
  ...(flagString(args, "provider") === null
748
801
  ? {}
749
802
  : { provider: flagString(args, "provider") }),
750
- ...(flagString(args, "model") === null
751
- ? {}
752
- : { model: flagString(args, "model") }),
803
+ ...(modelId === null ? {} : { model: modelId }),
753
804
  ...(flagString(args, "intent") === null
754
805
  ? {}
755
806
  : { intent: flagString(args, "intent") }),
@@ -1040,6 +1091,308 @@ async function resolveInputAssetId(input, args, token) {
1040
1091
  return { ok: true, assetId };
1041
1092
  }
1042
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
+
1043
1396
  async function uploadPayload(input) {
1044
1397
  const isRemote = /^https?:\/\//i.test(input);
1045
1398
  let bytes;
@@ -1168,6 +1521,104 @@ function parseEnvelope(text, command, statusCode) {
1168
1521
  };
1169
1522
  }
1170
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
+ return changed;
1608
+ }
1609
+
1610
+ function stripeCheckoutCompactUrl(checkoutUrl) {
1611
+ const trimmed = checkoutUrl.trim();
1612
+ if (trimmed.length === 0) {
1613
+ return checkoutUrl;
1614
+ }
1615
+ return trimmed;
1616
+ }
1617
+
1618
+ function isRecord(value) {
1619
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1620
+ }
1621
+
1171
1622
  async function promptValue(args) {
1172
1623
  const prompt = flagString(args, "prompt");
1173
1624
  const promptFile = flagString(args, "prompt-file");
@@ -1192,6 +1643,69 @@ async function promptValue(args) {
1192
1643
  };
1193
1644
  }
1194
1645
 
1646
+ async function editPromptValue(args, modelId) {
1647
+ if (args.flags.has("prompt") && flagString(args, "prompt") === null) {
1648
+ return {
1649
+ ok: false,
1650
+ result: invalid("image-skill edit", "--prompt requires a value"),
1651
+ };
1652
+ }
1653
+ if (
1654
+ args.flags.has("prompt-file") &&
1655
+ flagString(args, "prompt-file") === null
1656
+ ) {
1657
+ return {
1658
+ ok: false,
1659
+ result: invalid("image-skill edit", "--prompt-file requires a value"),
1660
+ };
1661
+ }
1662
+ const prompt = flagString(args, "prompt");
1663
+ const promptFile = flagString(args, "prompt-file");
1664
+ if (prompt !== null && promptFile !== null) {
1665
+ return {
1666
+ ok: false,
1667
+ result: invalid(
1668
+ "image-skill edit",
1669
+ "provide either --prompt or --prompt-file, not both",
1670
+ ),
1671
+ };
1672
+ }
1673
+ const isPromptlessModel =
1674
+ modelId !== null && PROMPTLESS_EDIT_MODEL_IDS.has(modelId);
1675
+ let value = null;
1676
+ if (prompt !== null) {
1677
+ value = prompt;
1678
+ } else if (promptFile !== null) {
1679
+ value = await readFile(promptFile, "utf8");
1680
+ }
1681
+ const trimmed = value?.trim() ?? "";
1682
+ if (isPromptlessModel) {
1683
+ if (trimmed.length > 0) {
1684
+ return {
1685
+ ok: false,
1686
+ result: invalid(
1687
+ "image-skill edit",
1688
+ `model ${modelId} does not accept --prompt`,
1689
+ ),
1690
+ };
1691
+ }
1692
+ return { ok: true, value: "" };
1693
+ }
1694
+ if (value === null) {
1695
+ return {
1696
+ ok: false,
1697
+ result: invalid("image-skill edit", "edit requires --prompt"),
1698
+ };
1699
+ }
1700
+ if (trimmed.length === 0) {
1701
+ return {
1702
+ ok: false,
1703
+ result: invalid("image-skill edit", "edit prompt cannot be empty"),
1704
+ };
1705
+ }
1706
+ return { ok: true, value: trimmed };
1707
+ }
1708
+
1195
1709
  async function resolveToken(args, options = {}) {
1196
1710
  if (flagBool(args, "token-stdin")) {
1197
1711
  if (process.stdin.isTTY) {
@@ -1348,6 +1862,12 @@ function flagString(args, name) {
1348
1862
  return typeof value === "string" ? value : null;
1349
1863
  }
1350
1864
 
1865
+ function flagStrings(args, name) {
1866
+ return (args.flags.get(name) ?? []).filter(
1867
+ (value) => typeof value === "string",
1868
+ );
1869
+ }
1870
+
1351
1871
  function flagBool(args, name) {
1352
1872
  return args.flags.has(name) && args.flags.get(name)?.at(-1) !== "false";
1353
1873
  }
@@ -1404,6 +1924,21 @@ function flagNumber(args, name) {
1404
1924
  return Number.isFinite(number) ? number : null;
1405
1925
  }
1406
1926
 
1927
+ function positiveIntegerFlag(args, name, input) {
1928
+ const value = flagString(args, name);
1929
+ if (value === null) {
1930
+ return { ok: true, value: null };
1931
+ }
1932
+ const number = Number(value);
1933
+ if (!Number.isInteger(number) || number <= 0) {
1934
+ return {
1935
+ ok: false,
1936
+ result: invalid(input.command, `${name} must be a positive integer`),
1937
+ };
1938
+ }
1939
+ return { ok: true, value: number };
1940
+ }
1941
+
1407
1942
  function jsonObjectFlag(args, name) {
1408
1943
  const raw = flagString(args, name);
1409
1944
  if (raw === null) {