image-skill 0.1.42 → 0.1.44

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.
@@ -1,18 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash, randomBytes } from "node:crypto";
3
3
  import { createWriteStream } from "node:fs";
4
- import { chmod, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
+ import {
5
+ chmod,
6
+ mkdir,
7
+ readdir,
8
+ readFile,
9
+ rm,
10
+ stat,
11
+ writeFile,
12
+ } from "node:fs/promises";
5
13
  import { basename, dirname, extname, join, resolve } from "node:path";
6
14
  import { Readable } from "node:stream";
7
15
  import { pipeline } from "node:stream/promises";
8
16
  import os from "node:os";
9
17
 
10
- const VERSION = "0.1.42";
18
+ const VERSION = "0.1.44";
11
19
  const PACKAGE_NAME = "image-skill";
12
20
  const DEFAULT_API_BASE_URL = "https://api.image-skill.com";
13
21
  const DEFAULT_DOCS_BASE_URL = "https://image-skill.com";
14
22
  const DEFAULT_NPM_REGISTRY_BASE_URL = "https://registry.npmjs.org";
15
23
  const PUBLIC_REPO_URL = "https://github.com/danielgwilson/image-skill-cli";
24
+ const IN_FLIGHT_RESERVATION_TTL_MS = 15 * 60 * 1000;
25
+ const IN_FLIGHT_SWEEP_AFTER_MS = 24 * 60 * 60 * 1000;
16
26
  const PROMPTLESS_EDIT_MODEL_IDS = new Set([
17
27
  "fal.flux-dev-redux",
18
28
  "fal.flux-krea-redux",
@@ -39,6 +49,9 @@ const HOSTED_SIGNUP_TOKEN_RETURNED_WARNING =
39
49
  const PUBLIC_NPX_COMMAND_PREFIX =
40
50
  "npm_config_update_notifier=false npx -y image-skill@latest";
41
51
  const CREDIT_UNIT_USD = 0.01;
52
+ const TARGET_GROSS_MARGIN = 0.4;
53
+ const PAYMENT_BACKED_CREDIT_PAYMENT_FEE_RATE = 0.015;
54
+ const PAYMENT_BACKED_CREDIT_PAYMENT_FEE_MODEL = "stripe_stablecoin_usd_percent";
42
55
  const MODALITY_COMMAND_ALIASES = new Map([
43
56
  ["image", { command: "create", intent: null }],
44
57
  ["video", { command: "create", intent: "video" }],
@@ -327,7 +340,8 @@ function commandHelpByKey(key) {
327
340
  usage: "image-skill doctor --json",
328
341
  docs_url: "https://image-skill.com/cli.md#image-skill-doctor",
329
342
  description:
330
- "Check hosted API reachability, CLI version, auth state, and health.",
343
+ "Check hosted API reachability, CLI version, auth state, health, and live-spend recovery breadcrumbs.",
344
+ optional_flags: ["--sweep-in-flight"],
331
345
  },
332
346
  trust: {
333
347
  command: "image-skill trust help",
@@ -518,6 +532,7 @@ function commandHelpByKey(key) {
518
532
  docs_url: "https://image-skill.com/cli.md#image-skill-edit",
519
533
  required_flags: ["--input"],
520
534
  optional_flags: [
535
+ "--guide",
521
536
  "--dry-run",
522
537
  "--prompt",
523
538
  "--model",
@@ -606,6 +621,10 @@ async function doctor(argv) {
606
621
  const args = parseArgs(argv);
607
622
  const apiBaseUrl = apiBase(args);
608
623
  const config = await readConfig(configPath());
624
+ const inFlight = await inFlightSpendDoctorReport({
625
+ sweep: flagBool(args, "sweep-in-flight"),
626
+ now: new Date(),
627
+ });
609
628
  const health = await apiRequest({
610
629
  command: "image-skill doctor",
611
630
  method: "GET",
@@ -628,6 +647,7 @@ async function doctor(argv) {
628
647
  saved_token: config.tokenPresent,
629
648
  env_token: hasEnvToken(),
630
649
  },
650
+ in_flight: inFlight,
631
651
  docs: {
632
652
  skill: "https://image-skill.com/skill.md",
633
653
  llms: "https://image-skill.com/llms.txt",
@@ -1591,17 +1611,19 @@ async function capabilities(argv) {
1591
1611
  });
1592
1612
  }
1593
1613
 
1594
- async function createGuide(args) {
1614
+ async function createGuide(args, options = {}) {
1615
+ const guideOperation = options.guideOperation ?? "create";
1616
+ const command = `image-skill ${guideOperation} --guide`;
1595
1617
  if (flagBool(args, "dry-run")) {
1596
1618
  return invalid(
1597
- "image-skill create --guide",
1598
- "create --guide cannot be combined with --dry-run; the guide returns the dry-run escape hatch separately",
1619
+ command,
1620
+ `${guideOperation} --guide cannot be combined with --dry-run; the guide returns the dry-run escape hatch separately`,
1599
1621
  );
1600
1622
  }
1601
1623
  if (hasReferenceFlags(args)) {
1602
1624
  return invalid(
1603
- "image-skill create --guide",
1604
- "create --guide does not upload or resolve reference images; inspect the model with models show, then run create --dry-run before live referenced creates",
1625
+ command,
1626
+ `${guideOperation} --guide does not upload or resolve reference images; inspect the model with models show, then run ${guideOperation} --dry-run before live referenced media calls`,
1605
1627
  );
1606
1628
  }
1607
1629
  const modelParameters = jsonObjectFlag(args, "model-parameters-json");
@@ -1616,6 +1638,10 @@ async function createGuide(args) {
1616
1638
  const requestedProviderId = flagString(args, "provider");
1617
1639
  const requestedIntentFlag = flagString(args, "intent");
1618
1640
  const requestedIntent = requestedIntentFlag ?? "explore";
1641
+ const requestedModelParametersJson =
1642
+ modelParameters.value === null
1643
+ ? null
1644
+ : JSON.stringify(modelParameters.value);
1619
1645
  const maxEstimatedUsdPerImage = flagNumber(
1620
1646
  args,
1621
1647
  "max-estimated-usd-per-image",
@@ -1646,17 +1672,58 @@ async function createGuide(args) {
1646
1672
  const selected =
1647
1673
  models.envelope.ok && models.envelope.data?.models
1648
1674
  ? selectCreateGuideModel(models.envelope.data.models, requestedModelId, {
1675
+ operation: guideOperation,
1649
1676
  prompt: trimmedPrompt,
1650
1677
  intent: requestedIntent,
1651
1678
  maxEstimatedUsdPerImage,
1652
1679
  })
1653
1680
  : null;
1654
1681
  const selectedAspectRatio = createGuideSuggestedAspectRatio(selected);
1655
- const pricing = createGuideModelCreditPricing(selected);
1682
+ const pricingContext = {
1683
+ aspectRatio: selectedAspectRatio ?? "1:1",
1684
+ outputCount: 1,
1685
+ };
1686
+ const defaultedModelParameters =
1687
+ selected === null || createGuideSelectedModelRequiresInputImage(selected)
1688
+ ? {
1689
+ modelParameters: modelParameters.value ?? {},
1690
+ defaultsApplied: [],
1691
+ }
1692
+ : createGuideDefaultModelParameters({
1693
+ model: selected,
1694
+ aspectRatio: pricingContext.aspectRatio,
1695
+ intent: requestedIntent,
1696
+ modelParameters: modelParameters.value ?? {},
1697
+ maxEstimatedUsdPerImage,
1698
+ });
1699
+ const shouldPriceModelParameters =
1700
+ selected !== null &&
1701
+ !createGuideSelectedModelRequiresInputImage(selected) &&
1702
+ createGuideCanPriceModelParameters(selected) &&
1703
+ (defaultedModelParameters.defaultsApplied.length > 0 ||
1704
+ Object.keys(modelParameters.value ?? {}).length > 0);
1705
+ const pricing =
1706
+ selected === null
1707
+ ? null
1708
+ : shouldPriceModelParameters
1709
+ ? createGuidePricingForModel(
1710
+ selected,
1711
+ defaultedModelParameters.modelParameters,
1712
+ pricingContext,
1713
+ )
1714
+ : createGuideModelCreditPricing(selected);
1715
+ const providerCostEstimate =
1716
+ selected === null || !shouldPriceModelParameters
1717
+ ? null
1718
+ : createGuideProviderCostEstimateForModel(
1719
+ selected,
1720
+ defaultedModelParameters.modelParameters,
1721
+ pricingContext,
1722
+ );
1656
1723
  const estimatedCredits = pricing?.credits_required ?? null;
1657
1724
  const estimatedProviderUsdPerImage =
1725
+ providerCostEstimate?.estimated_provider_cost_usd ??
1658
1726
  selected?.economics?.estimated_usd_per_image ??
1659
- pricing?.estimated_provider_cost_usd ??
1660
1727
  pricing?.fallback_provider_cost_usd ??
1661
1728
  (typeof selected?.estimated_usd_per_image === "number"
1662
1729
  ? selected.estimated_usd_per_image
@@ -1683,6 +1750,9 @@ async function createGuide(args) {
1683
1750
  token.source === "anonymous" ? "none" : token.source;
1684
1751
  const stage = createGuideStage({
1685
1752
  prompt: trimmedPrompt,
1753
+ promptRequired:
1754
+ trimmedPrompt.length === 0 &&
1755
+ (selected === null || !PROMPTLESS_EDIT_MODEL_IDS.has(selected.id)),
1686
1756
  health,
1687
1757
  models,
1688
1758
  selected,
@@ -1714,9 +1784,12 @@ async function createGuide(args) {
1714
1784
  requestedIntent,
1715
1785
  requestedIntentFlag,
1716
1786
  requestedModelId,
1787
+ guideOperation,
1788
+ inputReference: options.inputReference,
1717
1789
  maxEstimatedUsdPerImage,
1718
1790
  budgetGuard,
1719
1791
  aspectRatio: selectedAspectRatio,
1792
+ modelParametersJson: requestedModelParametersJson,
1720
1793
  apiBaseUrl: explicitApiBaseUrl(args),
1721
1794
  paymentSummary,
1722
1795
  commandPrefix: guideCommandPrefix,
@@ -1732,9 +1805,12 @@ async function createGuide(args) {
1732
1805
  requestedIntent,
1733
1806
  requestedIntentFlag,
1734
1807
  requestedModelId,
1808
+ guideOperation,
1809
+ inputReference: options.inputReference,
1735
1810
  maxEstimatedUsdPerImage,
1736
1811
  budgetGuard,
1737
1812
  aspectRatio: selectedAspectRatio,
1813
+ modelParametersJson: requestedModelParametersJson,
1738
1814
  apiBaseUrl: explicitApiBaseUrl(args),
1739
1815
  commandPrefix: guideCommandPrefix,
1740
1816
  });
@@ -1776,10 +1852,13 @@ async function createGuide(args) {
1776
1852
  explicitApiBaseUrl(args),
1777
1853
  guideCommandPrefix,
1778
1854
  {
1855
+ operation: guideOperation,
1856
+ inputReference: options.inputReference,
1779
1857
  modelId: requestedModelId,
1780
1858
  providerId: requestedProviderId,
1781
1859
  intent: requestedIntentFlag,
1782
1860
  maxEstimatedUsdPerImage,
1861
+ modelParametersJson: requestedModelParametersJson,
1783
1862
  },
1784
1863
  )
1785
1864
  : null;
@@ -1802,8 +1881,19 @@ async function createGuide(args) {
1802
1881
  tokenSource: publicTokenSource,
1803
1882
  commandPrefix: guideCommandPrefix,
1804
1883
  });
1805
- return createGuideSuccess(quota?.envelope.actor ?? null, {
1806
- schema: "image-skill.create-guide.v1",
1884
+ const guideRecovery = createGuideRecovery(stage, {
1885
+ blocker,
1886
+ nextCommand,
1887
+ noSpendNextCommand,
1888
+ afterNext,
1889
+ escapeHatches,
1890
+ selfFundNextCommand,
1891
+ });
1892
+ return createGuideSuccess(command, quota?.envelope.actor ?? null, {
1893
+ schema:
1894
+ guideOperation === "edit"
1895
+ ? "image-skill.edit-guide.v1"
1896
+ : "image-skill.create-guide.v1",
1807
1897
  ready: stage === "ready_to_create",
1808
1898
  stage,
1809
1899
  checks: {
@@ -1868,6 +1958,7 @@ async function createGuide(args) {
1868
1958
  selected,
1869
1959
  trimmedPrompt,
1870
1960
  requestedIntent,
1961
+ guideOperation,
1871
1962
  )
1872
1963
  : createGuideSelectedModelRequiresInputImage(selected)
1873
1964
  ? selected.modality === "3d"
@@ -1882,6 +1973,9 @@ async function createGuide(args) {
1882
1973
  estimated_provider_usd_per_image: estimatedProviderUsdPerImage,
1883
1974
  credit_unit_usd: pricing?.credit_unit_usd ?? CREDIT_UNIT_USD,
1884
1975
  pricing_confidence: pricing?.pricing_confidence ?? null,
1976
+ pricing_source: pricing?.pricing_source ?? null,
1977
+ model_parameter_defaults_applied:
1978
+ defaultedModelParameters.defaultsApplied,
1885
1979
  },
1886
1980
  blocker,
1887
1981
  guide_warning: guideWarning,
@@ -1894,6 +1988,7 @@ async function createGuide(args) {
1894
1988
  no_spend_next_command_label: noSpendNextCommandLabel,
1895
1989
  no_spend_next_command_effect: noSpendNextCommandEffect,
1896
1990
  no_spend_evaluation: noSpendEvaluation,
1991
+ guide_recovery: guideRecovery,
1897
1992
  recommended_no_spend_command: noSpendNextCommand,
1898
1993
  recommended_no_spend_command_label: noSpendNextCommandLabel,
1899
1994
  recommended_no_spend_command_effect: noSpendNextCommandEffect,
@@ -1914,8 +2009,8 @@ async function createGuide(args) {
1914
2009
  });
1915
2010
  }
1916
2011
 
1917
- function createGuideSuccess(actor, data) {
1918
- const result = success("image-skill create --guide", data);
2012
+ function createGuideSuccess(command, actor, data) {
2013
+ const result = success(command, data);
1919
2014
  result.envelope.actor = actor;
1920
2015
  return result;
1921
2016
  }
@@ -1923,7 +2018,12 @@ function createGuideSuccess(actor, data) {
1923
2018
  function selectCreateGuideModel(
1924
2019
  models,
1925
2020
  requestedModelId,
1926
- { prompt = "", intent = undefined, maxEstimatedUsdPerImage = null } = {},
2021
+ {
2022
+ operation = "create",
2023
+ prompt = "",
2024
+ intent = undefined,
2025
+ maxEstimatedUsdPerImage = null,
2026
+ } = {},
1927
2027
  ) {
1928
2028
  const isExecutableCreate = (model) =>
1929
2029
  model?.status === "available" &&
@@ -1937,14 +2037,19 @@ function selectCreateGuideModel(
1937
2037
  (model.supports.includes("edit") || model.supports.includes("variation")) &&
1938
2038
  createGuideSelectedModelRequiresInputImage(model);
1939
2039
  const isExecutableGuideModel = (model) =>
1940
- isExecutableCreate(model) || isExecutableInputImageEdit(model);
2040
+ operation === "edit"
2041
+ ? isExecutableInputImageEdit(model)
2042
+ : isExecutableCreate(model) || isExecutableInputImageEdit(model);
1941
2043
  if (requestedModelId !== null) {
1942
2044
  const requested = models.find((model) => model.id === requestedModelId);
1943
2045
  return requested !== undefined && isExecutableGuideModel(requested)
1944
2046
  ? requested
1945
2047
  : null;
1946
2048
  }
1947
- const candidates = models.filter(isExecutableCreate);
2049
+ const candidates =
2050
+ operation === "edit"
2051
+ ? models.filter(isExecutableInputImageEdit)
2052
+ : models.filter(isExecutableCreate);
1948
2053
  if (createGuideImplies3d({ prompt, intent })) {
1949
2054
  const eligible3d = guideCandidatesWithinBudget({
1950
2055
  candidates: models.filter(
@@ -2052,6 +2157,221 @@ function guideBudgetUsdForModel(model) {
2052
2157
  );
2053
2158
  }
2054
2159
 
2160
+ function createGuideDefaultModelParameters(input) {
2161
+ const modelParameters = { ...(input.modelParameters ?? {}) };
2162
+ const defaultsApplied = [];
2163
+
2164
+ if (
2165
+ input.model?.id === "xai.grok-imagine-image-quality" &&
2166
+ modelParameters.resolution === undefined
2167
+ ) {
2168
+ const twoKEstimate = createGuideProviderCostEstimateForModel(
2169
+ input.model,
2170
+ { resolution: "2k" },
2171
+ { aspectRatio: input.aspectRatio },
2172
+ ).estimated_provider_cost_usd;
2173
+ const twoKAllowedByBudget =
2174
+ input.maxEstimatedUsdPerImage === null ||
2175
+ twoKEstimate === null ||
2176
+ twoKEstimate <= input.maxEstimatedUsdPerImage;
2177
+ const intentClass = createGuideIntentClass(input.intent);
2178
+ const resolution =
2179
+ intentClass !== "budget_draft" && twoKAllowedByBudget ? "2k" : "1k";
2180
+ modelParameters.resolution = resolution;
2181
+ defaultsApplied.push(`resolution=${resolution}`);
2182
+ }
2183
+
2184
+ if (
2185
+ input.model?.id === "fal.flux-dev" &&
2186
+ modelParameters.image_size === undefined
2187
+ ) {
2188
+ const imageSize = falDefaultImageSize(input.aspectRatio);
2189
+ if (imageSize !== null) {
2190
+ modelParameters.image_size = imageSize;
2191
+ defaultsApplied.push(`image_size=${imageSize}`);
2192
+ }
2193
+ }
2194
+
2195
+ if (
2196
+ input.model?.id === "openai.gpt-image-2" &&
2197
+ modelParameters.quality === undefined
2198
+ ) {
2199
+ const mediumEstimate = createGuideProviderCostEstimateForModel(
2200
+ input.model,
2201
+ { ...modelParameters, quality: "medium" },
2202
+ { aspectRatio: input.aspectRatio },
2203
+ ).estimated_provider_cost_usd;
2204
+ const mediumAllowedByBudget =
2205
+ input.maxEstimatedUsdPerImage === null ||
2206
+ mediumEstimate === null ||
2207
+ mediumEstimate <= input.maxEstimatedUsdPerImage;
2208
+ if (mediumAllowedByBudget) {
2209
+ modelParameters.quality = "medium";
2210
+ defaultsApplied.push("quality=medium");
2211
+ }
2212
+ }
2213
+
2214
+ return { modelParameters, defaultsApplied };
2215
+ }
2216
+
2217
+ function createGuidePricingForModel(model, modelParameters, context = {}) {
2218
+ const estimate = createGuideProviderCostEstimateForModel(
2219
+ model,
2220
+ modelParameters,
2221
+ context,
2222
+ );
2223
+ if (estimate.estimated_provider_cost_usd === null) {
2224
+ return createGuideModelCreditPricing(model);
2225
+ }
2226
+ return createGuideCreditPricingForProviderCost({
2227
+ providerCostUsd: estimate.estimated_provider_cost_usd,
2228
+ pricingConfidence: estimate.pricing_confidence,
2229
+ pricingSource: estimate.pricing_source,
2230
+ });
2231
+ }
2232
+
2233
+ function createGuideCanPriceModelParameters(model) {
2234
+ return String(model?.id ?? "").startsWith("xai.grok-imagine-image");
2235
+ }
2236
+
2237
+ function createGuideProviderCostEstimateForModel(
2238
+ model,
2239
+ modelParameters = {},
2240
+ context = {},
2241
+ ) {
2242
+ if (String(model?.id ?? "").startsWith("xai.grok-imagine-image")) {
2243
+ return createGuideXaiImageCostEstimate(model, modelParameters, context);
2244
+ }
2245
+ return {
2246
+ estimated_provider_cost_usd:
2247
+ typeof model?.economics?.estimated_usd_per_image === "number"
2248
+ ? model.economics.estimated_usd_per_image
2249
+ : (createGuideModelCreditPricing(model)?.estimated_provider_cost_usd ??
2250
+ null),
2251
+ pricing_source: "model_registry",
2252
+ pricing_confidence: "known",
2253
+ };
2254
+ }
2255
+
2256
+ function createGuideXaiImageCostEstimate(model, modelParameters, context) {
2257
+ const modelId = String(model?.id ?? "");
2258
+ const quality = modelId.includes("-quality");
2259
+ const edit = modelId.endsWith("-edit");
2260
+ const resolution = modelParameters?.resolution === "2k" ? "2k" : "1k";
2261
+ const outputImageCount =
2262
+ Number.isInteger(context?.outputCount) && context.outputCount > 0
2263
+ ? context.outputCount
2264
+ : 1;
2265
+ const referenceAssetCount =
2266
+ Number.isInteger(context?.referenceAssetCount) &&
2267
+ context.referenceAssetCount > 0
2268
+ ? context.referenceAssetCount
2269
+ : 0;
2270
+ const sourceImageCount = edit ? 1 + referenceAssetCount : 0;
2271
+ const inputUsdPerImage = quality ? 0.01 : 0.002;
2272
+ const outputUsdPerImage =
2273
+ quality && resolution === "2k" ? 0.07 : quality ? 0.05 : 0.02;
2274
+ const defaultResolution =
2275
+ modelParameters?.resolution === undefined ||
2276
+ modelParameters?.resolution === null ||
2277
+ modelParameters?.resolution === "1k";
2278
+ const defaultShape =
2279
+ defaultResolution &&
2280
+ outputImageCount === 1 &&
2281
+ sourceImageCount === (edit ? 1 : 0);
2282
+ return {
2283
+ estimated_provider_cost_usd: roundUsdMicro(
2284
+ inputUsdPerImage * sourceImageCount +
2285
+ outputUsdPerImage * outputImageCount,
2286
+ ),
2287
+ pricing_source: defaultShape ? "model_registry" : "model_parameters",
2288
+ pricing_confidence: "known",
2289
+ };
2290
+ }
2291
+
2292
+ function createGuideCreditPricingForProviderCost(input) {
2293
+ const providerCostUsd = roundUsdMicro(input.providerCostUsd);
2294
+ const creditsRequired = Math.max(
2295
+ 1,
2296
+ Math.ceil(
2297
+ roundUsdMicro(
2298
+ providerCostUsd / (1 - TARGET_GROSS_MARGIN) / CREDIT_UNIT_USD,
2299
+ ),
2300
+ ),
2301
+ );
2302
+ const estimatedRevenueUsd = roundUsd(creditsRequired * CREDIT_UNIT_USD);
2303
+ const estimatedPaymentFeeUsd = roundUsdMicro(
2304
+ estimatedRevenueUsd * PAYMENT_BACKED_CREDIT_PAYMENT_FEE_RATE,
2305
+ );
2306
+ const estimatedNetRevenueUsd = roundUsdMicro(
2307
+ estimatedRevenueUsd - estimatedPaymentFeeUsd,
2308
+ );
2309
+ const estimatedGrossMargin =
2310
+ estimatedRevenueUsd > 0
2311
+ ? roundRatio(
2312
+ (estimatedRevenueUsd - providerCostUsd) / estimatedRevenueUsd,
2313
+ )
2314
+ : null;
2315
+ const estimatedFeeAdjustedMargin =
2316
+ estimatedRevenueUsd > 0
2317
+ ? roundRatio(
2318
+ (estimatedNetRevenueUsd - providerCostUsd) / estimatedRevenueUsd,
2319
+ )
2320
+ : null;
2321
+ const selfFundBlockReason =
2322
+ estimatedNetRevenueUsd + 1e-9 < providerCostUsd
2323
+ ? "payment_fee_margin_negative"
2324
+ : null;
2325
+ return {
2326
+ credits_required: creditsRequired,
2327
+ credit_unit_usd: CREDIT_UNIT_USD,
2328
+ estimated_provider_cost_usd: providerCostUsd,
2329
+ fallback_provider_cost_usd: null,
2330
+ estimated_revenue_usd: estimatedRevenueUsd,
2331
+ estimated_gross_margin: estimatedGrossMargin,
2332
+ payment_fee_rate: PAYMENT_BACKED_CREDIT_PAYMENT_FEE_RATE,
2333
+ payment_fee_model: PAYMENT_BACKED_CREDIT_PAYMENT_FEE_MODEL,
2334
+ estimated_payment_fee_usd: estimatedPaymentFeeUsd,
2335
+ estimated_net_revenue_usd: estimatedNetRevenueUsd,
2336
+ estimated_fee_adjusted_margin: estimatedFeeAdjustedMargin,
2337
+ self_fundable: selfFundBlockReason === null,
2338
+ self_fund_block_reason: selfFundBlockReason,
2339
+ target_gross_margin: TARGET_GROSS_MARGIN,
2340
+ pricing_confidence: input.pricingConfidence,
2341
+ pricing_source: input.pricingSource,
2342
+ margin_model: "provider_cost_plus_margin",
2343
+ };
2344
+ }
2345
+
2346
+ function falDefaultImageSize(aspectRatio) {
2347
+ switch (aspectRatio) {
2348
+ case "1:1":
2349
+ return "square_hd";
2350
+ case "4:3":
2351
+ return "landscape_4_3";
2352
+ case "3:4":
2353
+ return "portrait_4_3";
2354
+ case "16:9":
2355
+ return "landscape_16_9";
2356
+ case "9:16":
2357
+ return "portrait_16_9";
2358
+ default:
2359
+ return null;
2360
+ }
2361
+ }
2362
+
2363
+ function roundUsd(value) {
2364
+ return Math.round(value * 100) / 100;
2365
+ }
2366
+
2367
+ function roundRatio(value) {
2368
+ return Math.round(value * 1000) / 1000;
2369
+ }
2370
+
2371
+ function roundUsdMicro(value) {
2372
+ return Math.round(value * 1_000_000) / 1_000_000;
2373
+ }
2374
+
2055
2375
  function guideModelExecutionStatus(model) {
2056
2376
  if (
2057
2377
  isRecord(model?.execution) &&
@@ -2139,7 +2459,12 @@ function createGuideSelectedModelRequiresInputImage(model) {
2139
2459
  );
2140
2460
  }
2141
2461
 
2142
- function createGuideSelectionReason(model, prompt, intent) {
2462
+ function createGuideSelectionReason(
2463
+ model,
2464
+ prompt,
2465
+ intent,
2466
+ operation = "create",
2467
+ ) {
2143
2468
  if (
2144
2469
  createGuideSelectedModelRequiresInputImage(model) &&
2145
2470
  createGuideImplies3d({ prompt, intent })
@@ -2167,7 +2492,9 @@ function createGuideSelectionReason(model, prompt, intent) {
2167
2492
  ? "guide selected a draft/budget create model with high-definition defaults"
2168
2493
  : "guide selected the strongest currently available quality-first create model for this intent";
2169
2494
  }
2170
- return "guide selected the first available executable create model";
2495
+ return operation === "edit"
2496
+ ? "guide selected the first available executable input-image edit model"
2497
+ : "guide selected the first available executable create model";
2171
2498
  }
2172
2499
 
2173
2500
  function createGuidePaymentSummary(data, commandPrefix) {
@@ -2382,7 +2709,7 @@ function renderCopyRunnablePaymentCommand(commandPrefix, command) {
2382
2709
  }
2383
2710
 
2384
2711
  function createGuideStage(input) {
2385
- if (input.prompt.length === 0) {
2712
+ if (input.promptRequired) {
2386
2713
  return "prompt_required";
2387
2714
  }
2388
2715
  if (!input.health.envelope.ok || !input.models.envelope.ok) {
@@ -2789,6 +3116,64 @@ function createGuideNoSpendEvaluation(stage, input) {
2789
3116
  };
2790
3117
  }
2791
3118
 
3119
+ function createGuideRecovery(stage, input) {
3120
+ let noSpendCommand = null;
3121
+ let noSpendCommandField = null;
3122
+ if (stage === "ready_to_create" && input.noSpendNextCommand !== null) {
3123
+ noSpendCommand = input.noSpendNextCommand;
3124
+ noSpendCommandField = "recommended_no_spend_command";
3125
+ } else if (stage === "quota_required") {
3126
+ noSpendCommand = input.escapeHatches.quota;
3127
+ noSpendCommandField = "escape_hatches.quota";
3128
+ } else if (
3129
+ stage === "no_executable_model" ||
3130
+ stage === "service_unreachable"
3131
+ ) {
3132
+ noSpendCommand = input.nextCommand;
3133
+ noSpendCommandField = "next_command";
3134
+ } else if (stage === "auth_required" || stage === "prompt_required") {
3135
+ noSpendCommand = input.nextCommand;
3136
+ noSpendCommandField = "next_command";
3137
+ }
3138
+ const noSpendMissingInputs =
3139
+ noSpendCommand === null
3140
+ ? []
3141
+ : createGuideNextCommandMissingInputs(noSpendCommand);
3142
+ const liveCreateCommandField =
3143
+ stage === "ready_to_create" ? "next_command" : null;
3144
+ const livePaymentCommandField =
3145
+ stage === "quota_required" && input.selfFundNextCommand !== null
3146
+ ? "self_fund_next_command"
3147
+ : null;
3148
+ const doubleSpendGuardRequired =
3149
+ liveCreateCommandField !== null || livePaymentCommandField !== null;
3150
+ return {
3151
+ schema: "image-skill.guide-recovery.v1",
3152
+ stage,
3153
+ precondition_code: input.blocker?.code ?? null,
3154
+ precondition_message: input.blocker?.message ?? null,
3155
+ no_spend_command: noSpendCommand,
3156
+ no_spend_command_field: noSpendCommandField,
3157
+ no_spend_command_copy_runnable:
3158
+ noSpendCommand === null ? null : noSpendMissingInputs.length === 0,
3159
+ no_spend_command_missing_inputs: noSpendMissingInputs,
3160
+ after_success_command: input.afterNext,
3161
+ after_success_command_field: input.afterNext === null ? null : "after_next",
3162
+ live_create_command_field: liveCreateCommandField,
3163
+ live_payment_command_field: livePaymentCommandField,
3164
+ double_spend_guard: {
3165
+ required: doubleSpendGuardRequired,
3166
+ safe_rerun_command_field: noSpendCommandField,
3167
+ warning:
3168
+ liveCreateCommandField !== null
3169
+ ? "Do not blindly rerun data.next_command after a partial or unknown create/edit failure; use data.guide_recovery.no_spend_command, jobs/activity, or error.recovery before any live retry."
3170
+ : livePaymentCommandField !== null
3171
+ ? "Do not blindly rerun live payment commands with fresh identifiers after a partial or unknown payment failure; use data.guide_recovery.no_spend_command and payment status recovery before any new buy."
3172
+ : "No live payment or live media command is exposed for this stage; follow the no-spend command and rerun the guide after the precondition is satisfied.",
3173
+ },
3174
+ };
3175
+ }
3176
+
2792
3177
  function createGuideWarning(stage, input) {
2793
3178
  const effect = input.nextCommandEffect;
2794
3179
  const base = {
@@ -2909,10 +3294,13 @@ function escapeRegExp(value) {
2909
3294
  function createGuideNextCommand(stage, input) {
2910
3295
  if (stage === "prompt_required") {
2911
3296
  return renderGuideCommand("PROMPT", input.apiBaseUrl, input.commandPrefix, {
3297
+ operation: input.guideOperation,
3298
+ inputReference: input.inputReference,
2912
3299
  modelId: input.requestedModelId,
2913
3300
  providerId: input.requestedProviderId,
2914
3301
  intent: input.requestedIntentFlag,
2915
3302
  maxEstimatedUsdPerImage: input.maxEstimatedUsdPerImage,
3303
+ modelParametersJson: input.modelParametersJson,
2916
3304
  });
2917
3305
  }
2918
3306
  if (stage === "no_executable_model" || stage === "service_unreachable") {
@@ -2934,7 +3322,9 @@ function createGuideNextCommand(stage, input) {
2934
3322
  return renderInputImageGuideCommand({
2935
3323
  modelId: input.selected.id,
2936
3324
  prompt: input.prompt,
3325
+ inputReference: input.inputReference,
2937
3326
  budgetGuard: input.budgetGuard,
3327
+ modelParametersJson: input.modelParametersJson,
2938
3328
  dryRun: false,
2939
3329
  idempotencyKey: `edit-guide-${Date.now()}-${randomBytes(4).toString("hex")}`,
2940
3330
  apiBaseUrl: input.apiBaseUrl,
@@ -2948,6 +3338,7 @@ function createGuideNextCommand(stage, input) {
2948
3338
  intent: input.requestedIntent,
2949
3339
  budgetGuard: input.budgetGuard,
2950
3340
  aspectRatio: input.aspectRatio,
3341
+ modelParametersJson: input.modelParametersJson,
2951
3342
  dryRun: false,
2952
3343
  // Retry-safe by default (#1228): bake a stable idempotency key into the
2953
3344
  // advertised create command so an agent that copies it and retries after a
@@ -2977,16 +3368,22 @@ function createGuideEscapeHatches(input) {
2977
3368
  "usage quota --json",
2978
3369
  ),
2979
3370
  dry_run:
2980
- input.selected === null || input.prompt.length === 0
3371
+ input.selected === null ||
3372
+ (input.prompt.length === 0 &&
3373
+ !PROMPTLESS_EDIT_MODEL_IDS.has(input.selected.id))
2981
3374
  ? renderGuidePrefixedCommand(
2982
3375
  input.commandPrefix,
2983
- "create --dry-run --prompt PROMPT --json",
3376
+ input.guideOperation === "edit"
3377
+ ? "edit --dry-run --input image_... --prompt PROMPT --json"
3378
+ : "create --dry-run --prompt PROMPT --json",
2984
3379
  )
2985
3380
  : createGuideSelectedModelRequiresInputImage(input.selected)
2986
3381
  ? renderInputImageGuideCommand({
2987
3382
  modelId: input.selected.id,
2988
3383
  prompt: input.prompt,
3384
+ inputReference: input.inputReference,
2989
3385
  budgetGuard: input.budgetGuard,
3386
+ modelParametersJson: input.modelParametersJson,
2990
3387
  dryRun: true,
2991
3388
  apiBaseUrl: input.apiBaseUrl,
2992
3389
  commandPrefix: input.commandPrefix,
@@ -2998,6 +3395,7 @@ function createGuideEscapeHatches(input) {
2998
3395
  intent: input.requestedIntent,
2999
3396
  budgetGuard: input.budgetGuard,
3000
3397
  aspectRatio: input.aspectRatio,
3398
+ modelParametersJson: input.modelParametersJson,
3001
3399
  dryRun: true,
3002
3400
  apiBaseUrl: input.apiBaseUrl,
3003
3401
  commandPrefix: input.commandPrefix,
@@ -3011,10 +3409,16 @@ function renderGuideCommand(
3011
3409
  commandPrefix = "image-skill",
3012
3410
  options = {},
3013
3411
  ) {
3412
+ const operation = options.operation ?? "create";
3014
3413
  return [
3015
3414
  commandPrefix,
3016
- "create --guide --prompt",
3415
+ `${operation} --guide --prompt`,
3017
3416
  shellQuote(prompt),
3417
+ ...(operation === "edit" &&
3418
+ typeof options.inputReference === "string" &&
3419
+ options.inputReference.trim().length > 0
3420
+ ? ["--input", shellQuote(options.inputReference.trim())]
3421
+ : []),
3018
3422
  ...(options.modelId === null ||
3019
3423
  options.modelId === undefined ||
3020
3424
  options.modelId === ""
@@ -3037,6 +3441,10 @@ function renderGuideCommand(
3037
3441
  "--max-estimated-usd-per-image",
3038
3442
  shellQuote(formatUsd(options.maxEstimatedUsdPerImage)),
3039
3443
  ]),
3444
+ ...(options.modelParametersJson === null ||
3445
+ options.modelParametersJson === undefined
3446
+ ? []
3447
+ : ["--model-parameters-json", shellQuote(options.modelParametersJson)]),
3040
3448
  ...(apiBaseUrl === null ? [] : ["--api-base-url", shellQuote(apiBaseUrl)]),
3041
3449
  "--json",
3042
3450
  ].join(" ");
@@ -3091,12 +3499,18 @@ function renderInputImageGuideCommand(input) {
3091
3499
  "edit",
3092
3500
  ...(input.dryRun ? ["--dry-run"] : []),
3093
3501
  "--input",
3094
- "image_...",
3502
+ input.inputReference?.trim()
3503
+ ? shellQuote(input.inputReference.trim())
3504
+ : "image_...",
3095
3505
  "--model",
3096
3506
  shellQuote(input.modelId),
3097
3507
  ...(promptless ? [] : ["--prompt", shellQuote(input.prompt)]),
3098
3508
  "--max-estimated-usd-per-image",
3099
3509
  shellQuote(formatUsd(input.budgetGuard)),
3510
+ ...(input.modelParametersJson === null ||
3511
+ input.modelParametersJson === undefined
3512
+ ? []
3513
+ : ["--model-parameters-json", shellQuote(input.modelParametersJson)]),
3100
3514
  ...(input.idempotencyKey === undefined || input.idempotencyKey === null
3101
3515
  ? []
3102
3516
  : ["--idempotency-key", shellQuote(input.idempotencyKey)]),
@@ -3126,6 +3540,10 @@ function renderCreateCommand(input) {
3126
3540
  : ["--aspect-ratio", shellQuote(input.aspectRatio)]),
3127
3541
  "--max-estimated-usd-per-image",
3128
3542
  shellQuote(formatUsd(input.budgetGuard)),
3543
+ ...(input.modelParametersJson === null ||
3544
+ input.modelParametersJson === undefined
3545
+ ? []
3546
+ : ["--model-parameters-json", shellQuote(input.modelParametersJson)]),
3129
3547
  ...(input.idempotencyKey === undefined || input.idempotencyKey === null
3130
3548
  ? []
3131
3549
  : ["--idempotency-key", shellQuote(input.idempotencyKey)]),
@@ -3361,6 +3779,12 @@ async function edit(argv) {
3361
3779
  const args = parseArgs(argv);
3362
3780
  const input = flagString(args, "input") ?? args.positionals[0];
3363
3781
  const modelId = flagString(args, "model");
3782
+ if (flagBool(args, "guide")) {
3783
+ return createGuide(args, {
3784
+ guideOperation: "edit",
3785
+ inputReference: input,
3786
+ });
3787
+ }
3364
3788
  if (input === undefined) {
3365
3789
  return invalid(
3366
3790
  "image-skill edit",
@@ -4575,6 +4999,169 @@ function inFlightSpendFileName(idempotencyKey) {
4575
4999
  return `${trimmed.length === 0 ? "key" : trimmed}.json`;
4576
5000
  }
4577
5001
 
5002
+ async function inFlightSpendDoctorReport(input) {
5003
+ const dir = inFlightSpendDir();
5004
+ const now = input.now ?? new Date();
5005
+ const files = await readdir(dir).catch((error) => {
5006
+ if (error?.code === "ENOENT") {
5007
+ return [];
5008
+ }
5009
+ return null;
5010
+ });
5011
+ if (files === null) {
5012
+ return {
5013
+ schema: "image-skill.in-flight-spend-report.v1",
5014
+ directory: dir,
5015
+ count: null,
5016
+ recoverable_count: null,
5017
+ ttl_elapsed_count: null,
5018
+ sweep_eligible_count: null,
5019
+ invalid_count: null,
5020
+ entries: [],
5021
+ error: "in-flight directory could not be read",
5022
+ reservation_ttl_ms: IN_FLIGHT_RESERVATION_TTL_MS,
5023
+ sweep_after_ms: IN_FLIGHT_SWEEP_AFTER_MS,
5024
+ swept_count: 0,
5025
+ sweep_requested: input.sweep === true,
5026
+ };
5027
+ }
5028
+
5029
+ const entries = [];
5030
+ let invalidCount = 0;
5031
+ let sweptCount = 0;
5032
+ for (const file of files.sort()) {
5033
+ if (!file.endsWith(".json")) {
5034
+ continue;
5035
+ }
5036
+ const path = join(dir, file);
5037
+ const entry = await readInFlightSpendEntry({ path, file, now });
5038
+ if (entry === null) {
5039
+ invalidCount += 1;
5040
+ continue;
5041
+ }
5042
+ if (input.sweep === true && entry.sweep_eligible === true) {
5043
+ await rm(path, { force: true }).catch(() => {});
5044
+ sweptCount += 1;
5045
+ continue;
5046
+ }
5047
+ entries.push(entry);
5048
+ }
5049
+
5050
+ return {
5051
+ schema: "image-skill.in-flight-spend-report.v1",
5052
+ directory: dir,
5053
+ count: entries.length,
5054
+ recoverable_count: entries.filter((entry) => entry.state === "recoverable")
5055
+ .length,
5056
+ ttl_elapsed_count: entries.filter((entry) => entry.state === "ttl_elapsed")
5057
+ .length,
5058
+ sweep_eligible_count: entries.filter((entry) => entry.sweep_eligible)
5059
+ .length,
5060
+ invalid_count: invalidCount,
5061
+ swept_count: sweptCount,
5062
+ reservation_ttl_ms: IN_FLIGHT_RESERVATION_TTL_MS,
5063
+ sweep_after_ms: IN_FLIGHT_SWEEP_AFTER_MS,
5064
+ sweep_requested: input.sweep === true,
5065
+ entries,
5066
+ note:
5067
+ entries.length === 0
5068
+ ? "no in-flight live spend breadcrumbs found"
5069
+ : "rerun an entry's recover_command to settle or inspect a maybe-reserved spend before sweeping it",
5070
+ };
5071
+ }
5072
+
5073
+ async function readInFlightSpendEntry({ path, file, now }) {
5074
+ let parsed;
5075
+ let fileStat;
5076
+ try {
5077
+ parsed = JSON.parse(await readFile(path, "utf8"));
5078
+ fileStat = await stat(path);
5079
+ } catch {
5080
+ return null;
5081
+ }
5082
+ if (
5083
+ parsed?.schema !== "image-skill.in-flight-spend.v1" ||
5084
+ typeof parsed.idempotency_key !== "string" ||
5085
+ typeof parsed.operation !== "string"
5086
+ ) {
5087
+ return null;
5088
+ }
5089
+
5090
+ const startedAt =
5091
+ typeof parsed.started_at === "string" ? parsed.started_at : null;
5092
+ const startedTime =
5093
+ startedAt === null ? Number.NaN : new Date(startedAt).getTime();
5094
+ const fallbackTime = fileStat.mtime.getTime();
5095
+ const basisTime = Number.isFinite(startedTime) ? startedTime : fallbackTime;
5096
+ const ageMs = Math.max(0, now.getTime() - basisTime);
5097
+ const state =
5098
+ ageMs >= IN_FLIGHT_RESERVATION_TTL_MS ? "ttl_elapsed" : "recoverable";
5099
+ const sweepEligible = ageMs >= IN_FLIGHT_SWEEP_AFTER_MS;
5100
+ const argv = Array.isArray(parsed.argv)
5101
+ ? parsed.argv.filter((value) => typeof value === "string")
5102
+ : [];
5103
+ const recoverCommand = renderRecoverCommand({
5104
+ operation: parsed.operation,
5105
+ argv,
5106
+ idempotencyKey: parsed.idempotency_key,
5107
+ fallback: parsed.recover_command,
5108
+ });
5109
+
5110
+ return {
5111
+ file,
5112
+ path,
5113
+ operation: parsed.operation,
5114
+ command:
5115
+ typeof parsed.command === "string"
5116
+ ? parsed.command
5117
+ : `image-skill ${parsed.operation}`,
5118
+ idempotency_key: parsed.idempotency_key,
5119
+ started_at: startedAt,
5120
+ age_ms: ageMs,
5121
+ state,
5122
+ sweep_eligible: sweepEligible,
5123
+ recover_command: recoverCommand,
5124
+ original_recover_command:
5125
+ typeof parsed.recover_command === "string"
5126
+ ? parsed.recover_command
5127
+ : null,
5128
+ warning:
5129
+ state === "recoverable"
5130
+ ? "the hosted reservation TTL has not elapsed; recover before cleanup"
5131
+ : sweepEligible
5132
+ ? "reservation TTL has long elapsed; recover first if the original result still matters, or run doctor --sweep-in-flight to remove this breadcrumb"
5133
+ : "reservation TTL has elapsed; recover if you need the result, otherwise leave it until it becomes sweep-eligible",
5134
+ };
5135
+ }
5136
+
5137
+ function renderRecoverCommand(input) {
5138
+ const argv = withRecoveryArgs(input.argv, input.idempotencyKey);
5139
+ if (argv.length === 0 && typeof input.fallback === "string") {
5140
+ return input.fallback;
5141
+ }
5142
+ return renderImageSkillCommand(input.operation, argv);
5143
+ }
5144
+
5145
+ function withRecoveryArgs(argv, idempotencyKey) {
5146
+ const args = [...argv];
5147
+ const hasIdempotency = args.some(
5148
+ (arg) =>
5149
+ arg === "--idempotency-key" || arg.startsWith("--idempotency-key="),
5150
+ );
5151
+ if (!hasIdempotency) {
5152
+ args.push("--idempotency-key", idempotencyKey);
5153
+ }
5154
+ const hasJson = args.some((arg) => arg === "--json");
5155
+ if (!hasJson) {
5156
+ args.push("--json");
5157
+ }
5158
+ return args;
5159
+ }
5160
+
5161
+ function renderImageSkillCommand(operation, argv) {
5162
+ return ["image-skill", operation, ...argv.map(shellQuote)].join(" ");
5163
+ }
5164
+
4578
5165
  async function recordInFlightSpend(input) {
4579
5166
  const { command, operation, idempotencyKey, argv } = input;
4580
5167
  const recoverCommand = recoverCommandFor(operation, idempotencyKey);