chatroom-cli 1.136.0 → 1.137.1

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
@@ -2140,7 +2140,156 @@ var require_commander = __commonJS((exports) => {
2140
2140
  exports.InvalidOptionArgumentError = InvalidArgumentError;
2141
2141
  });
2142
2142
 
2143
+ // src/commands/generate/model-capabilities.ts
2144
+ function getModelCapability(id) {
2145
+ return Object.prototype.hasOwnProperty.call(MODEL_CAPABILITIES, id) ? MODEL_CAPABILITIES[id] : undefined;
2146
+ }
2147
+ function describeKey(capability, key, source) {
2148
+ const support = capability.params[key];
2149
+ if (support === undefined) {
2150
+ return [
2151
+ {
2152
+ kind: "unknown",
2153
+ key,
2154
+ message: `not in the CLI's capability data for ${capability.id}; not checked.`
2155
+ }
2156
+ ];
2157
+ }
2158
+ if (support.support === "unsupported") {
2159
+ return [{ kind: "unsupported", key, source, message: support.reason }];
2160
+ }
2161
+ return [];
2162
+ }
2163
+ function describeInputKeys(capability, input) {
2164
+ const disclosures = [];
2165
+ for (const key of Object.keys(input)) {
2166
+ if (key === "aspect_ratio")
2167
+ continue;
2168
+ disclosures.push(...describeKey(capability, key, "input"));
2169
+ }
2170
+ return disclosures;
2171
+ }
2172
+ function describeFlagKeys(capability, flags) {
2173
+ return flags.aspectRatio === undefined ? [] : describeKey(capability, "aspect_ratio", "flag");
2174
+ }
2175
+ function describeDefaults(capability, input) {
2176
+ return Object.entries(capability.params).flatMap(([key, support]) => {
2177
+ if (support.support !== "supported" || support.default === undefined)
2178
+ return [];
2179
+ if (input[key] !== undefined)
2180
+ return [];
2181
+ return [
2182
+ {
2183
+ kind: "default",
2184
+ key,
2185
+ message: `${key} is not set; the model default ${support.default} applies. Set one explicit value for every call of a set.`
2186
+ }
2187
+ ];
2188
+ });
2189
+ }
2190
+ function describeNotes(capability) {
2191
+ return capability.notes.map((message) => ({ kind: "note", message }));
2192
+ }
2193
+ function describeConstraints(model, input, flags) {
2194
+ const capability = getModelCapability(model);
2195
+ if (capability === undefined) {
2196
+ return [
2197
+ {
2198
+ kind: "note",
2199
+ message: `The CLI has no capability data for ${model}; no constraints were checked.`
2200
+ }
2201
+ ];
2202
+ }
2203
+ return [
2204
+ ...describeInputKeys(capability, input),
2205
+ ...describeFlagKeys(capability, flags),
2206
+ ...describeDefaults(capability, input),
2207
+ ...describeNotes(capability)
2208
+ ];
2209
+ }
2210
+ var FLUX_2_FLASH_MODEL = "fal-ai/flux-2/flash", FLUX_2_FLASH_EDIT_MODEL = "fal-ai/flux-2/flash/edit", FLASH_SOURCE = "https://fal.ai/api/openapi/queue/openapi.json?endpoint_id=fal-ai/flux-2/flash (read 2026-10-24); no negative prompt: https://help.bfl.ai/articles/7734566352-does-flux-2-support-negative-prompting (read 2026-10-24)", EDIT_SOURCE = "https://fal.ai/api/openapi/queue/openapi.json?endpoint_id=fal-ai/flux-2/flash/edit (read 2026-10-24); no negative prompt: https://help.bfl.ai/articles/7734566352-does-flux-2-support-negative-prompting (read 2026-10-24)", MODEL_CAPABILITIES;
2211
+ var init_model_capabilities = __esm(() => {
2212
+ MODEL_CAPABILITIES = {
2213
+ [FLUX_2_FLASH_MODEL]: {
2214
+ id: FLUX_2_FLASH_MODEL,
2215
+ name: "FLUX 2 Flash",
2216
+ task: "text-to-image",
2217
+ params: {
2218
+ prompt: { support: "supported" },
2219
+ seed: { support: "supported" },
2220
+ output_format: { support: "supported" },
2221
+ image_size: {
2222
+ support: "supported",
2223
+ default: "landscape_4_3",
2224
+ note: "set one explicit value for every call of a set"
2225
+ },
2226
+ aspect_ratio: {
2227
+ support: "unsupported",
2228
+ reason: `FLUX 2 Flash sizes output with image_size. Pass --input '{"image_size":"landscape_4_3"}' instead.`
2229
+ },
2230
+ negative_prompt: {
2231
+ support: "unsupported",
2232
+ reason: "FLUX 2 Flash has no negative prompt; describe what you want in --prompt instead."
2233
+ },
2234
+ image_urls: {
2235
+ support: "unsupported",
2236
+ reason: "Text-to-image only; use --model fal-ai/flux-2/flash/edit with --input-image."
2237
+ }
2238
+ },
2239
+ notes: [
2240
+ "FLUX 2 Flash has no camera or perspective parameter; perspective comes only from the prompt.",
2241
+ "Same-seed reproducibility is not documented by fal.",
2242
+ "Returns PNG without an alpha channel by default."
2243
+ ],
2244
+ source: FLASH_SOURCE
2245
+ },
2246
+ [FLUX_2_FLASH_EDIT_MODEL]: {
2247
+ id: FLUX_2_FLASH_EDIT_MODEL,
2248
+ name: "FLUX 2 Flash (edit)",
2249
+ task: "image-edit",
2250
+ params: {
2251
+ prompt: { support: "supported" },
2252
+ seed: { support: "supported" },
2253
+ image_urls: {
2254
+ support: "supported",
2255
+ note: "set by --input-image; the CLI sends one image"
2256
+ },
2257
+ image_size: {
2258
+ support: "supported",
2259
+ default: "square_hd",
2260
+ note: "set one explicit value for every call of a set"
2261
+ },
2262
+ aspect_ratio: {
2263
+ support: "unsupported",
2264
+ reason: `FLUX 2 Flash (edit) sizes output with image_size. Pass --input '{"image_size":"square_hd"}' instead.`
2265
+ },
2266
+ negative_prompt: {
2267
+ support: "unsupported",
2268
+ reason: "FLUX 2 Flash has no negative prompt; describe what you want in --prompt instead."
2269
+ }
2270
+ },
2271
+ notes: [
2272
+ "FLUX 2 Flash has no camera or perspective parameter; perspective comes only from the prompt.",
2273
+ "Not documented to carry a reference image's perspective over to new content; it may return a modified copy of the input.",
2274
+ "Same-seed reproducibility is not documented by fal."
2275
+ ],
2276
+ source: EDIT_SOURCE
2277
+ }
2278
+ };
2279
+ });
2280
+
2143
2281
  // src/commands/generate/image.ts
2282
+ var exports_image = {};
2283
+ __export(exports_image, {
2284
+ planGenerateImage: () => planGenerateImage,
2285
+ parseInputJson: () => parseInputJson,
2286
+ generateImage: () => generateImage,
2287
+ formatGenerateImagePlan: () => formatGenerateImagePlan,
2288
+ GenerateImageError: () => GenerateImageError,
2289
+ GENERATE_IMAGE_PLATFORMS: () => GENERATE_IMAGE_PLATFORMS,
2290
+ FAL_API_KEY_MISSING_MESSAGE: () => FAL_API_KEY_MISSING_MESSAGE,
2291
+ DEFAULT_GENERATE_IMAGE_PLATFORM: () => DEFAULT_GENERATE_IMAGE_PLATFORM
2292
+ });
2144
2293
  import { createHash } from "node:crypto";
2145
2294
  import { extname, resolve } from "node:path";
2146
2295
  function resolvePlatform(platform) {
@@ -2163,6 +2312,14 @@ function requireNonEmpty(value, message) {
2163
2312
  }
2164
2313
  return trimmed;
2165
2314
  }
2315
+ function inputImageContentType(path) {
2316
+ const extension = extname(path).toLowerCase();
2317
+ const contentType = INPUT_IMAGE_CONTENT_TYPES[extension];
2318
+ if (contentType === undefined) {
2319
+ throw new GenerateImageError(`Unsupported --input-image extension ${extension || "(none)"}. Use one of: ${Object.keys(INPUT_IMAGE_CONTENT_TYPES).join(", ")}.`);
2320
+ }
2321
+ return contentType;
2322
+ }
2166
2323
  function validateInputObject(input) {
2167
2324
  if ("sync_mode" in input && input.sync_mode !== undefined) {
2168
2325
  throw new GenerateImageError('--input must not contain "sync_mode": it returns data URIs that this command cannot download. Omit it.');
@@ -2176,6 +2333,9 @@ function validateInputObject(input) {
2176
2333
  }
2177
2334
  }
2178
2335
  }
2336
+ function isPlainObject(value) {
2337
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2338
+ }
2179
2339
  function parseInputJson(text) {
2180
2340
  let parsed;
2181
2341
  try {
@@ -2183,28 +2343,30 @@ function parseInputJson(text) {
2183
2343
  } catch {
2184
2344
  throw new GenerateImageError("--input must be valid JSON.");
2185
2345
  }
2186
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2346
+ if (!isPlainObject(parsed)) {
2187
2347
  throw new GenerateImageError("--input must be a JSON object.");
2188
2348
  }
2189
- const input = parsed;
2190
- validateInputObject(input);
2191
- return input;
2349
+ validateInputObject(parsed);
2350
+ return parsed;
2192
2351
  }
2193
- function buildInput(prompt, opts) {
2194
- if (opts.input !== undefined) {
2195
- if (typeof opts.input !== "object" || opts.input === null || Array.isArray(opts.input)) {
2196
- throw new GenerateImageError("--input must be a JSON object.");
2197
- }
2198
- validateInputObject(opts.input);
2199
- }
2352
+ function buildInput(prompt, opts, imageUrl) {
2200
2353
  return {
2201
2354
  prompt,
2202
2355
  ...opts.input ?? {},
2203
2356
  ...opts.aspectRatio !== undefined ? { aspect_ratio: opts.aspectRatio } : {},
2204
2357
  ...opts.outputFormat !== undefined ? { output_format: opts.outputFormat } : {},
2205
- ...opts.seed !== undefined ? { seed: opts.seed } : {}
2358
+ ...opts.seed !== undefined ? { seed: opts.seed } : {},
2359
+ ...imageUrl !== undefined ? { image_urls: [imageUrl] } : {}
2206
2360
  };
2207
2361
  }
2362
+ function validateInputOption(opts) {
2363
+ if (opts.input === undefined)
2364
+ return;
2365
+ if (!isPlainObject(opts.input)) {
2366
+ throw new GenerateImageError("--input must be a JSON object.");
2367
+ }
2368
+ validateInputObject(opts.input);
2369
+ }
2208
2370
  function requireImage(images, model) {
2209
2371
  const image = images[0];
2210
2372
  if (!image?.url) {
@@ -2266,6 +2428,7 @@ function buildManifest(args) {
2266
2428
  sdk: `@fal-ai/client@${args.sdkVersion}`,
2267
2429
  model: args.model,
2268
2430
  input: args.input,
2431
+ inputImage: args.inputImage,
2269
2432
  requestId: args.requestId ?? null,
2270
2433
  seed: resolveSeed(args.resultSeed, args.requestedSeed),
2271
2434
  output: {
@@ -2278,21 +2441,118 @@ function buildManifest(args) {
2278
2441
  licensing: LICENSING
2279
2442
  };
2280
2443
  }
2281
- async function generateImage(opts, deps) {
2444
+ function requireTaskPairing(capability, model, inputImage) {
2445
+ if (capability.task === "image-edit" && inputImage === undefined) {
2446
+ throw new GenerateImageError(`${model} edits an image; pass --input-image <path-or-url>.`);
2447
+ }
2448
+ if (capability.task === "text-to-image" && inputImage !== undefined) {
2449
+ throw new GenerateImageError(`${model} does not take an input image; use --model ${FLUX_2_FLASH_EDIT_MODEL} with --input-image.`);
2450
+ }
2451
+ }
2452
+ function requireAspectRatioSupport(capability, model, aspectRatio) {
2453
+ if (aspectRatio === undefined)
2454
+ return;
2455
+ const support = capability.params.aspect_ratio;
2456
+ if (support?.support !== "unsupported")
2457
+ return;
2458
+ throw new GenerateImageError(`--aspect-ratio is not supported by ${model}. ${support.reason}`);
2459
+ }
2460
+ function requireModelPairing(model, inputImage, aspectRatio) {
2461
+ const capability = getModelCapability(model);
2462
+ if (capability === undefined)
2463
+ return;
2464
+ requireTaskPairing(capability, model, inputImage);
2465
+ requireAspectRatioSupport(capability, model, aspectRatio);
2466
+ }
2467
+ function resolveInputImageOption(opts) {
2468
+ return opts.inputImage === undefined ? undefined : requireNonEmpty(opts.inputImage, "--input-image must not be empty.");
2469
+ }
2470
+ async function validateInputImage(deps, inputImage) {
2471
+ if (inputImage === undefined)
2472
+ return;
2473
+ if (HTTP_URL_PATTERN.test(inputImage))
2474
+ return { kind: "url", url: inputImage };
2475
+ const localPath = resolve(inputImage);
2476
+ const contentType = inputImageContentType(localPath);
2477
+ const status = await deps.pathStatus(localPath);
2478
+ if (status !== "file") {
2479
+ throw new GenerateImageError(`--input-image is not a file: ${localPath}`);
2480
+ }
2481
+ return { kind: "file", path: localPath, contentType };
2482
+ }
2483
+ function planInputImageUrl(target) {
2484
+ if (target === undefined)
2485
+ return;
2486
+ return target.kind === "url" ? target.url : `<fal storage URL for ${target.path}>`;
2487
+ }
2488
+ async function uploadInputImage(deps, target) {
2489
+ if (target === undefined)
2490
+ return { url: undefined, record: null };
2491
+ if (target.kind === "url") {
2492
+ return { url: target.url, record: { source: target.url, url: target.url } };
2493
+ }
2494
+ const url = await deps.uploadFile(target.path, target.contentType);
2495
+ return { url, record: { source: target.path, url } };
2496
+ }
2497
+ async function planGenerateImage(opts, deps) {
2282
2498
  const platform = resolvePlatform(opts.platform);
2283
2499
  requireSupportedPlatform(platform);
2284
- requireFalKey(deps);
2500
+ const model = requireNonEmpty(opts.model, "--model must not be empty.");
2285
2501
  const prompt = requireNonEmpty(opts.prompt, "--prompt must not be empty.");
2286
2502
  const outPath = resolve(requireNonEmpty(opts.out, "--out must not be empty."));
2287
2503
  requireSupportedExtension(outPath);
2288
2504
  if (opts.outputFormat !== undefined) {
2289
2505
  requireFormatMatchesExtension(outPath, opts.outputFormat);
2290
2506
  }
2507
+ const inputImage = resolveInputImageOption(opts);
2508
+ requireModelPairing(model, inputImage, opts.aspectRatio);
2509
+ validateInputOption(opts);
2510
+ const inputTarget = await validateInputImage(deps, inputImage);
2291
2511
  const manifestPath = `${outPath}.json`;
2292
2512
  await requireWritableOutput(deps, outPath, manifestPath, opts.force === true);
2293
- await deps.ensureParentDir(outPath);
2294
- const model = opts.model ?? DEFAULT_IMAGE_MODEL;
2295
- const input = buildInput(prompt, opts);
2513
+ const input = buildInput(prompt, opts, planInputImageUrl(inputTarget));
2514
+ return {
2515
+ platform,
2516
+ model,
2517
+ outPath,
2518
+ manifestPath,
2519
+ input,
2520
+ inputTarget,
2521
+ disclosures: describeConstraints(model, input, { aspectRatio: opts.aspectRatio })
2522
+ };
2523
+ }
2524
+ function disclosureFlag(key) {
2525
+ return key === "aspect_ratio" ? "--aspect-ratio" : "--input";
2526
+ }
2527
+ function formatDisclosure(disclosure) {
2528
+ if (disclosure.kind === "unsupported" || disclosure.kind === "unknown") {
2529
+ return `- ${disclosure.kind}: ${disclosure.key} (${disclosureFlag(disclosure.key)}) — ${disclosure.message}`;
2530
+ }
2531
+ return `- ${disclosure.kind}: ${disclosure.message}`;
2532
+ }
2533
+ function formatInputImageLine(target) {
2534
+ if (target === undefined)
2535
+ return;
2536
+ return target.kind === "url" ? `Input image ${target.url} (passed through)` : `Input image ${target.path} (would be uploaded to fal storage, 1-day lifetime)`;
2537
+ }
2538
+ function formatGenerateImagePlan(plan) {
2539
+ const capability = getModelCapability(plan.model);
2540
+ const lines = [
2541
+ `Would generate ${plan.outPath}`,
2542
+ `Manifest ${plan.manifestPath}`,
2543
+ capability === undefined ? `Model ${plan.model} (no capability data)` : `Model ${plan.model} (${capability.name}, ${capability.task})`
2544
+ ];
2545
+ const inputLine = formatInputImageLine(plan.inputTarget);
2546
+ if (inputLine !== undefined)
2547
+ lines.push(inputLine);
2548
+ lines.push("Request:", JSON.stringify(plan.input, null, 2), "Constraints:");
2549
+ for (const disclosure of plan.disclosures)
2550
+ lines.push(formatDisclosure(disclosure));
2551
+ lines.push("No upload or fal call was made.");
2552
+ return lines.join(`
2553
+ `);
2554
+ }
2555
+ async function generateAndSave(deps, model, input, outPath) {
2296
2556
  const result = await deps.falSubscribe(model, input);
2297
2557
  if (result.data.images.length > 1) {
2298
2558
  deps.warn(`⚠️ fal returned ${result.data.images.length} images; only the first is saved.`);
@@ -2300,25 +2560,46 @@ async function generateImage(opts, deps) {
2300
2560
  const image = requireImage(result.data.images, model);
2301
2561
  const bytes = await downloadImage(deps, result.requestId, image.url);
2302
2562
  await saveImage(deps, outPath, bytes, result.requestId, image.url);
2303
- const sha256 = createHash("sha256").update(bytes).digest("hex");
2304
- await deps.writeJson(manifestPath, buildManifest({
2305
- platform,
2306
- model,
2307
- input,
2563
+ return {
2564
+ image,
2565
+ bytes,
2566
+ sha256: createHash("sha256").update(bytes).digest("hex"),
2308
2567
  requestId: result.requestId,
2309
- resultSeed: result.data.seed,
2568
+ seed: result.data.seed
2569
+ };
2570
+ }
2571
+ async function generateImage(opts, deps) {
2572
+ requireFalKey(deps);
2573
+ const plan = await planGenerateImage(opts, deps);
2574
+ for (const disclosure of plan.disclosures) {
2575
+ if (disclosure.kind === "unsupported") {
2576
+ deps.warn(`⚠️ ${disclosure.key}: ${disclosure.message}`);
2577
+ }
2578
+ }
2579
+ await deps.ensureParentDir(plan.outPath);
2580
+ const preparedInput = await uploadInputImage(deps, plan.inputTarget);
2581
+ const input = preparedInput.url === undefined ? plan.input : { ...plan.input, image_urls: [preparedInput.url] };
2582
+ const saved = await generateAndSave(deps, plan.model, input, plan.outPath);
2583
+ await deps.writeJson(plan.manifestPath, buildManifest({
2584
+ platform: plan.platform,
2585
+ model: plan.model,
2586
+ input,
2587
+ inputImage: preparedInput.record,
2588
+ requestId: saved.requestId,
2589
+ resultSeed: saved.seed,
2310
2590
  requestedSeed: opts.seed,
2311
- outPath,
2312
- sha256,
2313
- contentType: image.content_type,
2314
- byteLength: bytes.byteLength,
2591
+ outPath: plan.outPath,
2592
+ sha256: saved.sha256,
2593
+ contentType: saved.image.content_type,
2594
+ byteLength: saved.bytes.byteLength,
2315
2595
  generatedAt: deps.now().toISOString(),
2316
2596
  sdkVersion: deps.sdkVersion
2317
2597
  }));
2318
- return { outPath, manifestPath, sha256 };
2598
+ return { outPath: plan.outPath, manifestPath: plan.manifestPath, sha256: saved.sha256 };
2319
2599
  }
2320
- var DEFAULT_IMAGE_MODEL = "fal-ai/flux-2/flash", GENERATE_IMAGE_PLATFORMS, DEFAULT_GENERATE_IMAGE_PLATFORM = "fal", GenerateImageError, LICENSING = "unverified", FAL_API_KEY_MISSING_MESSAGE = "FAL_API_KEY is not set. Set FAL_API_KEY in your environment to generate images with fal. (FAL_KEY_ID/FAL_KEY_SECRET are not supported by this command.)", EXTENSION_FORMATS, UNSUPPORTED_OUTPUT_EXTENSIONS, INPUT_DEDICATED_FLAGS;
2600
+ var GENERATE_IMAGE_PLATFORMS, DEFAULT_GENERATE_IMAGE_PLATFORM = "fal", GenerateImageError, LICENSING = "unverified", FAL_API_KEY_MISSING_MESSAGE = "FAL_API_KEY is not set. Set FAL_API_KEY in your environment to generate images with fal. (FAL_KEY_ID/FAL_KEY_SECRET are not supported by this command.)", EXTENSION_FORMATS, UNSUPPORTED_OUTPUT_EXTENSIONS, INPUT_DEDICATED_FLAGS, INPUT_IMAGE_CONTENT_TYPES, HTTP_URL_PATTERN;
2321
2601
  var init_image = __esm(() => {
2602
+ init_model_capabilities();
2322
2603
  GENERATE_IMAGE_PLATFORMS = ["fal"];
2323
2604
  GenerateImageError = class GenerateImageError extends Error {
2324
2605
  constructor(message) {
@@ -2335,8 +2616,17 @@ var init_image = __esm(() => {
2335
2616
  prompt: "--prompt",
2336
2617
  seed: "--seed",
2337
2618
  output_format: "--output-format",
2338
- aspect_ratio: "--aspect-ratio"
2619
+ aspect_ratio: "--aspect-ratio",
2620
+ image_urls: "--input-image"
2339
2621
  };
2622
+ INPUT_IMAGE_CONTENT_TYPES = {
2623
+ ".png": "image/png",
2624
+ ".jpg": "image/jpeg",
2625
+ ".jpeg": "image/jpeg",
2626
+ ".webp": "image/webp",
2627
+ ".gif": "image/gif"
2628
+ };
2629
+ HTTP_URL_PATTERN = /^https?:\/\//i;
2340
2630
  });
2341
2631
 
2342
2632
  // src/commands/generate/local-image-common.ts
@@ -2350,6 +2640,13 @@ function sha256(bytes) {
2350
2640
  function errorMessage(err) {
2351
2641
  return err instanceof Error ? err.message : String(err);
2352
2642
  }
2643
+ function median(values) {
2644
+ const sorted = [...values].sort((a, b) => a - b);
2645
+ const middle = Math.floor(sorted.length / 2);
2646
+ if (sorted.length % 2 === 1)
2647
+ return sorted[middle] ?? 0;
2648
+ return Math.round(((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2);
2649
+ }
2353
2650
  function hasPngMagic(bytes) {
2354
2651
  if (bytes.length < PNG_MAGIC.length)
2355
2652
  return false;
@@ -2668,7 +2965,7 @@ var require_utils = __commonJS((exports) => {
2668
2965
  exports.isValidUrl = isValidUrl;
2669
2966
  exports.throttle = throttle;
2670
2967
  exports.isReact = isReact;
2671
- exports.isPlainObject = isPlainObject;
2968
+ exports.isPlainObject = isPlainObject2;
2672
2969
  exports.sleep = sleep;
2673
2970
  function ensureEndpointIdFormat(id) {
2674
2971
  const parts = id.split("/");
@@ -2744,7 +3041,7 @@ var require_utils = __commonJS((exports) => {
2744
3041
  }
2745
3042
  return isRunningInReact;
2746
3043
  }
2747
- function isPlainObject(value) {
3044
+ function isPlainObject2(value) {
2748
3045
  return !!value && Object.getPrototypeOf(value) === Object.prototype;
2749
3046
  }
2750
3047
  function sleep(ms) {
@@ -6495,7 +6792,7 @@ __export(exports_deps, {
6495
6792
  createFalRawSubscribe: () => createFalRawSubscribe,
6496
6793
  createDefaultGenerateImageDeps: () => createDefaultGenerateImageDeps
6497
6794
  });
6498
- import { mkdir } from "node:fs/promises";
6795
+ import { mkdir, readFile } from "node:fs/promises";
6499
6796
  import { dirname as dirname2 } from "node:path";
6500
6797
  function isRecord2(value) {
6501
6798
  return typeof value === "object" && value !== null;
@@ -6598,13 +6895,16 @@ function describeFalError(err) {
6598
6895
  return formatApiError(err);
6599
6896
  return err instanceof Error ? err.message : String(err);
6600
6897
  }
6898
+ function configureFal(env) {
6899
+ const credentials = env.FAL_API_KEY;
6900
+ if (!credentials) {
6901
+ throw new GenerateImageError(FAL_API_KEY_MISSING_MESSAGE);
6902
+ }
6903
+ import_client.fal.config({ credentials });
6904
+ }
6601
6905
  function createFalRawSubscribe(env) {
6602
6906
  return async (model, input) => {
6603
- const credentials = env.FAL_API_KEY;
6604
- if (!credentials) {
6605
- throw new GenerateImageError(FAL_API_KEY_MISSING_MESSAGE);
6606
- }
6607
- import_client.fal.config({ credentials });
6907
+ configureFal(env);
6608
6908
  try {
6609
6909
  return await import_client.fal.subscribe(model, {
6610
6910
  input,
@@ -6620,6 +6920,18 @@ function createFalSubscribe(env) {
6620
6920
  const subscribe = createFalRawSubscribe(env);
6621
6921
  return async (model, input) => validateFalResult(await subscribe(model, input));
6622
6922
  }
6923
+ function createFalUploadFile(env) {
6924
+ return async (path, contentType) => {
6925
+ configureFal(env);
6926
+ try {
6927
+ const bytes = await readFile(path);
6928
+ const blob = new Blob([new Uint8Array(bytes)], { type: contentType });
6929
+ return await import_client.fal.storage.upload(blob, { lifecycle: { expiresIn: "1d" } });
6930
+ } catch (err) {
6931
+ throw new GenerateImageError(`Failed to upload input image ${path}: ${describeFalError(err)}`);
6932
+ }
6933
+ };
6934
+ }
6623
6935
  async function ensureParentDir(path) {
6624
6936
  await mkdir(dirname2(path), { recursive: true });
6625
6937
  }
@@ -6642,6 +6954,7 @@ function createDefaultGenerateImageDeps() {
6642
6954
  return {
6643
6955
  env,
6644
6956
  falSubscribe: createFalSubscribe(env),
6957
+ uploadFile: createFalUploadFile(env),
6645
6958
  fetchBytes,
6646
6959
  writeFile: writeFileBytes,
6647
6960
  writeJson: writeJsonFile,
@@ -8552,7 +8865,7 @@ var exports_slice_deps = {};
8552
8865
  __export(exports_slice_deps, {
8553
8866
  createDefaultSliceImageDeps: () => createDefaultSliceImageDeps
8554
8867
  });
8555
- import { lstat, mkdir as mkdir2, readFile as readFile2, stat as stat3 } from "node:fs/promises";
8868
+ import { lstat, mkdir as mkdir2, readFile as readFile3, stat as stat3 } from "node:fs/promises";
8556
8869
  function decodePng(bytes) {
8557
8870
  const png = PNG.sync.read(Buffer.from(bytes));
8558
8871
  return { width: png.width, height: png.height, data: png.data };
@@ -8582,7 +8895,7 @@ async function statInput(path) {
8582
8895
  async function readJsonIfExists(path) {
8583
8896
  let text;
8584
8897
  try {
8585
- text = await readFile2(path, "utf8");
8898
+ text = await readFile3(path, "utf8");
8586
8899
  } catch (err) {
8587
8900
  if (err.code === "ENOENT") {
8588
8901
  return;
@@ -8619,7 +8932,7 @@ function createDefaultSliceImageDeps() {
8619
8932
  return {
8620
8933
  pathStatus,
8621
8934
  statInput,
8622
- readFile: readFile2,
8935
+ readFile: readFile3,
8623
8936
  readJsonIfExists,
8624
8937
  decodePng,
8625
8938
  encodePng,
@@ -27379,7 +27692,7 @@ __export(exports_util, {
27379
27692
  jsonStringifyReplacer: () => jsonStringifyReplacer,
27380
27693
  joinValues: () => joinValues,
27381
27694
  issue: () => issue,
27382
- isPlainObject: () => isPlainObject,
27695
+ isPlainObject: () => isPlainObject2,
27383
27696
  isObject: () => isObject,
27384
27697
  hexToUint8Array: () => hexToUint8Array,
27385
27698
  getSizableOrigin: () => getSizableOrigin,
@@ -27544,7 +27857,7 @@ function slugify(input) {
27544
27857
  function isObject(data) {
27545
27858
  return typeof data === "object" && data !== null && !Array.isArray(data);
27546
27859
  }
27547
- function isPlainObject(o) {
27860
+ function isPlainObject2(o) {
27548
27861
  if (isObject(o) === false)
27549
27862
  return false;
27550
27863
  const ctor = o.constructor;
@@ -27561,7 +27874,7 @@ function isPlainObject(o) {
27561
27874
  return true;
27562
27875
  }
27563
27876
  function shallowClone(o) {
27564
- if (isPlainObject(o))
27877
+ if (isPlainObject2(o))
27565
27878
  return { ...o };
27566
27879
  if (Array.isArray(o))
27567
27880
  return [...o];
@@ -27701,7 +28014,7 @@ function omit2(schema, mask) {
27701
28014
  return clone(schema, def);
27702
28015
  }
27703
28016
  function extend(schema, shape) {
27704
- if (!isPlainObject(shape)) {
28017
+ if (!isPlainObject2(shape)) {
27705
28018
  throw new Error("Invalid input to extend: expected a plain object");
27706
28019
  }
27707
28020
  const checks = schema._zod.def.checks;
@@ -27724,7 +28037,7 @@ function extend(schema, shape) {
27724
28037
  return clone(schema, def);
27725
28038
  }
27726
28039
  function safeExtend(schema, shape) {
27727
- if (!isPlainObject(shape)) {
28040
+ if (!isPlainObject2(shape)) {
27728
28041
  throw new Error("Invalid input to safeExtend: expected a plain object");
27729
28042
  }
27730
28043
  const def = mergeDefs(schema._zod.def, {
@@ -29207,7 +29520,7 @@ function mergeValues(a, b) {
29207
29520
  if (a instanceof Date && b instanceof Date && +a === +b) {
29208
29521
  return { valid: true, data: a };
29209
29522
  }
29210
- if (isPlainObject(a) && isPlainObject(b)) {
29523
+ if (isPlainObject2(a) && isPlainObject2(b)) {
29211
29524
  const bKeys = Object.keys(b);
29212
29525
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
29213
29526
  const newObj = { ...a, ...b };
@@ -30464,7 +30777,7 @@ var init_schemas = __esm(() => {
30464
30777
  $ZodType.init(inst, def);
30465
30778
  inst._zod.parse = (payload, ctx) => {
30466
30779
  const input = payload.value;
30467
- if (!isPlainObject(input)) {
30780
+ if (!isPlainObject2(input)) {
30468
30781
  payload.issues.push({
30469
30782
  expected: "record",
30470
30783
  code: "invalid_type",
@@ -71424,7 +71737,7 @@ var require_filesystem = __commonJS((exports, module) => {
71424
71737
  fs6.close(fd, () => {});
71425
71738
  return buffer.subarray(0, bytesRead);
71426
71739
  };
71427
- var readFile6 = (path2) => new Promise((resolve6, reject) => {
71740
+ var readFile7 = (path2) => new Promise((resolve6, reject) => {
71428
71741
  fs6.open(path2, "r", (err, fd) => {
71429
71742
  if (err) {
71430
71743
  reject(err);
@@ -71441,7 +71754,7 @@ var require_filesystem = __commonJS((exports, module) => {
71441
71754
  LDD_PATH,
71442
71755
  SELF_PATH,
71443
71756
  readFileSync: readFileSync12,
71444
- readFile: readFile6
71757
+ readFile: readFile7
71445
71758
  };
71446
71759
  });
71447
71760
 
@@ -71483,7 +71796,7 @@ var require_elf = __commonJS((exports, module) => {
71483
71796
  var require_detect_libc = __commonJS((exports, module) => {
71484
71797
  var childProcess = __require("child_process");
71485
71798
  var { isLinux, getReport } = require_process();
71486
- var { LDD_PATH, SELF_PATH, readFile: readFile6, readFileSync: readFileSync12 } = require_filesystem();
71799
+ var { LDD_PATH, SELF_PATH, readFile: readFile7, readFileSync: readFileSync12 } = require_filesystem();
71487
71800
  var { interpreterPath } = require_elf();
71488
71801
  var cachedFamilyInterpreter;
71489
71802
  var cachedFamilyFilesystem;
@@ -71563,7 +71876,7 @@ var require_detect_libc = __commonJS((exports, module) => {
71563
71876
  }
71564
71877
  cachedFamilyFilesystem = null;
71565
71878
  try {
71566
- const lddContent = await readFile6(LDD_PATH);
71879
+ const lddContent = await readFile7(LDD_PATH);
71567
71880
  cachedFamilyFilesystem = getFamilyFromLddContent(lddContent);
71568
71881
  } catch (e) {}
71569
71882
  return cachedFamilyFilesystem;
@@ -71585,7 +71898,7 @@ var require_detect_libc = __commonJS((exports, module) => {
71585
71898
  }
71586
71899
  cachedFamilyInterpreter = null;
71587
71900
  try {
71588
- const selfContent = await readFile6(SELF_PATH);
71901
+ const selfContent = await readFile7(SELF_PATH);
71589
71902
  const path2 = interpreterPath(selfContent);
71590
71903
  cachedFamilyInterpreter = familyFromInterpreterPath(path2);
71591
71904
  } catch (e) {}
@@ -71645,7 +71958,7 @@ var require_detect_libc = __commonJS((exports, module) => {
71645
71958
  }
71646
71959
  cachedVersionFilesystem = null;
71647
71960
  try {
71648
- const lddContent = await readFile6(LDD_PATH);
71961
+ const lddContent = await readFile7(LDD_PATH);
71649
71962
  const versionMatch = lddContent.match(RE_GLIBC_VERSION);
71650
71963
  if (versionMatch) {
71651
71964
  cachedVersionFilesystem = versionMatch[1];
@@ -82140,7 +82453,7 @@ var require_loadImage = __commonJS((exports, module) => {
82140
82453
  var fs8 = __require("fs");
82141
82454
  var fetch2 = global.fetch || require_lib3();
82142
82455
  var isURL = require_is_url();
82143
- var readFile6 = util.promisify(fs8.readFile);
82456
+ var readFile7 = util.promisify(fs8.readFile);
82144
82457
  module.exports = async (image) => {
82145
82458
  let data = image;
82146
82459
  if (typeof image === "undefined") {
@@ -82153,7 +82466,7 @@ var require_loadImage = __commonJS((exports, module) => {
82153
82466
  } else if (/data:image\/([a-zA-Z]*);base64,([^"]*)/.test(image)) {
82154
82467
  data = Buffer.from(image.split(",")[1], "base64");
82155
82468
  } else {
82156
- data = await readFile6(image);
82469
+ data = await readFile7(image);
82157
82470
  }
82158
82471
  } else if (Buffer.isBuffer(image)) {
82159
82472
  data = image;
@@ -82753,9 +83066,9 @@ function merge5(...objs) {
82753
83066
  }
82754
83067
  const targetKey = caseless && typeof key === "string" && findKey(result3, key) || key;
82755
83068
  const existing = hasOwnProperty(result3, targetKey) ? result3[targetKey] : undefined;
82756
- if (isPlainObject2(existing) && isPlainObject2(val)) {
83069
+ if (isPlainObject3(existing) && isPlainObject3(val)) {
82757
83070
  result3[targetKey] = merge5(existing, val);
82758
- } else if (isPlainObject2(val)) {
83071
+ } else if (isPlainObject3(val)) {
82759
83072
  result3[targetKey] = merge5({}, val);
82760
83073
  } else if (isArray2(val)) {
82761
83074
  result3[targetKey] = val.slice();
@@ -82858,7 +83171,7 @@ var toString2, getPrototypeOf, iterator, toStringTag, hasOwnProperty, isUnsafeOb
82858
83171
  }, kindOf, kindOfTest = (type) => {
82859
83172
  type = type.toLowerCase();
82860
83173
  return (thing) => kindOf(thing) === type;
82861
- }, typeOfTest = (type) => (thing) => typeof thing === type, isArray2, isUndefined2, isArrayBuffer, isString2, isFunction2, isNumber2, isObject3 = (thing) => thing !== null && typeof thing === "object", isBoolean = (thing) => thing === true || thing === false, isPlainObject2 = (val) => {
83174
+ }, typeOfTest = (type) => (thing) => typeof thing === type, isArray2, isUndefined2, isArrayBuffer, isString2, isFunction2, isNumber2, isObject3 = (thing) => thing !== null && typeof thing === "object", isBoolean = (thing) => thing === true || thing === false, isPlainObject3 = (val) => {
82862
83175
  if (!isObject3(val)) {
82863
83176
  return false;
82864
83177
  }
@@ -83134,7 +83447,7 @@ var init_utils = __esm(() => {
83134
83447
  isNumber: isNumber2,
83135
83448
  isBoolean,
83136
83449
  isObject: isObject3,
83137
- isPlainObject: isPlainObject2,
83450
+ isPlainObject: isPlainObject3,
83138
83451
  isEmptyObject,
83139
83452
  isReadableStream,
83140
83453
  isRequest,
@@ -106274,7 +106587,7 @@ var exports_parse_pdf = {};
106274
106587
  __export(exports_parse_pdf, {
106275
106588
  parsePdf: () => parsePdf
106276
106589
  });
106277
- import { access, readFile as readFile6, writeFile as writeFile7, mkdir as mkdir7, appendFile } from "node:fs/promises";
106590
+ import { access, readFile as readFile7, writeFile as writeFile7, mkdir as mkdir7, appendFile } from "node:fs/promises";
106278
106591
  import { resolve as resolve6 } from "node:path";
106279
106592
  function isUrl(input) {
106280
106593
  return input.startsWith("http://") || input.startsWith("https://");
@@ -106284,9 +106597,9 @@ function createDefaultDeps6() {
106284
106597
  fs: {
106285
106598
  access: (p) => access(p),
106286
106599
  mkdir: (p, opts) => mkdir7(p, opts).then(() => {}),
106287
- readFile: (p, enc) => readFile6(p, { encoding: enc }).then((b) => b.toString()),
106600
+ readFile: (p, enc) => readFile7(p, { encoding: enc }).then((b) => b.toString()),
106288
106601
  appendFile: (p, content) => appendFile(p, content),
106289
- readFileAsBuffer: (p) => readFile6(p),
106602
+ readFileAsBuffer: (p) => readFile7(p),
106290
106603
  writeFile: (p, content, enc) => writeFile7(p, content, enc)
106291
106604
  },
106292
106605
  parser: {
@@ -107472,9 +107785,11 @@ function getEntryPointToArchitectHandoffTemplate(query) {
107472
107785
  const entryPoint = query.fromRole;
107473
107786
  return `The automatically injected \`<user-message>\` is the authoritative request. Add only concise context that the Architect needs to design within the existing repository and team scope.
107474
107787
 
107788
+ Use this before any backend contract, API, or schema/persistence change.
107789
+
107475
107790
  Provide the request, relevant constraints, and the files or product surfaces that need inspection. Ask for one evidence-backed design focused on ${getArchitectFocus()}.
107476
107791
 
107477
- After completing the design, hand it back to \`${entryPoint}\` with the normal handoff command. Stop after the successful handoff; do not implement the design or delegate further work.
107792
+ After completing the design, the \`architect\` must hand it back to \`${entryPoint}\` with the normal handoff command, then stop; it must not implement the design or delegate further work.
107478
107793
 
107479
107794
  \`\`\`bash
107480
107795
  ${handoffCommand({
@@ -107914,7 +108229,7 @@ function formatProofOfPrinciples() {
107914
108229
  var GAME_ASSET_RULES;
107915
108230
  var init_game_asset_designer_workflow = __esm(() => {
107916
108231
  GAME_ASSET_RULES = [
107917
- "Generate only with `chatroom generate-image --platform=fal`, using the CLI default model `fal-ai/flux-2/flash` unless the brief names one.",
108232
+ "Generate only with `chatroom generate-image --platform=fal --model <Model>`, where <Model> is the brief's Model: `fal-ai/flux-2/flash` (FLUX 2 Flash), or `fal-ai/flux-2/flash/edit` with --input-image for reference edits.",
107918
108233
  "Process images only with `chatroom slice-image` and `chatroom remove-image-background`; no other tool, script, or remote service.",
107919
108234
  "A sheet is at most the brief's `Sheet grid` (default 2x2); a larger grid needs a user request quoted in the brief.",
107920
108235
  "Names come from looking at each tile, never from cell order or prompt order.",
@@ -107928,14 +108243,15 @@ var init_game_asset_designer_workflow = __esm(() => {
107928
108243
  "Never switch generators.",
107929
108244
  "Describe generated output as production-ready only when every checklist item for it passes with `measured` or `inspected` evidence.",
107930
108245
  "Commit only when the brief names the paths and the commit message.",
107931
- "Generate only when the brief names an output location and allowed tools."
108246
+ "Generate only when the brief names an output location and allowed tools.",
108247
+ "Before the first real call of each command shape (sheet, per-asset, or reference edit), run the same command with --dry-run and apply every constraint it prints; take model-specific constraints only from the dry run, never from memory."
107932
108248
  ];
107933
108249
  });
107934
108250
 
107935
108251
  // ../../services/backend/prompts/teams/game-asset-designer-handoff-templates.ts
107936
108252
  function getEntryPointToGameAssetDesignerHandoffTemplate(query) {
107937
108253
  const entryPoint = query.fromRole;
107938
- return `Instructions (not part of the message): Pass the asset request as a brief the designer can act on without guessing. Provide every field below; write \`Not specified.\` for a field you cannot fill and the designer will ask or apply a stated default. Give each asset a stable ID (\`A01\`, \`A02\`…). Use \`spec only\` only when you want a specification with no files; the default is \`spec and generate\`. The \`## Allowed tools\` value is fixed: \`chatroom generate-image --platform=fal (default model), chatroom slice-image, chatroom remove-image-background\`. Do not name any other generator.
108254
+ return `Instructions (not part of the message): Pass the asset request as a brief the designer can act on without guessing. Provide every field below; write \`Not specified.\` for a field you cannot fill and the designer will ask or apply a stated default. Give each asset a stable ID (\`A01\`, \`A02\`…). Use \`spec only\` only when you want a specification with no files; the default is \`spec and generate\`. The \`## Allowed tools\` value is fixed: \`chatroom generate-image --platform=fal --model <Model> (dry run first), chatroom slice-image, chatroom remove-image-background\`. Do not name any other generator.
107939
108255
 
107940
108256
  ## Asset request
107941
108257
  <one to three sentences: what the assets are for and the user-visible goal>
@@ -107952,14 +108268,17 @@ function getEntryPointToGameAssetDesignerHandoffTemplate(query) {
107952
108268
  ## Style reference
107953
108269
  <repository paths, URLs, or a written style sheet; or \`Not specified.\`>
107954
108270
 
108271
+ ## Perspective
108272
+ <one camera perspective shared by every asset in the set, for example \`orthographic front view\`, \`orthographic isometric\`, \`dimetric 2:1\`, or \`side view\`; optionally \`reference: <repository path>\` of an existing asset to match (requires \`## Model\` = \`fal-ai/flux-2/flash/edit\`); a reference makes every asset an individual edit, with no sheets; \`Not Applicable.\` for 3D models and audio; or \`Not specified.\`>
108273
+
107955
108274
  ## Model
107956
- <the CLI default \`fal-ai/flux-2/flash\`>
108275
+ <\`fal-ai/flux-2/flash\` (FLUX 2 Flash), passed as --model; \`fal-ai/flux-2/flash/edit\` only when \`## Perspective\` names a reference asset>
107957
108276
 
107958
108277
  ## Model parameters
107959
- <a JSON object as text passed to \`generate-image --input\`; \`Not specified.\` if none>
108278
+ <a JSON object as text passed to \`generate-image --input\`; one object for every call of a set; its keys must pass the model's --dry-run check; \`Not specified.\` if none>
107960
108279
 
107961
108280
  ## Spec consumer
107962
- <who integrates these assets from the spec: \`builder\` (application code) or \`user\`; asset files themselves are generated only by \`game-asset-designer\`>
108281
+ <who integrates these assets from the spec: \`builder\` (application logic outside the allowed edit paths) or \`user\`; asset files themselves are generated only by \`game-asset-designer\`>
107963
108282
 
107964
108283
  ## Existing assets
107965
108284
  <repository paths the designer must reuse or must not overwrite; or \`Not specified.\`>
@@ -107968,7 +108287,7 @@ function getEntryPointToGameAssetDesignerHandoffTemplate(query) {
107968
108287
  <\`spec and generate\` (default when you name an output location and allowed tools) or \`spec only\`>
107969
108288
 
107970
108289
  ## Allowed tools
107971
- \`chatroom generate-image --platform=fal (default model), chatroom slice-image, chatroom remove-image-background\`
108290
+ \`chatroom generate-image --platform=fal --model <Model> (dry run first), chatroom slice-image, chatroom remove-image-background\`
107972
108291
 
107973
108292
  ## Allowed edit paths
107974
108293
  <repository-relative paths the designer may edit; \`None.\` for none>
@@ -107997,7 +108316,7 @@ Instructions after the body (not part of the message): the planner may append ex
107997
108316
  - assets must meet engine import conventions or budgets before a builder integrates them;
107998
108317
  - the user asks for an asset spec, style sheet, or asset audit.
107999
108318
 
108000
- Skip it for code-only work and for a trivial asset rename. Non-game UI icons and illustrations also go to this role; they are not sent to \`uiux-engineer\`.
108319
+ Skip it for code-only work. Non-game UI icons and illustrations also go to this role; they are not sent to \`uiux-engineer\`.
108001
108320
 
108002
108321
  \`\`\`bash
108003
108322
  ${handoffCommand({
@@ -108028,7 +108347,7 @@ ${PROOF_OF_PRINCIPLES_REPORT}
108028
108347
  <each assumption you made and why; each deviation from the planner's target or budgets; or \`None.\`>
108029
108348
 
108030
108349
  ## Asset manifest
108031
- <one entry per asset, keyed by the planner's stable ID (\`A01\`…): name, category, file format, path (or \`Specified only.\`), dimensions/units/scale, pivot, naming, LOD list with triangle budgets, texture set with resolution, color space and channel packing, atlas/pixel grid/frame data, audio format and loudness target, import settings, dependencies and variants; for each budget, the acceptance check (exact command and expected value); for each generated file, the generator command line (\`chatroom generate-image\` with its \`--platform\`, \`--model\`, and \`--seed\` values), the sheet and tile manifests, and the isolated asset manifest path>
108350
+ <one entry per asset, keyed by the planner's stable ID (\`A01\`…): name, category, file format, path (or \`Specified only.\`), dimensions/units/scale, pivot, naming, LOD list with triangle budgets, texture set with resolution, color space and channel packing, atlas/pixel grid/frame data, audio format and loudness target, import settings, dependencies and variants; for each budget, the acceptance check (exact command and expected value); for each generated file, the generator command line (\`chatroom generate-image\` with its \`--platform\`, \`--model\`, \`--seed\`, \`--input\`, and \`--input-image\` values), the sheet and tile manifests, and the isolated asset manifest path>
108032
108351
 
108033
108352
  ## Quality checklist results
108034
108353
  <per asset: each applicable checklist item as \`pass\`, \`fail\`, \`n/a\`, or \`not checked\`, with its evidence label and, for \`measured\`, the command and value; write exactly \`None failed.\` after the list if no item failed>
@@ -108058,7 +108377,7 @@ Write exactly \`None.\` if no sheet was used.
108058
108377
  <every property you could not measure or inspect; write exactly \`None.\` if none>
108059
108378
 
108060
108379
  ## Recommended next step
108061
- <exactly one target: \`game-asset-designer\` (re-run with the listed asset IDs), \`builder\` (integrate, or apply the listed import settings), \`uiux-engineer\` (layout, component, or styling changes only, never asset files), or \`planner\` (decision needed on an open question); then the affected asset IDs>
108380
+ <exactly one target: \`game-asset-designer\` (re-run with the listed asset IDs), \`builder\` (application logic outside the allowed edit paths), \`uiux-engineer\` (layout, component, or styling changes only, never asset files), or \`planner\` (decision needed on an open question); then the affected asset IDs>
108062
108381
 
108063
108382
  Instructions after the body (not part of the message): run the normal handoff command to \`${entryPoint}\` with the complete body above, then stop.
108064
108383
 
@@ -108129,11 +108448,11 @@ var init_planner_architect_chat = __esm(() => {
108129
108448
  function getPlannerBuilderCodeBody() {
108130
108449
  return `${getDelegationBriefIntro()}
108131
108450
 
108132
- **Before you delegate:** if this slice introduces anything not yet grounded in the repository (a new library, API, external platform behavior, or an unresolved design choice), hand off to \`researcher\` first and delegate only after its brief returns. Skip this for small, well-understood edits. The \`researcher\` handoff template defines the criteria.
108451
+ **Before you delegate:** if this slice introduces anything not yet grounded in the repository (a new library, API, external platform behavior, or an unresolved choice that depends on missing facts), hand off to \`researcher\` first and delegate only after its brief returns. Skip this when the \`researcher\` template's skip condition applies. The \`researcher\` handoff template defines the criteria.
108133
108452
 
108134
108453
  **Bug fixes:** if this slice fixes a reported bug, delegate only after a \`triage\` report for that bug has returned, and build the brief from its root cause and proposed fix. The \`triage\` handoff template defines the investigation.
108135
108454
 
108136
- **Division of labor:** You (planner) own architecture and API shape. The UI/UX engineer owns UI/UX design. Include its target markup, classes, and styles in UI implementation briefs; the builder implements the components.
108455
+ **Division of labor:** For backend contract, API, or schema/persistence changes, the architect designs first and you delegate only after its design returns. You (planner) own slicing and sequencing of builder work. The UI/UX engineer owns UI/UX design. Include its target markup, classes, styles, and screen and flow drawings in UI implementation briefs; the builder implements the components.
108137
108456
 
108138
108457
  **Detail bar:** Specify down to **every file** the builder will create or modify (full repo paths). Include code snippets — types, signatures, stubs, or target implementations — until a competent builder **cannot misinterpret** what to write. Vague layers ("update the backend", "fix the component") are not acceptable.
108139
108458
 
@@ -108236,7 +108555,7 @@ function getEntryPointToResearcherHandoffTemplate(query) {
108236
108555
  Research before delegating when the slice introduces something not already grounded in this repository:
108237
108556
  - a new library, API, external service, platform behavior, or version-sensitive dependency;
108238
108557
  - an unfamiliar subsystem;
108239
- - a change with several plausible designs;
108558
+ - a choice between several plausible approaches that depends on facts not yet in the repository (the \`architect\` still owns backend contract, API, and schema/persistence design);
108240
108559
  - any ambiguity the user has not resolved.
108241
108560
 
108242
108561
  Skip research when the change is a small, well-understood edit in code you have already inspected.
@@ -108323,7 +108642,7 @@ Review before delivery for code-producing work:
108323
108642
  - the user asks for a code review;
108324
108643
  - a change is large, risky, or touches security-sensitive or shared code.
108325
108644
 
108326
- Skip review for a trivial edit that has already been verified.
108645
+ Skip review only for an edit of a few lines in one file that the builder already verified with a test or command.
108327
108646
 
108328
108647
  After completing the review, the reviewer must return its issue report to \`${entryPoint}\` with the normal handoff command.
108329
108648
 
@@ -108385,6 +108704,8 @@ function getEntryPointToTriageHandoffTemplate(query) {
108385
108704
  const entryPoint = query.fromRole;
108386
108705
  return `The automatically injected \`<user-message>\` is the authoritative request. Pass the reported bug, observed and expected behavior, reproduction details and commands, relevant constraints, and the files or symbols already suspected. Ask triage to investigate and report only. Triage may inspect code and attempt a focused automated regression test as evidence, but must not edit implementation files or implement a fix.
108387
108706
 
108707
+ Use this for every reported bug (something broken or behaving differently than expected) before any fix is implemented.
108708
+
108388
108709
  After completing the investigation, triage must return the evidence-backed report to \`${entryPoint}\` with the normal handoff command.
108389
108710
 
108390
108711
  \`\`\`bash
@@ -108451,11 +108772,12 @@ var init_planner_triage_chat = __esm(() => {
108451
108772
 
108452
108773
  // ../../services/backend/prompts/teams/uiux-engineer-handoff-templates.ts
108453
108774
  function getUiuxEngineerFocus() {
108454
- return "complete user flows, loading/empty/error/success states, component ownership, theme tokens and layout, responsive details, and UI tests";
108775
+ return "complete user flows, screen and flow drawings, loading/empty/error/success states, component ownership, theme tokens and layout, responsive details, and UI tests";
108455
108776
  }
108456
108777
  function getUiuxEngineerRequirements() {
108457
108778
  return `
108458
108779
  - Specify complete user flows with explicit loading, empty, error, success, disabled, and retry states where applicable.
108780
+ - For any user-facing workflow, draw each affected screen as an ASCII wireframe and each flow as a Mermaid flowchart, using the "Screens, Flows, and Layouts" section. Each screen drawing labels every button, input, and link with its exact text and the repository component it uses; each flow drawing shows the loading, empty, error, success, disabled, and retry transitions that apply.
108459
108781
  - Define component hierarchy and ownership, props/state/events, data boundaries, and interaction behavior.
108460
108782
  - Choose concrete existing ShadCN components with the Base UI backend and lucide-react/react-icons conventions; do not write "use an appropriate component."
108461
108783
  - Specify Tailwind utility classes or semantic theme-token classes for layout, spacing, sizing, typography, states, responsive behavior, and dark mode.
@@ -108470,7 +108792,7 @@ function getEntryPointToUiuxEngineerHandoffTemplate(query) {
108470
108792
 
108471
108793
  Provide the request, relevant constraints, and the files or product surfaces that need inspection. Ask for one evidence-backed design focused on ${getUiuxEngineerFocus()}.
108472
108794
 
108473
- After completing the design, hand it back to \`${entryPoint}\` with the normal handoff command. Stop after the successful handoff; do not implement the design or delegate further work.
108795
+ After completing the design, the \`uiux-engineer\` must hand it back to \`${entryPoint}\` with the normal handoff command, then stop; it must not implement the design or delegate further work.
108474
108796
 
108475
108797
  \`\`\`bash
108476
108798
  ${handoffCommand({
@@ -108498,6 +108820,24 @@ function getUiuxEngineerToEntryPointHandoffTemplate(query) {
108498
108820
  - Name concrete repository paths, existing components/tokens, props/state/events, data boundaries, acceptance criteria, risks, and verification steps.
108499
108821
  ${getUiuxEngineerRequirements()}
108500
108822
 
108823
+ ## Screens, Flows, and Layouts
108824
+ <required for a user-facing workflow; write exactly \`Not Applicable.\` for a non-visual change>
108825
+
108826
+ ### Flow: <flow name>
108827
+ \`\`\`mermaid
108828
+ flowchart TD
108829
+ <one node per screen or state; each edge labelled with the user action or system event that triggers it; include the loading, empty, error, success, disabled, and retry states that apply>
108830
+ \`\`\`
108831
+
108832
+ ### Screen: <screen name> (\`<route or entry point>\`)
108833
+ \`\`\`text
108834
+ <ASCII wireframe: the layout regions, headings, and every button, input, and link labelled with its exact text, with the repository component in brackets, for example [Button variant="destructive"]. Show the empty, loading, and error variants as separate drawings if they change the layout.>
108835
+ \`\`\`
108836
+ - States: <loading, empty, error, success, disabled, retry — which apply and how each changes the layout>
108837
+ - Responsive: <breakpoint behavior, one line per breakpoint that changes the layout>
108838
+
108839
+ <repeat one ### Flow block per flow and one ### Screen block per screen; do not collapse several screens into one drawing>
108840
+
108501
108841
  ## Force Multipliers
108502
108842
  <name the existing components, patterns, tokens, utilities, and platform conventions to reuse, and state why each reduces implementation risk or duplication>
108503
108843
  - Keep the design within the repository's established source-of-truth boundaries and avoid introducing a new registry or abstraction without evidence.
@@ -109928,7 +110268,7 @@ var init_architect3 = __esm(() => {
109928
110268
  init_sections();
109929
110269
  ARCHITECT_GUIDANCE = `## Architect Operating Model
109930
110270
 
109931
- You are the architect responsible for careful coding and system design. You are advisory only: do not implement code, edit files, or act as the builder.
110271
+ You are the architect responsible for system design and code-level implementation plans. You are advisory only: do not implement code, edit files, or act as the builder.
109932
110272
 
109933
110273
  ${"{SESSION_CONTINUITY}"}
109934
110274
 
@@ -109962,7 +110302,7 @@ var init_game_asset_designer2 = __esm(() => {
109962
110302
  init_sections();
109963
110303
  GAME_ASSET_DESIGNER_GUIDANCE = `## Game Asset Designer Operating Model
109964
110304
 
109965
- You specify production-quality game assets and generate them only with \`chatroom generate-image --platform=fal\` when the planner's request allows it. You are not the builder or planner; do not implement application logic outside the asset-integration paths the brief allows. Your delivery report uses Proof of Principles and Proof of Completion.
110305
+ You specify and generate production-quality game and UI assets (generation is the default unless the brief says \`spec only\`), using \`chatroom generate-image --platform=fal\`. You are not the builder or planner; do not implement application logic outside the asset-integration paths the brief allows. Your delivery report uses Proof of Principles and Proof of Completion.
109966
110306
 
109967
110307
  ${"{SESSION_CONTINUITY}"}
109968
110308
 
@@ -109980,7 +110320,7 @@ P1 Recover the brief. IF engine, tier, style, or budgets are missing and the rep
109980
110320
  P2 Inspect assets, names, folders, engine config; cite paths.
109981
110321
  P3 Classify each asset; one manifest entry per ID (\`A01\`…).
109982
110322
  P4 IF \`Deliverable mode\` = \`spec only\` OR \`Output location\`/\`Allowed tools\` missing/ambiguous THEN no files, deliver \`Specified only.\`, GOTO P10.
109983
- P5 Count N. IF N=1 or styles differ THEN per-asset \`generate-image\`, GOTO P8. ELSE use ceil(N / (C×R)) sheets of ≤(C×R) items per \`Sheet grid\` (2x2 default). Prompt: one item per cell, CxR grid, no lines/borders/frames/duplicates. Generate \`generate-image --platform=fal --prompt "<prompt>" --out <Output location>/_work/<sheet>.png\`.
110323
+ P5 Count N. IF N=1 or styles differ THEN per-asset \`generate-image\`, GOTO P8. ELSE use ceil(N / (C×R)) sheets of ≤(C×R) items per \`Sheet grid\` (2x2 default). Prompt: one item per cell, CxR grid, no lines/borders/frames/duplicates. Generate \`generate-image --platform=fal --model <Model> --prompt "<prompt>" --out <Output location>/_work/<sheet>.png\`.
109984
110324
  P6 \`chatroom slice-image --in <sheet> --out-dir <Output location>/_work/tiles --cols C --rows R\`. IF refused regenerate once; IF still refused one \`generate-image\` per asset (P5).
109985
110325
  P7 View each tile. IF any tile has 0/2+ items, a cut item, or count differs from the brief's requested count (extra cells OK) THEN regenerate once; IF still bad THEN one \`generate-image\` per asset (P5). Name from sight.
109986
110326
  P8 IF \`remove-image-background --help\` succeeds THEN run \`remove-image-background --in-dir <tiles> --out-dir <Output location> --map <stem>=<name>,... --trim\` (single: \`--in <image> --out <Output location>/<name>.png --trim\`); view results. IF warnings/halo THEN rerun with \`--tolerance\`/\`--force\` max twice, ELSE accept. ELSE rerun \`slice-image --names\`; record trimming not delivered as a failed item/open question.
@@ -110019,11 +110359,20 @@ P11 Hand off to \`{ENTRY_POINT_ROLE}\`, then stop.
110019
110359
  - Animation: uniform frame size per clip, frame count, frame rate, loop flag, and pivot per clip.
110020
110360
  - Atlas: padding of at least the engine default (Unity default 4 px), atlas size within the platform max, filter mode Point for pixel art and no mip maps.
110021
110361
  - Export PNG (lossless with alpha); name states size and frame count where relevant.
110362
+ - Camera perspective matches the perspective chosen for the set (the brief's \`Perspective\`, or the one recorded under assumptions) and every other asset in the set (compare them side by side); \`n/a\` for a single asset.
110022
110363
 
110023
110364
  **Audio**
110024
110365
  - Loudness measured against the platform target stated in the request (for example console -24 LUFS, mobile -18 LUFS, true peak at or below -1 dBTP); no clipping. Measure with a tool: \`ffmpeg -i <file> -af ebur128=peak=true -f null -\` for integrated loudness and true peak; \`ffprobe <file>\` for sample rate, bit depth and channels; for glTF, \`gltf-transform inspect <file>\` (quote the command's actual output). Cite the platform source. If the request does not state the target, mark the loudness target \`unverified\` and name the source. Mobile targets vary (-18 vs -16 LUFS).
110025
110366
  - Sample rate, bit depth, channel count, loop points, and format stated; short noisy SFX use PCM or ADPCM, long audio uses compressed or streamed formats, per the engine's guidance.
110026
110367
 
110368
+ ### Perspective consistency (generated 2D sets)
110369
+ Assets used together must share one camera perspective. The model's constraints (unsupported parameters, size defaults, what it does not guarantee) come only from \`generate-image --dry-run\` (R16); apply them, and never add a parameter the dry run reports as unsupported.
110370
+ - Repeat the perspective term from the brief's \`Perspective\` word for word in every prompt of the set (never the \`reference:\` path), and add the same flattening terms to every prompt: \`flat 2D game asset, orthographic, centered, plain background, even lighting\`. Suggested perspective terms (\`unverified\`): side view, front view, orthographic top-down, orthographic isometric, dimetric 2:1, three-quarter view. If the field names only a reference, describe that reference's view in words and record the description under assumptions. If \`Perspective\` is \`Not specified.\`, pick one, record it under assumptions, and use it for the whole set. If the \`Style reference\` asks for a lighting style these terms contradict (for example dramatic lighting), keep the perspective term and drop only the conflicting flattening term.
110371
+ - Use one \`--input\` JSON (from \`Model parameters\`) on every call of the set; if the dry run reports a size parameter as defaulted, set it once in that JSON and reuse it. Add \`--input '<JSON>'\` and the seed to every P5 command.
110372
+ - Use one shared \`--seed\` for the first call of each item or sheet. For every regeneration (P6, P7, or a perspective mismatch), use a new seed and record it (P9). A shared seed does not keep perspective across different prompts.
110373
+ - Pass \`--input-image <path>\` with \`--model fal-ai/flux-2/flash/edit\` only when the brief's \`Model\` is \`fal-ai/flux-2/flash/edit\` and \`Perspective\` names a reference path. In this mode generate every asset individually (the P5 per-asset path, one \`--input-image\` per asset); never use a sheet.
110374
+ - Inspect the set side by side. A mismatched item is regenerated individually (the P5 per-asset path, with a new seed), at most twice per item, separately from the sheet regenerations of P6 and P7. If it still differs, mark the perspective checklist item \`fail\` with \`inspected\` evidence.
110375
+
110027
110376
  ### Scope boundary
110028
110377
  You may specify, name, organize, generate, validate, and integrate assets within the paths the brief allows. You must not claim fidelity you did not establish. If you cannot open or measure a file, write \`unverified\`. Do not present alternative art directions beyond what the request needs; recommend one.
110029
110378
 
@@ -110128,17 +110477,18 @@ ${getInterRoleHandoffRule()}
110128
110477
 
110129
110478
  1. Recover the authoritative user request and relevant chatroom history before designing. Inspect existing UI patterns, components, tokens, interaction conventions, and nearby tests that constrain the experience.
110130
110479
  2. Treat the injected \`<handoff-templates>\` UI/UX Engineer Design Handoff Brief as authoritative. Complete every section in its exact order, using \`Not Applicable.\` only for a genuinely inapplicable section and never as filler.
110131
- 3. Produce exactly one recommended, evidence-backed interface and experience design. Do not present alternatives or generic UX advice. Include concrete repository paths, complete user flows, and explicit loading, empty, error, success states (with loading, empty, error, and success states covered), plus disabled and retry states where applicable.
110132
- 4. Specify exact keyboard shortcuts, focus order, keyboard navigation, focus restoration, accessible semantics and labels, component hierarchy and ownership, props/state/events, data boundaries, and interaction behavior.
110133
- 5. Choose concrete ShadCN components using the Base UI backend and existing icon conventions. Specify exact Tailwind utility or semantic theme-token classes for layout, spacing, sizing, typography, states, responsive behavior, and dark mode; keep the styling theme-aware and make state ownership explicit. Also specify responsive breakpoints, visual feedback, destructive-action safeguards, and the UI/integration/accessibility test plan.
110134
- 6. Keep the proposal within the user's request and the repository's established patterns. Do not expand scope, spawn subagents, implement the design, edit files, or act as the builder. You do not generate asset files: any icon or illustration is specified by \`game-asset-designer\` and generated by \`game-asset-designer\`.
110135
- 7. Hand the complete design brief to \`{ENTRY_POINT_ROLE}\` with the normal command, then stop:
110480
+ 3. Produce exactly one recommended, evidence-backed interface and experience design. Do not present alternatives or generic UX advice. Include concrete repository paths, complete user flows, and explicit loading, empty, error, and success states, plus disabled and retry states where applicable.
110481
+ 4. For any user-facing workflow, draw each affected screen as an ASCII wireframe (every button and state labelled) and each flow as a Mermaid flowchart in the handback. Write \`Not Applicable.\` for the drawings section only when the change is non-visual.
110482
+ 5. Specify component hierarchy and ownership, props/state/events, data boundaries, and interaction behavior. Do not propose or require web accessibility implementation work unless the user explicitly requests it.
110483
+ 6. Choose concrete ShadCN components using the Base UI backend and existing icon conventions. Specify exact Tailwind utility or semantic theme-token classes for layout, spacing, sizing, typography, states, responsive behavior, and dark mode; keep the styling theme-aware and make state ownership explicit. Also specify responsive breakpoints, visual feedback, destructive-action safeguards, and the UI/integration test plan.
110484
+ 7. Keep the proposal within the user's request and the repository's established patterns. Do not expand scope, spawn subagents, implement the design, edit files, or act as the builder. You do not generate asset files: any icon or illustration is specified by \`game-asset-designer\` and generated by \`game-asset-designer\`.
110485
+ 8. Hand the complete design brief to \`{ENTRY_POINT_ROLE}\` with the normal command, then stop:
110136
110486
 
110137
110487
  \`\`\`bash
110138
110488
  chatroom handoff --chatroom-id="<chatroom-id>" --role="uiux-engineer" --next-role="{ENTRY_POINT_ROLE}"
110139
110489
  \`\`\`
110140
110490
 
110141
- The handoff must contain every required brief section, repository evidence, chosen flow, exact component/prop/state details, explicit states, accessibility details, ownership decisions, concrete classes/tokens, risks, acceptance criteria, tests, and implementation sequence \`{ENTRY_POINT_ROLE}\` needs to coordinate implementation. It must not contain implementation edits or alternative designs.`;
110491
+ The handoff must contain every required brief section, repository evidence, chosen flow, exact component/prop/state details, explicit states, ownership decisions, concrete classes/tokens, risks, acceptance criteria, tests, and implementation sequence \`{ENTRY_POINT_ROLE}\` needs to coordinate implementation. It must not contain implementation edits or alternative designs.`;
110142
110492
  });
110143
110493
 
110144
110494
  // ../../services/backend/prompts/cli/roles/verifier.ts
@@ -110304,7 +110654,7 @@ var init_templates = __esm(() => {
110304
110654
  "game-asset-designer": {
110305
110655
  role: "game-asset-designer",
110306
110656
  title: "Game Asset Designer",
110307
- description: "You are the game asset designer responsible for specifying production-quality game assets (and producing them when the planner allows); you do not implement application code.",
110657
+ description: "You are the game asset designer responsible for specifying and generating production-quality game and UI assets (generation is the default unless the brief says `spec only`); you do not implement application code.",
110308
110658
  responsibilities: [
110309
110659
  "Recover the target, asset request, style reference, and budgets from the planner brief",
110310
110660
  "Inspect the repository for existing assets, naming and folder conventions, and engine import configuration",
@@ -110317,13 +110667,13 @@ var init_templates = __esm(() => {
110317
110667
  verifier: {
110318
110668
  role: "verifier",
110319
110669
  title: "Verifier",
110320
- description: "You are the verifier responsible for judging, from chatroom logs only, whether the user's last message was achieved; you do not implement, test, or inspect code.",
110670
+ description: "You are the verifier responsible for judging, from chatroom logs only, whether the user's last message was achieved; you do not implement, build, or edit code, and you run only the tests the user explicitly asked for.",
110321
110671
  responsibilities: [
110322
110672
  "Recover the user's last message and the pinned context from the chatroom logs",
110323
110673
  "Break the objective into numbered requirements",
110324
110674
  "Check each requirement against concrete evidence in the logs, treating the planner claim as unverified",
110325
110675
  "Return one verdict (ACHIEVED, NOT_ACHIEVED, or INCONCLUSIVE) with evidence and the missing items",
110326
- "Execute nothing: no tests, builds, or repository inspection"
110676
+ "Execute nothing except the tests the user explicitly asked for: no builds, lints, or edits"
110327
110677
  ],
110328
110678
  defaultHandoffTarget: "planner"
110329
110679
  },
@@ -118959,7 +119309,7 @@ function parseYaml(text) {
118959
119309
  }
118960
119310
  }
118961
119311
  function validateFields(data, body) {
118962
- if (!isPlainObject3(data))
119312
+ if (!isPlainObject4(data))
118963
119313
  return fail6("invalid-frontmatter", "frontmatter is not a mapping");
118964
119314
  const name = data.name;
118965
119315
  if (!isValidName(name))
@@ -118969,7 +119319,7 @@ function validateFields(data, body) {
118969
119319
  return fail6("invalid-description");
118970
119320
  return { ok: true, name, description, body };
118971
119321
  }
118972
- function isPlainObject3(value2) {
119322
+ function isPlainObject4(value2) {
118973
119323
  return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
118974
119324
  }
118975
119325
  function isValidName(value2) {
@@ -118989,7 +119339,7 @@ var init_parse_skill_file = __esm(() => {
118989
119339
  });
118990
119340
 
118991
119341
  // src/infrastructure/external-skills/discover-external-skills.ts
118992
- import { lstat as lstat2, readdir as readdir2, readFile as readFile8, realpath, stat as stat6 } from "node:fs/promises";
119342
+ import { lstat as lstat2, readdir as readdir2, readFile as readFile9, realpath, stat as stat6 } from "node:fs/promises";
118993
119343
  import os2 from "node:os";
118994
119344
  import { join as join20 } from "node:path";
118995
119345
  function errorCode(err) {
@@ -119097,7 +119447,7 @@ async function isSymlink2(path3) {
119097
119447
  }
119098
119448
  async function readSkillFile(sourcePath, issues) {
119099
119449
  try {
119100
- return await readFile8(join20(sourcePath, SKILL_FILE_NAME), "utf8");
119450
+ return await readFile9(join20(sourcePath, SKILL_FILE_NAME), "utf8");
119101
119451
  } catch (err) {
119102
119452
  issues.push(issue2(sourcePath, "unreadable", errorCode(err)));
119103
119453
  return;
@@ -121588,7 +121938,7 @@ var init_orphan_tracker = __esm(() => {
121588
121938
  });
121589
121939
 
121590
121940
  // src/daemon/entry/handlers/process/output-store.ts
121591
- import { appendFile as appendFile2, mkdir as mkdir10, readFile as readFile9, rm as rm2 } from "node:fs/promises";
121941
+ import { appendFile as appendFile2, mkdir as mkdir10, readFile as readFile10, rm as rm2 } from "node:fs/promises";
121592
121942
  import { tmpdir } from "node:os";
121593
121943
  import { join as join23 } from "node:path";
121594
121944
 
@@ -121621,7 +121971,7 @@ class TempFileOutputStore {
121621
121971
  async getLastNLines(maxLines) {
121622
121972
  let source;
121623
121973
  try {
121624
- source = await readFile9(this.state.filePath, "utf-8");
121974
+ source = await readFile10(this.state.filePath, "utf-8");
121625
121975
  } catch {
121626
121976
  source = this.state.inMemory;
121627
121977
  }
@@ -121638,7 +121988,7 @@ class TempFileOutputStore {
121638
121988
  }
121639
121989
  async getFullOutput() {
121640
121990
  try {
121641
- return await readFile9(this.state.filePath, "utf-8");
121991
+ return await readFile10(this.state.filePath, "utf-8");
121642
121992
  } catch {
121643
121993
  return this.state.inMemory;
121644
121994
  }
@@ -123268,7 +123618,7 @@ var init_workspace_visibility_policy = __esm(() => {
123268
123618
 
123269
123619
  // src/infrastructure/services/workspace/workspace-ignore.ts
123270
123620
  import { promises as fsPromises } from "node:fs";
123271
- import { readFile as readFile10 } from "node:fs/promises";
123621
+ import { readFile as readFile11 } from "node:fs/promises";
123272
123622
  import { homedir as homedir11 } from "node:os";
123273
123623
  import path3 from "node:path";
123274
123624
  function normalizeRelativePath(relativePath) {
@@ -123333,7 +123683,7 @@ async function loadGitExtraExcludeRuleSets(rootDir) {
123333
123683
  }
123334
123684
  async function readIgnoreFile(filePath) {
123335
123685
  try {
123336
- return await readFile10(filePath, "utf-8");
123686
+ return await readFile11(filePath, "utf-8");
123337
123687
  } catch {
123338
123688
  return null;
123339
123689
  }
@@ -127928,7 +128278,7 @@ function parseMakefileTargets(content) {
127928
128278
  var MAX_NAME_LENGTH2 = 256;
127929
128279
 
127930
128280
  // src/infrastructure/services/workspace/workspace-resolver.ts
127931
- import { readFile as readFile12, readdir as readdir5, stat as stat10 } from "node:fs/promises";
128281
+ import { readFile as readFile13, readdir as readdir5, stat as stat10 } from "node:fs/promises";
127932
128282
  import { join as join28, basename as basename7, resolve as resolve10 } from "node:path";
127933
128283
  async function resolveGlobPatternStar(rootDir, cleaned) {
127934
128284
  const parentDir2 = join28(rootDir, cleaned.slice(0, -2));
@@ -127969,7 +128319,7 @@ async function resolveGlobPattern(rootDir, pattern) {
127969
128319
  }
127970
128320
  async function readPackageJson(dir) {
127971
128321
  try {
127972
- const content = await readFile12(join28(dir, "package.json"), "utf-8");
128322
+ const content = await readFile13(join28(dir, "package.json"), "utf-8");
127973
128323
  const pkg = JSON.parse(content);
127974
128324
  return {
127975
128325
  name: pkg.name || basename7(dir),
@@ -127981,7 +128331,7 @@ async function readPackageJson(dir) {
127981
128331
  }
127982
128332
  async function readPnpmWorkspacePatterns(rootDir) {
127983
128333
  try {
127984
- const content = await readFile12(join28(rootDir, "pnpm-workspace.yaml"), "utf-8");
128334
+ const content = await readFile13(join28(rootDir, "pnpm-workspace.yaml"), "utf-8");
127985
128335
  const patterns = [];
127986
128336
  let inPackages = false;
127987
128337
  for (const line of content.split(`
@@ -128008,7 +128358,7 @@ async function readPnpmWorkspacePatterns(rootDir) {
128008
128358
  }
128009
128359
  async function readPackageJsonWorkspacePatterns(rootDir) {
128010
128360
  try {
128011
- const content = await readFile12(join28(rootDir, "package.json"), "utf-8");
128361
+ const content = await readFile13(join28(rootDir, "package.json"), "utf-8");
128012
128362
  const pkg = JSON.parse(content);
128013
128363
  if (!pkg.workspaces)
128014
128364
  return [];
@@ -128056,7 +128406,7 @@ async function resolveSubWorkspaces(rootDir, pm) {
128056
128406
  var init_workspace_resolver = () => {};
128057
128407
 
128058
128408
  // src/infrastructure/services/workspace/command-discovery.ts
128059
- import { access as access2, readFile as readFile13 } from "node:fs/promises";
128409
+ import { access as access2, readFile as readFile14 } from "node:fs/promises";
128060
128410
  import { join as join29, relative as relative3, basename as basename8 } from "node:path";
128061
128411
  async function detectPackageManager(workingDir) {
128062
128412
  for (const { file: file2, manager } of LOCKFILE_MAP) {
@@ -128105,7 +128455,7 @@ function isValidScriptEntry(name, script) {
128105
128455
  }
128106
128456
  async function readJsonFile(filePath, label) {
128107
128457
  try {
128108
- const content = await readFile13(filePath, "utf-8");
128458
+ const content = await readFile14(filePath, "utf-8");
128109
128459
  return parseJsonc2(content);
128110
128460
  } catch (error51) {
128111
128461
  if (error51.code !== "ENOENT") {
@@ -128182,7 +128532,7 @@ async function readMakefile(workingDir) {
128182
128532
  const commands = [];
128183
128533
  for (const fileName of ["Makefile", "makefile"]) {
128184
128534
  try {
128185
- const content = await readFile13(join29(workingDir, fileName), "utf-8");
128535
+ const content = await readFile14(join29(workingDir, fileName), "utf-8");
128186
128536
  const targets = parseMakefileTargets(content);
128187
128537
  if (targets.length === 0)
128188
128538
  continue;
@@ -134302,7 +134652,7 @@ function gunzipBase64Payload(base643, maxBytes) {
134302
134652
  var init_workspace_path_security = () => {};
134303
134653
 
134304
134654
  // src/daemon/entry/files/file-content-fulfillment.ts
134305
- import { readFile as readFile14 } from "node:fs/promises";
134655
+ import { readFile as readFile15 } from "node:fs/promises";
134306
134656
  import { gzipSync as gzipSync3 } from "node:zlib";
134307
134657
  function getErrorCause(error51) {
134308
134658
  if (typeof error51 === "object" && error51 !== null && "cause" in error51 && error51.cause !== undefined) {
@@ -134377,7 +134727,7 @@ var init_file_content_fulfillment = __esm(() => {
134377
134727
  continue;
134378
134728
  }
134379
134729
  let fileNotFound = false;
134380
- const buffer = yield* exports_Effect.catch(exports_Effect.tryPromise(() => readFile14(absolutePath)), (error51) => {
134730
+ const buffer = yield* exports_Effect.catch(exports_Effect.tryPromise(() => readFile15(absolutePath)), (error51) => {
134381
134731
  if (isENOENT(error51)) {
134382
134732
  fileNotFound = true;
134383
134733
  return exports_Effect.succeed(Buffer.alloc(0));
@@ -146086,9 +146436,10 @@ var {
146086
146436
  } = import__.default;
146087
146437
 
146088
146438
  // src/commands/generate/index.ts
146089
- import { readFile, stat as stat2 } from "node:fs/promises";
146439
+ import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
146090
146440
  init_image();
146091
146441
  init_local_image_common();
146442
+ init_model_capabilities();
146092
146443
  var OUTPUT_FORMATS = ["png", "webp"];
146093
146444
  function parsePlatform(value) {
146094
146445
  if (!GENERATE_IMAGE_PLATFORMS.includes(value)) {
@@ -146114,7 +146465,7 @@ async function readInputOption(value) {
146114
146465
  if (stats.size > MAX_SIDECAR_BYTES) {
146115
146466
  throw new Error(`--input file is ${stats.size} bytes; the maximum is ${MAX_SIDECAR_BYTES} bytes (1 MiB).`);
146116
146467
  }
146117
- return parseInputJson(await readFile(path, "utf8"));
146468
+ return parseInputJson(await readFile2(path, "utf8"));
146118
146469
  }
146119
146470
  function parseSeed(value) {
146120
146471
  const seed = Number(value);
@@ -146125,38 +146476,49 @@ function parseSeed(value) {
146125
146476
  }
146126
146477
  var HELP_AFTER = `
146127
146478
  Examples:
146128
- chatroom generate-image --platform=fal --prompt "a hero" --out hero.png
146129
- chatroom generate-image --platform=fal --prompt 'pixel-art potion, transparent-friendly flat background' --output-format png --out potion.png
146479
+ chatroom generate-image --platform=fal --model ${FLUX_2_FLASH_MODEL} --prompt "a hero" --out hero.png
146480
+ chatroom generate-image --platform=fal --model ${FLUX_2_FLASH_MODEL} --prompt 'pixel-art potion, transparent-friendly flat background' --output-format png --out potion.png
146481
+ chatroom generate-image --platform=fal --model ${FLUX_2_FLASH_EDIT_MODEL} --prompt "make the sky stormy" --input-image ./hero.png --out hero-stormy.png
146130
146482
  `;
146131
- function registerGenerateImageCommand(program2) {
146132
- program2.command("generate-image").description("Generate one image with a fal model and write a JSON manifest next to it").requiredOption("--prompt <text>", "Text prompt describing the image").requiredOption("--out <path>", "Output image path (manifest is written to <path>.json)").option("--platform <name>", "Generation platform: fal", parsePlatform, DEFAULT_GENERATE_IMAGE_PLATFORM).option("--model <id>", `fal model id. Recommended and default: ${DEFAULT_IMAGE_MODEL}`, DEFAULT_IMAGE_MODEL).option("--aspect-ratio <ratio>", `Aspect ratio, e.g. 1:1, 4:3, 16:9, 9:16 (omitted = model default). Not supported by the default model ${DEFAULT_IMAGE_MODEL}, which uses its own image size`).option("--output-format <format>", "Output format: png or webp (omitted = model default)", parseOutputFormat).option("--seed <n>", "Seed for the random number generator", parseSeed).option("--input <json|@file>", "Model-specific JSON input object merged into the fal request; @file reads JSON from a file").option("--force", "Overwrite existing output and manifest files").addHelpText("after", HELP_AFTER).action(async (options) => {
146133
- try {
146134
- const input = options.input === undefined ? undefined : await readInputOption(options.input);
146135
- const { createDefaultGenerateImageDeps: createDefaultGenerateImageDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
146136
- const result = await generateImage({
146137
- prompt: options.prompt,
146138
- out: options.out,
146139
- platform: options.platform,
146140
- model: options.model,
146141
- aspectRatio: options.aspectRatio,
146142
- outputFormat: options.outputFormat,
146143
- seed: options.seed,
146144
- input,
146145
- force: options.force
146146
- }, createDefaultGenerateImageDeps2());
146147
- console.log(`Generated ${result.outPath}`);
146148
- console.log(`Manifest ${result.manifestPath}`);
146149
- } catch (err) {
146150
- console.error(`❌ ${err instanceof Error ? err.message : String(err)}`);
146151
- process.exitCode = 1;
146483
+ async function runGenerateImage(options) {
146484
+ try {
146485
+ const input = options.input === undefined ? undefined : await readInputOption(options.input);
146486
+ const { createDefaultGenerateImageDeps: createDefaultGenerateImageDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
146487
+ const deps = createDefaultGenerateImageDeps2();
146488
+ const generateOptions = {
146489
+ prompt: options.prompt,
146490
+ out: options.out,
146491
+ platform: options.platform,
146492
+ model: options.model,
146493
+ aspectRatio: options.aspectRatio,
146494
+ outputFormat: options.outputFormat,
146495
+ seed: options.seed,
146496
+ input,
146497
+ inputImage: options.inputImage,
146498
+ force: options.force
146499
+ };
146500
+ if (options.dryRun === true) {
146501
+ const { formatGenerateImagePlan: formatGenerateImagePlan2, planGenerateImage: planGenerateImage2 } = await Promise.resolve().then(() => (init_image(), exports_image));
146502
+ console.log(formatGenerateImagePlan2(await planGenerateImage2(generateOptions, deps)));
146503
+ return;
146152
146504
  }
146153
- });
146505
+ const result = await generateImage(generateOptions, deps);
146506
+ console.log(`Generated ${result.outPath}`);
146507
+ console.log(`Manifest ${result.manifestPath}`);
146508
+ } catch (err) {
146509
+ console.error(`❌ ${err instanceof Error ? err.message : String(err)}`);
146510
+ process.exitCode = 1;
146511
+ }
146512
+ }
146513
+ function registerGenerateImageCommand(program2) {
146514
+ program2.command("generate-image").description("Generate one image with a fal model and write a JSON manifest next to it").requiredOption("--prompt <text>", "Text prompt describing the image").requiredOption("--out <path>", "Output image path (manifest is written to <path>.json)").option("--platform <name>", "Generation platform: fal", parsePlatform, DEFAULT_GENERATE_IMAGE_PLATFORM).requiredOption("--model <id>", `fal model id (required), for example ${FLUX_2_FLASH_MODEL}, or ${FLUX_2_FLASH_EDIT_MODEL} with --input-image`).option("--aspect-ratio <ratio>", "Aspect ratio for models that take one, e.g. 1:1, 16:9 (omitted = model default). Rejected for models the CLI knows do not support it").option("--output-format <format>", "Output format: png or webp (omitted = model default)", parseOutputFormat).option("--seed <n>", "Seed for the random number generator", parseSeed).option("--input <json|@file>", "Model-specific JSON input object merged into the fal request; @file reads JSON from a file").option("--input-image <path-or-url>", `Image to edit: a local .png, .jpg, .jpeg, .webp, or .gif (uploaded to fal) or an http(s) URL. Requires an edit model such as ${FLUX_2_FLASH_EDIT_MODEL}`).option("--force", "Overwrite existing output and manifest files").option("--dry-run", "Validate and print the request and the model constraints the CLI knows, without uploading or calling fal (FAL_API_KEY not needed)").addHelpText("after", HELP_AFTER).action(runGenerateImage);
146154
146515
  }
146155
146516
 
146156
146517
  // src/commands/generate/remove-background.ts
146157
146518
  import { basename, dirname as dirname3, extname as extname2, join as join2, resolve as resolve2 } from "node:path";
146158
146519
 
146159
146520
  // src/commands/generate/background-key.ts
146521
+ init_local_image_common();
146160
146522
  var DEFAULT_TOLERANCE = 24;
146161
146523
  var DEFAULT_MIN_AREA_PERCENT = 0.5;
146162
146524
  var BORDER_NOISE_REFUSAL_RATIO = 0.02;
@@ -146174,13 +146536,6 @@ function borderCoordinates(width, height) {
146174
146536
  }
146175
146537
  return coordinates;
146176
146538
  }
146177
- function median(values) {
146178
- const sorted = [...values].sort((a, b) => a - b);
146179
- const middle = Math.floor(sorted.length / 2);
146180
- if (sorted.length % 2 === 1)
146181
- return sorted[middle] ?? 0;
146182
- return Math.round(((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2);
146183
- }
146184
146539
  function forEachNeighbour(index, width, height, visit) {
146185
146540
  const x = index % width;
146186
146541
  const y = Math.floor(index / width);
@@ -147313,14 +147668,7 @@ function mostFrequentBorderColor(image) {
147313
147668
  }
147314
147669
  if (red.length === 0)
147315
147670
  return null;
147316
- const median2 = (values) => {
147317
- const sorted = [...values].sort((a, b) => a - b);
147318
- const middle = Math.floor(sorted.length / 2);
147319
- if (sorted.length % 2 === 1)
147320
- return sorted[middle] ?? 0;
147321
- return Math.round(((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2);
147322
- };
147323
- return [median2(red), median2(green), median2(blue)];
147671
+ return [median(red), median(green), median(blue)];
147324
147672
  }
147325
147673
  function countCellEdgeForeground(image, cell, background) {
147326
147674
  const { width, height, data } = image;
@@ -147408,7 +147756,7 @@ async function sliceImage(opts, deps) {
147408
147756
  throw new SliceImageError(`--in is ${bytes.byteLength} bytes; the maximum is ${MAX_SOURCE_BYTES} bytes (64 MiB).`);
147409
147757
  }
147410
147758
  if (!hasPngMagic(bytes)) {
147411
- throw new SliceImageError("--in is not a PNG. slice-image only reads PNG sheets; regenerate with 'chatroom generate-image --output-format png' (the default model returns PNG).");
147759
+ throw new SliceImageError("--in is not a PNG. slice-image only reads PNG sheets; regenerate with 'chatroom generate-image --model <id> --output-format png'.");
147412
147760
  }
147413
147761
  const dimensions = readPngDimensions(bytes);
147414
147762
  if (dimensions === null) {
@@ -148369,4 +148717,4 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
148369
148717
  });
148370
148718
  program2.parse();
148371
148719
 
148372
- //# debugId=CBBD6308FFB368C664756E2164756E21
148720
+ //# debugId=E840D0ACF405C0D664756E2164756E21