image-skill 0.1.36 → 0.1.37

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,23 @@ 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.37 - 2026-06-09
8
+
9
+ - Fix (recovery): a live `create`/`edit` now leaves a recovery handle _before_
10
+ the blocking request. Every live (non-dry-run) call carries an idempotency
11
+ key even when you did not pass `--idempotency-key`, emits an `in_flight`
12
+ notice with that key to stderr, and writes a durable breadcrumb at
13
+ `<config-dir>/in-flight/<key>.json`. If the command is interrupted (for
14
+ example you kill a create that hangs on a long provider wait after the credit
15
+ was already reserved), re-run it with the surfaced key: the hosted API
16
+ replays the original job (returning the asset you already paid for) or
17
+ releases the reserved credit — never a double charge. The stdout JSON
18
+ envelope is unchanged. Fixes the "create debited credits but no live job or
19
+ asset surfaced" report (#1789).
20
+ - Fix (recovery): the proxy-killed non-JSON 5xx retry recovery now echoes the
21
+ same idempotency key the charged request used, so the advertised retry
22
+ genuinely dedupes instead of minting a non-matching key (#1228 follow-up).
23
+
7
24
  ## 0.1.36 - 2026-06-04
8
25
 
9
26
  - Fix (guide): `create --guide --json` now marks templated follow-up commands
@@ -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.36";
10
+ const VERSION = "0.1.37";
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";
@@ -3126,7 +3126,21 @@ async function create(argv) {
3126
3126
  if (!references.ok) {
3127
3127
  return references.result;
3128
3128
  }
3129
- return apiRequest({
3129
+ // A live (non-dry-run, authenticated) create is the only branch that spends
3130
+ // credits. Give it a recovery handle BEFORE the blocking request (#1789).
3131
+ const isLiveSpend = !flagBool(args, "dry-run") && token.token !== null;
3132
+ const idempotencyKey = isLiveSpend
3133
+ ? liveSpendIdempotencyKey(args, "create")
3134
+ : flagString(args, "idempotency-key");
3135
+ const inFlight = isLiveSpend
3136
+ ? await recordInFlightSpend({
3137
+ command: "image-skill create",
3138
+ operation: "create",
3139
+ idempotencyKey,
3140
+ argv,
3141
+ })
3142
+ : null;
3143
+ const result = await apiRequest({
3130
3144
  command: "image-skill create",
3131
3145
  method: "POST",
3132
3146
  apiBaseUrl: apiBase(args),
@@ -3161,15 +3175,15 @@ async function create(argv) {
3161
3175
  ...(modelParameters.value === null
3162
3176
  ? {}
3163
3177
  : { model_parameters: modelParameters.value }),
3164
- // Retry-safe dedupe (#1228): when provided, a retry with the same key does
3165
- // not double-charge after a transient 502 that already debited a credit.
3166
- ...(flagString(args, "idempotency-key") === null
3167
- ? {}
3168
- : { idempotency_key: flagString(args, "idempotency-key") }),
3178
+ // Retry-safe dedupe (#1228/#1789): a live create always carries a key so a
3179
+ // retry (or an interrupted-then-recovered run) dedupes to one charge.
3180
+ ...(idempotencyKey === null ? {} : { idempotency_key: idempotencyKey }),
3169
3181
  dry_run: flagBool(args, "dry-run"),
3170
3182
  accept_unknown_cost: flagBool(args, "accept-unknown-cost"),
3171
3183
  },
3172
3184
  });
3185
+ await clearInFlightSpend(inFlight);
3186
+ return result;
3173
3187
  }
3174
3188
 
3175
3189
  async function upload(argv) {
@@ -3240,7 +3254,21 @@ async function edit(argv) {
3240
3254
  if (!modelParameters.ok) {
3241
3255
  return modelParameters.result;
3242
3256
  }
3243
- return apiRequest({
3257
+ // A live (non-dry-run) edit spends credits; give it a recovery handle BEFORE
3258
+ // the blocking request (#1789). Edit always carries a token.
3259
+ const isLiveSpend = !flagBool(args, "dry-run");
3260
+ const idempotencyKey = isLiveSpend
3261
+ ? liveSpendIdempotencyKey(args, "edit")
3262
+ : flagString(args, "idempotency-key");
3263
+ const inFlight = isLiveSpend
3264
+ ? await recordInFlightSpend({
3265
+ command: "image-skill edit",
3266
+ operation: "edit",
3267
+ idempotencyKey,
3268
+ argv,
3269
+ })
3270
+ : null;
3271
+ const result = await apiRequest({
3244
3272
  command: "image-skill edit",
3245
3273
  method: "POST",
3246
3274
  apiBaseUrl: apiBase(args),
@@ -3273,14 +3301,14 @@ async function edit(argv) {
3273
3301
  ? {}
3274
3302
  : { model_parameters: modelParameters.value }),
3275
3303
  ...(flagBool(args, "dry-run") ? { dry_run: true } : {}),
3276
- // Retry-safe dedupe (#1228): see create same key dedupes a retry that
3277
- // follows a transient 502 which already debited a credit.
3278
- ...(flagString(args, "idempotency-key") === null
3279
- ? {}
3280
- : { idempotency_key: flagString(args, "idempotency-key") }),
3304
+ // Retry-safe dedupe (#1228/#1789): a live edit always carries a key so a
3305
+ // retry (or an interrupted-then-recovered run) dedupes to one charge.
3306
+ ...(idempotencyKey === null ? {} : { idempotency_key: idempotencyKey }),
3281
3307
  accept_unknown_cost: flagBool(args, "accept-unknown-cost"),
3282
3308
  },
3283
3309
  });
3310
+ await clearInFlightSpend(inFlight);
3311
+ return result;
3284
3312
  }
3285
3313
 
3286
3314
  async function assets(argv) {
@@ -4357,6 +4385,102 @@ async function fetchPublicText(url, options = {}) {
4357
4385
  }
4358
4386
  }
4359
4387
 
4388
+ // --- In-flight live-spend recovery breadcrumb (#1789) -----------------------
4389
+ // A live create/edit is one synchronous request that can block for the full
4390
+ // provider duration (often minutes). If the agent kills the process during that
4391
+ // wait, the hosted API may have already reserved a credit, yet the agent is left
4392
+ // with no job id, no trace, and no recovery handle — an orphaned debit it cannot
4393
+ // see or reconcile (the reservation only auto-releases on a 15-minute TTL).
4394
+ //
4395
+ // We close that loop on the client, BEFORE the blocking request: every live
4396
+ // spend carries an idempotency key, and we hand the agent that key (stderr) plus
4397
+ // a durable local breadcrumb. On interruption the agent re-runs the same command
4398
+ // with the same key; the hosted API replays the original job (returning the
4399
+ // asset already paid for) or releases the reserved credit — never a double
4400
+ // charge. See https://image-skill.com/cli.md#image-skill-create.
4401
+
4402
+ function liveSpendIdempotencyKey(args, operation) {
4403
+ const explicit = flagString(args, "idempotency-key");
4404
+ if (explicit !== null) {
4405
+ return explicit;
4406
+ }
4407
+ return `${operation}-${Date.now()}-${randomBytes(6).toString("hex")}`;
4408
+ }
4409
+
4410
+ function inFlightSpendDir() {
4411
+ return join(dirname(configPath()), "in-flight");
4412
+ }
4413
+
4414
+ function recoverCommandFor(operation, idempotencyKey) {
4415
+ return `image-skill ${operation} --idempotency-key ${idempotencyKey} <same arguments> --json`;
4416
+ }
4417
+
4418
+ async function recordInFlightSpend(input) {
4419
+ const { command, operation, idempotencyKey, argv } = input;
4420
+ const recoverCommand = recoverCommandFor(operation, idempotencyKey);
4421
+ const note =
4422
+ "live spend may already be reserved. If this command is interrupted before it returns a result, re-run it with the idempotency_key above; the hosted API replays the original job or releases the reserved credit and never double-charges.";
4423
+ // Persist the durable breadcrumb FIRST, then announce — so by the time the
4424
+ // agent sees the stderr handle, the on-disk copy already exists (an operator
4425
+ // or a later session can find an orphaned spend even when the transcript is
4426
+ // gone).
4427
+ const dir = inFlightSpendDir();
4428
+ const path = join(dir, `${idempotencyKey}.json`);
4429
+ let recordedPath = null;
4430
+ try {
4431
+ await mkdir(dir, { recursive: true });
4432
+ await writeFile(
4433
+ path,
4434
+ `${JSON.stringify(
4435
+ {
4436
+ schema: "image-skill.in-flight-spend.v1",
4437
+ command,
4438
+ operation,
4439
+ idempotency_key: idempotencyKey,
4440
+ recover_command: recoverCommand,
4441
+ argv,
4442
+ started_at: new Date().toISOString(),
4443
+ },
4444
+ null,
4445
+ 2,
4446
+ )}\n`,
4447
+ { mode: 0o600 },
4448
+ );
4449
+ recordedPath = path;
4450
+ } catch {
4451
+ // The stderr notice below is the primary handle; a filesystem failure must
4452
+ // not block the create/edit.
4453
+ }
4454
+ // stderr only — the stdout JSON envelope contract is unchanged. Even a killed
4455
+ // process leaves this line in the agent's captured transcript.
4456
+ try {
4457
+ process.stderr.write(
4458
+ `${JSON.stringify({
4459
+ in_flight: {
4460
+ command,
4461
+ idempotency_key: idempotencyKey,
4462
+ recover_command: recoverCommand,
4463
+ note,
4464
+ },
4465
+ })}\n`,
4466
+ );
4467
+ } catch {
4468
+ // diagnostics are best-effort; never block the spend on a write failure.
4469
+ }
4470
+ return recordedPath;
4471
+ }
4472
+
4473
+ async function clearInFlightSpend(path) {
4474
+ if (path === null || path === undefined) {
4475
+ return;
4476
+ }
4477
+ try {
4478
+ await rm(path, { force: true });
4479
+ } catch {
4480
+ // best-effort cleanup; a leftover breadcrumb is harmless.
4481
+ }
4482
+ }
4483
+
4360
4484
  async function apiRequest(input) {
4361
4485
  const url = new URL(input.path, ensureTrailingSlash(input.apiBaseUrl));
4362
4486
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-skill",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "description": "Zero-setup durable creative-media CLI for agents (image + video + audio + 3D): guide-first creation, model and cost inspection, owned URLs, JSON recovery, payments, reusable assets, and feedback.",
5
5
  "type": "module",
6
6
  "private": false,