pixelkiln 0.82.0 → 0.84.0

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/dist/index.js CHANGED
@@ -9,6 +9,7 @@ var RevisionModeSchema = z.enum([
9
9
  "outpaint",
10
10
  "reduce-colors",
11
11
  "correct-pixelart",
12
+ "remove-background",
12
13
  "animate",
13
14
  "animate-pixminimax",
14
15
  "animate-skeleton",
@@ -340,7 +341,22 @@ var RevisionSchema = z.object({
340
341
  * `interpolate`: required, the ending keyframe; the parent is the start.
341
342
  */
342
343
  lastFrame: z.string().min(1).optional(),
343
- /** `animate-pixminimax` only: facing direction, used only alongside `enhancePrompt` to hold the sprite's facing. */
344
+ /**
345
+ * `animate`/`animate-pixminimax` only: colour de-flicker sensitivity.
346
+ * Frames whose foreground drifts from the first frame beyond this are
347
+ * corrected toward it; 0 corrects every frame and higher values correct
348
+ * fewer. Omit for PixelLab's default.
349
+ */
350
+ driftThreshold: z.number().min(0).optional(),
351
+ /**
352
+ * `animate-pixminimax` only: one noun phrase for what the parent shows.
353
+ * PixelLab reads it off the image when omitted, and can decline some
354
+ * images, failing the job until it is sent.
355
+ */
356
+ subjectDescription: z.string().min(1).max(300).optional(),
357
+ /** `animate-pixminimax` only: the pose the parent is in, read off the image by PixelLab when omitted. */
358
+ initialPose: z.string().min(1).max(300).optional(),
359
+ /** `animate-pixminimax` only: facing direction, one of the model's caption fields, so it also holds the sprite's facing. */
344
360
  direction: CharacterDirectionSchema.optional(),
345
361
  /** `animate`/`animate-pixminimax` only: let PixelLab expand the action into a fuller motion description first. */
346
362
  enhancePrompt: z.boolean().optional(),
@@ -360,12 +376,15 @@ var RevisionSchema = z.object({
360
376
  * same `mannequin`/`bear`/`cat`/`dog`/`horse`/`lion` vocabulary.
361
377
  */
362
378
  skeletonTemplate: z.string().min(1).optional(),
379
+ /** `remove-background` only: `simple` is faster and suits flat backgrounds; `complex` handles detailed edges. PixelLab defaults to `simple`. */
380
+ removalTask: z.enum(["simple", "complex"]).optional(),
363
381
  /**
364
- * `animate-skeleton` only: what the character *looks like*, a noun
382
+ * `animate-skeleton`: what the character *looks like*, a noun
365
383
  * phrase (colours, clothing, held items) — distinct from the asset's
366
384
  * own `prompt`, which this mode sends as the motion's short label
367
385
  * (PixelLab's `action`), the same `prompt`-as-motion convention
368
- * `animate`/`animate-pixminimax` already use.
386
+ * `animate`/`animate-pixminimax` already use. `remove-background`: a
387
+ * short description of the foreground, which PixelLab uses as a hint.
369
388
  */
370
389
  description: z.string().min(1).optional(),
371
390
  /**
@@ -482,7 +501,44 @@ var RevisionSchema = z.object({
482
501
  path: ["keypointsFile"]
483
502
  });
484
503
  }
485
- for (const field of ["keypointsFile", "skeletonTemplate", "description"]) {
504
+ if (revision.driftThreshold !== void 0 && revision.mode !== "animate" && revision.mode !== "animate-pixminimax") {
505
+ context.addIssue({
506
+ code: z.ZodIssueCode.custom,
507
+ message: "driftThreshold applies to animate/animate-pixminimax revisions only",
508
+ path: ["driftThreshold"]
509
+ });
510
+ }
511
+ for (const field of ["subjectDescription", "initialPose"]) {
512
+ if (revision[field] !== void 0 && revision.mode !== "animate-pixminimax") {
513
+ context.addIssue({
514
+ code: z.ZodIssueCode.custom,
515
+ message: `${field} applies to animate-pixminimax revisions only`,
516
+ path: [field]
517
+ });
518
+ }
519
+ }
520
+ if (revision.removalTask !== void 0 && revision.mode !== "remove-background") {
521
+ context.addIssue({
522
+ code: z.ZodIssueCode.custom,
523
+ message: "removalTask applies to remove-background revisions only",
524
+ path: ["removalTask"]
525
+ });
526
+ }
527
+ if (revision.strength !== void 0 && revision.mode === "remove-background") {
528
+ context.addIssue({
529
+ code: z.ZodIssueCode.custom,
530
+ message: "remove-background revisions do not take a strength",
531
+ path: ["strength"]
532
+ });
533
+ }
534
+ if (revision.description !== void 0 && revision.mode !== "animate-skeleton" && revision.mode !== "remove-background") {
535
+ context.addIssue({
536
+ code: z.ZodIssueCode.custom,
537
+ message: "description applies to animate-skeleton/remove-background revisions only",
538
+ path: ["description"]
539
+ });
540
+ }
541
+ for (const field of ["keypointsFile", "skeletonTemplate"]) {
486
542
  if (revision[field] !== void 0 && revision.mode !== "animate-skeleton") {
487
543
  context.addIssue({
488
544
  code: z.ZodIssueCode.custom,
@@ -1136,6 +1192,10 @@ var LockEntrySchema = z.object({
1136
1192
  keypointsSha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
1137
1193
  skeletonTemplate: z.string().min(1).optional(),
1138
1194
  description: z.string().min(1).optional(),
1195
+ removalTask: z.enum(["simple", "complex"]).optional(),
1196
+ driftThreshold: z.number().min(0).optional(),
1197
+ subjectDescription: z.string().min(1).max(300).optional(),
1198
+ initialPose: z.string().min(1).max(300).optional(),
1139
1199
  engine: z.enum(["pro-flash"]).optional()
1140
1200
  }).strict().nullable().default(null),
1141
1201
  /** For a mirror: the asset it flips and a hash over that asset's output hashes when this was made. */
@@ -1564,6 +1624,7 @@ var CorrectPixelartResponseSchema = z3.object({
1564
1624
  images: z3.array(z3.object({ base64: z3.string().min(1) }).passthrough()).min(1),
1565
1625
  usage: z3.unknown().optional()
1566
1626
  }).passthrough();
1627
+ var RemoveBackgroundResponseSchema = z3.object({ image: z3.object({ base64: z3.string().min(1) }).passthrough(), usage: z3.unknown().optional() }).passthrough();
1567
1628
  var ProFlashImageSubmitSchema = z3.object({
1568
1629
  background_job_id: z3.string().min(1),
1569
1630
  image_id: z3.string().min(1),
@@ -1755,9 +1816,9 @@ var PixelLabClient = class {
1755
1816
  apiKey;
1756
1817
  http;
1757
1818
  /** Retries transport failures, 429, and 5xx; see http.ts for the policy. */
1758
- async request(path36, init) {
1819
+ async request(path37, init) {
1759
1820
  const auth = this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`;
1760
- const res = await this.http(`${BASE}${path36}`, {
1821
+ const res = await this.http(`${BASE}${path37}`, {
1761
1822
  ...init,
1762
1823
  headers: {
1763
1824
  Authorization: auth,
@@ -1767,13 +1828,13 @@ var PixelLabClient = class {
1767
1828
  });
1768
1829
  const text = await res.text();
1769
1830
  if (!res.ok) {
1770
- throw new PixelLabError(`${init?.method ?? "GET"} ${path36} \u2192 ${res.status}`, res.status, text);
1831
+ throw new PixelLabError(`${init?.method ?? "GET"} ${path37} \u2192 ${res.status}`, res.status, text);
1771
1832
  }
1772
1833
  if (!text) return {};
1773
1834
  try {
1774
1835
  return JSON.parse(text);
1775
1836
  } catch {
1776
- throw new Error(`${init?.method ?? "GET"} ${path36} returned invalid JSON`);
1837
+ throw new Error(`${init?.method ?? "GET"} ${path37} returned invalid JSON`);
1777
1838
  }
1778
1839
  }
1779
1840
  async balance() {
@@ -2178,10 +2239,10 @@ var PixelLabClient = class {
2178
2239
  const palette = args.paletteSwatchBase64 ? { color_image: { type: "base64", base64: args.paletteSwatchBase64, format: "png" }, force_colors: true } : {};
2179
2240
  const encode = (image) => ({ type: "base64", base64: image.base64, format: image.format });
2180
2241
  const south = args.reference?.south;
2181
- let path36;
2242
+ let path37;
2182
2243
  let body;
2183
2244
  if (args.mode === "v3") {
2184
- path36 = "/create-character-v3";
2245
+ path37 = "/create-character-v3";
2185
2246
  body = {
2186
2247
  description: args.description,
2187
2248
  image_size: imageSize,
@@ -2195,7 +2256,7 @@ var PixelLabClient = class {
2195
2256
  ...args.enhancePrompt ? { enhance_prompt: true } : {}
2196
2257
  };
2197
2258
  } else if (args.mode === "pro-flash") {
2198
- path36 = "/create-character-pro-flash";
2259
+ path37 = "/create-character-pro-flash";
2199
2260
  const traits = args.styleTraits;
2200
2261
  body = {
2201
2262
  description: args.description,
@@ -2219,7 +2280,7 @@ var PixelLabClient = class {
2219
2280
  } : {}
2220
2281
  };
2221
2282
  } else if (args.mode === "pro") {
2222
- path36 = "/create-character-pro";
2283
+ path37 = "/create-character-pro";
2223
2284
  body = {
2224
2285
  description: args.description,
2225
2286
  image_size: imageSize,
@@ -2233,7 +2294,7 @@ var PixelLabClient = class {
2233
2294
  ...!south && args.styleCharacterId ? { style_character_id: args.styleCharacterId } : {}
2234
2295
  };
2235
2296
  } else {
2236
- path36 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2297
+ path37 = args.directions === 4 ? "/create-character-with-4-directions" : "/create-character-with-8-directions";
2237
2298
  body = {
2238
2299
  description: args.description,
2239
2300
  image_size: imageSize,
@@ -2250,8 +2311,8 @@ var PixelLabClient = class {
2250
2311
  ...palette
2251
2312
  };
2252
2313
  }
2253
- const raw = await this.request(path36, { method: "POST", body: JSON.stringify(body) });
2254
- return validateResponse(CharacterSubmitSchema, raw, path36.slice(1));
2314
+ const raw = await this.request(path37, { method: "POST", body: JSON.stringify(body) });
2315
+ return validateResponse(CharacterSubmitSchema, raw, path37.slice(1));
2255
2316
  }
2256
2317
  /** A text edit of an existing character, applied to every direction and stored as a sibling. */
2257
2318
  async createCharacterState(args) {
@@ -2628,6 +2689,27 @@ var PixelLabClient = class {
2628
2689
  const pngs = cleanupResults(res.images, images.length, "correct-pixelart");
2629
2690
  return { png: pngs[0], pngs, usage: res.usage };
2630
2691
  }
2692
+ /**
2693
+ * `/remove-background`: a transparent PNG from one pixel-art image,
2694
+ * synchronously, like the rest of the Cleanup tier. The API takes at most
2695
+ * 400x400 worth of area, one image per call. `simple` is faster and suits
2696
+ * flat backgrounds; `complex` is slower and handles detailed edges. The
2697
+ * schema's response example is dollar-denominated (`usd: 0.01`); the
2698
+ * subscription cost measured 1 generation (docs/ENDPOINTS.md), and the
2699
+ * default `simple` task is the one measured.
2700
+ */
2701
+ async removeBackground(args) {
2702
+ const body = { image: args.image, image_size: { width: args.width, height: args.height } };
2703
+ if (args.task) body.background_removal_task = args.task === "complex" ? "remove_complex_background" : "remove_simple_background";
2704
+ if (args.text) body.text = args.text;
2705
+ if (args.seed != null) body.seed = args.seed;
2706
+ const res = validateResponse(
2707
+ RemoveBackgroundResponseSchema,
2708
+ await this.request("/remove-background", { method: "POST", body: JSON.stringify(body) }),
2709
+ "remove-background"
2710
+ );
2711
+ return { png: Buffer.from(res.image.base64, "base64"), usage: res.usage };
2712
+ }
2631
2713
  /**
2632
2714
  * `/unzoom`: recover native-resolution pixel art from an upscaled image (a
2633
2715
  * 32x32 sprite saved at 512x512). Synchronous like the Cleanup tier.
@@ -2736,6 +2818,7 @@ var PixelLabClient = class {
2736
2818
  */
2737
2819
  async animateWithTextV3(args) {
2738
2820
  const body = { first_frame: args.firstFrame, action: args.action };
2821
+ if (args.driftThreshold != null) body.drift_threshold = args.driftThreshold;
2739
2822
  if (args.lastFrame) body.last_frame = args.lastFrame;
2740
2823
  if (args.frameCount != null) body.frame_count = args.frameCount;
2741
2824
  if (args.seed != null) body.seed = args.seed;
@@ -2755,6 +2838,10 @@ var PixelLabClient = class {
2755
2838
  */
2756
2839
  async animatePixminimax(args) {
2757
2840
  const body = { first_frame: args.firstFrame, description: args.description };
2841
+ if (args.view) body.view = args.view;
2842
+ if (args.subjectDescription) body.subject_description = args.subjectDescription;
2843
+ if (args.initialPose) body.initial_pose = args.initialPose;
2844
+ if (args.driftThreshold != null) body.drift_threshold = args.driftThreshold;
2758
2845
  if (args.lastFrame) body.last_frame = args.lastFrame;
2759
2846
  if (args.frameCount != null) body.frame_count = args.frameCount;
2760
2847
  if (args.seed != null) body.seed = args.seed;
@@ -2790,6 +2877,7 @@ var PixelLabClient = class {
2790
2877
  keypoints: args.keypoints,
2791
2878
  direction: args.direction
2792
2879
  };
2880
+ if (args.view) body.view = args.view;
2793
2881
  if (args.templateId) body.template_id = args.templateId;
2794
2882
  if (args.action) body.action = args.action;
2795
2883
  if (args.description) body.description = args.description;
@@ -3437,10 +3525,10 @@ import { createReadStream } from "fs";
3437
3525
  function sha256(data) {
3438
3526
  return createHash("sha256").update(data).digest("hex");
3439
3527
  }
3440
- async function sha256File(path36) {
3528
+ async function sha256File(path37) {
3441
3529
  return new Promise((resolve, reject) => {
3442
3530
  const hash = createHash("sha256");
3443
- const stream = createReadStream(path36);
3531
+ const stream = createReadStream(path37);
3444
3532
  stream.on("data", (chunk2) => hash.update(chunk2));
3445
3533
  stream.on("error", reject);
3446
3534
  stream.on("end", () => resolve(hash.digest("hex")));
@@ -3564,6 +3652,10 @@ function specHash(spec, styleImageHashes, providerOptionIdentity = spec.provider
3564
3652
  keypointsSha256: spec.revision.keypointsSha256,
3565
3653
  skeletonTemplate: spec.revision.skeletonTemplate,
3566
3654
  description: spec.revision.description,
3655
+ removalTask: spec.revision.removalTask,
3656
+ driftThreshold: spec.revision.driftThreshold,
3657
+ subjectDescription: spec.revision.subjectDescription,
3658
+ initialPose: spec.revision.initialPose,
3567
3659
  engine: spec.revision.engine
3568
3660
  } : void 0
3569
3661
  })
@@ -3739,7 +3831,7 @@ var PixelLabProvider = class _PixelLabProvider {
3739
3831
  * outpaint path yet.
3740
3832
  */
3741
3833
  supportsRevision(mode2) {
3742
- return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "animate" || mode2 === "animate-pixminimax" || mode2 === "animate-skeleton" || mode2 === "interpolate" || mode2 === "edit-animation";
3834
+ return mode2 === "inpaint" || mode2 === "image-to-image" || mode2 === "reduce-colors" || mode2 === "correct-pixelart" || mode2 === "remove-background" || mode2 === "animate" || mode2 === "animate-pixminimax" || mode2 === "animate-skeleton" || mode2 === "interpolate" || mode2 === "edit-animation";
3743
3835
  }
3744
3836
  /** PixelLab's own constraints: submissions must be >2s apart, and
3745
3837
  * background jobs in flight are capped by subscription tier (Tier 1=8,
@@ -3767,6 +3859,9 @@ var PixelLabProvider = class _PixelLabProvider {
3767
3859
  if (spec.batch?.role === "member") {
3768
3860
  return { unit: "generations", amount: 0, candidates: 1 };
3769
3861
  }
3862
+ if (spec.revision?.mode === "remove-background") {
3863
+ return { unit: "generations", amount: 1, candidates: 1 };
3864
+ }
3770
3865
  if (spec.revision?.mode === "reduce-colors" || spec.revision?.mode === "correct-pixelart") {
3771
3866
  return { unit: "generations", amount: 0.1, candidates: 1 };
3772
3867
  }
@@ -3855,6 +3950,12 @@ var PixelLabProvider = class _PixelLabProvider {
3855
3950
  throw new Error(`PixelLab correct-pixelart source is ${width}x${height}; the API takes at most 1024 pixels per side`);
3856
3951
  }
3857
3952
  }
3953
+ if (spec.revision?.mode === "remove-background") {
3954
+ const { sourceWidth: width, sourceHeight: height } = spec.revision;
3955
+ if (width != null && height != null && (width > 400 || height > 400)) {
3956
+ throw new Error(`PixelLab remove-background source is ${width}x${height}; the API takes at most 400x400`);
3957
+ }
3958
+ }
3858
3959
  if (spec.revision?.mode === "animate-skeleton") {
3859
3960
  const { sourceWidth: width, sourceHeight: height, frames, skeletonTemplate } = spec.revision;
3860
3961
  if (width != null && height != null && (width > 256 || height > 256)) {
@@ -3875,6 +3976,9 @@ var PixelLabProvider = class _PixelLabProvider {
3875
3976
  if (mode2 === "animate" && frames != null && frames > 16) {
3876
3977
  throw new Error(`PixelLab animate takes 4 to 16 frames; ${frames} is too many (use animate-pixminimax for up to 40)`);
3877
3978
  }
3979
+ if (mode2 === "animate-pixminimax" && frames != null && frames % 4 !== 0) {
3980
+ throw new Error(`PixelLab animate-pixminimax takes a multiple of 4 frames (4 to 40); ${frames} is not one`);
3981
+ }
3878
3982
  if (lastFrameWidth != null && lastFrameHeight != null && width != null && height != null && (lastFrameWidth !== width || lastFrameHeight !== height)) {
3879
3983
  throw new Error(
3880
3984
  `PixelLab ${mode2} lastFrame is ${lastFrameWidth}x${lastFrameHeight}; source is ${width}x${height} \u2014 they must match`
@@ -4966,6 +5070,18 @@ var PixelLabProvider = class _PixelLabProvider {
4966
5070
  const res2 = await this.client.correctPixelart({ images, strength: revision.strength });
4967
5071
  return this.cacheCleanupResult(res2.pngs, res2.usage, roles, revision.sourceFps);
4968
5072
  }
5073
+ if (revision.mode === "remove-background") {
5074
+ if (width == null || height == null) throw new Error(`${spec.styleId}/${spec.assetId}: remove-background needs the source's size`);
5075
+ const res2 = await this.client.removeBackground({
5076
+ image,
5077
+ width,
5078
+ height,
5079
+ task: revision.removalTask,
5080
+ text: revision.description,
5081
+ seed: spec.seed
5082
+ });
5083
+ return this.cacheCleanupResult([res2.png], res2.usage, roles, revision.sourceFps);
5084
+ }
4969
5085
  if (revision.mode === "interpolate") {
4970
5086
  const lastFrame = this.readLastFrame(spec);
4971
5087
  if (!lastFrame) throw new Error(`${spec.styleId}/${spec.assetId}: interpolate needs lastFrame, the ending keyframe`);
@@ -5001,7 +5117,8 @@ var PixelLabProvider = class _PixelLabProvider {
5001
5117
  frameCount: revision.frames,
5002
5118
  seed: spec.seed,
5003
5119
  noBackground: spec.noBackground,
5004
- enhancePrompt: revision.enhancePrompt
5120
+ enhancePrompt: revision.enhancePrompt,
5121
+ driftThreshold: revision.driftThreshold
5005
5122
  });
5006
5123
  return { jobId: res3.background_job_id };
5007
5124
  }
@@ -5013,7 +5130,11 @@ var PixelLabProvider = class _PixelLabProvider {
5013
5130
  seed: spec.seed,
5014
5131
  noBackground: spec.noBackground,
5015
5132
  enhancePrompt: revision.enhancePrompt,
5016
- direction: revision.direction
5133
+ direction: revision.direction,
5134
+ ...spec.view === "side" || spec.view === "low top-down" || spec.view === "high top-down" ? { view: spec.view } : {},
5135
+ subjectDescription: revision.subjectDescription,
5136
+ initialPose: revision.initialPose,
5137
+ driftThreshold: revision.driftThreshold
5017
5138
  });
5018
5139
  return { jobId: res2.background_job_id };
5019
5140
  }
@@ -5030,6 +5151,9 @@ var PixelLabProvider = class _PixelLabProvider {
5030
5151
  firstFrameKeypoints: revision.skeleton.firstFrameKeypoints,
5031
5152
  keypoints: revision.skeleton.frames,
5032
5153
  direction: revision.direction,
5154
+ // The style's camera, when it is one this endpoint knows; its enum is
5155
+ // narrower than a style's free-string `view`.
5156
+ ...spec.view === "side" || spec.view === "low top-down" || spec.view === "high top-down" ? { view: spec.view } : {},
5033
5157
  templateId: revision.skeletonTemplate,
5034
5158
  action: spec.prompt,
5035
5159
  description: revision.description,
@@ -5100,7 +5224,7 @@ var PixelLabProvider = class _PixelLabProvider {
5100
5224
  }
5101
5225
  async poll(jobId, generator, context) {
5102
5226
  const revisionMode = context?.spec?.revision?.mode;
5103
- if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart") {
5227
+ if (revisionMode === "reduce-colors" || revisionMode === "correct-pixelart" || revisionMode === "remove-background") {
5104
5228
  return this.pollCachedRevision(jobId, context);
5105
5229
  }
5106
5230
  if (revisionMode === "animate" || revisionMode === "animate-pixminimax" || revisionMode === "animate-skeleton" || revisionMode === "interpolate" || revisionMode === "edit-animation") {
@@ -5808,9 +5932,9 @@ var RetroDiffusionClient = class {
5808
5932
  if (!response.ok) throw new ProviderError("retrodiffusion", `Retro Diffusion download failed (${response.status})`, { status: response.status });
5809
5933
  return Buffer.from(await response.arrayBuffer());
5810
5934
  }
5811
- async call(path36, init = {}) {
5935
+ async call(path37, init = {}) {
5812
5936
  if (!this.token) throw new Error("RD_API_KEY is not set");
5813
- const response = await this.request(`${this.baseUrl}${path36}`, {
5937
+ const response = await this.request(`${this.baseUrl}${path37}`, {
5814
5938
  ...init,
5815
5939
  headers: {
5816
5940
  "Content-Type": "application/json",
@@ -8816,6 +8940,10 @@ async function resolveSpecs(loaded, filter) {
8816
8940
  } : {},
8817
8941
  ...asset.revision.skeletonTemplate ? { skeletonTemplate: asset.revision.skeletonTemplate } : {},
8818
8942
  ...asset.revision.description ? { description: asset.revision.description } : {},
8943
+ ...asset.revision.removalTask ? { removalTask: asset.revision.removalTask } : {},
8944
+ ...asset.revision.driftThreshold == null ? {} : { driftThreshold: asset.revision.driftThreshold },
8945
+ ...asset.revision.subjectDescription ? { subjectDescription: asset.revision.subjectDescription } : {},
8946
+ ...asset.revision.initialPose ? { initialPose: asset.revision.initialPose } : {},
8819
8947
  ...asset.revision.engine ? { engine: asset.revision.engine } : {}
8820
8948
  };
8821
8949
  }
@@ -12099,10 +12227,10 @@ function isSha256Hash(value) {
12099
12227
  function parseCache(value) {
12100
12228
  return HashCacheSchema.parse(value);
12101
12229
  }
12102
- async function loadCache(path36) {
12103
- if (!existsSync13(path36)) return { version: 1, hashes: {} };
12230
+ async function loadCache(path37) {
12231
+ if (!existsSync13(path37)) return { version: 1, hashes: {} };
12104
12232
  try {
12105
- const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile11(path36, "utf8")));
12233
+ const parsed = HashCacheSchema.safeParse(JSON.parse(await readFile11(path37, "utf8")));
12106
12234
  if (!parsed.success) return { version: 1, hashes: {} };
12107
12235
  return {
12108
12236
  version: 1,
@@ -12114,18 +12242,18 @@ async function loadCache(path36) {
12114
12242
  return { version: 1, hashes: {} };
12115
12243
  }
12116
12244
  }
12117
- async function saveCache(path36, cache) {
12245
+ async function saveCache(path37, cache) {
12118
12246
  const sorted = {};
12119
12247
  for (const key of Object.keys(cache.hashes).sort()) {
12120
12248
  const hash = cache.hashes[key];
12121
12249
  if (!isSha256Hash(hash)) throw new Error(`Refusing to cache invalid SHA-256 for ${key}`);
12122
12250
  sorted[key] = hash;
12123
12251
  }
12124
- await mkdir5(pathModule.dirname(pathModule.resolve(path36)), { recursive: true });
12125
- const tmp = `${path36}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12252
+ await mkdir5(pathModule.dirname(pathModule.resolve(path37)), { recursive: true });
12253
+ const tmp = `${path37}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
12126
12254
  try {
12127
12255
  await writeFile5(tmp, JSON.stringify({ version: 1, hashes: sorted }, null, 2) + "\n");
12128
- await rename4(tmp, path36);
12256
+ await rename4(tmp, path37);
12129
12257
  } finally {
12130
12258
  await rm4(tmp, { force: true });
12131
12259
  }
@@ -13641,6 +13769,10 @@ async function submit(provider, loaded, items, lock, lockPath, opts = {}) {
13641
13769
  ...spec.revision.keypointsSha256 ? { keypointsSha256: spec.revision.keypointsSha256 } : {},
13642
13770
  ...spec.revision.skeletonTemplate ? { skeletonTemplate: spec.revision.skeletonTemplate } : {},
13643
13771
  ...spec.revision.description ? { description: spec.revision.description } : {},
13772
+ ...spec.revision.removalTask ? { removalTask: spec.revision.removalTask } : {},
13773
+ ...spec.revision.driftThreshold == null ? {} : { driftThreshold: spec.revision.driftThreshold },
13774
+ ...spec.revision.subjectDescription ? { subjectDescription: spec.revision.subjectDescription } : {},
13775
+ ...spec.revision.initialPose ? { initialPose: spec.revision.initialPose } : {},
13644
13776
  ...spec.revision.engine ? { engine: spec.revision.engine } : {}
13645
13777
  } : null,
13646
13778
  outfit: spec.character?.kind === "outfit" && outfitParentEntry ? {
@@ -14930,9 +15062,9 @@ async function runPicker(provider, lock, lockPath, opts = {}) {
14930
15062
 
14931
15063
  // src/gallery/snapshot.ts
14932
15064
  import { createHash as createHash4 } from "crypto";
14933
- import { existsSync as existsSync25 } from "fs";
14934
- import { readFile as readFile23, stat as stat3 } from "fs/promises";
14935
- import path31 from "path";
15065
+ import { existsSync as existsSync26 } from "fs";
15066
+ import { readFile as readFile24, stat as stat3 } from "fs/promises";
15067
+ import path32 from "path";
14936
15068
 
14937
15069
  // src/workspace.ts
14938
15070
  import { mkdir as mkdir7, readFile as readFile18, rename as rename6, rm as rm7, writeFile as writeFile9 } from "fs/promises";
@@ -15068,15 +15200,16 @@ function validateWorkspace(ws, dir) {
15068
15200
  }
15069
15201
 
15070
15202
  // src/gallery/edit.ts
15071
- import { readFile as readFile22, rename as rename9, writeFile as writeFile14 } from "fs/promises";
15072
- import path30 from "path";
15073
- import { z as z12 } from "zod";
15203
+ import { readFile as readFile23, rename as rename9, writeFile as writeFile15 } from "fs/promises";
15204
+ import path31 from "path";
15205
+ import { z as z13 } from "zod";
15074
15206
 
15075
15207
  // src/manifest-edit.ts
15076
15208
  import { readFile as readFile19, rename as rename7, rm as rm8, writeFile as writeFile10 } from "fs/promises";
15077
15209
  import path26 from "path";
15078
15210
  import { z as z8 } from "zod";
15079
15211
  var HexSha = z8.string().regex(/^[0-9a-f]{64}$/);
15212
+ var ProjectFileSchema = z8.string().min(1).refine((file) => !path26.isAbsolute(file) && !file.split(/[\\/]/).includes(".."), "must be a path inside the project");
15080
15213
  var AssetPatchSchema = z8.object({
15081
15214
  prompt: z8.string().optional(),
15082
15215
  /** Per-style override for the style being viewed; null removes it. */
@@ -15125,10 +15258,11 @@ var NewAssetSchema = z8.object({
15125
15258
  styles: z8.array(z8.string().min(1)).optional(),
15126
15259
  /**
15127
15260
  * A controlled regeneration of another asset in the same manifest,
15128
- * instead of a fresh generation. `inpaint` needs a mask upload the
15129
- * gallery does not offer yet, so only `image-to-image` and
15130
- * `animate-skeleton` (whose keypoints file the gallery writes first) can
15131
- * be created here; hand-edit the manifest for the other modes.
15261
+ * instead of a fresh generation. Every mode but `outpaint` can be
15262
+ * created here: `inpaint` names a mask file the gallery's mask editor
15263
+ * wrote first, `interpolate` names an existing image as its ending
15264
+ * keyframe, and `animate-skeleton` names the keypoints file the gallery
15265
+ * wrote first.
15132
15266
  */
15133
15267
  revision: z8.discriminatedUnion("mode", [
15134
15268
  z8.object({
@@ -15137,6 +15271,52 @@ var NewAssetSchema = z8.object({
15137
15271
  from: z8.string().min(1),
15138
15272
  strength: z8.number().min(0).max(1).optional()
15139
15273
  }).strict(),
15274
+ z8.object({
15275
+ mode: z8.literal("reduce-colors"),
15276
+ from: z8.string().min(1),
15277
+ /** A target colour count; the gallery leaves PixelLab's default when this is omitted. */
15278
+ numColors: z8.number().int().min(2).max(256).optional(),
15279
+ dithering: RevisionDitheringSchema.optional(),
15280
+ ditheringStrength: z8.number().min(0).max(10).optional()
15281
+ }).strict(),
15282
+ z8.object({
15283
+ mode: z8.literal("inpaint"),
15284
+ from: z8.string().min(1),
15285
+ /** Black-and-white PNG the same size as the parent; white is repainted. */
15286
+ mask: ProjectFileSchema
15287
+ }).strict(),
15288
+ z8.object({
15289
+ mode: z8.literal("interpolate"),
15290
+ from: z8.string().min(1),
15291
+ /** The ending keyframe: an image already in the project, the parent's size. */
15292
+ lastFrame: ProjectFileSchema,
15293
+ fps: z8.number().int().min(1).max(60).optional()
15294
+ }).strict(),
15295
+ z8.object({ mode: z8.literal("correct-pixelart"), from: z8.string().min(1), strength: z8.number().min(0).max(1).optional() }).strict(),
15296
+ z8.object({
15297
+ mode: z8.literal("remove-background"),
15298
+ from: z8.string().min(1),
15299
+ removalTask: z8.enum(["simple", "complex"]).optional(),
15300
+ /** A short description of the foreground, sent as PixelLab's text hint. */
15301
+ description: z8.string().min(1).optional()
15302
+ }).strict(),
15303
+ z8.object({
15304
+ mode: z8.enum(["animate", "animate-pixminimax"]),
15305
+ from: z8.string().min(1),
15306
+ frames: z8.number().int().min(4).max(40).refine((n) => n % 2 === 0, "frames must be even").optional(),
15307
+ fps: z8.number().int().min(1).max(60).optional(),
15308
+ enhancePrompt: z8.boolean().optional(),
15309
+ driftThreshold: z8.number().min(0).optional(),
15310
+ /** `animate-pixminimax` only; the loader refuses these on `animate`. */
15311
+ direction: CharacterDirectionSchema.optional(),
15312
+ subjectDescription: z8.string().min(1).max(300).optional(),
15313
+ initialPose: z8.string().min(1).max(300).optional()
15314
+ }).strict(),
15315
+ z8.object({
15316
+ mode: z8.literal("edit-animation"),
15317
+ from: z8.string().min(1),
15318
+ fps: z8.number().int().min(1).max(60).optional()
15319
+ }).strict(),
15140
15320
  z8.object({
15141
15321
  mode: z8.literal("animate-skeleton"),
15142
15322
  from: z8.string().min(1),
@@ -15781,62 +15961,118 @@ async function saveProjectImage(root, request) {
15781
15961
  return relative.split(path28.sep).join("/");
15782
15962
  }
15783
15963
 
15784
- // src/gallery/skeleton.ts
15964
+ // src/gallery/image-revision.ts
15785
15965
  import { existsSync as existsSync24 } from "fs";
15786
- import { mkdir as mkdir10, readFile as readFile21, rm as rm10, writeFile as writeFile13 } from "fs/promises";
15966
+ import { readFile as readFile21, rm as rm10, writeFile as writeFile13 } from "fs/promises";
15787
15967
  import path29 from "path";
15788
15968
  import { z as z11 } from "zod";
15789
- var SkeletonEstimateRequestSchema = z11.object({
15790
- project: z11.string().min(1).optional(),
15791
- styleId: z11.string().min(1),
15792
- assetId: z11.string().min(1),
15793
- frames: z11.number().int().min(3).max(15).optional()
15794
- }).strict();
15795
- var SkeletonAnimationRequestSchema = z11.object({
15796
- action: z11.literal("create-skeleton-animation"),
15969
+ var InpaintRevisionRequestSchema = z11.object({
15970
+ action: z11.literal("create-inpaint-revision"),
15797
15971
  project: z11.string().min(1).optional(),
15798
15972
  expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
15799
15973
  styleId: z11.string().min(1),
15800
- /** The sprite the animation poses. */
15974
+ /** The sprite being repainted. */
15801
15975
  from: z11.string().min(1),
15802
15976
  assetId: z11.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
15803
- /** The motion, sent as PixelLab's `action`. */
15977
+ /** What to paint into the masked area. */
15804
15978
  prompt: z11.string().min(1),
15979
+ /** Manifest-relative `.png` destination for the mask. */
15980
+ maskPath: z11.string().min(1),
15981
+ maskBase64: z11.string().regex(/^[A-Za-z0-9+/]*={0,2}$/),
15982
+ /** Replace a file already at `maskPath`. */
15983
+ overwrite: z11.boolean().optional()
15984
+ }).strict();
15985
+ async function createInpaintRevision(manifestPath, request) {
15986
+ const root = path29.dirname(path29.resolve(manifestPath));
15987
+ if (path29.extname(request.maskPath).toLowerCase() !== ".png") {
15988
+ throw new ManifestEditError("the mask must be a .png path inside the project");
15989
+ }
15990
+ const bytes = Buffer.from(request.maskBase64, "base64");
15991
+ const metadata = imageMetadata(bytes);
15992
+ if (!metadata || metadata.format !== "png") throw new ManifestEditError("the mask is not a readable PNG");
15993
+ const file = path29.resolve(root, request.maskPath);
15994
+ const previous = existsSync24(file) ? await readFile21(file) : null;
15995
+ const mask = await saveProjectImage(root, {
15996
+ action: "upload-image",
15997
+ path: request.maskPath,
15998
+ base64: request.maskBase64,
15999
+ ...request.overwrite ? { overwrite: true } : {}
16000
+ });
16001
+ try {
16002
+ await applyManifestEdit(manifestPath, {
16003
+ action: "add-asset",
16004
+ assetId: request.assetId,
16005
+ expectedSha256: request.expectedSha256,
16006
+ asset: {
16007
+ prompt: request.prompt,
16008
+ styles: [request.styleId],
16009
+ revision: { mode: "inpaint", from: request.from, mask }
16010
+ }
16011
+ });
16012
+ } catch (error) {
16013
+ if (previous) await writeFile13(file, previous);
16014
+ else await rm10(file, { force: true });
16015
+ throw error;
16016
+ }
16017
+ return mask;
16018
+ }
16019
+
16020
+ // src/gallery/skeleton.ts
16021
+ import { existsSync as existsSync25 } from "fs";
16022
+ import { mkdir as mkdir10, readFile as readFile22, rm as rm11, writeFile as writeFile14 } from "fs/promises";
16023
+ import path30 from "path";
16024
+ import { z as z12 } from "zod";
16025
+ var SkeletonEstimateRequestSchema = z12.object({
16026
+ project: z12.string().min(1).optional(),
16027
+ styleId: z12.string().min(1),
16028
+ assetId: z12.string().min(1),
16029
+ frames: z12.number().int().min(3).max(15).optional()
16030
+ }).strict();
16031
+ var SkeletonAnimationRequestSchema = z12.object({
16032
+ action: z12.literal("create-skeleton-animation"),
16033
+ project: z12.string().min(1).optional(),
16034
+ expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
16035
+ styleId: z12.string().min(1),
16036
+ /** The sprite the animation poses. */
16037
+ from: z12.string().min(1),
16038
+ assetId: z12.string().min(1).regex(/^[^/\\]+$/, "asset ids cannot contain slashes"),
16039
+ /** The motion, sent as PixelLab's `action`. */
16040
+ prompt: z12.string().min(1),
15805
16041
  direction: CharacterDirectionSchema,
15806
- description: z11.string().min(1).optional(),
15807
- skeletonTemplate: z11.string().min(1).optional(),
16042
+ description: z12.string().min(1).optional(),
16043
+ skeletonTemplate: z12.string().min(1).optional(),
15808
16044
  /** Manifest-relative JSON path inside the project. */
15809
- keypointsFile: z11.string().min(1),
16045
+ keypointsFile: z12.string().min(1),
15810
16046
  /** Poses to write to `keypointsFile`; omitted to use a file that already exists. */
15811
16047
  set: SkeletonSetSchema.optional(),
15812
16048
  /** Replace an existing `keypointsFile` with `set`. */
15813
- overwrite: z11.boolean().optional()
16049
+ overwrite: z12.boolean().optional()
15814
16050
  }).strict();
15815
- var SkeletonKeypointsUpdateSchema = z11.object({
15816
- action: z11.literal("update-skeleton-keypoints"),
15817
- project: z11.string().min(1).optional(),
15818
- styleId: z11.string().min(1),
15819
- assetId: z11.string().min(1),
16051
+ var SkeletonKeypointsUpdateSchema = z12.object({
16052
+ action: z12.literal("update-skeleton-keypoints"),
16053
+ project: z12.string().min(1).optional(),
16054
+ styleId: z12.string().min(1),
16055
+ assetId: z12.string().min(1),
15820
16056
  /** The file's SHA-256 when the page loaded it; anything else on disk is a concurrent edit. */
15821
- expectedSha256: z11.string().regex(/^[0-9a-f]{64}$/),
16057
+ expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
15822
16058
  set: SkeletonSetSchema
15823
16059
  }).strict();
15824
16060
  async function createSkeletonAnimation(manifestPath, request) {
15825
- const root = path29.dirname(path29.resolve(manifestPath));
15826
- const file = path29.resolve(root, request.keypointsFile);
15827
- const relative = path29.relative(root, file);
15828
- if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path29.isAbsolute(relative)) {
16061
+ const root = path30.dirname(path30.resolve(manifestPath));
16062
+ const file = path30.resolve(root, request.keypointsFile);
16063
+ const relative = path30.relative(root, file);
16064
+ if (!request.keypointsFile.endsWith(".json") || relative.startsWith("..") || path30.isAbsolute(relative)) {
15829
16065
  throw new ManifestEditError("the keypoints file must be a .json path inside the project");
15830
16066
  }
15831
- const existed = existsSync24(file);
16067
+ const existed = existsSync25(file);
15832
16068
  let previous = null;
15833
16069
  if (request.set) {
15834
16070
  if (existed && !request.overwrite) {
15835
16071
  throw new ManifestEditError(`${request.keypointsFile} already exists; choose another path, or replace it`);
15836
16072
  }
15837
- if (existed) previous = await readFile21(file);
15838
- await mkdir10(path29.dirname(file), { recursive: true });
15839
- await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
16073
+ if (existed) previous = await readFile22(file);
16074
+ await mkdir10(path30.dirname(file), { recursive: true });
16075
+ await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
15840
16076
  } else if (!existed) {
15841
16077
  throw new ManifestEditError(`${request.keypointsFile} does not exist; paste poses or estimate them from the sprite`);
15842
16078
  }
@@ -15851,7 +16087,7 @@ async function createSkeletonAnimation(manifestPath, request) {
15851
16087
  revision: {
15852
16088
  mode: "animate-skeleton",
15853
16089
  from: request.from,
15854
- keypointsFile: relative.split(path29.sep).join("/"),
16090
+ keypointsFile: relative.split(path30.sep).join("/"),
15855
16091
  direction: request.direction,
15856
16092
  ...request.description ? { description: request.description } : {},
15857
16093
  ...request.skeletonTemplate ? { skeletonTemplate: request.skeletonTemplate } : {}
@@ -15860,8 +16096,8 @@ async function createSkeletonAnimation(manifestPath, request) {
15860
16096
  });
15861
16097
  } catch (error) {
15862
16098
  if (request.set) {
15863
- if (previous) await writeFile13(file, previous);
15864
- else await rm10(file, { force: true });
16099
+ if (previous) await writeFile14(file, previous);
16100
+ else await rm11(file, { force: true });
15865
16101
  }
15866
16102
  throw error;
15867
16103
  }
@@ -15872,64 +16108,64 @@ async function updateSkeletonKeypoints(ctx, request) {
15872
16108
  if (!spec) throw new ManifestEditError(`"${key}" is not declared by the manifest`);
15873
16109
  const file = spec.revision?.mode === "animate-skeleton" ? spec.revision.keypointsFile : void 0;
15874
16110
  if (!file) throw new ManifestEditError(`${key} is not an animate-skeleton revision`);
15875
- const relative = path29.relative(ctx.loaded.root, file);
15876
- if (relative.startsWith("..") || path29.isAbsolute(relative)) {
16111
+ const relative = path30.relative(ctx.loaded.root, file);
16112
+ if (relative.startsWith("..") || path30.isAbsolute(relative)) {
15877
16113
  throw new ManifestEditError(`${key}'s keypoints file is outside the project; edit it by hand`);
15878
16114
  }
15879
- const current = existsSync24(file) ? sha256(await readFile21(file)) : null;
16115
+ const current = existsSync25(file) ? sha256(await readFile22(file)) : null;
15880
16116
  if (current !== request.expectedSha256) {
15881
16117
  throw Object.assign(
15882
- new Error(`${relative.split(path29.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
16118
+ new Error(`${relative.split(path30.sep).join("/")} changed on disk since this page loaded it; press Refresh and edit again`),
15883
16119
  { status: 409 }
15884
16120
  );
15885
16121
  }
15886
- await writeFile13(file, JSON.stringify(request.set, null, 2) + "\n");
15887
- return relative.split(path29.sep).join("/");
16122
+ await writeFile14(file, JSON.stringify(request.set, null, 2) + "\n");
16123
+ return relative.split(path30.sep).join("/");
15888
16124
  }
15889
16125
 
15890
16126
  // src/gallery/edit.ts
15891
- var Base64 = z12.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
15892
- var HandEditRequestSchema = z12.discriminatedUnion("action", [
15893
- z12.object({
15894
- action: z12.literal("start-edit"),
15895
- project: z12.string().min(1).optional(),
15896
- assetId: z12.string().min(1),
15897
- styleId: z12.string().min(1),
15898
- expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
16127
+ var Base64 = z13.string().regex(/^[A-Za-z0-9+/]*={0,2}$/).max(Math.ceil(MAX_HAND_EDIT_BYTES / 3) * 4);
16128
+ var HandEditRequestSchema = z13.discriminatedUnion("action", [
16129
+ z13.object({
16130
+ action: z13.literal("start-edit"),
16131
+ project: z13.string().min(1).optional(),
16132
+ assetId: z13.string().min(1),
16133
+ styleId: z13.string().min(1),
16134
+ expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/),
15899
16135
  /** Also launch the author's editor on the file. */
15900
- open: z12.boolean().optional()
16136
+ open: z13.boolean().optional()
15901
16137
  }).strict(),
15902
- z12.object({
15903
- action: z12.literal("detach-edit"),
15904
- project: z12.string().min(1).optional(),
15905
- assetId: z12.string().min(1),
15906
- styleId: z12.string().min(1),
15907
- expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/)
16138
+ z13.object({
16139
+ action: z13.literal("detach-edit"),
16140
+ project: z13.string().min(1).optional(),
16141
+ assetId: z13.string().min(1),
16142
+ styleId: z13.string().min(1),
16143
+ expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/)
15908
16144
  }).strict(),
15909
16145
  /** From the in-browser editor: the page holds the bytes, the server validates and writes. */
15910
- z12.object({
15911
- action: z12.literal("save-edit"),
15912
- project: z12.string().min(1).optional(),
15913
- assetId: z12.string().min(1),
15914
- styleId: z12.string().min(1),
15915
- expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/),
16146
+ z13.object({
16147
+ action: z13.literal("save-edit"),
16148
+ project: z13.string().min(1).optional(),
16149
+ assetId: z13.string().min(1),
16150
+ styleId: z13.string().min(1),
16151
+ expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/),
15916
16152
  /** The flattened image; for a frame set, `frames` instead. */
15917
16153
  png: Base64.min(1).optional(),
15918
16154
  /** Every frame of a set, tagged with the role it was opened under (null for one added in the editor). */
15919
- frames: z12.array(z12.object({ role: z12.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
16155
+ frames: z13.array(z13.object({ role: z13.string().min(1).nullable(), png: Base64.min(1) }).strict()).min(1).max(256).optional(),
15920
16156
  /** The editor's layered project file, kept beside the edit. */
15921
16157
  pxo: Base64.optional(),
15922
16158
  /** Editor identity from the bridge's ready message. */
15923
- editor: z12.string().min(1).max(200),
15924
- protocol: z12.number().int().positive()
16159
+ editor: z13.string().min(1).max(200),
16160
+ protocol: z13.number().int().positive()
15925
16161
  }).strict()
15926
16162
  ]);
15927
16163
  var UNDO_DEPTH = 20;
15928
- var UndoRequestSchema = z12.object({
15929
- action: z12.literal("undo-edit"),
15930
- project: z12.string().min(1).optional(),
16164
+ var UndoRequestSchema = z13.object({
16165
+ action: z13.literal("undo-edit"),
16166
+ project: z13.string().min(1).optional(),
15931
16167
  /** The manifest as the page last saw it; undo refuses a manifest someone else has changed since. */
15932
- expectedSha256: z12.string().regex(/^[0-9a-f]{64}$/)
16168
+ expectedSha256: z13.string().regex(/^[0-9a-f]{64}$/)
15933
16169
  }).strict();
15934
16170
  function createGalleryEditHandler(opts) {
15935
16171
  const log = opts.onProgress ?? (() => {
@@ -15938,7 +16174,7 @@ function createGalleryEditHandler(opts) {
15938
16174
  const history = /* @__PURE__ */ new Map();
15939
16175
  const manifestOf = async (project) => {
15940
16176
  try {
15941
- return path30.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
16177
+ return path31.resolve(await opts.manifestFor(typeof project === "string" ? project : void 0));
15942
16178
  } catch {
15943
16179
  return null;
15944
16180
  }
@@ -15948,7 +16184,7 @@ function createGalleryEditHandler(opts) {
15948
16184
  if (undo.success) {
15949
16185
  const manifestPath2 = await manifestOf(undo.data.project);
15950
16186
  if (!manifestPath2) throw new ManifestEditError("no manifest to undo an edit in");
15951
- const current = await readFile22(manifestPath2, "utf8");
16187
+ const current = await readFile23(manifestPath2, "utf8");
15952
16188
  if (sha256(current) !== undo.data.expectedSha256) throw new ManifestDriftError(manifestPath2);
15953
16189
  const stack = history.get(manifestPath2) ?? [];
15954
16190
  const last = stack[stack.length - 1];
@@ -15958,17 +16194,17 @@ function createGalleryEditHandler(opts) {
15958
16194
  throw Object.assign(new Error("the manifest changed outside the gallery since its last edit; undo would discard that change"), { status: 409 });
15959
16195
  }
15960
16196
  const tmp = manifestPath2 + ".pixelkiln-undo";
15961
- await writeFile14(tmp, last.before);
16197
+ await writeFile15(tmp, last.before);
15962
16198
  await rename9(tmp, manifestPath2);
15963
16199
  stack.pop();
15964
- log(` manifest edit undone in ${path30.relative(process.cwd(), manifestPath2) || manifestPath2}`);
16200
+ log(` manifest edit undone in ${path31.relative(process.cwd(), manifestPath2) || manifestPath2}`);
15965
16201
  return opts.reload();
15966
16202
  }
15967
16203
  const manifestPath = await manifestOf(body?.project);
15968
- const before = manifestPath ? await readFile22(manifestPath, "utf8").catch(() => null) : null;
16204
+ const before = manifestPath ? await readFile23(manifestPath, "utf8").catch(() => null) : null;
15969
16205
  const build = await apply(body);
15970
16206
  if (manifestPath && before !== null) {
15971
- const after = await readFile22(manifestPath, "utf8").catch(() => null);
16207
+ const after = await readFile23(manifestPath, "utf8").catch(() => null);
15972
16208
  if (after !== null && after !== before) {
15973
16209
  const stack = history.get(manifestPath) ?? [];
15974
16210
  stack.push({ before, afterSha: sha256(after) });
@@ -16023,7 +16259,7 @@ function createEditApplier(opts, log) {
16023
16259
  } catch (error) {
16024
16260
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16025
16261
  }
16026
- const written = await saveProjectImage(path30.dirname(path30.resolve(manifestPath2)), upload.data);
16262
+ const written = await saveProjectImage(path31.dirname(path31.resolve(manifestPath2)), upload.data);
16027
16263
  log(` image added: ${written}`);
16028
16264
  return opts.reload();
16029
16265
  }
@@ -16059,6 +16295,23 @@ function createEditApplier(opts, log) {
16059
16295
  "invalid skeleton animation: " + skeleton.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
16060
16296
  );
16061
16297
  }
16298
+ const inpaint = InpaintRevisionRequestSchema.safeParse(body);
16299
+ if (inpaint.success) {
16300
+ let manifestPath2;
16301
+ try {
16302
+ manifestPath2 = await opts.manifestFor(inpaint.data.project);
16303
+ } catch (error) {
16304
+ throw new ManifestEditError(error instanceof Error ? error.message : String(error));
16305
+ }
16306
+ const mask = await createInpaintRevision(manifestPath2, inpaint.data);
16307
+ log(` manifest edited: added inpaint revision ${inpaint.data.assetId} (mask ${mask})`);
16308
+ return opts.reload();
16309
+ }
16310
+ if (body?.action === "create-inpaint-revision") {
16311
+ throw new ManifestEditError(
16312
+ "invalid inpaint revision: " + inpaint.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")
16313
+ );
16314
+ }
16062
16315
  const parsed = ManifestEditSchema.safeParse(body);
16063
16316
  if (!parsed.success) {
16064
16317
  throw new ManifestEditError(
@@ -16073,7 +16326,7 @@ function createEditApplier(opts, log) {
16073
16326
  }
16074
16327
  const result = await applyManifestEdit(manifestPath, parsed.data);
16075
16328
  if (result.changed) {
16076
- const relative = path30.relative(process.cwd(), result.manifestPath);
16329
+ const relative = path31.relative(process.cwd(), result.manifestPath);
16077
16330
  const shown = !relative || relative.startsWith("..") ? result.manifestPath : relative;
16078
16331
  log(` manifest edited: ${describeEdit(parsed.data)} in ${shown}`);
16079
16332
  }
@@ -16101,7 +16354,7 @@ function describeEdit(edit) {
16101
16354
  // src/gallery/snapshot.ts
16102
16355
  var PROJECT_FILE_TYPE = "application/octet-stream";
16103
16356
  function galleryMediaId(absolutePath) {
16104
- return createHash4("sha256").update(path31.resolve(absolutePath)).digest("hex").slice(0, 24);
16357
+ return createHash4("sha256").update(path32.resolve(absolutePath)).digest("hex").slice(0, 24);
16105
16358
  }
16106
16359
  function galleryMediaRoute(id) {
16107
16360
  return `/media/${id}`;
@@ -16120,9 +16373,9 @@ function describeHistory(media, entry, cacheDir) {
16120
16373
  return (entry.history ?? []).map((generation, i) => {
16121
16374
  const outputs = generation.outputs.map((output) => {
16122
16375
  const mediaType = output.mediaType ?? MediaType.PNG;
16123
- const file = path31.join(cacheDir, cacheFileName(output.sha256, mediaType));
16376
+ const file = path32.join(cacheDir, cacheFileName(output.sha256, mediaType));
16124
16377
  let url = null;
16125
- if (existsSync25(file)) {
16378
+ if (existsSync26(file)) {
16126
16379
  const id = galleryMediaId(file);
16127
16380
  media.set(id, { path: file, contentType: mediaType });
16128
16381
  url = `${galleryMediaRoute(id)}?v=${output.sha256.slice(0, 24)}`;
@@ -16154,7 +16407,7 @@ function describeHistory(media, entry, cacheDir) {
16154
16407
  var MAX_COMPARED_BYTES = 8 * 1024 * 1024;
16155
16408
  async function samePixels(a, b) {
16156
16409
  try {
16157
- const [left, right] = await Promise.all([readFile23(a), readFile23(b)]);
16410
+ const [left, right] = await Promise.all([readFile24(a), readFile24(b)]);
16158
16411
  if (left.length > MAX_COMPARED_BYTES || right.length > MAX_COMPARED_BYTES) return false;
16159
16412
  const x = decodePng(left);
16160
16413
  const y = decodePng(right);
@@ -16252,10 +16505,10 @@ async function describeOutput(media, root, absolutePath, recorded = {}) {
16252
16505
  async function describeQuality(media, root, inspection) {
16253
16506
  const outputPaths = inspection.outputs ?? [inspection.output];
16254
16507
  const outputs = await Promise.all(
16255
- outputPaths.map((file) => describeOutput(media, root, path31.resolve(root, file)))
16508
+ outputPaths.map((file) => describeOutput(media, root, path32.resolve(root, file)))
16256
16509
  );
16257
- const recordPath = path31.resolve(root, inspection.record);
16258
- const recordExists = existsSync25(recordPath);
16510
+ const recordPath = path32.resolve(root, inspection.record);
16511
+ const recordExists = existsSync26(recordPath);
16259
16512
  let options = null;
16260
16513
  let check = null;
16261
16514
  if (recordExists) {
@@ -16274,7 +16527,7 @@ async function describeQuality(media, root, inspection) {
16274
16527
  return {
16275
16528
  state: inspection.state,
16276
16529
  reason: inspection.reason,
16277
- output: portableOutputPath(path31.resolve(root, inspection.output), root),
16530
+ output: portableOutputPath(path32.resolve(root, inspection.output), root),
16278
16531
  record: portableOutputPath(recordPath, root),
16279
16532
  recordExists,
16280
16533
  outputs,
@@ -16312,9 +16565,9 @@ async function buildGallerySnapshot(opts) {
16312
16565
  const media = /* @__PURE__ */ new Map();
16313
16566
  const plan = await buildPlan(specs, lock);
16314
16567
  const items = [];
16315
- const manifestText = await readFile23(loaded.path, "utf8");
16568
+ const manifestText = await readFile24(loaded.path, "utf8");
16316
16569
  const manifestSha256 = sha256(manifestText);
16317
- const cacheDir = path31.resolve(path31.dirname(opts.lockPath), ".pixelkiln", "cache");
16570
+ const cacheDir = path32.resolve(path32.dirname(opts.lockPath), ".pixelkiln", "cache");
16318
16571
  let rawStyles = {};
16319
16572
  try {
16320
16573
  const raw = JSON.parse(manifestText);
@@ -16338,12 +16591,12 @@ async function buildGallerySnapshot(opts) {
16338
16591
  )
16339
16592
  );
16340
16593
  if (spec.source) {
16341
- const editPath = path31.resolve(root, spec.source);
16594
+ const editPath = path32.resolve(root, spec.source);
16342
16595
  const memberCount = sourceIsStem(spec.source, entry, root) ? entry.outputs.length : 1;
16343
16596
  const editShas = await Promise.all(
16344
16597
  Array.from({ length: memberCount }, async (_, index) => {
16345
16598
  const file = sourceOutputPath(spec.source, entry, index, root);
16346
- return { file, sha256: existsSync25(file) ? await sha256File(file) : null };
16599
+ return { file, sha256: existsSync26(file) ? await sha256File(file) : null };
16347
16600
  })
16348
16601
  );
16349
16602
  edits = await Promise.all(editShas.map(({ file, sha256: editSha }) => describeOutput(media, root, file, { sha256: editSha })));
@@ -16351,7 +16604,7 @@ async function buildGallerySnapshot(opts) {
16351
16604
  const companion = await readHandEditCompanion(editPath);
16352
16605
  if (companion) {
16353
16606
  const projectPath = handEditProjectPath(editPath);
16354
- const project = companion.project && existsSync25(projectPath) ? await fileInfo(projectPath) : null;
16607
+ const project = companion.project && existsSync26(projectPath) ? await fileInfo(projectPath) : null;
16355
16608
  let projectUrl = null;
16356
16609
  if (project) {
16357
16610
  const id = galleryMediaId(projectPath);
@@ -16387,8 +16640,8 @@ async function buildGallerySnapshot(opts) {
16387
16640
  editStatus = edits.some((member) => !member.exists) ? "missing" : editChanged.every((changed) => !changed) ? "same" : regenerated ? "regenerated-since" : "edited";
16388
16641
  }
16389
16642
  } else if (spec.source) {
16390
- outputs = [await describeOutput(media, root, path31.resolve(root, spec.source))];
16391
- } else if (!entry && existsSync25(spec.outFile)) {
16643
+ outputs = [await describeOutput(media, root, path32.resolve(root, spec.source))];
16644
+ } else if (!entry && existsSync26(spec.outFile)) {
16392
16645
  outputs = [await describeOutput(media, root, spec.outFile)];
16393
16646
  } else {
16394
16647
  outputs = [];
@@ -16591,7 +16844,7 @@ async function buildGallerySnapshot(opts) {
16591
16844
  id: loaded.manifest.name,
16592
16845
  name: loaded.manifest.name,
16593
16846
  manifest: loaded.path,
16594
- lock: path31.resolve(opts.lockPath),
16847
+ lock: path32.resolve(opts.lockPath),
16595
16848
  root,
16596
16849
  provider: loaded.manifest.provider,
16597
16850
  account: null,
@@ -16617,7 +16870,7 @@ async function buildGallerySnapshot(opts) {
16617
16870
  return { snapshot, media };
16618
16871
  }
16619
16872
  async function buildWorkspaceGallerySnapshot(opts) {
16620
- const dir = path31.dirname(path31.resolve(opts.workspacePath));
16873
+ const dir = path32.dirname(path32.resolve(opts.workspacePath));
16621
16874
  const media = /* @__PURE__ */ new Map();
16622
16875
  const projects = [];
16623
16876
  const items = [];
@@ -16660,11 +16913,11 @@ async function buildWorkspaceGallerySnapshot(opts) {
16660
16913
  id: project.id,
16661
16914
  name: loaded.manifest.name,
16662
16915
  manifest: manifestPath,
16663
- lock: path31.resolve(lockPath),
16916
+ lock: path32.resolve(lockPath),
16664
16917
  root: loaded.root,
16665
16918
  provider: loaded.manifest.provider,
16666
16919
  account: project.account ?? null,
16667
- manifestSha256: manifestSha256 ?? sha256(await readFile23(manifestPath, "utf8")),
16920
+ manifestSha256: manifestSha256 ?? sha256(await readFile24(manifestPath, "utf8")),
16668
16921
  historyLimit: historyLimit(loaded.manifest),
16669
16922
  entries: Object.keys(lock.entries).length,
16670
16923
  items: projectItems.length,
@@ -16676,8 +16929,8 @@ async function buildWorkspaceGallerySnapshot(opts) {
16676
16929
  id: project.id,
16677
16930
  name: project.id,
16678
16931
  manifest: manifestPath,
16679
- lock: path31.resolve(lockPath),
16680
- root: path31.dirname(manifestPath),
16932
+ lock: path32.resolve(lockPath),
16933
+ root: path32.dirname(manifestPath),
16681
16934
  provider: project.provider,
16682
16935
  account: project.account ?? null,
16683
16936
  manifestSha256: null,
@@ -16704,7 +16957,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16704
16957
  version: 1,
16705
16958
  generatedAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString(),
16706
16959
  project: null,
16707
- workspace: { path: path31.resolve(opts.workspacePath), dir, projects },
16960
+ workspace: { path: path32.resolve(opts.workspacePath), dir, projects },
16708
16961
  filter,
16709
16962
  totals: {
16710
16963
  entries: projects.reduce((sum, project) => sum + project.entries, 0),
@@ -16721,7 +16974,7 @@ async function buildWorkspaceGallerySnapshot(opts) {
16721
16974
  }
16722
16975
 
16723
16976
  // src/gallery/page.ts
16724
- import { existsSync as existsSync26, readFileSync as readFileSync5 } from "fs";
16977
+ import { existsSync as existsSync27, readFileSync as readFileSync5 } from "fs";
16725
16978
 
16726
16979
  // src/gallery/client-bundle.ts
16727
16980
  import { createRequire } from "module";
@@ -16762,7 +17015,7 @@ function clientAssets() {
16762
17015
  const prebuilt = new URL("gallery.js", CLIENT_DIR);
16763
17016
  client ??= {
16764
17017
  css: readFileSync5(new URL("gallery.css", CLIENT_DIR), "utf8"),
16765
- js: existsSync26(prebuilt) ? readFileSync5(prebuilt, "utf8") : bundleGalleryClient()
17018
+ js: existsSync27(prebuilt) ? readFileSync5(prebuilt, "utf8") : bundleGalleryClient()
16766
17019
  };
16767
17020
  return client;
16768
17021
  }
@@ -16905,10 +17158,10 @@ var EDITOR_RELEASE_BASE = "https://github.com/gfargo/pixelkiln/releases/download
16905
17158
 
16906
17159
  // src/editor/install.ts
16907
17160
  import { createHash as createHash5 } from "crypto";
16908
- import { existsSync as existsSync27 } from "fs";
16909
- import { mkdir as mkdir11, readFile as readFile24, rename as rename10, rm as rm11, stat as stat4, writeFile as writeFile15 } from "fs/promises";
17161
+ import { existsSync as existsSync28 } from "fs";
17162
+ import { mkdir as mkdir11, readFile as readFile25, rename as rename10, rm as rm12, stat as stat4, writeFile as writeFile16 } from "fs/promises";
16910
17163
  import os2 from "os";
16911
- import path32 from "path";
17164
+ import path33 from "path";
16912
17165
  var EditorInstallError = class extends Error {
16913
17166
  constructor(message6, file) {
16914
17167
  super(message6);
@@ -16919,15 +17172,15 @@ var EditorInstallError = class extends Error {
16919
17172
  };
16920
17173
  function editorToolsRoot() {
16921
17174
  const override = process.env.PIXELKILN_TOOLS_DIR?.trim();
16922
- if (override) return path32.resolve(override);
16923
- if (process.platform === "darwin") return path32.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
17175
+ if (override) return path33.resolve(override);
17176
+ if (process.platform === "darwin") return path33.join(os2.homedir(), "Library", "Caches", "pixelkiln", "tools");
16924
17177
  if (process.platform === "win32") {
16925
- return path32.join(process.env.LOCALAPPDATA ?? path32.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
17178
+ return path33.join(process.env.LOCALAPPDATA ?? path33.join(os2.homedir(), "AppData", "Local"), "pixelkiln", "tools");
16926
17179
  }
16927
- return path32.join(process.env.XDG_CACHE_HOME ?? path32.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
17180
+ return path33.join(process.env.XDG_CACHE_HOME ?? path33.join(os2.homedir(), ".cache"), "pixelkiln", "tools");
16928
17181
  }
16929
17182
  function editorDir(pin = EDITOR_PIN, root = editorToolsRoot()) {
16930
- return path32.join(root, "pixelorama", pin.release ?? "unpublished");
17183
+ return path33.join(root, "pixelorama", pin.release ?? "unpublished");
16931
17184
  }
16932
17185
  function editorBaseUrl(pin = EDITOR_PIN) {
16933
17186
  const override = process.env.PIXELKILN_EDITOR_URL?.trim();
@@ -16937,7 +17190,7 @@ function editorBaseUrl(pin = EDITOR_PIN) {
16937
17190
  }
16938
17191
  async function sha256Of(file) {
16939
17192
  try {
16940
- return createHash5("sha256").update(await readFile24(file)).digest("hex");
17193
+ return createHash5("sha256").update(await readFile25(file)).digest("hex");
16941
17194
  } catch {
16942
17195
  return null;
16943
17196
  }
@@ -16950,8 +17203,8 @@ async function editorStatus(opts = {}) {
16950
17203
  let totalBytes = 0;
16951
17204
  for (const [name, expected] of Object.entries(pin.files)) {
16952
17205
  totalBytes += expected.bytes;
16953
- const file = path32.join(dir, name);
16954
- const info = existsSync27(file) ? await stat4(file).catch(() => null) : null;
17206
+ const file = path33.join(dir, name);
17207
+ const info = existsSync28(file) ? await stat4(file).catch(() => null) : null;
16955
17208
  if (info && info.size === expected.bytes && await sha256Of(file) === expected.sha256) {
16956
17209
  installedBytes += expected.bytes;
16957
17210
  } else {
@@ -17008,13 +17261,13 @@ async function installEditor(opts = {}) {
17008
17261
  if (digest2 !== expected.sha256) {
17009
17262
  throw new EditorInstallError(`${name}: sha256 ${digest2.slice(0, 12)}\u2026 does not match the pinned ${expected.sha256.slice(0, 12)}\u2026`, name);
17010
17263
  }
17011
- const final = path32.join(dir, name);
17264
+ const final = path33.join(dir, name);
17012
17265
  const tmp = `${final}.${process.pid}.part`;
17013
17266
  try {
17014
- await writeFile15(tmp, bytes);
17267
+ await writeFile16(tmp, bytes);
17015
17268
  await rename10(tmp, final);
17016
17269
  } finally {
17017
- await rm11(tmp, { force: true });
17270
+ await rm12(tmp, { force: true });
17018
17271
  }
17019
17272
  fetchedBytes += bytes.length;
17020
17273
  downloaded.push(name);
@@ -17054,12 +17307,12 @@ var EDITOR_CONTENT_TYPES = {
17054
17307
  ".json": "application/json; charset=utf-8"
17055
17308
  };
17056
17309
  function editorContentType(name) {
17057
- return EDITOR_CONTENT_TYPES[path32.extname(name).toLowerCase()] ?? null;
17310
+ return EDITOR_CONTENT_TYPES[path33.extname(name).toLowerCase()] ?? null;
17058
17311
  }
17059
17312
 
17060
17313
  // src/gallery/editor.ts
17061
- import { readFile as readFile25 } from "fs/promises";
17062
- import path33 from "path";
17314
+ import { readFile as readFile26 } from "fs/promises";
17315
+ import path34 from "path";
17063
17316
  function editorRoute(release, name = "index.html") {
17064
17317
  return `/editor/${encodeURIComponent(release)}/${encodeURIComponent(name)}`;
17065
17318
  }
@@ -17132,7 +17385,7 @@ function createGalleryEditorHandlers(opts = {}) {
17132
17385
  if (!status.installed) return null;
17133
17386
  let bytes;
17134
17387
  try {
17135
- bytes = await readFile25(path33.join(dir, name));
17388
+ bytes = await readFile26(path34.join(dir, name));
17136
17389
  } catch {
17137
17390
  verified = null;
17138
17391
  return null;
@@ -17149,7 +17402,7 @@ function createGalleryEditorHandlers(opts = {}) {
17149
17402
  // src/gallery/server.ts
17150
17403
  import { randomBytes } from "crypto";
17151
17404
  import { createServer as createServer2 } from "http";
17152
- import { readFile as readFile26 } from "fs/promises";
17405
+ import { readFile as readFile27 } from "fs/promises";
17153
17406
  var MAX_EDIT_BYTES = 48 * 1024 * 1024;
17154
17407
  var EDITOR_CSP = [
17155
17408
  "default-src 'self'",
@@ -17330,7 +17583,7 @@ async function serveGallery(opts) {
17330
17583
  const asset = reviewAssets.get(jobId)?.get(url2.pathname);
17331
17584
  if (!asset) return fail(404, "no such review asset");
17332
17585
  try {
17333
- const bytes = await readFile26(asset.path);
17586
+ const bytes = await readFile27(asset.path);
17334
17587
  res.writeHead(200, {
17335
17588
  "Content-Type": asset.contentType,
17336
17589
  "Cache-Control": "no-store",
@@ -17413,7 +17666,7 @@ async function serveGallery(opts) {
17413
17666
  if (!asset) return fail(404, "no such gallery media");
17414
17667
  let bytes;
17415
17668
  try {
17416
- bytes = await readFile26(asset.path);
17669
+ bytes = await readFile27(asset.path);
17417
17670
  } catch {
17418
17671
  return fail(404, "gallery media is no longer on disk; refresh the page");
17419
17672
  }
@@ -17457,27 +17710,27 @@ async function serveGallery(opts) {
17457
17710
 
17458
17711
  // src/gallery/generate.ts
17459
17712
  import { randomBytes as randomBytes2 } from "crypto";
17460
- import { z as z13 } from "zod";
17461
- var GenerateRequestSchema = z13.object({
17462
- project: z13.string().min(1).optional(),
17463
- keys: z13.array(z13.string().min(1)).min(1).max(500),
17713
+ import { z as z14 } from "zod";
17714
+ var GenerateRequestSchema = z14.object({
17715
+ project: z14.string().min(1).optional(),
17716
+ keys: z14.array(z14.string().min(1)).min(1).max(500),
17464
17717
  /**
17465
17718
  * Regenerate work that is up to date, as `gen --force` does, and let the
17466
17719
  * fetch replace a locally modified or untracked file. With `restore`, put
17467
17720
  * the recorded bytes back over a modified file.
17468
17721
  */
17469
- force: z13.boolean().optional(),
17722
+ force: z14.boolean().optional(),
17470
17723
  /** Advance in-flight, review, or selected work without submitting anything. */
17471
- resume: z13.boolean().optional(),
17724
+ resume: z14.boolean().optional(),
17472
17725
  /** Re-download downloaded outputs and replace files whose object changed upstream. */
17473
- refresh: z13.boolean().optional(),
17726
+ refresh: z14.boolean().optional(),
17474
17727
  /** Put recorded outputs back on disk from the cache or the provider, without generating. */
17475
- restore: z13.boolean().optional(),
17728
+ restore: z14.boolean().optional(),
17476
17729
  /**
17477
17730
  * Bring a replaced generation back: 1-based position in the record's
17478
17731
  * history (newest first) or an output hash prefix. One key at a time.
17479
17732
  */
17480
- revert: z13.string().min(1).max(64).optional()
17733
+ revert: z14.string().min(1).max(64).optional()
17481
17734
  }).strict().refine((request) => [request.resume, request.refresh, request.restore, request.revert !== void 0].filter(Boolean).length <= 1, {
17482
17735
  message: "resume, refresh, restore, and revert are exclusive"
17483
17736
  }).refine((request) => request.revert === void 0 || request.keys.length === 1, { message: "revert takes one key" });
@@ -17823,19 +18076,19 @@ function createGenerateHandlers(opts) {
17823
18076
  }
17824
18077
 
17825
18078
  // src/pipeline/salvage.ts
17826
- import { readFile as readFile27 } from "fs/promises";
17827
- import { existsSync as existsSync28 } from "fs";
17828
- import path34 from "path";
18079
+ import { readFile as readFile28 } from "fs/promises";
18080
+ import { existsSync as existsSync29 } from "fs";
18081
+ import path35 from "path";
17829
18082
  function providerClaimId(provider, objectId) {
17830
18083
  return `${encodeURIComponent(provider)}:${encodeURIComponent(objectId)}`;
17831
18084
  }
17832
18085
  async function loadClaims(lockPaths, opts = {}) {
17833
18086
  const claimed = /* @__PURE__ */ new Set();
17834
18087
  for (const p of lockPaths) {
17835
- if (!existsSync28(p)) throw new Error(`Claim lockfile not found: ${p}`);
18088
+ if (!existsSync29(p)) throw new Error(`Claim lockfile not found: ${p}`);
17836
18089
  let parsed;
17837
18090
  try {
17838
- parsed = parseLock(JSON.parse(await readFile27(p, "utf8")));
18091
+ parsed = parseLock(JSON.parse(await readFile28(p, "utf8")));
17839
18092
  } catch {
17840
18093
  throw new Error(`Claim lockfile is malformed: ${p}`);
17841
18094
  }
@@ -17939,20 +18192,20 @@ function matchOrphanStyle(prompt, manifest) {
17939
18192
  return matchStyleByPattern(prompt, manifest);
17940
18193
  }
17941
18194
  async function loadSiblingManifests(ownManifestPath, workspaceManifestPaths, claimPaths) {
17942
- const own = path34.resolve(ownManifestPath);
18195
+ const own = path35.resolve(ownManifestPath);
17943
18196
  const siblingManifestPaths = [
17944
18197
  .../* @__PURE__ */ new Set([
17945
18198
  ...workspaceManifestPaths,
17946
- ...claimPaths.map((c) => path34.join(path34.dirname(path34.resolve(c)), "pixelkiln.manifest.json"))
18199
+ ...claimPaths.map((c) => path35.join(path35.dirname(path35.resolve(c)), "pixelkiln.manifest.json"))
17947
18200
  ])
17948
18201
  ];
17949
18202
  const siblings = [];
17950
18203
  for (const siblingManifestPath of siblingManifestPaths) {
17951
- if (path34.resolve(siblingManifestPath) === own) continue;
17952
- if (!existsSync28(siblingManifestPath)) continue;
18204
+ if (path35.resolve(siblingManifestPath) === own) continue;
18205
+ if (!existsSync29(siblingManifestPath)) continue;
17953
18206
  try {
17954
18207
  const { manifest } = await loadManifest(siblingManifestPath);
17955
- siblings.push({ label: path34.basename(path34.dirname(siblingManifestPath)), manifest });
18208
+ siblings.push({ label: path35.basename(path35.dirname(siblingManifestPath)), manifest });
17956
18209
  } catch {
17957
18210
  }
17958
18211
  }
@@ -18072,8 +18325,8 @@ async function applyTags(provider, decisions, existing, opts = {}) {
18072
18325
  }
18073
18326
 
18074
18327
  // src/pick/salvage-server.ts
18075
- import { mkdir as mkdir12, writeFile as writeFile16, readFile as readFile28 } from "fs/promises";
18076
- import path35 from "path";
18328
+ import { mkdir as mkdir12, writeFile as writeFile17, readFile as readFile29 } from "fs/promises";
18329
+ import path36 from "path";
18077
18330
 
18078
18331
  // src/pick/salvage-sheet.ts
18079
18332
  var escapeHtml3 = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
@@ -18239,7 +18492,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18239
18492
  });
18240
18493
  const html = renderSalvageSheet(orphans, {
18241
18494
  styleId: ctx.styleId,
18242
- importDir: path35.relative(process.cwd(), ctx.importDir) || "."
18495
+ importDir: path36.relative(process.cwd(), ctx.importDir) || "."
18243
18496
  });
18244
18497
  const byId = new Map(orphans.map((o) => [o.id, o]));
18245
18498
  const existingTags = new Map(orphans.map((o) => [o.id, o.tags]));
@@ -18295,10 +18548,10 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18295
18548
  if (!buf.subarray(0, 8).equals(PNG_SIGNATURE2)) throw new Error("not a PNG");
18296
18549
  decodePng(buf);
18297
18550
  const assetId = idFromPrompt(orphan.prompt, taken);
18298
- const rel = path35.join("_salvaged", `${assetId}.png`);
18299
- const outFile = path35.resolve(ctx.importDir, rel);
18300
- await mkdir12(path35.dirname(outFile), { recursive: true });
18301
- await writeFile16(outFile, buf);
18551
+ const rel = path36.join("_salvaged", `${assetId}.png`);
18552
+ const outFile = path36.resolve(ctx.importDir, rel);
18553
+ await mkdir12(path36.dirname(outFile), { recursive: true });
18554
+ await writeFile17(outFile, buf);
18302
18555
  ctx.manifest.assets[assetId] = {
18303
18556
  prompt: orphan.prompt,
18304
18557
  promptByStyle: {},
@@ -18330,7 +18583,7 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18330
18583
  sourceUrl: durableSource,
18331
18584
  sourceUrls: durableSource ? [{ url: durableSource }] : [],
18332
18585
  outputs: [{
18333
- path: portableOutputPath(outFile, path35.dirname(ctx.manifestPath)),
18586
+ path: portableOutputPath(outFile, path36.dirname(ctx.manifestPath)),
18334
18587
  sha256: sha256(buf)
18335
18588
  }],
18336
18589
  submittedAt: orphan.createdAt,
@@ -18354,9 +18607,9 @@ async function runSalvage(provider, orphans, ctx, opts = {}) {
18354
18607
  }
18355
18608
  }
18356
18609
  await applyTags(provider, decisions, existingTags, { onProgress: log, characters: characterMembers });
18357
- const raw = JSON.parse(await readFile28(ctx.manifestPath, "utf8"));
18610
+ const raw = JSON.parse(await readFile29(ctx.manifestPath, "utf8"));
18358
18611
  for (const id of importedAssetIds) raw.assets[id] = ctx.manifest.assets[id];
18359
- await writeFile16(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18612
+ await writeFile17(ctx.manifestPath, JSON.stringify(raw, null, 2) + "\n");
18360
18613
  await saveLock(ctx.lockPath, ctx.lock);
18361
18614
  if (characterAssetIds.length) result.characterAssetIds = characterAssetIds;
18362
18615
  return result;