image-skill 0.1.35 → 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 +28 -0
- package/bin/image-skill.mjs +317 -42
- package/cli.md +38 -26
- package/commands.json +515 -0
- package/llms.txt +1 -1
- package/package.json +2 -1
- package/skill.md +4 -2
- package/skills/ai-audio-generation/SKILL.md +1 -1
- package/skills/ai-image-generation/SKILL.md +1 -1
- package/skills/ai-video-generation/SKILL.md +1 -1
- package/skills/creative-media/SKILL.md +1 -1
- package/skills/image-edit/SKILL.md +1 -1
- package/skills/image-generation/SKILL.md +1 -1
- package/skills/image-skill/SKILL.md +4 -2
- package/skills/image-skill/references/cli.md +38 -26
- package/skills/image-skill/references/commands.json +515 -0
- package/skills/image-skill/references/llms.txt +1 -1
- package/skills/image-to-3d/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@ 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
|
+
|
|
24
|
+
## 0.1.36 - 2026-06-04
|
|
25
|
+
|
|
26
|
+
- Fix (guide): `create --guide --json` now marks templated follow-up commands
|
|
27
|
+
explicitly with `data.next_command_copy_runnable`,
|
|
28
|
+
`data.next_command_missing_inputs`, and
|
|
29
|
+
`data.next_command_effect.requires_placeholder_substitution`. Auth signup,
|
|
30
|
+
prompt recovery, payment handoff, and input-asset templates remain visible to
|
|
31
|
+
agents, but placeholder values such as `AGENT_OR_OPERATOR_INBOX`,
|
|
32
|
+
`AGENT_NAME`, `RUNTIME_NAME`, `QUOTE_ID`, and `PAYMENT_ATTEMPT_ID` are no
|
|
33
|
+
longer presented as if the command can be copied blindly.
|
|
34
|
+
|
|
7
35
|
## 0.1.35 - 2026-06-04
|
|
8
36
|
|
|
9
37
|
- Fix (CLI aliases): natural modality-first commands now route into the
|
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";
|
|
@@ -65,6 +65,70 @@ const PAYMENT_CREDENTIAL_FLAGS = new Set([
|
|
|
65
65
|
"provider-key",
|
|
66
66
|
"provider-receipt",
|
|
67
67
|
]);
|
|
68
|
+
const GUIDE_NEXT_COMMAND_PLACEHOLDERS = [
|
|
69
|
+
{
|
|
70
|
+
placeholder: "AGENT_OR_OPERATOR_INBOX",
|
|
71
|
+
flag: "--agent-contact",
|
|
72
|
+
value_description:
|
|
73
|
+
"Email-shaped durable contact inbox for the restricted agent identity; use an agent-owned inbox when available, otherwise an operator, team, or sponsor inbox.",
|
|
74
|
+
effect_description: "email-shaped durable contact inbox",
|
|
75
|
+
example: "agent-inbox@example.com",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
placeholder: "AGENT_NAME",
|
|
79
|
+
flag: "--agent-name",
|
|
80
|
+
value_description:
|
|
81
|
+
"Stable display name for this restricted agent identity.",
|
|
82
|
+
effect_description: "stable agent identity name",
|
|
83
|
+
example: "codex-image-worker",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
placeholder: "RUNTIME_NAME",
|
|
87
|
+
flag: "--runtime",
|
|
88
|
+
value_description:
|
|
89
|
+
"Stable name for the agent runtime or substrate using Image Skill.",
|
|
90
|
+
effect_description: "agent/runtime substrate name",
|
|
91
|
+
example: "codex-cli",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
placeholder: "PROMPT",
|
|
95
|
+
flag: "--prompt",
|
|
96
|
+
value_description: "The real creative prompt to plan or create.",
|
|
97
|
+
effect_description: "real creative prompt",
|
|
98
|
+
example: "a compact field camera on a stainless workbench",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
placeholder: "KEY",
|
|
102
|
+
flag: "--idempotency-key",
|
|
103
|
+
value_description:
|
|
104
|
+
"Unique idempotency key for this payment or create attempt.",
|
|
105
|
+
effect_description: "unique idempotency key",
|
|
106
|
+
example: "agent-generated-idempotency-key",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
placeholder: "QUOTE_ID",
|
|
110
|
+
flag: "--quote-id",
|
|
111
|
+
value_description: "Quote id returned by the preceding credits quote call.",
|
|
112
|
+
effect_description: "quote id from credits quote",
|
|
113
|
+
example: null,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
placeholder: "PAYMENT_ATTEMPT_ID",
|
|
117
|
+
flag: "--payment-attempt-id",
|
|
118
|
+
value_description:
|
|
119
|
+
"Payment attempt id returned by the preceding credits buy call.",
|
|
120
|
+
effect_description: "payment attempt id from credits buy",
|
|
121
|
+
example: null,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
placeholder: "image_...",
|
|
125
|
+
flag: "--input",
|
|
126
|
+
value_description:
|
|
127
|
+
"Image Skill input asset id, usually from upload, assets, jobs, or a previous create.",
|
|
128
|
+
effect_description: "Image Skill input asset id",
|
|
129
|
+
example: null,
|
|
130
|
+
},
|
|
131
|
+
];
|
|
68
132
|
|
|
69
133
|
const argv = normalizePublicArgv(process.argv.slice(2));
|
|
70
134
|
const result = await main(argv);
|
|
@@ -212,24 +276,26 @@ function helpKey(path) {
|
|
|
212
276
|
function commandHelpByKey(key) {
|
|
213
277
|
return {
|
|
214
278
|
"": {
|
|
215
|
-
command: "
|
|
279
|
+
command: "help",
|
|
216
280
|
usage:
|
|
217
|
-
"image-skill <doctor|trust|signup|
|
|
281
|
+
"image-skill <doctor|trust|signup|whoami|usage|quota|credits|capabilities|models|create|upload|edit|assets|jobs|activity|feedback> --json",
|
|
218
282
|
docs_url: "https://image-skill.com/cli.md",
|
|
219
283
|
commands: [
|
|
220
284
|
"doctor",
|
|
221
285
|
"trust",
|
|
222
286
|
"signup --agent --agent-contact --agent-name NAME --runtime RUNTIME",
|
|
223
|
-
"auth status",
|
|
224
|
-
"auth save",
|
|
225
|
-
"auth logout",
|
|
226
287
|
"whoami",
|
|
227
288
|
"usage quota",
|
|
289
|
+
"quota",
|
|
228
290
|
"credits methods",
|
|
229
|
-
"credits packs list",
|
|
230
291
|
"credits quote",
|
|
292
|
+
"credits packs list",
|
|
231
293
|
"credits buy",
|
|
232
294
|
"credits status",
|
|
295
|
+
"capabilities",
|
|
296
|
+
"capabilities list",
|
|
297
|
+
"capabilities show",
|
|
298
|
+
"models",
|
|
233
299
|
"models list",
|
|
234
300
|
"models show",
|
|
235
301
|
"create --guide",
|
|
@@ -237,8 +303,7 @@ function commandHelpByKey(key) {
|
|
|
237
303
|
"video create --guide",
|
|
238
304
|
"audio create --guide",
|
|
239
305
|
"3d create --guide",
|
|
240
|
-
"
|
|
241
|
-
"capabilities show",
|
|
306
|
+
"create --dry-run",
|
|
242
307
|
"create",
|
|
243
308
|
"image edit",
|
|
244
309
|
"upload",
|
|
@@ -329,7 +394,7 @@ function commandHelpByKey(key) {
|
|
|
329
394
|
},
|
|
330
395
|
"credits methods": {
|
|
331
396
|
command: "image-skill credits methods help",
|
|
332
|
-
usage: "image-skill credits methods
|
|
397
|
+
usage: "image-skill credits methods",
|
|
333
398
|
docs_url: "https://image-skill.com/cli.md#image-skill-credits",
|
|
334
399
|
},
|
|
335
400
|
"credits packs": {
|
|
@@ -372,6 +437,16 @@ function commandHelpByKey(key) {
|
|
|
372
437
|
usage:
|
|
373
438
|
"image-skill models list --available --operation image.generate --json",
|
|
374
439
|
docs_url: "https://image-skill.com/cli.md#image-skill-models",
|
|
440
|
+
optional_flags: [
|
|
441
|
+
"--available",
|
|
442
|
+
"--executable",
|
|
443
|
+
"--catalog-only",
|
|
444
|
+
"--operation",
|
|
445
|
+
"--modality",
|
|
446
|
+
"--provider",
|
|
447
|
+
"--summary",
|
|
448
|
+
"--details",
|
|
449
|
+
],
|
|
375
450
|
},
|
|
376
451
|
"models show": {
|
|
377
452
|
command: "image-skill models show help",
|
|
@@ -405,6 +480,9 @@ function commandHelpByKey(key) {
|
|
|
405
480
|
"--model",
|
|
406
481
|
"--aspect-ratio",
|
|
407
482
|
"--output-count",
|
|
483
|
+
"--element-frontal",
|
|
484
|
+
"--element-reference",
|
|
485
|
+
"--reference-image",
|
|
408
486
|
"--model-parameters-json",
|
|
409
487
|
"--idempotency-key",
|
|
410
488
|
],
|
|
@@ -425,6 +503,8 @@ function commandHelpByKey(key) {
|
|
|
425
503
|
"--model",
|
|
426
504
|
"--mask",
|
|
427
505
|
"--element-reference",
|
|
506
|
+
"--element-frontal",
|
|
507
|
+
"--reference-image",
|
|
428
508
|
"--model-parameters-json",
|
|
429
509
|
"--idempotency-key",
|
|
430
510
|
],
|
|
@@ -937,12 +1017,6 @@ async function credits(argv) {
|
|
|
937
1017
|
(flag) =>
|
|
938
1018
|
!["json", "api-base-url", "token", "token-stdin"].includes(flag),
|
|
939
1019
|
);
|
|
940
|
-
if (!flagBool(args, "json")) {
|
|
941
|
-
return invalid(
|
|
942
|
-
"image-skill credits methods",
|
|
943
|
-
"credits methods requires --json",
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
1020
|
if (args.positionals.length > 0 || unknownFlags.length > 0) {
|
|
947
1021
|
return invalid(
|
|
948
1022
|
"image-skill credits methods",
|
|
@@ -1033,7 +1107,7 @@ async function credits(argv) {
|
|
|
1033
1107
|
if (paymentMethod === null) {
|
|
1034
1108
|
return invalid(
|
|
1035
1109
|
"image-skill credits quote",
|
|
1036
|
-
"credits quote requires --payment-method from credits methods
|
|
1110
|
+
"credits quote requires --payment-method from credits methods; use stripe_x402.exact.usdc for an agent-settleable browserless rail or stripe_checkout for a human Checkout handoff",
|
|
1037
1111
|
);
|
|
1038
1112
|
}
|
|
1039
1113
|
if (!PUBLIC_QUOTE_PAYMENT_METHODS.includes(paymentMethod)) {
|
|
@@ -1174,13 +1248,13 @@ async function models(argv) {
|
|
|
1174
1248
|
subcommand === "list" || subcommand === "show" ? rest : argv,
|
|
1175
1249
|
);
|
|
1176
1250
|
if (subcommand === "show") {
|
|
1177
|
-
|
|
1178
|
-
if (modelId === undefined) {
|
|
1251
|
+
if (args.positionals.length !== 1) {
|
|
1179
1252
|
return invalid(
|
|
1180
1253
|
"image-skill models show",
|
|
1181
|
-
"models show requires MODEL_ID",
|
|
1254
|
+
"models show requires exactly one MODEL_ID",
|
|
1182
1255
|
);
|
|
1183
1256
|
}
|
|
1257
|
+
const modelId = args.positionals[0];
|
|
1184
1258
|
return apiRequest({
|
|
1185
1259
|
command: "image-skill models show",
|
|
1186
1260
|
method: "GET",
|
|
@@ -1209,13 +1283,21 @@ async function models(argv) {
|
|
|
1209
1283
|
apiBaseUrl: apiBase(args),
|
|
1210
1284
|
path: query.path,
|
|
1211
1285
|
});
|
|
1212
|
-
return flagBool(args, "
|
|
1286
|
+
return flagBool(args, "details") ? result : withModelSummary(result);
|
|
1213
1287
|
}
|
|
1214
1288
|
|
|
1215
1289
|
function modelListQuery(args) {
|
|
1216
1290
|
const available = flagBool(args, "available");
|
|
1217
1291
|
const executable = flagBool(args, "executable");
|
|
1218
1292
|
const catalogOnly = flagBool(args, "catalog-only");
|
|
1293
|
+
const summary = flagBool(args, "summary");
|
|
1294
|
+
const details = flagBool(args, "details");
|
|
1295
|
+
if (summary && details) {
|
|
1296
|
+
return {
|
|
1297
|
+
ok: false,
|
|
1298
|
+
message: "models list --summary cannot be combined with --details",
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1219
1301
|
if (catalogOnly && (available || executable)) {
|
|
1220
1302
|
return {
|
|
1221
1303
|
ok: false,
|
|
@@ -1233,6 +1315,9 @@ function modelListQuery(args) {
|
|
|
1233
1315
|
if (catalogOnly) {
|
|
1234
1316
|
params.set("catalog_only", "true");
|
|
1235
1317
|
}
|
|
1318
|
+
if (details) {
|
|
1319
|
+
params.set("details", "true");
|
|
1320
|
+
}
|
|
1236
1321
|
addQueryValue(params, "operation", flagString(args, "operation"));
|
|
1237
1322
|
addQueryValue(params, "modality", flagString(args, "modality"));
|
|
1238
1323
|
addQueryValue(params, "provider", flagString(args, "provider"));
|
|
@@ -1248,6 +1333,9 @@ function withModelSummary(result) {
|
|
|
1248
1333
|
if (!isRecord(data) || !Array.isArray(data.models)) {
|
|
1249
1334
|
return result;
|
|
1250
1335
|
}
|
|
1336
|
+
if (data.summary?.result_shape === "compact_model_summary") {
|
|
1337
|
+
return result;
|
|
1338
|
+
}
|
|
1251
1339
|
return {
|
|
1252
1340
|
...result,
|
|
1253
1341
|
envelope: {
|
|
@@ -1257,7 +1345,7 @@ function withModelSummary(result) {
|
|
|
1257
1345
|
summary: {
|
|
1258
1346
|
...(isRecord(data.summary) ? data.summary : {}),
|
|
1259
1347
|
result_shape: "compact_model_summary",
|
|
1260
|
-
full_list_command: "image-skill models list --json",
|
|
1348
|
+
full_list_command: "image-skill models list --details --json",
|
|
1261
1349
|
},
|
|
1262
1350
|
models: data.models.map(modelSummaryRow),
|
|
1263
1351
|
},
|
|
@@ -1268,11 +1356,13 @@ function withModelSummary(result) {
|
|
|
1268
1356
|
function modelSummaryRow(model) {
|
|
1269
1357
|
return {
|
|
1270
1358
|
id: model.id,
|
|
1359
|
+
default: model.default === true,
|
|
1271
1360
|
display_name: model.display_name,
|
|
1272
1361
|
provider_id: model.provider_id,
|
|
1273
1362
|
mode: model.mode,
|
|
1274
1363
|
status: model.status,
|
|
1275
1364
|
availability_reason: model.availability_reason ?? null,
|
|
1365
|
+
modality: model.modality ?? "image",
|
|
1276
1366
|
supports: Array.isArray(model.supports) ? [...model.supports] : [],
|
|
1277
1367
|
operations: Array.isArray(model.operations) ? [...model.operations] : [],
|
|
1278
1368
|
task_tags: modelSummaryTaskTags(model),
|
|
@@ -1545,6 +1635,9 @@ async function createGuide(args) {
|
|
|
1545
1635
|
commandPrefix: guideCommandPrefix,
|
|
1546
1636
|
authConfigWritable: authConfigWrite?.ok ?? true,
|
|
1547
1637
|
});
|
|
1638
|
+
const nextCommandMissingInputs =
|
|
1639
|
+
createGuideNextCommandMissingInputs(nextCommand);
|
|
1640
|
+
const nextCommandCopyRunnable = nextCommandMissingInputs.length === 0;
|
|
1548
1641
|
const escapeHatches = createGuideEscapeHatches({
|
|
1549
1642
|
prompt: trimmedPrompt,
|
|
1550
1643
|
selected,
|
|
@@ -1561,6 +1654,8 @@ async function createGuide(args) {
|
|
|
1561
1654
|
const nextCommandEffect = createGuideNextCommandEffect(stage, {
|
|
1562
1655
|
estimatedCredits,
|
|
1563
1656
|
estimatedDebitUsdPerImage,
|
|
1657
|
+
nextCommandCopyRunnable,
|
|
1658
|
+
nextCommandMissingInputs,
|
|
1564
1659
|
});
|
|
1565
1660
|
const noSpendNextCommand =
|
|
1566
1661
|
stage === "ready_to_create" ? escapeHatches.dry_run : null;
|
|
@@ -1580,6 +1675,7 @@ async function createGuide(args) {
|
|
|
1580
1675
|
const guideWarning = createGuideWarning(stage, {
|
|
1581
1676
|
nextCommandEffect,
|
|
1582
1677
|
paymentSummary,
|
|
1678
|
+
nextCommandCopyRunnable,
|
|
1583
1679
|
});
|
|
1584
1680
|
const selfFundNextCommand = stage === "quota_required" ? nextCommand : null;
|
|
1585
1681
|
const selfFundNextCommandLabel = createGuideSelfFundNextCommandLabel(
|
|
@@ -1610,6 +1706,7 @@ async function createGuide(args) {
|
|
|
1610
1706
|
authenticated,
|
|
1611
1707
|
tokenSource: publicTokenSource,
|
|
1612
1708
|
savedConfigPath: configPath(),
|
|
1709
|
+
nextCommandCopyRunnable,
|
|
1613
1710
|
});
|
|
1614
1711
|
const selfFundHandoff = createGuideSelfFundHandoff(stage, {
|
|
1615
1712
|
paymentSummary,
|
|
@@ -1703,6 +1800,8 @@ async function createGuide(args) {
|
|
|
1703
1800
|
guide_warning: guideWarning,
|
|
1704
1801
|
auth_ready: authReady,
|
|
1705
1802
|
next_command: nextCommand,
|
|
1803
|
+
next_command_copy_runnable: nextCommandCopyRunnable,
|
|
1804
|
+
next_command_missing_inputs: nextCommandMissingInputs,
|
|
1706
1805
|
next_command_effect: nextCommandEffect,
|
|
1707
1806
|
no_spend_next_command: noSpendNextCommand,
|
|
1708
1807
|
no_spend_next_command_label: noSpendNextCommandLabel,
|
|
@@ -2302,7 +2401,9 @@ function createGuideAuthReady(stage, input) {
|
|
|
2302
2401
|
warning: ready
|
|
2303
2402
|
? "Current hosted auth is ready; data.next_command can reuse this auth context without exposing a raw token."
|
|
2304
2403
|
: stage === "auth_required"
|
|
2305
|
-
?
|
|
2404
|
+
? input.nextCommandCopyRunnable
|
|
2405
|
+
? "Auth is not ready yet; run data.next_command to create a restricted agent identity, then rerun the guide."
|
|
2406
|
+
: "Auth is not ready yet; fill data.next_command_missing_inputs before running the data.next_command signup template, then rerun the guide."
|
|
2306
2407
|
: null,
|
|
2307
2408
|
};
|
|
2308
2409
|
}
|
|
@@ -2433,6 +2534,9 @@ function createGuideWalletSettlementHandoff({
|
|
|
2433
2534
|
}
|
|
2434
2535
|
|
|
2435
2536
|
function createGuideNextCommandEffect(stage, input) {
|
|
2537
|
+
const placeholders = createGuideEffectPlaceholders(
|
|
2538
|
+
input.nextCommandMissingInputs,
|
|
2539
|
+
);
|
|
2436
2540
|
const base = {
|
|
2437
2541
|
label: "read_only_or_no_media_setup",
|
|
2438
2542
|
no_spend: true,
|
|
@@ -2444,6 +2548,9 @@ function createGuideNextCommandEffect(stage, input) {
|
|
|
2444
2548
|
media_write: false,
|
|
2445
2549
|
estimated_credits: null,
|
|
2446
2550
|
estimated_debit_usd_per_image: null,
|
|
2551
|
+
copy_runnable: input.nextCommandCopyRunnable,
|
|
2552
|
+
requires_placeholder_substitution: placeholders.length > 0,
|
|
2553
|
+
placeholders,
|
|
2447
2554
|
warning: null,
|
|
2448
2555
|
};
|
|
2449
2556
|
if (stage === "auth_required") {
|
|
@@ -2477,6 +2584,9 @@ function createGuideNextCommandEffect(stage, input) {
|
|
|
2477
2584
|
media_write: true,
|
|
2478
2585
|
estimated_credits: input.estimatedCredits,
|
|
2479
2586
|
estimated_debit_usd_per_image: input.estimatedDebitUsdPerImage,
|
|
2587
|
+
copy_runnable: input.nextCommandCopyRunnable,
|
|
2588
|
+
requires_placeholder_substitution: placeholders.length > 0,
|
|
2589
|
+
placeholders,
|
|
2480
2590
|
warning:
|
|
2481
2591
|
"data.next_command creates hosted media and can debit credits. For no-spend verification, run data.recommended_no_spend_command (same value as data.no_spend_next_command) instead.",
|
|
2482
2592
|
};
|
|
@@ -2572,8 +2682,9 @@ function createGuideWarning(stage, input) {
|
|
|
2572
2682
|
...base,
|
|
2573
2683
|
next_command_safety: "rerun_guide_no_spend",
|
|
2574
2684
|
recommended_command_field: "next_command",
|
|
2575
|
-
warning:
|
|
2576
|
-
"data.next_command reruns the free guide with a real prompt; it does not call a provider, open payment, debit credits, or create media."
|
|
2685
|
+
warning: input.nextCommandCopyRunnable
|
|
2686
|
+
? "data.next_command reruns the free guide with a real prompt; it does not call a provider, open payment, debit credits, or create media."
|
|
2687
|
+
: "data.next_command is a no-spend guide template; fill data.next_command_missing_inputs before running it. It does not call a provider, open payment, debit credits, or create media.",
|
|
2577
2688
|
};
|
|
2578
2689
|
}
|
|
2579
2690
|
if (stage === "no_executable_model" || stage === "service_unreachable") {
|
|
@@ -2590,8 +2701,9 @@ function createGuideWarning(stage, input) {
|
|
|
2590
2701
|
...base,
|
|
2591
2702
|
next_command_safety: "hosted_signup_no_spend_setup",
|
|
2592
2703
|
recommended_command_field: "next_command",
|
|
2593
|
-
warning:
|
|
2594
|
-
"data.next_command is no-spend hosted signup/setup; it creates a restricted agent identity but does not call a provider, open payment, debit credits, or create media."
|
|
2704
|
+
warning: input.nextCommandCopyRunnable
|
|
2705
|
+
? "data.next_command is no-spend hosted signup/setup; it creates a restricted agent identity but does not call a provider, open payment, debit credits, or create media."
|
|
2706
|
+
: "data.next_command is a no-spend hosted signup/setup template; fill data.next_command_missing_inputs before running it. It creates a restricted agent identity but does not call a provider, open payment, debit credits, or create media.",
|
|
2595
2707
|
};
|
|
2596
2708
|
}
|
|
2597
2709
|
if (stage === "quota_required") {
|
|
@@ -2605,12 +2717,17 @@ function createGuideWarning(stage, input) {
|
|
|
2605
2717
|
spend_required: true,
|
|
2606
2718
|
recommended_command_field: "escape_hatches",
|
|
2607
2719
|
payment_top_up_path: paymentTopUpPath,
|
|
2608
|
-
warning:
|
|
2609
|
-
paymentTopUpPath === "browserless_agent_self_fund"
|
|
2720
|
+
warning: input.nextCommandCopyRunnable
|
|
2721
|
+
? paymentTopUpPath === "browserless_agent_self_fund"
|
|
2610
2722
|
? "data.next_command starts the browserless live-money top-up path; stay within the delegated cap, or use data.escape_hatches.payment_methods for read-only payment inspection."
|
|
2611
2723
|
: paymentTopUpPath === "human_payment_handoff"
|
|
2612
2724
|
? "data.next_command starts a live-money payment handoff that needs human or browser completion; stay within the delegated cap, or use data.escape_hatches.payment_methods for read-only inspection."
|
|
2613
|
-
: "data.next_command starts payment or quota recovery; inspect data.checks.payments before attempting live money, or use data.escape_hatches.payment_methods for read-only inspection."
|
|
2725
|
+
: "data.next_command starts payment or quota recovery; inspect data.checks.payments before attempting live money, or use data.escape_hatches.payment_methods for read-only inspection."
|
|
2726
|
+
: paymentTopUpPath === "browserless_agent_self_fund"
|
|
2727
|
+
? "data.next_command is a browserless live-money top-up template; fill data.next_command_missing_inputs before running it, stay within the delegated cap, or use data.escape_hatches.payment_methods for read-only payment inspection."
|
|
2728
|
+
: paymentTopUpPath === "human_payment_handoff"
|
|
2729
|
+
? "data.next_command is a live-money payment handoff template; fill data.next_command_missing_inputs before running it, stay within the delegated cap, or use data.escape_hatches.payment_methods for read-only inspection."
|
|
2730
|
+
: "data.next_command is a live-money payment template; fill data.next_command_missing_inputs before running it, stay within the delegated cap, or use data.escape_hatches.payment_methods for read-only inspection.",
|
|
2614
2731
|
};
|
|
2615
2732
|
}
|
|
2616
2733
|
return {
|
|
@@ -2624,6 +2741,40 @@ function createGuideWarning(stage, input) {
|
|
|
2624
2741
|
};
|
|
2625
2742
|
}
|
|
2626
2743
|
|
|
2744
|
+
function createGuideNextCommandMissingInputs(command) {
|
|
2745
|
+
return GUIDE_NEXT_COMMAND_PLACEHOLDERS.filter((placeholder) =>
|
|
2746
|
+
commandContainsTemplateToken(command, placeholder.placeholder),
|
|
2747
|
+
).map((placeholder) => ({
|
|
2748
|
+
flag: placeholder.flag,
|
|
2749
|
+
placeholder: placeholder.placeholder,
|
|
2750
|
+
value_description: placeholder.value_description,
|
|
2751
|
+
example: placeholder.example,
|
|
2752
|
+
}));
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
function createGuideEffectPlaceholders(missingInputs) {
|
|
2756
|
+
return missingInputs.map((input) => {
|
|
2757
|
+
const placeholder = GUIDE_NEXT_COMMAND_PLACEHOLDERS.find(
|
|
2758
|
+
(candidate) => candidate.placeholder === input.placeholder,
|
|
2759
|
+
);
|
|
2760
|
+
return {
|
|
2761
|
+
token: input.placeholder,
|
|
2762
|
+
description: placeholder?.effect_description ?? input.value_description,
|
|
2763
|
+
required: true,
|
|
2764
|
+
};
|
|
2765
|
+
});
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
function commandContainsTemplateToken(command, token) {
|
|
2769
|
+
return new RegExp(
|
|
2770
|
+
`(^|[^A-Za-z0-9_])${escapeRegExp(token)}(?=$|[^A-Za-z0-9_])`,
|
|
2771
|
+
).test(command);
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
function escapeRegExp(value) {
|
|
2775
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2627
2778
|
function createGuideNextCommand(stage, input) {
|
|
2628
2779
|
if (stage === "prompt_required") {
|
|
2629
2780
|
return renderGuideCommand("PROMPT", input.apiBaseUrl, input.commandPrefix, {
|
|
@@ -2975,7 +3126,21 @@ async function create(argv) {
|
|
|
2975
3126
|
if (!references.ok) {
|
|
2976
3127
|
return references.result;
|
|
2977
3128
|
}
|
|
2978
|
-
|
|
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({
|
|
2979
3144
|
command: "image-skill create",
|
|
2980
3145
|
method: "POST",
|
|
2981
3146
|
apiBaseUrl: apiBase(args),
|
|
@@ -3010,15 +3175,15 @@ async function create(argv) {
|
|
|
3010
3175
|
...(modelParameters.value === null
|
|
3011
3176
|
? {}
|
|
3012
3177
|
: { model_parameters: modelParameters.value }),
|
|
3013
|
-
// Retry-safe dedupe (#1228):
|
|
3014
|
-
//
|
|
3015
|
-
...(
|
|
3016
|
-
? {}
|
|
3017
|
-
: { 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 }),
|
|
3018
3181
|
dry_run: flagBool(args, "dry-run"),
|
|
3019
3182
|
accept_unknown_cost: flagBool(args, "accept-unknown-cost"),
|
|
3020
3183
|
},
|
|
3021
3184
|
});
|
|
3185
|
+
await clearInFlightSpend(inFlight);
|
|
3186
|
+
return result;
|
|
3022
3187
|
}
|
|
3023
3188
|
|
|
3024
3189
|
async function upload(argv) {
|
|
@@ -3089,7 +3254,21 @@ async function edit(argv) {
|
|
|
3089
3254
|
if (!modelParameters.ok) {
|
|
3090
3255
|
return modelParameters.result;
|
|
3091
3256
|
}
|
|
3092
|
-
|
|
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({
|
|
3093
3272
|
command: "image-skill edit",
|
|
3094
3273
|
method: "POST",
|
|
3095
3274
|
apiBaseUrl: apiBase(args),
|
|
@@ -3122,14 +3301,14 @@ async function edit(argv) {
|
|
|
3122
3301
|
? {}
|
|
3123
3302
|
: { model_parameters: modelParameters.value }),
|
|
3124
3303
|
...(flagBool(args, "dry-run") ? { dry_run: true } : {}),
|
|
3125
|
-
// Retry-safe dedupe (#1228):
|
|
3126
|
-
//
|
|
3127
|
-
...(
|
|
3128
|
-
? {}
|
|
3129
|
-
: { 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 }),
|
|
3130
3307
|
accept_unknown_cost: flagBool(args, "accept-unknown-cost"),
|
|
3131
3308
|
},
|
|
3132
3309
|
});
|
|
3310
|
+
await clearInFlightSpend(inFlight);
|
|
3311
|
+
return result;
|
|
3133
3312
|
}
|
|
3134
3313
|
|
|
3135
3314
|
async function assets(argv) {
|
|
@@ -4206,6 +4385,102 @@ async function fetchPublicText(url, options = {}) {
|
|
|
4206
4385
|
}
|
|
4207
4386
|
}
|
|
4208
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
|
+
|
|
4209
4484
|
async function apiRequest(input) {
|
|
4210
4485
|
const url = new URL(input.path, ensureTrailingSlash(input.apiBaseUrl));
|
|
4211
4486
|
try {
|