image-skill 0.1.16 → 0.1.17

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ This changelog tracks the public `image-skill` CLI package and public skill
4
4
  mirror. The npm package metadata remains the authority for tarball integrity and
5
5
  provenance; this file is the human- and agent-readable release map.
6
6
 
7
+ ## 0.1.17 - 2026-06-01
8
+
9
+ - Money integrity: `create` and `edit` now send `--idempotency-key` to the
10
+ server so a retry of a transiently-failed generation REPLAYS the original
11
+ job instead of charging again. `create --guide` bakes a generated key into
12
+ its suggested command, and a proxy-killed 502 (`HOSTED_API_NON_JSON_RESPONSE`)
13
+ now returns a recovery block with the request's idempotency key so the
14
+ advertised retry is charge-safe. (0.1.16 parsed the flag but did not send it
15
+ on create, so same-key retries still double-charged against the live server's
16
+ dedup; this build closes that end-to-end.)
17
+
7
18
  ## 0.1.16 - 2026-06-01
8
19
 
9
20
  - `credits buy` now accepts `--provider stripe_x402` to execute the agent-native
@@ -7,7 +7,7 @@ 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.16";
10
+ const VERSION = "0.1.17";
11
11
  const PACKAGE_NAME = "image-skill";
12
12
  const DEFAULT_API_BASE_URL = "https://api.image-skill.com";
13
13
  const DEFAULT_DOCS_BASE_URL = "https://image-skill.com";
@@ -1197,6 +1197,10 @@ function createGuideNextCommand(stage, input) {
1197
1197
  intent: input.requestedIntent,
1198
1198
  budgetGuard: input.budgetGuard,
1199
1199
  dryRun: false,
1200
+ // Retry-safe by default (#1228): bake a stable idempotency key into the
1201
+ // advertised create command so an agent that copies it and retries after a
1202
+ // transient 502 does not double-charge.
1203
+ idempotencyKey: `create-guide-${Date.now()}-${randomBytes(4).toString("hex")}`,
1200
1204
  apiBaseUrl: input.apiBaseUrl,
1201
1205
  commandPrefix: input.commandPrefix,
1202
1206
  });
@@ -1228,6 +1232,9 @@ function renderCreateCommand(input) {
1228
1232
  shellQuote(input.intent),
1229
1233
  "--max-estimated-usd-per-image",
1230
1234
  shellQuote(formatUsd(input.budgetGuard)),
1235
+ ...(input.idempotencyKey === undefined || input.idempotencyKey === null
1236
+ ? []
1237
+ : ["--idempotency-key", shellQuote(input.idempotencyKey)]),
1231
1238
  ...(input.apiBaseUrl === null
1232
1239
  ? []
1233
1240
  : ["--api-base-url", shellQuote(input.apiBaseUrl)]),
@@ -1353,6 +1360,11 @@ async function create(argv) {
1353
1360
  ...(modelParameters.value === null
1354
1361
  ? {}
1355
1362
  : { model_parameters: modelParameters.value }),
1363
+ // Retry-safe dedupe (#1228): when provided, a retry with the same key does
1364
+ // not double-charge after a transient 502 that already debited a credit.
1365
+ ...(flagString(args, "idempotency-key") === null
1366
+ ? {}
1367
+ : { idempotency_key: flagString(args, "idempotency-key") }),
1356
1368
  dry_run: flagBool(args, "dry-run"),
1357
1369
  accept_unknown_cost: flagBool(args, "accept-unknown-cost"),
1358
1370
  },
@@ -1459,6 +1471,11 @@ async function edit(argv) {
1459
1471
  ...(modelParameters.value === null
1460
1472
  ? {}
1461
1473
  : { model_parameters: modelParameters.value }),
1474
+ // Retry-safe dedupe (#1228): see create — same key dedupes a retry that
1475
+ // follows a transient 502 which already debited a credit.
1476
+ ...(flagString(args, "idempotency-key") === null
1477
+ ? {}
1478
+ : { idempotency_key: flagString(args, "idempotency-key") }),
1462
1479
  accept_unknown_cost: flagBool(args, "accept-unknown-cost"),
1463
1480
  },
1464
1481
  });
@@ -2556,7 +2573,9 @@ async function apiRequest(input) {
2556
2573
  body: input.body === undefined ? undefined : JSON.stringify(input.body),
2557
2574
  });
2558
2575
  const text = await response.text();
2559
- const envelope = parseEnvelope(text, input.command, response.status);
2576
+ const envelope = parseEnvelope(text, input.command, response.status, {
2577
+ requestBody: input.body,
2578
+ });
2560
2579
  const exitCodeHeader = response.headers.get("x-image-skill-exit-code");
2561
2580
  return {
2562
2581
  exitCode:
@@ -2583,7 +2602,7 @@ async function apiRequest(input) {
2583
2602
  }
2584
2603
  }
2585
2604
 
2586
- function parseEnvelope(text, command, statusCode) {
2605
+ function parseEnvelope(text, command, statusCode, options = {}) {
2587
2606
  try {
2588
2607
  const parsed = JSON.parse(text);
2589
2608
  if (parsed && typeof parsed === "object" && "ok" in parsed) {
@@ -2592,21 +2611,62 @@ function parseEnvelope(text, command, statusCode) {
2592
2611
  } catch {
2593
2612
  // Fall through to normalized public error.
2594
2613
  }
2614
+ const retryable = statusCode >= 500;
2615
+ // Money integrity (#1228): a proxy-killed 502 returns a non-JSON body, so the
2616
+ // server's own recovery guidance never reaches the agent. For a retryable
2617
+ // create/edit (which may already have debited a credit) synthesize an
2618
+ // idempotency-keyed retry command so the advertised retry dedupes to one
2619
+ // charge instead of double-charging. Echo the request's key when present;
2620
+ // otherwise mint a stable key so the NEXT retry is safe.
2621
+ const recovery =
2622
+ retryable && isCreateOrEditCommand(command)
2623
+ ? nonJsonRetryRecovery(command, options.requestBody)
2624
+ : undefined;
2595
2625
  return {
2596
2626
  ok: false,
2597
2627
  command,
2598
2628
  trace_id: traceId(),
2599
2629
  actor: null,
2600
2630
  data: null,
2601
- warnings: [],
2631
+ warnings: retryable
2632
+ ? [
2633
+ "the hosted API may have already reserved a credit; retry with the returned idempotency_key so the retry is not double-charged",
2634
+ ]
2635
+ : [],
2602
2636
  error: {
2603
2637
  code: "HOSTED_API_NON_JSON_RESPONSE",
2604
2638
  message: `hosted API returned HTTP ${statusCode} without a JSON envelope`,
2605
- retryable: statusCode >= 500,
2639
+ retryable,
2640
+ ...(recovery === undefined ? {} : { recovery }),
2606
2641
  },
2607
2642
  };
2608
2643
  }
2609
2644
 
2645
+ function isCreateOrEditCommand(command) {
2646
+ return command === "image-skill create" || command === "image-skill edit";
2647
+ }
2648
+
2649
+ function nonJsonRetryRecovery(command, requestBody) {
2650
+ const operation = command === "image-skill edit" ? "edit" : "create";
2651
+ const existingKey =
2652
+ requestBody &&
2653
+ typeof requestBody === "object" &&
2654
+ typeof requestBody.idempotency_key === "string"
2655
+ ? requestBody.idempotency_key
2656
+ : null;
2657
+ const idempotencyKey =
2658
+ existingKey ??
2659
+ `${operation}-retry-${Date.now()}-${randomBytes(4).toString("hex")}`;
2660
+ const anchor =
2661
+ operation === "edit" ? "image-skill-edit" : "image-skill-create";
2662
+ return {
2663
+ suggested_command: `${command} --idempotency-key ${idempotencyKey} --json`,
2664
+ idempotency_key: idempotencyKey,
2665
+ docs_url: `https://image-skill.com/cli.md#${anchor}`,
2666
+ retry_after_seconds: 5,
2667
+ };
2668
+ }
2669
+
2610
2670
  function withStripeCheckoutCopyFallback(result) {
2611
2671
  const data = result.envelope.data;
2612
2672
  if (!isRecord(data)) {
package/cli.md CHANGED
@@ -876,6 +876,21 @@ If provider generation succeeds but artifact storage fails, the command returns
876
876
  should not retry the whole create blindly, because that may duplicate paid
877
877
  provider spend.
878
878
 
879
+ For retry-safe create automation, pass an explicit non-secret
880
+ `--idempotency-key`. A retry that reuses the same key does not create a second
881
+ credit reservation, so a transient `502`/`PROVIDER_FAILURE` that already
882
+ reserved a credit cannot double-charge on retry. `create --guide` bakes a
883
+ generated `--idempotency-key` into its advertised create `next_command`, and a
884
+ retryable create error returns an `error.recovery.idempotency_key` plus an
885
+ `error.recovery.suggested_command` that re-runs the same create with that key.
886
+
887
+ ```bash
888
+ image-skill create \
889
+ --prompt "A compact field camera on a stainless workbench" \
890
+ --idempotency-key create-run-001 \
891
+ --json
892
+ ```
893
+
879
894
  Hosted free-preview API equivalent:
880
895
 
881
896
  ```bash
@@ -1074,6 +1089,12 @@ public UX. The public selection surface should be Image Skill capabilities and
1074
1089
  model-parameter schemas; provider/model details belong in explicit
1075
1090
  provenance/debug output.
1076
1091
 
1092
+ Edit accepts the same retry-safe `--idempotency-key` as create. A retry that
1093
+ reuses the same key does not create a second credit reservation, so a transient
1094
+ `502`/`PROVIDER_FAILURE` after a reservation cannot double-charge; a retryable
1095
+ edit error returns an `error.recovery.idempotency_key` and an
1096
+ `error.recovery.suggested_command` that re-runs the same edit with that key.
1097
+
1077
1098
  ### `image-skill assets show`
1078
1099
 
1079
1100
  Inspects an Image Skill-owned asset URL or hosted asset id.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-skill",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Zero-setup durable creative-media CLI for agents (image + video): guide-first creation, model and cost inspection, owned URLs, JSON recovery, payments, reusable assets, and feedback.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -876,6 +876,21 @@ If provider generation succeeds but artifact storage fails, the command returns
876
876
  should not retry the whole create blindly, because that may duplicate paid
877
877
  provider spend.
878
878
 
879
+ For retry-safe create automation, pass an explicit non-secret
880
+ `--idempotency-key`. A retry that reuses the same key does not create a second
881
+ credit reservation, so a transient `502`/`PROVIDER_FAILURE` that already
882
+ reserved a credit cannot double-charge on retry. `create --guide` bakes a
883
+ generated `--idempotency-key` into its advertised create `next_command`, and a
884
+ retryable create error returns an `error.recovery.idempotency_key` plus an
885
+ `error.recovery.suggested_command` that re-runs the same create with that key.
886
+
887
+ ```bash
888
+ image-skill create \
889
+ --prompt "A compact field camera on a stainless workbench" \
890
+ --idempotency-key create-run-001 \
891
+ --json
892
+ ```
893
+
879
894
  Hosted free-preview API equivalent:
880
895
 
881
896
  ```bash
@@ -1074,6 +1089,12 @@ public UX. The public selection surface should be Image Skill capabilities and
1074
1089
  model-parameter schemas; provider/model details belong in explicit
1075
1090
  provenance/debug output.
1076
1091
 
1092
+ Edit accepts the same retry-safe `--idempotency-key` as create. A retry that
1093
+ reuses the same key does not create a second credit reservation, so a transient
1094
+ `502`/`PROVIDER_FAILURE` after a reservation cannot double-charge; a retryable
1095
+ edit error returns an `error.recovery.idempotency_key` and an
1096
+ `error.recovery.suggested_command` that re-runs the same edit with that key.
1097
+
1077
1098
  ### `image-skill assets show`
1078
1099
 
1079
1100
  Inspects an Image Skill-owned asset URL or hosted asset id.