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 +17 -0
- package/bin/image-skill.mjs +137 -13
- package/package.json +1 -1
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
|
package/bin/image-skill.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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):
|
|
3165
|
-
//
|
|
3166
|
-
...(
|
|
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
|
-
|
|
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):
|
|
3277
|
-
//
|
|
3278
|
-
...(
|
|
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.
|
|
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,
|