image-skill 0.1.42 → 0.1.43

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,18 @@ 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.43 - 2026-06-12
8
+
9
+ - Feature (recovery): `doctor --json` now reports `data.in_flight` with
10
+ outstanding live-spend breadcrumbs, idempotency keys, TTL state, sweep
11
+ eligibility, and copy-runnable recovery commands.
12
+ - Feature (recovery): `doctor --sweep-in-flight --json` explicitly removes
13
+ only sweep-eligible stale breadcrumbs after the long grace window; plain
14
+ `doctor` remains inspect-only.
15
+ - Docs (recovery): the CLI contract now documents the stderr `in_flight` JSON
16
+ diagnostic emitted by live create/edit before the blocking request, including
17
+ the `2>&1` parsing caveat for combined-stream consumers.
18
+
7
19
  ## 0.1.42 - 2026-06-12
8
20
 
9
21
  - Feature (distribution): the public repo/package now ships a root `SKILL.md`
@@ -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.43";
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",
@@ -327,7 +337,8 @@ function commandHelpByKey(key) {
327
337
  usage: "image-skill doctor --json",
328
338
  docs_url: "https://image-skill.com/cli.md#image-skill-doctor",
329
339
  description:
330
- "Check hosted API reachability, CLI version, auth state, and health.",
340
+ "Check hosted API reachability, CLI version, auth state, health, and live-spend recovery breadcrumbs.",
341
+ optional_flags: ["--sweep-in-flight"],
331
342
  },
332
343
  trust: {
333
344
  command: "image-skill trust help",
@@ -606,6 +617,10 @@ async function doctor(argv) {
606
617
  const args = parseArgs(argv);
607
618
  const apiBaseUrl = apiBase(args);
608
619
  const config = await readConfig(configPath());
620
+ const inFlight = await inFlightSpendDoctorReport({
621
+ sweep: flagBool(args, "sweep-in-flight"),
622
+ now: new Date(),
623
+ });
609
624
  const health = await apiRequest({
610
625
  command: "image-skill doctor",
611
626
  method: "GET",
@@ -628,6 +643,7 @@ async function doctor(argv) {
628
643
  saved_token: config.tokenPresent,
629
644
  env_token: hasEnvToken(),
630
645
  },
646
+ in_flight: inFlight,
631
647
  docs: {
632
648
  skill: "https://image-skill.com/skill.md",
633
649
  llms: "https://image-skill.com/llms.txt",
@@ -4575,6 +4591,169 @@ function inFlightSpendFileName(idempotencyKey) {
4575
4591
  return `${trimmed.length === 0 ? "key" : trimmed}.json`;
4576
4592
  }
4577
4593
 
4594
+ async function inFlightSpendDoctorReport(input) {
4595
+ const dir = inFlightSpendDir();
4596
+ const now = input.now ?? new Date();
4597
+ const files = await readdir(dir).catch((error) => {
4598
+ if (error?.code === "ENOENT") {
4599
+ return [];
4600
+ }
4601
+ return null;
4602
+ });
4603
+ if (files === null) {
4604
+ return {
4605
+ schema: "image-skill.in-flight-spend-report.v1",
4606
+ directory: dir,
4607
+ count: null,
4608
+ recoverable_count: null,
4609
+ ttl_elapsed_count: null,
4610
+ sweep_eligible_count: null,
4611
+ invalid_count: null,
4612
+ entries: [],
4613
+ error: "in-flight directory could not be read",
4614
+ reservation_ttl_ms: IN_FLIGHT_RESERVATION_TTL_MS,
4615
+ sweep_after_ms: IN_FLIGHT_SWEEP_AFTER_MS,
4616
+ swept_count: 0,
4617
+ sweep_requested: input.sweep === true,
4618
+ };
4619
+ }
4620
+
4621
+ const entries = [];
4622
+ let invalidCount = 0;
4623
+ let sweptCount = 0;
4624
+ for (const file of files.sort()) {
4625
+ if (!file.endsWith(".json")) {
4626
+ continue;
4627
+ }
4628
+ const path = join(dir, file);
4629
+ const entry = await readInFlightSpendEntry({ path, file, now });
4630
+ if (entry === null) {
4631
+ invalidCount += 1;
4632
+ continue;
4633
+ }
4634
+ if (input.sweep === true && entry.sweep_eligible === true) {
4635
+ await rm(path, { force: true }).catch(() => {});
4636
+ sweptCount += 1;
4637
+ continue;
4638
+ }
4639
+ entries.push(entry);
4640
+ }
4641
+
4642
+ return {
4643
+ schema: "image-skill.in-flight-spend-report.v1",
4644
+ directory: dir,
4645
+ count: entries.length,
4646
+ recoverable_count: entries.filter((entry) => entry.state === "recoverable")
4647
+ .length,
4648
+ ttl_elapsed_count: entries.filter((entry) => entry.state === "ttl_elapsed")
4649
+ .length,
4650
+ sweep_eligible_count: entries.filter((entry) => entry.sweep_eligible)
4651
+ .length,
4652
+ invalid_count: invalidCount,
4653
+ swept_count: sweptCount,
4654
+ reservation_ttl_ms: IN_FLIGHT_RESERVATION_TTL_MS,
4655
+ sweep_after_ms: IN_FLIGHT_SWEEP_AFTER_MS,
4656
+ sweep_requested: input.sweep === true,
4657
+ entries,
4658
+ note:
4659
+ entries.length === 0
4660
+ ? "no in-flight live spend breadcrumbs found"
4661
+ : "rerun an entry's recover_command to settle or inspect a maybe-reserved spend before sweeping it",
4662
+ };
4663
+ }
4664
+
4665
+ async function readInFlightSpendEntry({ path, file, now }) {
4666
+ let parsed;
4667
+ let fileStat;
4668
+ try {
4669
+ parsed = JSON.parse(await readFile(path, "utf8"));
4670
+ fileStat = await stat(path);
4671
+ } catch {
4672
+ return null;
4673
+ }
4674
+ if (
4675
+ parsed?.schema !== "image-skill.in-flight-spend.v1" ||
4676
+ typeof parsed.idempotency_key !== "string" ||
4677
+ typeof parsed.operation !== "string"
4678
+ ) {
4679
+ return null;
4680
+ }
4681
+
4682
+ const startedAt =
4683
+ typeof parsed.started_at === "string" ? parsed.started_at : null;
4684
+ const startedTime =
4685
+ startedAt === null ? Number.NaN : new Date(startedAt).getTime();
4686
+ const fallbackTime = fileStat.mtime.getTime();
4687
+ const basisTime = Number.isFinite(startedTime) ? startedTime : fallbackTime;
4688
+ const ageMs = Math.max(0, now.getTime() - basisTime);
4689
+ const state =
4690
+ ageMs >= IN_FLIGHT_RESERVATION_TTL_MS ? "ttl_elapsed" : "recoverable";
4691
+ const sweepEligible = ageMs >= IN_FLIGHT_SWEEP_AFTER_MS;
4692
+ const argv = Array.isArray(parsed.argv)
4693
+ ? parsed.argv.filter((value) => typeof value === "string")
4694
+ : [];
4695
+ const recoverCommand = renderRecoverCommand({
4696
+ operation: parsed.operation,
4697
+ argv,
4698
+ idempotencyKey: parsed.idempotency_key,
4699
+ fallback: parsed.recover_command,
4700
+ });
4701
+
4702
+ return {
4703
+ file,
4704
+ path,
4705
+ operation: parsed.operation,
4706
+ command:
4707
+ typeof parsed.command === "string"
4708
+ ? parsed.command
4709
+ : `image-skill ${parsed.operation}`,
4710
+ idempotency_key: parsed.idempotency_key,
4711
+ started_at: startedAt,
4712
+ age_ms: ageMs,
4713
+ state,
4714
+ sweep_eligible: sweepEligible,
4715
+ recover_command: recoverCommand,
4716
+ original_recover_command:
4717
+ typeof parsed.recover_command === "string"
4718
+ ? parsed.recover_command
4719
+ : null,
4720
+ warning:
4721
+ state === "recoverable"
4722
+ ? "the hosted reservation TTL has not elapsed; recover before cleanup"
4723
+ : sweepEligible
4724
+ ? "reservation TTL has long elapsed; recover first if the original result still matters, or run doctor --sweep-in-flight to remove this breadcrumb"
4725
+ : "reservation TTL has elapsed; recover if you need the result, otherwise leave it until it becomes sweep-eligible",
4726
+ };
4727
+ }
4728
+
4729
+ function renderRecoverCommand(input) {
4730
+ const argv = withRecoveryArgs(input.argv, input.idempotencyKey);
4731
+ if (argv.length === 0 && typeof input.fallback === "string") {
4732
+ return input.fallback;
4733
+ }
4734
+ return renderImageSkillCommand(input.operation, argv);
4735
+ }
4736
+
4737
+ function withRecoveryArgs(argv, idempotencyKey) {
4738
+ const args = [...argv];
4739
+ const hasIdempotency = args.some(
4740
+ (arg) =>
4741
+ arg === "--idempotency-key" || arg.startsWith("--idempotency-key="),
4742
+ );
4743
+ if (!hasIdempotency) {
4744
+ args.push("--idempotency-key", idempotencyKey);
4745
+ }
4746
+ const hasJson = args.some((arg) => arg === "--json");
4747
+ if (!hasJson) {
4748
+ args.push("--json");
4749
+ }
4750
+ return args;
4751
+ }
4752
+
4753
+ function renderImageSkillCommand(operation, argv) {
4754
+ return ["image-skill", operation, ...argv.map(shellQuote)].join(" ");
4755
+ }
4756
+
4578
4757
  async function recordInFlightSpend(input) {
4579
4758
  const { command, operation, idempotencyKey, argv } = input;
4580
4759
  const recoverCommand = recoverCommandFor(operation, idempotencyKey);
package/cli.md CHANGED
@@ -52,6 +52,16 @@ Checks thin CLI/client health, hosted service reachability, auth state, local ou
52
52
  image-skill doctor --json
53
53
  ```
54
54
 
55
+ `doctor` also reports `data.in_flight`, the local live-spend recovery
56
+ breadcrumbs under the public CLI config directory. Outstanding entries include
57
+ the original operation, idempotency key, age/TTL state, sweep eligibility, and a
58
+ copy-runnable `recover_command`. Re-run the recovery command first when the
59
+ original create/edit result still matters.
60
+
61
+ Use `image-skill doctor --sweep-in-flight --json` to remove only
62
+ sweep-eligible stale breadcrumbs after the long grace window. Plain `doctor`
63
+ never deletes recovery breadcrumbs.
64
+
55
65
  ### `image-skill trust`
56
66
 
57
67
  Returns a no-auth, no-spend evidence packet for tool selection and package
@@ -1229,6 +1239,24 @@ generated `--idempotency-key` into its advertised create `next_command`, and a
1229
1239
  retryable create error returns an `error.recovery.idempotency_key` plus an
1230
1240
  `error.recovery.suggested_command` that re-runs the same create with that key.
1231
1241
 
1242
+ Live non-dry-run create/edit emits one JSON diagnostic line to stderr before
1243
+ the blocking hosted request:
1244
+
1245
+ ```json
1246
+ {
1247
+ "in_flight": {
1248
+ "command": "image-skill create",
1249
+ "idempotency_key": "create-...",
1250
+ "recover_command": "image-skill create --idempotency-key create-... <same arguments> --json"
1251
+ }
1252
+ }
1253
+ ```
1254
+
1255
+ stdout remains the command JSON envelope. If an agent combines streams with
1256
+ `2>&1`, split stderr diagnostics from the stdout envelope before parsing. The
1257
+ same recovery breadcrumb is stored under `<config-dir>/in-flight/` and appears
1258
+ in `image-skill doctor --json` at `data.in_flight`.
1259
+
1232
1260
  ```bash
1233
1261
  image-skill create \
1234
1262
  --prompt "A compact field camera on a stainless workbench" \
@@ -1470,6 +1498,8 @@ reuses the same key does not create a second credit reservation, so a transient
1470
1498
  `502`/`PROVIDER_FAILURE` after a reservation cannot double-charge; a retryable
1471
1499
  edit error returns an `error.recovery.idempotency_key` and an
1472
1500
  `error.recovery.suggested_command` that re-runs the same edit with that key.
1501
+ Live non-dry-run edit emits the same stderr `in_flight` diagnostic and local
1502
+ doctor-visible recovery breadcrumb as create.
1473
1503
 
1474
1504
  ### `image-skill assets show`
1475
1505
 
package/commands.json CHANGED
@@ -195,7 +195,8 @@
195
195
  "command": "image-skill doctor help",
196
196
  "usage": "image-skill doctor --json",
197
197
  "docs_url": "https://image-skill.com/cli.md#image-skill-doctor",
198
- "description": "Check hosted API reachability, CLI version, auth state, and health."
198
+ "description": "Check hosted API reachability, CLI version, auth state, health, and live-spend recovery breadcrumbs.",
199
+ "optional_flags": ["--sweep-in-flight"]
199
200
  }
200
201
  },
201
202
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-skill",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
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,
@@ -52,6 +52,16 @@ Checks thin CLI/client health, hosted service reachability, auth state, local ou
52
52
  image-skill doctor --json
53
53
  ```
54
54
 
55
+ `doctor` also reports `data.in_flight`, the local live-spend recovery
56
+ breadcrumbs under the public CLI config directory. Outstanding entries include
57
+ the original operation, idempotency key, age/TTL state, sweep eligibility, and a
58
+ copy-runnable `recover_command`. Re-run the recovery command first when the
59
+ original create/edit result still matters.
60
+
61
+ Use `image-skill doctor --sweep-in-flight --json` to remove only
62
+ sweep-eligible stale breadcrumbs after the long grace window. Plain `doctor`
63
+ never deletes recovery breadcrumbs.
64
+
55
65
  ### `image-skill trust`
56
66
 
57
67
  Returns a no-auth, no-spend evidence packet for tool selection and package
@@ -1229,6 +1239,24 @@ generated `--idempotency-key` into its advertised create `next_command`, and a
1229
1239
  retryable create error returns an `error.recovery.idempotency_key` plus an
1230
1240
  `error.recovery.suggested_command` that re-runs the same create with that key.
1231
1241
 
1242
+ Live non-dry-run create/edit emits one JSON diagnostic line to stderr before
1243
+ the blocking hosted request:
1244
+
1245
+ ```json
1246
+ {
1247
+ "in_flight": {
1248
+ "command": "image-skill create",
1249
+ "idempotency_key": "create-...",
1250
+ "recover_command": "image-skill create --idempotency-key create-... <same arguments> --json"
1251
+ }
1252
+ }
1253
+ ```
1254
+
1255
+ stdout remains the command JSON envelope. If an agent combines streams with
1256
+ `2>&1`, split stderr diagnostics from the stdout envelope before parsing. The
1257
+ same recovery breadcrumb is stored under `<config-dir>/in-flight/` and appears
1258
+ in `image-skill doctor --json` at `data.in_flight`.
1259
+
1232
1260
  ```bash
1233
1261
  image-skill create \
1234
1262
  --prompt "A compact field camera on a stainless workbench" \
@@ -1470,6 +1498,8 @@ reuses the same key does not create a second credit reservation, so a transient
1470
1498
  `502`/`PROVIDER_FAILURE` after a reservation cannot double-charge; a retryable
1471
1499
  edit error returns an `error.recovery.idempotency_key` and an
1472
1500
  `error.recovery.suggested_command` that re-runs the same edit with that key.
1501
+ Live non-dry-run edit emits the same stderr `in_flight` diagnostic and local
1502
+ doctor-visible recovery breadcrumb as create.
1473
1503
 
1474
1504
  ### `image-skill assets show`
1475
1505
 
@@ -195,7 +195,8 @@
195
195
  "command": "image-skill doctor help",
196
196
  "usage": "image-skill doctor --json",
197
197
  "docs_url": "https://image-skill.com/cli.md#image-skill-doctor",
198
- "description": "Check hosted API reachability, CLI version, auth state, and health."
198
+ "description": "Check hosted API reachability, CLI version, auth state, health, and live-spend recovery breadcrumbs.",
199
+ "optional_flags": ["--sweep-in-flight"]
199
200
  }
200
201
  },
201
202
  {