@genex-ai/cli-demo 1.19.0-dev.601 → 1.21.0-dev.603

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
@@ -42,7 +42,7 @@ import {
42
42
  writeProject,
43
43
  writeUserToken,
44
44
  writeWorkspace
45
- } from "./chunk-YSMZVFJM.js";
45
+ } from "./chunk-HEDRWUVP.js";
46
46
  import {
47
47
  CLI_CHANNEL,
48
48
  DEFAULT_API_URL,
@@ -2397,6 +2397,16 @@ async function recordTerminal(id, status, urls, cwd = process.cwd()) {
2397
2397
  async function countFailed(kind, cwd = process.cwd()) {
2398
2398
  return (await readLedger(cwd)).filter((e) => e.kind === kind && e.status === "failed").length;
2399
2399
  }
2400
+ async function trailingFailures(kind, cwd = process.cwd()) {
2401
+ const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
2402
+ let run2 = 0;
2403
+ for (let i = rows.length - 1; i >= 0; i -= 1) {
2404
+ const status = rows[i].status;
2405
+ if (status === "completed") break;
2406
+ if (status === "failed") run2 += 1;
2407
+ }
2408
+ return run2;
2409
+ }
2400
2410
  async function countOutcomes(kind, cwd = process.cwd()) {
2401
2411
  const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
2402
2412
  return {
@@ -2516,26 +2526,32 @@ function auditGenerationPlan(input) {
2516
2526
  const lane = m[1] ? LANE_OF_SKILL[m[1]] : void 0;
2517
2527
  if (lane) declaredLanes.add(lane);
2518
2528
  }
2519
- const lanesRun = new Set(ledger.map((e) => LANE_OF_KIND[e.kind] ?? e.kind));
2529
+ const lanesRun = new Set(
2530
+ ledger.filter((e) => e.status === "completed").map((e) => LANE_OF_KIND[e.kind] ?? e.kind)
2531
+ );
2532
+ const lanesPending = new Set(
2533
+ ledger.filter((e) => e.status === "queued").map((e) => LANE_OF_KIND[e.kind] ?? e.kind).filter((lane) => !lanesRun.has(lane))
2534
+ );
2520
2535
  const warnings = [];
2521
2536
  const owed = /* @__PURE__ */ new Map();
2522
2537
  for (const row of rows) {
2523
2538
  if (row.procedural || row.lane === null) continue;
2524
2539
  if (row.status !== "planned" && row.status !== "generating") continue;
2525
2540
  if (lanesRun.has(row.lane)) continue;
2541
+ if (lanesPending.has(row.lane)) continue;
2526
2542
  if (row.lane === "skybox") continue;
2527
2543
  const held = owed.get(row.lane) ?? [];
2528
2544
  held.push(row.status === "generating" ? `${row.name} (says generating, not in this project's ledger)` : row.name);
2529
2545
  owed.set(row.lane, held);
2530
2546
  }
2531
2547
  for (const lane of declaredLanes) {
2532
- if (lanesRun.has(lane) || owed.has(lane) || lane === "skybox") continue;
2548
+ if (lanesRun.has(lane) || lanesPending.has(lane) || owed.has(lane) || lane === "skybox") continue;
2533
2549
  if (!rows.some((r) => r.lane === lane)) owed.set(lane, ["declared as this subsystem's lane"]);
2534
2550
  }
2535
2551
  for (const [lane, names] of owed) {
2536
2552
  const shown = names.slice(0, 3).join(", ") + (names.length > 3 ? `, +${names.length - 3} more` : "");
2537
2553
  warnings.push(
2538
- `DESIGN.md plans generated ${lane} work (${shown}) and this project has never run a ${lane} generation \u2014 the plan says so and the generation ledger has nothing. Generate it (${LANE_COMMAND[lane] ?? `npx genex ${lane}`} "<prompt>" --no-wait), or change the row to say what happens instead and why.`
2554
+ `DESIGN.md plans generated ${lane} work (${shown}) and this project has no ${lane} generation that landed \u2014 the plan says so and the generation ledger has nothing completed. Generate it (${LANE_COMMAND[lane] ?? `npx genex ${lane}`} "<prompt>" --no-wait), or change the row to say what happens instead and why.`
2539
2555
  );
2540
2556
  }
2541
2557
  const paidRows = rows.filter((r) => !r.procedural);
@@ -6056,7 +6072,18 @@ async function detectSurfaceScan(cwd = process.cwd()) {
6056
6072
  }
6057
6073
  return found;
6058
6074
  }
6059
- var WIRED_BY_URL = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "music", "texture", "video"]);
6075
+ var WIRED_BY_URL = /* @__PURE__ */ new Set([
6076
+ "model",
6077
+ "model_segment",
6078
+ "model_rig",
6079
+ "model_animation",
6080
+ "skybox",
6081
+ "sfx",
6082
+ "music",
6083
+ "voice",
6084
+ "texture",
6085
+ "video"
6086
+ ]);
6060
6087
  var UNPICKED_AFTER_MS = 15 * 60 * 1e3;
6061
6088
  async function detectGenerationAudit(cwd = process.cwd()) {
6062
6089
  const ledger = await readLedger(cwd);
@@ -6067,7 +6094,10 @@ async function detectGenerationAudit(cwd = process.cwd()) {
6067
6094
  let haystack = "";
6068
6095
  const read = async (file) => {
6069
6096
  try {
6070
- haystack += await fs15.readFile(file, "utf8");
6097
+ const raw = await fs15.readFile(file, "utf8");
6098
+ const ext = path14.extname(file).toLowerCase();
6099
+ if (ext === ".txt") return;
6100
+ haystack += ext === ".ts" || ext === ".tsx" || ext === ".js" || ext === ".jsx" || ext === ".css" ? blankComments(raw) : ext === ".html" ? raw.replace(/<!--[\s\S]*?-->/g, (m) => m.replace(/[^\n]/g, " ")) : raw;
6071
6101
  } catch {
6072
6102
  }
6073
6103
  };
@@ -7164,9 +7194,20 @@ function applySkyboxGuard(prompt, raw) {
7164
7194
  if (guarded.length > SKYBOX_PROMPT_MAX) return { prompt, guarded: false, nouns };
7165
7195
  return { prompt: guarded, guarded: true, nouns };
7166
7196
  }
7197
+ var MODEL_TEXTURE_TIERS = ["standard", "detailed", "none"];
7167
7198
  function buildGenOptions(kind, opts) {
7168
7199
  const options = { ...opts.generationOptions };
7169
7200
  if (kind === "model" && opts.imageUrl) options.imageUrl = opts.imageUrl;
7201
+ if (kind === "model") {
7202
+ if (opts.texture === "none") options.texture = false;
7203
+ else if (opts.texture !== void 0) options.textureQuality = opts.texture;
7204
+ if (opts.geometry !== void 0) options.geometryQuality = opts.geometry;
7205
+ if (opts.quad) options.quad = true;
7206
+ if (opts.lowPoly) options.smartLowPoly = true;
7207
+ if (opts.parts) options.generateParts = true;
7208
+ if (opts.faceLimit !== void 0) options.faceLimit = opts.faceLimit;
7209
+ if (opts.autoSize) options.autoSize = true;
7210
+ }
7170
7211
  if (kind === "texture" && opts.terrain) options.terrain = true;
7171
7212
  if ((kind === "sfx" || kind === "music") && opts.duration) options.durationSeconds = opts.duration;
7172
7213
  if (kind === "voice") {
@@ -7217,6 +7258,11 @@ async function runGenerate(kind, opts) {
7217
7258
  const ref = opts.imageUrl.startsWith("data:") ? "local image" : opts.imageUrl;
7218
7259
  typedPrompt = `from image: ${path16.basename(ref).slice(0, 120)}`;
7219
7260
  }
7261
+ if (kind === "model" && opts.texture !== void 0 && !MODEL_TEXTURE_TIERS.includes(opts.texture)) {
7262
+ log.error(`--texture ${opts.texture} is a character texture size. \`genex model\` takes a texture TIER: ${MODEL_TEXTURE_TIERS.join("|")} (default detailed).`);
7263
+ process.exitCode = 1;
7264
+ return void 0;
7265
+ }
7220
7266
  if (!typedPrompt) {
7221
7267
  log.error(`Missing prompt. Usage: ${c.cyan(`genex ${kind} "<prompt>"`)}`);
7222
7268
  process.exitCode = 1;
@@ -7550,7 +7596,10 @@ async function reportTerminal(kind, view, log, open = false, json = false, local
7550
7596
  const failures = await countFailed("video");
7551
7597
  (failures >= 2 ? log.plain : log.dim)(videoFailureAdvice(failures));
7552
7598
  } else if (!failure?.billing) {
7553
- const advice = laneFailureAdvice(kind, await countOutcomes(kind));
7599
+ const advice = laneFailureAdvice(kind, {
7600
+ ...await countOutcomes(kind),
7601
+ trailingFailed: await trailingFailures(kind)
7602
+ });
7554
7603
  if (advice) log.plain(advice);
7555
7604
  }
7556
7605
  }
@@ -7644,8 +7693,11 @@ function reportLocalFiles(result, log, json) {
7644
7693
  }
7645
7694
  }
7646
7695
  function laneFailureAdvice(kind, outcomes) {
7647
- if (outcomes.failed < 2 || outcomes.completed > 0) return null;
7648
- return ` ${c.bold(`\u270B ${outcomes.failed} ${kind} generations attempted in this project, ${outcomes.failed} failed, none succeeded.`)} That pattern is the LANE, not your prompt \u2014 a fourth attempt bills the same and returns the same. Build this one in code, use a different lane, or tell the user in one plain line what is unavailable and carry on with the rest of the game.`;
7696
+ const run2 = outcomes.trailingFailed ?? (outcomes.completed > 0 ? 0 : outcomes.failed);
7697
+ if (run2 < 2) return null;
7698
+ const everSucceeded = outcomes.completed > 0;
7699
+ const opening = everSucceeded ? `\u270B the last ${run2} ${kind} generations in this project all failed.` : `\u270B ${run2} ${kind} generations attempted in this project, ${run2} failed, none succeeded.`;
7700
+ return ` ${c.bold(opening)} That pattern is the LANE, not your prompt \u2014 a fourth attempt bills the same and returns the same. Build this one in code, use a different lane, or tell the user in one plain line what is unavailable and carry on with the rest of the game.`;
7649
7701
  }
7650
7702
  function videoFailureAdvice(failures) {
7651
7703
  if (failures >= 2) {
@@ -7780,7 +7832,14 @@ var HINT_URL_KINDS = /* @__PURE__ */ new Set([
7780
7832
  ]);
7781
7833
  function assetHint(kind, view, url) {
7782
7834
  const hint = {
7783
- model: `Standard GLB \u2014 load with GLTFLoader straight from the URL \u2014 see the genex-ai-model skill.`,
7835
+ // "straight from the URL" was the OPPOSITE of the kit's own rule, which
7836
+ // reads: "EVERY tier loads a game-ready rung — provider-raw originals are
7837
+ // archival/remix source, not game assets" (controllers/quality/pick-asset.ts).
7838
+ // A bare GLTFLoader on this URL fetches the original — a Tripo prop is
7839
+ // ~500k tris and 3x4096 textures, and a scene of them floored an M4 Max.
7840
+ // The url clause stays: the BARE url is the right thing to store, because
7841
+ // `pickModel` resolves the tier's rung at load time.
7842
+ model: `Standard GLB. Load it through the quality kit \u2014 createGltfLoader(renderer) then loadModelWithFallback(url, tier, (u) => gltf.loader.loadAsync(u), { ktx2: gltf.ktx2 }) \u2014 so the tier gets a game-ready rung; a bare GLTFLoader on this URL fetches the provider-raw original (~500k tris, 3x4096 textures). Install it with npx genex controller quality. See genex-ai-model.`,
7784
7843
  // Unreachable while the lane is paused (see src/index.ts) — kept so a
7785
7844
  // restore is one deletion. Games that already ship a panorama still load
7786
7845
  // it this way.
@@ -7801,9 +7860,9 @@ function assetHint(kind, view, url) {
7801
7860
  character: `Install the controller and current manifest with genex controller character --character ${view.id}.`,
7802
7861
  character_animation: "The action is now part of the character's current manifest; refresh the local controller manifest before testing.",
7803
7862
  character_motion: `The clips are part of the character's current manifest \u2014 install them with genex controller character --character ${view.id}, then play the game and watch the motion on the real character before calling it done.`,
7804
- model_segment: `One GLB with NAMED parts \u2014 load with GLTFLoader, then getObjectByName / traverse scene.children to move, detach, or swap a part. Part names print above when the provider reports them.`,
7805
- model_rig: `Rigged GLB (Tripo skeleton) \u2014 load with GLTFLoader; give it motion with genex model animate ${view.id} --preset walk, or drive the bones in code.`,
7806
- model_animation: `Animated GLB \u2014 load with GLTFLoader and play its clips via THREE.AnimationMixer (gltf.animations).`
7863
+ model_segment: `One GLB with NAMED parts \u2014 load it through the quality kit (loadModelWithFallback, as for a plain model), then getObjectByName / traverse scene.children to move, detach, or swap a part. Part names print above when the provider reports them.`,
7864
+ model_rig: `Rigged GLB (Tripo skeleton) \u2014 load it through the quality kit (loadModelWithFallback); give it motion with genex model animate ${view.id} --preset walk, or drive the bones in code.`,
7865
+ model_animation: `Animated GLB \u2014 load it through the quality kit (loadModelWithFallback) and play its clips via THREE.AnimationMixer (gltf.animations).`
7807
7866
  };
7808
7867
  const base = hint[kind];
7809
7868
  return url && HINT_URL_KINDS.has(kind) ? `${base} url = "${url}"` : base;
@@ -7982,7 +8041,111 @@ function writeJson(value) {
7982
8041
  }
7983
8042
 
7984
8043
  // src/commands/model-sub.ts
7985
- var MODEL_SUBCOMMANDS = ["segment", "rig", "animate"];
8044
+ import fs18 from "fs/promises";
8045
+ import path17 from "path";
8046
+ var MODEL_SUBCOMMANDS = ["segment", "rig", "animate", "import"];
8047
+ function apiErrorMessage(data, fallback) {
8048
+ if (typeof data !== "object" || data === null) return fallback;
8049
+ const body = data;
8050
+ if (typeof body.message === "string" && body.message) return body.message;
8051
+ if (typeof body.error === "string" && body.error) return body.error;
8052
+ return fallback;
8053
+ }
8054
+ var MODEL_IMPORT_MAX_BYTES = 64 * 1024 * 1024;
8055
+ var GLB_MAGIC = 1179937895;
8056
+ async function importModelFile(args) {
8057
+ const { log } = args;
8058
+ const filePath = args.filePath.trim();
8059
+ if (!/\.glb$/i.test(filePath)) {
8060
+ log.error(`\`import\` takes a .glb file (binary glTF 2.0). Export ${path17.basename(filePath) || "the model"} as GLB first \u2014 Blender: File \u2192 Export \u2192 glTF 2.0, format "glTF Binary".`);
8061
+ return null;
8062
+ }
8063
+ let bytes;
8064
+ try {
8065
+ bytes = await fs18.readFile(filePath);
8066
+ } catch {
8067
+ log.error(`Couldn't read ${filePath}.`);
8068
+ return null;
8069
+ }
8070
+ if (bytes.byteLength > MODEL_IMPORT_MAX_BYTES) {
8071
+ log.error(`${path17.basename(filePath)} is ${(bytes.byteLength / 1e6).toFixed(1)} MB; imports are capped at ${MODEL_IMPORT_MAX_BYTES / 1024 / 1024} MB. Shrink its textures (they are usually the bulk) and try again.`);
8072
+ return null;
8073
+ }
8074
+ if (bytes.byteLength < 12 || bytes.readUInt32LE(0) !== GLB_MAGIC) {
8075
+ log.error(`${path17.basename(filePath)} is not a GLB (no glTF magic). A .gltf + .bin pair must be exported as one binary .glb.`);
8076
+ return null;
8077
+ }
8078
+ const headers = { "Content-Type": "application/json", Authorization: `Bearer ${args.token}` };
8079
+ const minted = await apiFetch(`${args.apiUrl}/api/generations/import`, {
8080
+ method: "POST",
8081
+ headers,
8082
+ body: JSON.stringify({ filename: path17.basename(filePath), bytes: bytes.byteLength, contentType: "model/gltf-binary" })
8083
+ });
8084
+ if (printedStructuredError(minted)) return null;
8085
+ if (!minted.ok) {
8086
+ const data = await minted.json().catch(() => ({}));
8087
+ log.error(apiErrorMessage(data, `Couldn't start the import (HTTP ${minted.status}).`));
8088
+ return null;
8089
+ }
8090
+ const { id, uploadUrl, url } = await minted.json();
8091
+ log.dim(` uploading ${path17.basename(filePath)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
8092
+ const put = await fetch(uploadUrl, {
8093
+ method: "PUT",
8094
+ headers: { "Content-Type": "model/gltf-binary", "Content-Length": String(bytes.byteLength) },
8095
+ body: new Uint8Array(bytes)
8096
+ });
8097
+ if (!put.ok) {
8098
+ log.error(`Upload failed (HTTP ${put.status}). Nothing was charged \u2014 run the import again.`);
8099
+ return null;
8100
+ }
8101
+ const done = await apiFetch(`${args.apiUrl}/api/generations/import/${encodeURIComponent(id)}/complete`, {
8102
+ method: "POST",
8103
+ headers
8104
+ });
8105
+ if (printedStructuredError(done)) return null;
8106
+ if (!done.ok) {
8107
+ const data = await done.json().catch(() => ({}));
8108
+ log.error(apiErrorMessage(data, `The import was refused (HTTP ${done.status}).`));
8109
+ return null;
8110
+ }
8111
+ const view = await done.json();
8112
+ return { id, url, inspection: view.inspection ?? null };
8113
+ }
8114
+ async function runModelImport(opts) {
8115
+ const log = createLogger({ quiet: opts.quiet || opts.json });
8116
+ const filePath = opts.sourceId?.trim();
8117
+ if (!filePath) {
8118
+ log.error(`Missing file. Usage: ${c.cyan("genex model import <file.glb>")}`);
8119
+ process.exitCode = 1;
8120
+ return;
8121
+ }
8122
+ const token = opts.token ?? await readUserToken(opts.envPath);
8123
+ if (!token) {
8124
+ log.error("Not authorized. Run `genex init` first to sign in.");
8125
+ process.exitCode = 1;
8126
+ return;
8127
+ }
8128
+ const project = await readProject();
8129
+ const apiUrl = getApiUrl(opts.apiUrl ?? project?.apiUrl);
8130
+ const imported = await importModelFile({ apiUrl, token, filePath, log });
8131
+ if (!imported) {
8132
+ process.exitCode = 1;
8133
+ return;
8134
+ }
8135
+ const next = {
8136
+ rig: `genex model rig ${imported.id}`,
8137
+ character: `genex character import ${filePath}`
8138
+ };
8139
+ if (opts.json) {
8140
+ writeJson({ kind: "model", id: imported.id, status: "completed", url: imported.url, inspection: imported.inspection, nextCommands: next });
8141
+ return;
8142
+ }
8143
+ const facts = imported.inspection ? ` \xB7 ${imported.inspection.faceCount.toLocaleString("en-US")} faces${imported.inspection.skinned ? " \xB7 already skinned" : ""}` : "";
8144
+ log.success(`Imported (${imported.id})${facts}`);
8145
+ log.plain(` ${imported.url}`);
8146
+ log.plain(` Rig it (7 body plans): ${c.cyan(next.rig)}`);
8147
+ log.plain(` Or make it a playable humanoid: ${c.cyan(next.character)}`);
8148
+ }
7986
8149
  var SEGMENT_GRANULARITIES = ["simple", "balanced", "detailed"];
7987
8150
  var MODEL_RIG_TYPES = [
7988
8151
  "biped",
@@ -8271,8 +8434,8 @@ async function toRow(e, v, cwd) {
8271
8434
  }
8272
8435
 
8273
8436
  // src/commands/controller.ts
8274
- import fs19 from "fs/promises";
8275
- import path18 from "path";
8437
+ import fs20 from "fs/promises";
8438
+ import path19 from "path";
8276
8439
 
8277
8440
  // ../../packages/meshy-animation-catalog/src/index.ts
8278
8441
  import { createHash } from "crypto";
@@ -17402,9 +17565,9 @@ function searchMeshyAnimations(query, options = {}) {
17402
17565
  }
17403
17566
 
17404
17567
  // src/lib/anims.ts
17405
- import fs18 from "fs/promises";
17406
- import path17 from "path";
17407
- var ANIMS_DEST = path17.join("public", "assets", "anims");
17568
+ import fs19 from "fs/promises";
17569
+ import path18 from "path";
17570
+ var ANIMS_DEST = path18.join("public", "assets", "anims");
17408
17571
  var HIDDEN_TAG = "reference";
17409
17572
  async function runAnims(opts) {
17410
17573
  const log = createLogger({ quiet: opts.quiet });
@@ -17420,7 +17583,7 @@ async function runAnims(opts) {
17420
17583
  printCatalog(log, manifest, selectors);
17421
17584
  return;
17422
17585
  }
17423
- const controllerMarker = path17.join(root, "src", "controllers", "character");
17586
+ const controllerMarker = path18.join(root, "src", "controllers", "character");
17424
17587
  if (!await exists2(controllerMarker)) {
17425
17588
  log.error(
17426
17589
  `No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
@@ -17429,11 +17592,11 @@ async function runAnims(opts) {
17429
17592
  process.exitCode = 1;
17430
17593
  return;
17431
17594
  }
17432
- const destDir = path17.join(root, ANIMS_DEST);
17433
- const gameManifestPath = path17.join(destDir, "manifest.json");
17595
+ const destDir = path18.join(root, ANIMS_DEST);
17596
+ const gameManifestPath = path18.join(destDir, "manifest.json");
17434
17597
  if (opts.reset) {
17435
- await fs18.rm(destDir, { recursive: true, force: true });
17436
- log.step(`Cleared ${c.cyan(ANIMS_DEST + path17.sep)} (--reset)`);
17598
+ await fs19.rm(destDir, { recursive: true, force: true });
17599
+ log.step(`Cleared ${c.cyan(ANIMS_DEST + path18.sep)} (--reset)`);
17437
17600
  }
17438
17601
  if (selectors.length === 0) {
17439
17602
  const installed = await readGameManifest(gameManifestPath);
@@ -17471,35 +17634,35 @@ async function runAnims(opts) {
17471
17634
  }
17472
17635
  }
17473
17636
  const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
17474
- const cacheDir = path17.join(
17637
+ const cacheDir = path18.join(
17475
17638
  opts.cacheDir ?? getAnimsCacheDir(),
17476
17639
  `${manifest.library}-v${manifest.version}`
17477
17640
  );
17478
- await fs18.mkdir(cacheDir, { recursive: true });
17479
- await fs18.mkdir(destDir, { recursive: true });
17641
+ await fs19.mkdir(cacheDir, { recursive: true });
17642
+ await fs19.mkdir(destDir, { recursive: true });
17480
17643
  const base = getAnimsBase(opts.animsBase);
17481
17644
  let installedCount = 0;
17482
17645
  let presentCount = 0;
17483
17646
  let addedBytes = 0;
17484
17647
  const failures = [];
17485
17648
  for (const entry of wanted) {
17486
- const dest = path17.join(destDir, entry.file);
17649
+ const dest = path18.join(destDir, entry.file);
17487
17650
  if (await hasSize(dest, entry.bytes)) {
17488
17651
  presentCount++;
17489
17652
  continue;
17490
17653
  }
17491
17654
  try {
17492
- const cached = path17.join(cacheDir, entry.file);
17655
+ const cached = path18.join(cacheDir, entry.file);
17493
17656
  if (!await hasSize(cached, entry.bytes)) {
17494
17657
  const res = await fetch(base + entry.file);
17495
17658
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
17496
17659
  const buf = Buffer.from(await res.arrayBuffer());
17497
- await fs18.writeFile(cached, buf);
17660
+ await fs19.writeFile(cached, buf);
17498
17661
  }
17499
- await fs18.copyFile(cached, dest);
17662
+ await fs19.copyFile(cached, dest);
17500
17663
  installedCount++;
17501
17664
  addedBytes += entry.bytes;
17502
- log.dim(` ${path17.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
17665
+ log.dim(` ${path18.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
17503
17666
  } catch (err) {
17504
17667
  failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
17505
17668
  }
@@ -17515,13 +17678,13 @@ async function runAnims(opts) {
17515
17678
  version: manifest.version,
17516
17679
  clips: [...union].sort((a, b) => a.localeCompare(b))
17517
17680
  };
17518
- await fs18.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
17681
+ await fs19.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
17519
17682
  log.plain("");
17520
17683
  const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
17521
17684
  if (presentCount > 0) parts.push(`${presentCount} already present`);
17522
17685
  if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
17523
17686
  log.success(
17524
- `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path17.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
17687
+ `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path18.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
17525
17688
  );
17526
17689
  for (const [selector, entries] of resolved) {
17527
17690
  const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
@@ -17553,8 +17716,8 @@ async function loadManifest(baseOverride) {
17553
17716
  }
17554
17717
  } catch {
17555
17718
  }
17556
- const snapshotPath = path17.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
17557
- const manifest = JSON.parse(await fs18.readFile(snapshotPath, "utf8"));
17719
+ const snapshotPath = path18.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
17720
+ const manifest = JSON.parse(await fs19.readFile(snapshotPath, "utf8"));
17558
17721
  return { manifest, source: "snapshot" };
17559
17722
  }
17560
17723
  function resolveSelectors(manifest, selectors) {
@@ -17672,21 +17835,21 @@ function printCatalog(log, manifest, selectors) {
17672
17835
  }
17673
17836
  async function readGameManifest(file) {
17674
17837
  try {
17675
- return JSON.parse(await fs18.readFile(file, "utf8"));
17838
+ return JSON.parse(await fs19.readFile(file, "utf8"));
17676
17839
  } catch {
17677
17840
  return null;
17678
17841
  }
17679
17842
  }
17680
17843
  async function hasSize(file, bytes) {
17681
17844
  try {
17682
- return (await fs18.stat(file)).size === bytes;
17845
+ return (await fs19.stat(file)).size === bytes;
17683
17846
  } catch {
17684
17847
  return false;
17685
17848
  }
17686
17849
  }
17687
17850
  async function exists2(p) {
17688
17851
  try {
17689
- await fs18.access(p);
17852
+ await fs19.access(p);
17690
17853
  return true;
17691
17854
  } catch {
17692
17855
  return false;
@@ -17819,6 +17982,7 @@ var CONTROLLER_FILE_SETS = {
17819
17982
  code: [
17820
17983
  "quality/tier.ts",
17821
17984
  "quality/governor.ts",
17985
+ "quality/deadline.ts",
17822
17986
  "quality/pick-asset.ts",
17823
17987
  "quality/gltf-loader.ts",
17824
17988
  "quality/depth.ts",
@@ -17886,8 +18050,8 @@ var CONTROLLER_FILE_SETS = {
17886
18050
  ]
17887
18051
  }
17888
18052
  };
17889
- var CODE_DEST = path18.join("src", "controllers");
17890
- var ASSETS_DEST = path18.join("public", "assets");
18053
+ var CODE_DEST = path19.join("src", "controllers");
18054
+ var ASSETS_DEST = path19.join("public", "assets");
17891
18055
  async function runController(opts) {
17892
18056
  const log = createLogger({ quiet: opts.quiet });
17893
18057
  if (opts.kind?.trim() === "anims") {
@@ -17904,31 +18068,31 @@ async function runController(opts) {
17904
18068
  process.exitCode = 1;
17905
18069
  return;
17906
18070
  }
17907
- const srcDir = path18.join(getTemplatesDir(), "controllers");
18071
+ const srcDir = path19.join(getTemplatesDir(), "controllers");
17908
18072
  const root = opts.cwd ?? process.cwd();
17909
18073
  const set = CONTROLLER_FILE_SETS[kind];
17910
18074
  log.plain(c.bold(`genex controller ${kind}`));
17911
18075
  log.plain("");
17912
- log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path18.sep)}`);
18076
+ log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path19.sep)}`);
17913
18077
  const plan = [
17914
- ...set.code.map((rel) => ({ from: rel, rel: path18.join(CODE_DEST, rel) })),
18078
+ ...set.code.map((rel) => ({ from: rel, rel: path19.join(CODE_DEST, rel) })),
17915
18079
  ...set.assets.map((rel) => ({
17916
18080
  from: rel,
17917
- rel: path18.join(ASSETS_DEST, path18.basename(rel))
18081
+ rel: path19.join(ASSETS_DEST, path19.basename(rel))
17918
18082
  }))
17919
18083
  ];
17920
18084
  let copied = 0;
17921
18085
  let skipped = 0;
17922
18086
  try {
17923
18087
  for (const file of plan) {
17924
- const dest = path18.join(root, file.rel);
18088
+ const dest = path19.join(root, file.rel);
17925
18089
  if (!opts.force && await exists3(dest)) {
17926
18090
  skipped++;
17927
18091
  log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
17928
18092
  continue;
17929
18093
  }
17930
- await fs19.mkdir(path18.dirname(dest), { recursive: true });
17931
- await fs19.copyFile(path18.join(srcDir, file.from), dest);
18094
+ await fs20.mkdir(path19.dirname(dest), { recursive: true });
18095
+ await fs20.copyFile(path19.join(srcDir, file.from), dest);
17932
18096
  copied++;
17933
18097
  log.dim(` ${file.rel}`);
17934
18098
  }
@@ -17981,7 +18145,7 @@ async function runController(opts) {
17981
18145
  for (const line of set.sketch) {
17982
18146
  log.dim(` ${line}`);
17983
18147
  }
17984
- if (kind === "character" && !await exists3(path18.join(root, ASSETS_DEST, "meshy-character.json"))) {
18148
+ if (kind === "character" && !await exists3(path19.join(root, ASSETS_DEST, "meshy-character.json"))) {
17985
18149
  log.plain("");
17986
18150
  log.plain(
17987
18151
  ` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
@@ -18009,13 +18173,13 @@ async function installMeshyCharacterManifest(args) {
18009
18173
  }
18010
18174
  const body = await response.json();
18011
18175
  const manifest = body.manifest;
18012
- if (!manifest || manifest.schema !== 1 || manifest.rig !== "meshy-biped" || manifest.characterId !== args.characterId || typeof manifest.model?.url !== "string" || typeof manifest.model.skeletonSignature !== "string" || !Array.isArray(manifest.clips) || !manifest.locomotion?.slots) {
18176
+ if (!manifest || manifest.schema !== 1 || manifest.rig !== "meshy-biped" && manifest.rig !== "uthana-biped" || manifest.characterId !== args.characterId || typeof manifest.model?.url !== "string" || typeof manifest.model.skeletonSignature !== "string" || !Array.isArray(manifest.clips) || !manifest.locomotion?.slots) {
18013
18177
  throw new Error("The API returned an invalid Meshy character manifest.");
18014
18178
  }
18015
18179
  assertCompleteMeshyControllerPack(manifest);
18016
- const destination = path18.join(args.root, ASSETS_DEST, "meshy-character.json");
18017
- await fs19.mkdir(path18.dirname(destination), { recursive: true });
18018
- await fs19.writeFile(
18180
+ const destination = path19.join(args.root, ASSETS_DEST, "meshy-character.json");
18181
+ await fs20.mkdir(path19.dirname(destination), { recursive: true });
18182
+ await fs20.writeFile(
18019
18183
  destination,
18020
18184
  `${JSON.stringify(manifest, null, 2)}
18021
18185
  `
@@ -18025,6 +18189,13 @@ async function installMeshyCharacterManifest(args) {
18025
18189
  const pack2 = manifest.controllerPack;
18026
18190
  if (typeof pack2?.key === "string" && typeof pack2.version === "number") {
18027
18191
  args.log.success(`Meshy controller pack ${pack2.key} v${pack2.version}`);
18192
+ } else if (manifest.rig === "uthana-biped") {
18193
+ const slotCount = Object.keys(manifest.locomotion?.slots ?? {}).length;
18194
+ if (slotCount === 0) {
18195
+ args.log.warn(`Uthana-rigged import with no locomotion yet \u2014 run \`genex character animate ${args.characterId} --locomotion\` or the body will stand still.`);
18196
+ } else {
18197
+ args.log.success(`Uthana-rigged import \xB7 ${slotCount} locomotion slot${slotCount === 1 ? "" : "s"} from generated motion`);
18198
+ }
18028
18199
  } else {
18029
18200
  args.log.warn("Legacy Meshy manifest \u2014 regenerate the character for the immutable preview-reviewed neutral-v3 locomotion pack.");
18030
18201
  }
@@ -18153,14 +18324,14 @@ function assertCompleteMeshyControllerPack(manifest) {
18153
18324
  }
18154
18325
  async function installFallbackAvatar(args) {
18155
18326
  const { root, srcDir, log } = args;
18156
- const dest = path18.join(root, ASSETS_DEST, "avatar.vrm");
18157
- await fs19.mkdir(path18.dirname(dest), { recursive: true });
18158
- await fs19.copyFile(path18.join(srcDir, "assets", "default-avatar.vrm"), dest);
18327
+ const dest = path19.join(root, ASSETS_DEST, "avatar.vrm");
18328
+ await fs20.mkdir(path19.dirname(dest), { recursive: true });
18329
+ await fs20.copyFile(path19.join(srcDir, "assets", "default-avatar.vrm"), dest);
18159
18330
  log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
18160
18331
  }
18161
18332
  async function exists3(p) {
18162
18333
  try {
18163
- await fs19.access(p);
18334
+ await fs20.access(p);
18164
18335
  return true;
18165
18336
  } catch {
18166
18337
  return false;
@@ -18168,8 +18339,8 @@ async function exists3(p) {
18168
18339
  }
18169
18340
 
18170
18341
  // src/commands/character.ts
18171
- import fs20 from "fs/promises";
18172
- import path19 from "path";
18342
+ import fs21 from "fs/promises";
18343
+ import path20 from "path";
18173
18344
  function exactAnimation(selector) {
18174
18345
  const trimmed = selector.trim();
18175
18346
  if (/^-?\d+$/.test(trimmed)) return animationById(Number(trimmed));
@@ -18201,7 +18372,7 @@ async function context(opts) {
18201
18372
  return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
18202
18373
  }
18203
18374
  var CHARACTER_BRIEF_MAX_CHARS = 600;
18204
- function apiErrorMessage(data, fallback) {
18375
+ function apiErrorMessage2(data, fallback) {
18205
18376
  if (typeof data !== "object" || data === null) return fallback;
18206
18377
  const body = data;
18207
18378
  if (typeof body.message === "string" && body.message) return body.message;
@@ -18225,7 +18396,7 @@ async function quote(url, token, body) {
18225
18396
  if (printedStructuredError(response)) return null;
18226
18397
  if (!response.ok) {
18227
18398
  const data = await response.json().catch(() => ({}));
18228
- throw new Error(apiErrorMessage(data, `Quote failed (HTTP ${response.status}).`));
18399
+ throw new Error(apiErrorMessage2(data, `Quote failed (HTTP ${response.status}).`));
18229
18400
  }
18230
18401
  return (await response.json()).quote;
18231
18402
  }
@@ -18249,7 +18420,7 @@ function showAmbiguity(selector, candidates, log, json = false) {
18249
18420
  }
18250
18421
  process.exitCode = 1;
18251
18422
  }
18252
- var INSTALLED_MANIFEST = path19.join("public", "assets", "meshy-character.json");
18423
+ var INSTALLED_MANIFEST = path20.join("public", "assets", "meshy-character.json");
18253
18424
  async function resolveAdoptTarget(selector) {
18254
18425
  const trimmed = selector?.trim();
18255
18426
  if (trimmed && !trimmed.endsWith(".json")) {
@@ -18258,7 +18429,7 @@ async function resolveAdoptTarget(selector) {
18258
18429
  const file = trimmed ?? INSTALLED_MANIFEST;
18259
18430
  let raw;
18260
18431
  try {
18261
- raw = await fs20.readFile(file, "utf8");
18432
+ raw = await fs21.readFile(file, "utf8");
18262
18433
  } catch {
18263
18434
  return {
18264
18435
  ok: false,
@@ -18334,6 +18505,81 @@ async function runCharacterAdopt(opts) {
18334
18505
  }
18335
18506
  });
18336
18507
  }
18508
+ async function runCharacterImport(opts) {
18509
+ const log = createLogger({ quiet: opts.quiet || opts.json });
18510
+ const filePath = opts.importPath?.trim();
18511
+ if (!filePath) {
18512
+ fail2(opts, "Missing file. Usage: genex character import <file.glb> [--height 1.8] [--no-fingers]");
18513
+ return;
18514
+ }
18515
+ if (opts.texture !== void 0 || opts.ultra === false || opts.pose !== void 0 || opts.polycount !== void 0) {
18516
+ fail2(opts, "--texture / --no-ultra / --pose / --polycount are Meshy generation knobs; an import keeps your mesh exactly as it is. Uthana only adds the skeleton (--no-fingers skips finger joints).");
18517
+ return;
18518
+ }
18519
+ if (opts.controllerPack === false || (opts.actions?.length ?? 0) > 0) {
18520
+ fail2(opts, "An imported character has no Meshy controller pack and takes no catalog --animation: its motion comes from `genex character animate <id> --locomotion` and plain-word verbs.");
18521
+ return;
18522
+ }
18523
+ const ctx = await context(opts);
18524
+ if (!ctx) {
18525
+ fail2(opts, "Not authorized. Run `genex init` first to sign in.");
18526
+ return;
18527
+ }
18528
+ const imported = await importModelFile({ apiUrl: ctx.apiUrl, token: ctx.token, filePath, log });
18529
+ if (!imported) {
18530
+ process.exitCode = 1;
18531
+ return;
18532
+ }
18533
+ const body = {
18534
+ sourceGenerationId: imported.id,
18535
+ includeFingers: opts.fingers !== false,
18536
+ ...opts.height === void 0 ? {} : { heightMeters: opts.height }
18537
+ };
18538
+ const price = await quote(`${ctx.apiUrl}/api/characters/import/quote`, ctx.token, body);
18539
+ if (!price) {
18540
+ process.exitCode = 1;
18541
+ return;
18542
+ }
18543
+ if (!opts.json) {
18544
+ log.plain(c.bold("Character import quote"));
18545
+ log.plain(` ${price.credits} Genex credits \xB7 Uthana auto-rig${opts.fingers === false ? " (no finger joints)" : " with finger joints"} \xB7 the model upload was free`);
18546
+ log.plain("");
18547
+ }
18548
+ await runWorkflow({
18549
+ opts,
18550
+ ctx,
18551
+ kind: "character",
18552
+ prompt: `Import ${path20.basename(filePath)} as a rigged character`,
18553
+ createPath: "/api/characters/import",
18554
+ body,
18555
+ quote: price,
18556
+ completed: (view) => {
18557
+ const confidence = typeof view.metadata?.autoRigConfidence === "number" ? view.metadata.autoRigConfidence : null;
18558
+ const next = {
18559
+ locomotion: `genex character animate ${view.id} --locomotion`,
18560
+ install: `genex controller character --character ${view.id}`
18561
+ };
18562
+ if (opts.json) {
18563
+ writeJson({
18564
+ kind: "character",
18565
+ status: view.status,
18566
+ characterId: view.id,
18567
+ modelGenerationId: imported.id,
18568
+ rigProvider: "uthana",
18569
+ autoRigConfidence: confidence,
18570
+ nextCommand: next.locomotion,
18571
+ nextCommands: next
18572
+ });
18573
+ return;
18574
+ }
18575
+ log.success(`Rigged by Uthana${confidence === null ? "" : ` (confidence ${confidence.toFixed(2)})`}: ${c.cyan(view.id)}`);
18576
+ log.plain(" It has a skeleton and no clips yet. Give it the walk/run set, then point the game at it:");
18577
+ log.plain(` ${c.cyan(next.locomotion)}`);
18578
+ log.plain(` ${c.cyan(next.install)}`);
18579
+ log.dim(' Any move in plain words: genex character animate <id> "<what it should do>" \xB7 the Meshy catalog does not apply to this rig.');
18580
+ }
18581
+ });
18582
+ }
18337
18583
  async function runCharacter(opts) {
18338
18584
  if (opts.directText) {
18339
18585
  await runDirectTextCharacter(opts);
@@ -18362,7 +18608,7 @@ async function postWorkflow(url, token, body) {
18362
18608
  if (printedStructuredError(response)) return null;
18363
18609
  if (!response.ok) {
18364
18610
  const data2 = await response.json().catch(() => ({}));
18365
- throw new Error(apiErrorMessage(data2, `Character workflow request failed (HTTP ${response.status}).`));
18611
+ throw new Error(apiErrorMessage2(data2, `Character workflow request failed (HTTP ${response.status}).`));
18366
18612
  }
18367
18613
  const data = await response.json();
18368
18614
  if (typeof data.id !== "string" || data.id.length === 0) {
@@ -18432,7 +18678,11 @@ async function runCharacterConcept(opts) {
18432
18678
  return;
18433
18679
  }
18434
18680
  if (opts.polycount !== void 0) {
18435
- fail2(opts, "--polycount is only available with --direct-text. The approval flow remeshes only after finalize approval.", "character_concept");
18681
+ fail2(opts, "--polycount is only available with --direct-text. The approval flow takes its face budget on `character finalize --approve-remesh <faces>`.", "character_concept");
18682
+ return;
18683
+ }
18684
+ if (opts.texture !== void 0 || opts.ultra === false || opts.pose !== void 0) {
18685
+ fail2(opts, "--texture / --no-ultra / --pose apply to the paid mesh call: pass them to `character preview` (guided) or `character --direct-text` / `creature` (one shot). Concepts are images.", "character_concept");
18436
18686
  return;
18437
18687
  }
18438
18688
  if (opts.controllerPack === false) {
@@ -18528,10 +18778,26 @@ async function runDirectTextCharacter(opts) {
18528
18778
  actionIds: resolved.actionIds,
18529
18779
  controllerPack,
18530
18780
  ...opts.height === void 0 ? {} : { heightMeters: opts.height },
18531
- targetPolycount: opts.polycount ?? 1e4
18781
+ targetPolycount: opts.polycount ?? 1e4,
18782
+ ...meshyQualityOptions(opts),
18783
+ ...opts.pose === void 0 ? {} : { poseMode: opts.pose }
18532
18784
  }
18533
18785
  });
18534
18786
  }
18787
+ var CHARACTER_TEXTURE_SIZES = ["2k", "4k", "8k"];
18788
+ var REMESH_TARGET_MIN = 1e4;
18789
+ var REMESH_TARGET_MAX = 1e5;
18790
+ function meshyQualityOptions(opts) {
18791
+ return {
18792
+ ...opts.texture === void 0 ? {} : { textureResolution: opts.texture },
18793
+ ...opts.ultra === false ? { ultraMode: false } : {}
18794
+ };
18795
+ }
18796
+ function badCharacterTexture(opts) {
18797
+ if (opts.texture === void 0) return null;
18798
+ if (CHARACTER_TEXTURE_SIZES.includes(opts.texture)) return null;
18799
+ return `--texture ${opts.texture} is a \`genex model\` tier. A character takes a texture SIZE: ${CHARACTER_TEXTURE_SIZES.join("|")} (default 4k).`;
18800
+ }
18535
18801
  async function runCharacterPreview(opts) {
18536
18802
  const log = createLogger({ quiet: opts.quiet || opts.json });
18537
18803
  const conceptId = opts.conceptId?.trim();
@@ -18552,7 +18818,12 @@ async function runCharacterPreview(opts) {
18552
18818
  fail2(opts, "Not authorized. Run `genex init` first to sign in.", "character_preview");
18553
18819
  return;
18554
18820
  }
18555
- const body = { candidateIndex: opts.candidate, userApproved: true };
18821
+ const badTexture = badCharacterTexture(opts);
18822
+ if (badTexture) {
18823
+ fail2(opts, badTexture, "character_preview");
18824
+ return;
18825
+ }
18826
+ const body = { candidateIndex: opts.candidate, userApproved: true, ...meshyQualityOptions(opts) };
18556
18827
  const basePath = `/api/characters/concepts/${encodeURIComponent(conceptId)}/previews`;
18557
18828
  const price = await quote(`${ctx.apiUrl}${basePath}/quote`, ctx.token, body);
18558
18829
  if (!price) {
@@ -18590,12 +18861,20 @@ async function runCharacterFinalize(opts) {
18590
18861
  fail2(opts, "STOP: finalization requires explicit user approval of all four preview views. Re-run with --user-approved only after approval.");
18591
18862
  return;
18592
18863
  }
18593
- if (opts.approveRemesh !== 1e4) {
18594
- fail2(opts, "--approve-remesh must be exactly 10000. No other rigging-copy target is accepted.");
18864
+ const remeshTarget = opts.approveRemesh;
18865
+ if (remeshTarget === void 0 || !Number.isInteger(remeshTarget) || remeshTarget < REMESH_TARGET_MIN || remeshTarget > REMESH_TARGET_MAX) {
18866
+ fail2(
18867
+ opts,
18868
+ `--approve-remesh <faces> is required: the face budget of the rigging copy, ${REMESH_TARGET_MIN}-${REMESH_TARGET_MAX} (10000 for a crowd body, 30000 or more for a hero). The number is the approval.`
18869
+ );
18595
18870
  return;
18596
18871
  }
18597
18872
  if (opts.polycount !== void 0) {
18598
- fail2(opts, "Do not combine --polycount with finalize; --approve-remesh 10000 is the only accepted rigging-copy target.");
18873
+ fail2(opts, "Do not combine --polycount with finalize; --approve-remesh <faces> is the rigging-copy target.");
18874
+ return;
18875
+ }
18876
+ if (opts.texture !== void 0 || opts.ultra === false) {
18877
+ fail2(opts, "--texture / --no-ultra belong to `genex character preview` \u2014 the preview is the paid Meshy call; finalize remeshes and rigs what it made.");
18599
18878
  return;
18600
18879
  }
18601
18880
  if (opts.controllerPack === false) {
@@ -18615,7 +18894,7 @@ async function runCharacterFinalize(opts) {
18615
18894
  const body = {
18616
18895
  ...requestedBuild(opts, resolved.actionIds),
18617
18896
  userApproved: true,
18618
- approvedRemeshTarget: 1e4
18897
+ approvedRemeshTarget: remeshTarget
18619
18898
  };
18620
18899
  const basePath = `/api/characters/previews/${encodeURIComponent(previewId)}/finalize`;
18621
18900
  const price = await quote(`${ctx.apiUrl}${basePath}/quote`, ctx.token, body);
@@ -18626,7 +18905,7 @@ async function runCharacterFinalize(opts) {
18626
18905
  if (!opts.json) {
18627
18906
  log.plain(c.bold("Character finalize quote"));
18628
18907
  log.plain(` ${price.credits} Genex credits = base ${price.baseCredits} + actions ${price.animationCredits}`);
18629
- log.plain(" Approved rigging copy: 10,000 faces; the high-detail source remains preserved in R2.");
18908
+ log.plain(` Approved rigging copy: ${remeshTarget.toLocaleString("en-US")} faces; the high-detail source remains preserved in R2.`);
18630
18909
  log.plain("");
18631
18910
  }
18632
18911
  await runWorkflow({
@@ -18741,22 +19020,22 @@ async function context2(opts) {
18741
19020
  const project = await readProject();
18742
19021
  return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
18743
19022
  }
18744
- async function readVideo(path26, log) {
19023
+ async function readVideo(path27, log) {
18745
19024
  let bytes;
18746
19025
  try {
18747
- bytes = await readFile(path26);
19026
+ bytes = await readFile(path27);
18748
19027
  } catch {
18749
- log.error(`Can't read ${path26}.`);
19028
+ log.error(`Can't read ${path27}.`);
18750
19029
  return null;
18751
19030
  }
18752
19031
  if (bytes.byteLength > MAX_VIDEO_BYTES) {
18753
- log.error(`${basename(path26)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
19032
+ log.error(`${basename(path27)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
18754
19033
  return null;
18755
19034
  }
18756
19035
  return bytes;
18757
19036
  }
18758
- async function uploadVideo(apiUrl, token, characterId, path26, bytes, log) {
18759
- const contentType = /\.mov$/i.test(path26) ? "video/quicktime" : "video/mp4";
19037
+ async function uploadVideo(apiUrl, token, characterId, path27, bytes, log) {
19038
+ const contentType = /\.mov$/i.test(path27) ? "video/quicktime" : "video/mp4";
18760
19039
  const minted = await apiFetch(
18761
19040
  `${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
18762
19041
  {
@@ -18771,7 +19050,7 @@ async function uploadVideo(apiUrl, token, characterId, path26, bytes, log) {
18771
19050
  return null;
18772
19051
  }
18773
19052
  const { uploadUrl, videoUrl } = await minted.json();
18774
- log.dim(` uploading ${basename(path26)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
19053
+ log.dim(` uploading ${basename(path27)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
18775
19054
  const put = await fetch(uploadUrl, {
18776
19055
  method: "PUT",
18777
19056
  headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
@@ -19087,8 +19366,8 @@ function rank(items, query) {
19087
19366
  }
19088
19367
 
19089
19368
  // src/commands/motion.ts
19090
- import fs21 from "fs/promises";
19091
- import path20 from "path";
19369
+ import fs22 from "fs/promises";
19370
+ import path21 from "path";
19092
19371
 
19093
19372
  // src/lib/motion/npz.ts
19094
19373
  import zlib from "zlib";
@@ -20339,7 +20618,7 @@ async function motionGen(opts, log) {
20339
20618
  }
20340
20619
  if (opts.constraintsPath !== void 0) {
20341
20620
  try {
20342
- const raw = await fs21.readFile(opts.constraintsPath, "utf8");
20621
+ const raw = await fs22.readFile(opts.constraintsPath, "utf8");
20343
20622
  generationOptions.constraints = JSON.parse(raw);
20344
20623
  } catch {
20345
20624
  log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
@@ -20363,10 +20642,10 @@ async function motionGen(opts, log) {
20363
20642
  async function expandTakes(selectors) {
20364
20643
  const out = [];
20365
20644
  for (const sel of selectors) {
20366
- const st = await fs21.stat(sel).catch(() => null);
20645
+ const st = await fs22.stat(sel).catch(() => null);
20367
20646
  if (st?.isDirectory()) {
20368
- const names = await fs21.readdir(sel);
20369
- for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path20.join(sel, n));
20647
+ const names = await fs22.readdir(sel);
20648
+ for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path21.join(sel, n));
20370
20649
  } else if (st?.isFile()) {
20371
20650
  out.push(sel);
20372
20651
  } else {
@@ -20401,7 +20680,7 @@ async function motionVerify(opts, log) {
20401
20680
  let gates = DEFAULT_GATES;
20402
20681
  if (opts.gatesPath) {
20403
20682
  try {
20404
- gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs21.readFile(opts.gatesPath, "utf8")));
20683
+ gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs22.readFile(opts.gatesPath, "utf8")));
20405
20684
  } catch {
20406
20685
  log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
20407
20686
  process.exitCode = 1;
@@ -20423,9 +20702,9 @@ async function motionVerify(opts, log) {
20423
20702
  }
20424
20703
  const reports = [];
20425
20704
  for (const file of files) {
20426
- const stem = path20.basename(file).replace(/\.npz$/, "");
20705
+ const stem = path21.basename(file).replace(/\.npz$/, "");
20427
20706
  try {
20428
- reports.push(analyzeTake(stem, await fs21.readFile(file), gates));
20707
+ reports.push(analyzeTake(stem, await fs22.readFile(file), gates));
20429
20708
  } catch (err) {
20430
20709
  reports.push({
20431
20710
  take: stem,
@@ -20463,7 +20742,7 @@ async function motionCompile(opts, log) {
20463
20742
  let cfg = DEFAULT_MOTION_CONFIG;
20464
20743
  if (opts.configPath) {
20465
20744
  try {
20466
- const patch = JSON.parse(await fs21.readFile(opts.configPath, "utf8"));
20745
+ const patch = JSON.parse(await fs22.readFile(opts.configPath, "utf8"));
20467
20746
  cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
20468
20747
  } catch {
20469
20748
  log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
@@ -20481,16 +20760,16 @@ async function motionCompile(opts, log) {
20481
20760
  }
20482
20761
  const inputs = [];
20483
20762
  for (const file of files) {
20484
- const stem = path20.basename(file).replace(/\.npz$/, "");
20763
+ const stem = path21.basename(file).replace(/\.npz$/, "");
20485
20764
  try {
20486
- inputs.push({ stem, take: loadTake(await fs21.readFile(file)) });
20765
+ inputs.push({ stem, take: loadTake(await fs22.readFile(file)) });
20487
20766
  } catch (err) {
20488
20767
  log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
20489
20768
  process.exitCode = 1;
20490
20769
  return;
20491
20770
  }
20492
20771
  }
20493
- const setName = opts.set ?? path20.basename(opts.out).replace(/\.json$/, "");
20772
+ const setName = opts.set ?? path21.basename(opts.out).replace(/\.json$/, "");
20494
20773
  let result;
20495
20774
  try {
20496
20775
  result = compileSet(inputs, setName, cfg);
@@ -20505,9 +20784,9 @@ async function motionCompile(opts, log) {
20505
20784
  process.exitCode = 1;
20506
20785
  return;
20507
20786
  }
20508
- await fs21.mkdir(path20.dirname(path20.resolve(opts.out)), { recursive: true });
20787
+ await fs22.mkdir(path21.dirname(path21.resolve(opts.out)), { recursive: true });
20509
20788
  const json = JSON.stringify(result.data);
20510
- await fs21.writeFile(opts.out, json);
20789
+ await fs22.writeFile(opts.out, json);
20511
20790
  if (opts.json) {
20512
20791
  writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
20513
20792
  return;
@@ -20525,9 +20804,9 @@ var MOTION_RUNTIME_FILES = [
20525
20804
  var MOTION_PRESETS = {
20526
20805
  rifle: ["sets/rifle.json", "sets/jumps.json"]
20527
20806
  };
20528
- var MOTION_DEST = path20.join("src", "motion");
20807
+ var MOTION_DEST = path21.join("src", "motion");
20529
20808
  async function motionInstall(opts, log) {
20530
- const srcDir = path20.join(getTemplatesDir(), "motion");
20809
+ const srcDir = path21.join(getTemplatesDir(), "motion");
20531
20810
  const root = opts.cwd ?? process.cwd();
20532
20811
  const preset = opts.set;
20533
20812
  if (preset !== void 0 && !MOTION_PRESETS[preset]) {
@@ -20538,21 +20817,21 @@ async function motionInstall(opts, log) {
20538
20817
  const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
20539
20818
  log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
20540
20819
  log.plain("");
20541
- log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path20.sep)}`);
20820
+ log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path21.sep)}`);
20542
20821
  let copied = 0, skipped = 0;
20543
20822
  try {
20544
20823
  for (const rel of files) {
20545
- const dest = path20.join(root, MOTION_DEST, rel);
20546
- const exists5 = await fs21.access(dest).then(() => true, () => false);
20824
+ const dest = path21.join(root, MOTION_DEST, rel);
20825
+ const exists5 = await fs22.access(dest).then(() => true, () => false);
20547
20826
  if (!opts.force && exists5) {
20548
20827
  skipped++;
20549
- log.dim(` skipped ${path20.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
20828
+ log.dim(` skipped ${path21.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
20550
20829
  continue;
20551
20830
  }
20552
- await fs21.mkdir(path20.dirname(dest), { recursive: true });
20553
- await fs21.copyFile(path20.join(srcDir, rel), dest);
20831
+ await fs22.mkdir(path21.dirname(dest), { recursive: true });
20832
+ await fs22.copyFile(path21.join(srcDir, rel), dest);
20554
20833
  copied++;
20555
- log.dim(` ${path20.join(MOTION_DEST, rel)}`);
20834
+ log.dim(` ${path21.join(MOTION_DEST, rel)}`);
20556
20835
  }
20557
20836
  } catch (err) {
20558
20837
  log.error(`Copy failed: ${String(err)}`);
@@ -20593,7 +20872,7 @@ async function motionConstraints(opts, log) {
20593
20872
  }
20594
20873
  const doc = directionConstraint(dir, speed, duration);
20595
20874
  const out = opts.out ?? "constraints.json";
20596
- await fs21.writeFile(out, JSON.stringify(doc));
20875
+ await fs22.writeFile(out, JSON.stringify(doc));
20597
20876
  if (opts.json) {
20598
20877
  writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
20599
20878
  return;
@@ -20630,14 +20909,14 @@ async function runMotion(opts) {
20630
20909
  }
20631
20910
 
20632
20911
  // src/commands/blender.ts
20633
- import fs22 from "fs/promises";
20634
- import path21 from "path";
20912
+ import fs23 from "fs/promises";
20913
+ import path22 from "path";
20635
20914
  var SUBS2 = ["demo", "exec", "snap", "scene", "import", "export", "reset", "mcp", "serve", "seat", "release"];
20636
20915
  var DEFAULT_OUT_DIR = "assets/blender";
20637
20916
  async function writeB64(dir, name, b64) {
20638
- await fs22.mkdir(dir, { recursive: true });
20639
- const p = path21.join(dir, name);
20640
- await fs22.writeFile(p, Buffer.from(b64, "base64"));
20917
+ await fs23.mkdir(dir, { recursive: true });
20918
+ const p = path22.join(dir, name);
20919
+ await fs23.writeFile(p, Buffer.from(b64, "base64"));
20641
20920
  return p;
20642
20921
  }
20643
20922
  function reportScene(log, s) {
@@ -20663,7 +20942,7 @@ async function runBlender(opts) {
20663
20942
  return serveLocalBlender({ port, log });
20664
20943
  }
20665
20944
  if (sub === "mcp") {
20666
- const { runBlenderMcp } = await import("./blender-mcp-FQJFZETR.js");
20945
+ const { runBlenderMcp } = await import("./blender-mcp-PWOUSXK4.js");
20667
20946
  return runBlenderMcp();
20668
20947
  }
20669
20948
  if (sub === "seat") {
@@ -20721,7 +21000,7 @@ async function runBlender(opts) {
20721
21000
  log.plain(rest.join("\n"));
20722
21001
  return 1;
20723
21002
  }
20724
- const outDir = path21.resolve(opts.outDir ?? DEFAULT_OUT_DIR);
21003
+ const outDir = path22.resolve(opts.outDir ?? DEFAULT_OUT_DIR);
20725
21004
  const mode = opts.mode;
20726
21005
  if (mode !== void 0 && !isRenderMode(mode)) {
20727
21006
  log.error(`Unknown --mode ${mode}. Use one of: ${RENDER_MODES.join(", ")}.`);
@@ -20761,14 +21040,14 @@ async function runBlender(opts) {
20761
21040
  return 0;
20762
21041
  }
20763
21042
  case "export": {
20764
- const target = opts.out ?? path21.join(outDir, "scene.glb");
21043
+ const target = opts.out ?? path22.join(outDir, "scene.glb");
20765
21044
  const r = await blenderCall(base, "/export", { path: "/tmp/genex-export.glb" });
20766
21045
  if (!r.glbBase64) {
20767
21046
  log.error(`/export answered with no GLB bytes${r.uploaded ? " (it was uploaded, not inlined)" : ""}`);
20768
21047
  return 1;
20769
21048
  }
20770
- await fs22.mkdir(path21.dirname(target), { recursive: true });
20771
- await fs22.writeFile(target, Buffer.from(r.glbBase64, "base64"));
21049
+ await fs23.mkdir(path22.dirname(target), { recursive: true });
21050
+ await fs23.writeFile(target, Buffer.from(r.glbBase64, "base64"));
20772
21051
  log.success(`Exported ${r.bytes ?? 0} bytes`);
20773
21052
  log.plain(` ${c.cyan(target)}`);
20774
21053
  return 0;
@@ -20801,12 +21080,12 @@ async function runBlender(opts) {
20801
21080
  return 1;
20802
21081
  }
20803
21082
  try {
20804
- script = await fs22.readFile(opts.input, "utf8");
21083
+ script = await fs23.readFile(opts.input, "utf8");
20805
21084
  } catch {
20806
21085
  log.error(`Can't read ${opts.input}`);
20807
21086
  return 1;
20808
21087
  }
20809
- label = path21.basename(opts.input);
21088
+ label = path22.basename(opts.input);
20810
21089
  }
20811
21090
  const r = await blenderCall(base, "/exec", { script, ...mode ? { mode } : {}, sheet: { formats: SHEET_FORMATS } });
20812
21091
  if (r.stdout?.trim()) log.plain(r.stdout.trimEnd());
@@ -20925,9 +21204,9 @@ print(f"castle: {n} objects")
20925
21204
  `;
20926
21205
 
20927
21206
  // src/commands/asset-new.ts
20928
- import fs23 from "fs";
21207
+ import fs24 from "fs";
20929
21208
  import fsp from "fs/promises";
20930
- import path22 from "path";
21209
+ import path23 from "path";
20931
21210
  import { pathToFileURL } from "url";
20932
21211
  var EXTRA_FILES = [
20933
21212
  "genex-asset.example.json",
@@ -21021,7 +21300,7 @@ Every milestone ends with \`npm run verify\`, then \`npx genex preview --no-buil
21021
21300
  }
21022
21301
  async function runAssetNew(options) {
21023
21302
  const log = createLogger();
21024
- const cwd = options.dir ? path22.resolve(options.dir) : process.cwd();
21303
+ const cwd = options.dir ? path23.resolve(options.dir) : process.cwd();
21025
21304
  const slug = options.assetSlug;
21026
21305
  if (!slug) {
21027
21306
  log.error('Usage: genex asset new <slug> [--title "<Name>"] [--dims WxDxH] [--tri-band LOW-HIGH] [--holder "<your name>"] [--out <dir>]');
@@ -21031,14 +21310,14 @@ async function runAssetNew(options) {
21031
21310
  log.error(`The slug must be kebab-case (letters, digits, dashes), got: ${slug}`);
21032
21311
  return 1;
21033
21312
  }
21034
- const templateDir = path22.join(getTemplatesDir(), "asset-viewer");
21035
- if (!fs23.existsSync(templateDir)) {
21313
+ const templateDir = path23.join(getTemplatesDir(), "asset-viewer");
21314
+ if (!fs24.existsSync(templateDir)) {
21036
21315
  log.error(`Vendored asset-viewer template not found at ${templateDir} \u2014 reinstall the CLI.`);
21037
21316
  return 1;
21038
21317
  }
21039
- const manifestTools = await import(pathToFileURL(path22.join(templateDir, "tools", "emit-manifest.mjs")).href);
21318
+ const manifestTools = await import(pathToFileURL(path23.join(templateDir, "tools", "emit-manifest.mjs")).href);
21040
21319
  const { SHARED_FILES, PARITY_FILENAME, hashSharedFiles, pascalCase } = manifestTools;
21041
- const lockPath = path22.join(templateDir, "shared-files.sha256.json");
21320
+ const lockPath = path23.join(templateDir, "shared-files.sha256.json");
21042
21321
  const lock = JSON.parse(await fsp.readFile(lockPath, "utf8"));
21043
21322
  const actual = hashSharedFiles(templateDir);
21044
21323
  const drifted = SHARED_FILES.filter((rel) => lock.files[rel] !== actual[rel]);
@@ -21051,8 +21330,8 @@ async function runAssetNew(options) {
21051
21330
  const triBand = parseBand(options.triBand ?? "500-8000");
21052
21331
  const holder = options.holder?.trim() || HOLDER_PLACEHOLDER;
21053
21332
  const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
21054
- const outDir = path22.resolve(cwd, options.out ?? slug);
21055
- if (fs23.existsSync(outDir) && fs23.readdirSync(outDir).length > 0 && !options.force) {
21333
+ const outDir = path23.resolve(cwd, options.out ?? slug);
21334
+ if (fs24.existsSync(outDir) && fs24.readdirSync(outDir).length > 0 && !options.force) {
21056
21335
  log.error(`${outDir} already holds files. Nothing was touched \u2014 pass --force to write into it anyway.`);
21057
21336
  return 1;
21058
21337
  }
@@ -21080,24 +21359,24 @@ async function runAssetNew(options) {
21080
21359
  };
21081
21360
  await fsp.mkdir(outDir, { recursive: true });
21082
21361
  for (const rel of [...SHARED_FILES, ...EXTRA_FILES]) {
21083
- const to = path22.join(outDir, rel);
21084
- await fsp.mkdir(path22.dirname(to), { recursive: true });
21085
- await fsp.copyFile(path22.join(templateDir, rel), to);
21362
+ const to = path23.join(outDir, rel);
21363
+ await fsp.mkdir(path23.dirname(to), { recursive: true });
21364
+ await fsp.copyFile(path23.join(templateDir, rel), to);
21086
21365
  }
21087
- const pkg = fillTemplate(await fsp.readFile(path22.join(templateDir, "package.json"), "utf8"), {
21366
+ const pkg = fillTemplate(await fsp.readFile(path23.join(templateDir, "package.json"), "utf8"), {
21088
21367
  slug,
21089
21368
  name,
21090
21369
  version
21091
21370
  });
21092
- await fsp.writeFile(path22.join(outDir, "package.json"), pkg, "utf8");
21093
- await fsp.writeFile(path22.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
21094
- await fsp.writeFile(path22.join(outDir, ".gitignore"), GITIGNORE, "utf8");
21371
+ await fsp.writeFile(path23.join(outDir, "package.json"), pkg, "utf8");
21372
+ await fsp.writeFile(path23.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
21373
+ await fsp.writeFile(path23.join(outDir, ".gitignore"), GITIGNORE, "utf8");
21095
21374
  await fsp.writeFile(
21096
- path22.join(outDir, "DESIGN.md"),
21375
+ path23.join(outDir, "DESIGN.md"),
21097
21376
  designDoc({ name, slug, sizeMeters, triBand, holder }),
21098
21377
  "utf8"
21099
21378
  );
21100
- const placeholder = await fsp.readFile(path22.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
21379
+ const placeholder = await fsp.readFile(path23.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
21101
21380
  const seeded = seedAssetSource(placeholder, {
21102
21381
  slug,
21103
21382
  name,
@@ -21108,8 +21387,8 @@ async function runAssetNew(options) {
21108
21387
  pascalCase
21109
21388
  });
21110
21389
  const assetFilename = `${slug.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase())}.ts`;
21111
- await fsp.mkdir(path22.join(outDir, "src", "asset"), { recursive: true });
21112
- await fsp.writeFile(path22.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
21390
+ await fsp.mkdir(path23.join(outDir, "src", "asset"), { recursive: true });
21391
+ await fsp.writeFile(path23.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
21113
21392
  const copied = hashSharedFiles(outDir);
21114
21393
  const mismatched = SHARED_FILES.filter((rel) => copied[rel] !== lock.files[rel]);
21115
21394
  if (mismatched.length) {
@@ -21117,7 +21396,7 @@ async function runAssetNew(options) {
21117
21396
  return 1;
21118
21397
  }
21119
21398
  await fsp.writeFile(
21120
- path22.join(outDir, PARITY_FILENAME),
21399
+ path23.join(outDir, PARITY_FILENAME),
21121
21400
  JSON.stringify(
21122
21401
  {
21123
21402
  note: "Recorded by `genex asset new`. Gate G13 recomputes these; do not hand-edit the viewer.",
@@ -21161,11 +21440,11 @@ async function runAssetNew(options) {
21161
21440
  }
21162
21441
 
21163
21442
  // src/commands/tools.ts
21164
- import path25 from "path";
21443
+ import path26 from "path";
21165
21444
 
21166
21445
  // src/lib/local-install.ts
21167
- import fs24 from "fs/promises";
21168
- import path23 from "path";
21446
+ import fs25 from "fs/promises";
21447
+ import path24 from "path";
21169
21448
  import { spawn as spawn4 } from "child_process";
21170
21449
  var CLI_PACKAGE = "@genex-ai/cli-demo";
21171
21450
  var FULL_NAME_FALLBACK = `npx ${CLI_PACKAGE}@${CLI_CHANNEL}`;
@@ -21178,17 +21457,17 @@ var LOCKFILES = [
21178
21457
  ];
21179
21458
  async function exists4(p) {
21180
21459
  try {
21181
- await fs24.access(p);
21460
+ await fs25.access(p);
21182
21461
  return true;
21183
21462
  } catch {
21184
21463
  return false;
21185
21464
  }
21186
21465
  }
21187
21466
  async function detectPackageManager(cwd) {
21188
- let dir = path23.resolve(cwd);
21467
+ let dir = path24.resolve(cwd);
21189
21468
  for (; ; ) {
21190
21469
  try {
21191
- const raw = await fs24.readFile(path23.join(dir, "package.json"), "utf8");
21470
+ const raw = await fs25.readFile(path24.join(dir, "package.json"), "utf8");
21192
21471
  const pm = JSON.parse(raw).packageManager;
21193
21472
  if (typeof pm === "string") {
21194
21473
  const name = pm.split("@")[0];
@@ -21197,18 +21476,18 @@ async function detectPackageManager(cwd) {
21197
21476
  } catch {
21198
21477
  }
21199
21478
  for (const [file, pm] of LOCKFILES) {
21200
- if (await exists4(path23.join(dir, file))) return pm;
21479
+ if (await exists4(path24.join(dir, file))) return pm;
21201
21480
  }
21202
- const parent = path23.dirname(dir);
21481
+ const parent = path24.dirname(dir);
21203
21482
  if (parent === dir) return "npm";
21204
21483
  dir = parent;
21205
21484
  }
21206
21485
  }
21207
21486
  async function findLocalCli(cwd) {
21208
- let dir = path23.resolve(cwd);
21487
+ let dir = path24.resolve(cwd);
21209
21488
  for (; ; ) {
21210
- if (await exists4(path23.join(dir, "node_modules", CLI_PACKAGE, "package.json"))) return dir;
21211
- const parent = path23.dirname(dir);
21489
+ if (await exists4(path24.join(dir, "node_modules", CLI_PACKAGE, "package.json"))) return dir;
21490
+ const parent = path24.dirname(dir);
21212
21491
  if (parent === dir) return null;
21213
21492
  dir = parent;
21214
21493
  }
@@ -21226,7 +21505,7 @@ function installArgs(pm, spec) {
21226
21505
  }
21227
21506
  }
21228
21507
  function manifestName(cwd) {
21229
- const slug = path23.basename(path23.resolve(cwd)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
21508
+ const slug = path24.basename(path24.resolve(cwd)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
21230
21509
  return slug || "genex-tools-workspace";
21231
21510
  }
21232
21511
  function isSourceRun(moduleUrl = import.meta.url) {
@@ -21284,10 +21563,10 @@ async function ensureLocalCli(log, cwd = process.cwd(), opts = {}) {
21284
21563
  return;
21285
21564
  }
21286
21565
  const pm = await detectPackageManager(cwd);
21287
- const hadManifest = await exists4(path23.join(cwd, "package.json"));
21566
+ const hadManifest = await exists4(path24.join(cwd, "package.json"));
21288
21567
  if (!hadManifest) {
21289
- await fs24.writeFile(
21290
- path23.join(cwd, "package.json"),
21568
+ await fs25.writeFile(
21569
+ path24.join(cwd, "package.json"),
21291
21570
  JSON.stringify({ name: manifestName(cwd), private: true }, null, 2) + "\n"
21292
21571
  );
21293
21572
  await ensureIgnored(cwd, "node_modules/");
@@ -21307,20 +21586,20 @@ async function ensureLocalCli(log, cwd = process.cwd(), opts = {}) {
21307
21586
  }
21308
21587
  }
21309
21588
  async function ensureIgnored(dir, entry) {
21310
- const file = path23.join(dir, ".gitignore");
21589
+ const file = path24.join(dir, ".gitignore");
21311
21590
  let content = "";
21312
21591
  try {
21313
- content = await fs24.readFile(file, "utf8");
21592
+ content = await fs25.readFile(file, "utf8");
21314
21593
  } catch {
21315
21594
  }
21316
21595
  if (content.split("\n").some((l) => l.trim() === entry)) return;
21317
21596
  let next = content;
21318
21597
  if (next.length > 0 && !next.endsWith("\n")) next += "\n";
21319
- await fs24.writeFile(file, next + entry + "\n");
21598
+ await fs25.writeFile(file, next + entry + "\n");
21320
21599
  }
21321
21600
 
21322
21601
  // src/commands/doctor.ts
21323
- import path24 from "path";
21602
+ import path25 from "path";
21324
21603
  var LANE_ORDER = [
21325
21604
  "model",
21326
21605
  "image",
@@ -21601,7 +21880,7 @@ async function fetchLegalStatus(apiUrl, token) {
21601
21880
  }
21602
21881
  async function firstSkillsMarker() {
21603
21882
  for (const target of resolveAgentTargets()) {
21604
- const marker = await readSkillsMarker(path24.join(target.baseDir, "skills"));
21883
+ const marker = await readSkillsMarker(path25.join(target.baseDir, "skills"));
21605
21884
  if (marker) return marker;
21606
21885
  }
21607
21886
  return null;
@@ -21640,8 +21919,8 @@ async function runTools(opts) {
21640
21919
  let totalNew = 0;
21641
21920
  let totalUpdated = 0;
21642
21921
  for (const t of targets) {
21643
- const dest = path25.join(t.baseDir, "skills");
21644
- const { copied, updated } = await copyTemplates(path25.join(templatesDir, "skills"), dest, {
21922
+ const dest = path26.join(t.baseDir, "skills");
21923
+ const { copied, updated } = await copyTemplates(path26.join(templatesDir, "skills"), dest, {
21645
21924
  filter: (rel) => skillFamilyFilter("tools")(`skills/${rel}`)
21646
21925
  });
21647
21926
  await pruneRemovedSkills(dest, log);
@@ -21702,7 +21981,7 @@ async function runTools(opts) {
21702
21981
  // src/lib/costs.ts
21703
21982
  var TYPICAL_CREDITS = {
21704
21983
  model: 35,
21705
- // 46 from a reference image
21984
+ // 46 from a reference image; --texture standard 23, --no-texture 12, --geometry detailed 58, --quad 41, --low-poly 46, --parts 58 quote off the estimate
21706
21985
  image: 4,
21707
21986
  // gpt-image-2 tiers (--transparent, --quality high, --edit, 4K) quote higher
21708
21987
  texture: 5,
@@ -21717,17 +21996,22 @@ var TYPICAL_CREDITS = {
21717
21996
  model_rig: 29,
21718
21997
  model_animation: 12,
21719
21998
  // PER CLIP — `--preset walk,run` quotes ceil(2 × 10¢ × 1.15)
21720
- character: 58,
21999
+ character: 64,
22000
+ // one-shot on Meshy 7 Ultra at 4k with the controller pack; `genex creature` (no pack) 46, --no-ultra 58, --texture 8k 69
21721
22001
  character_concept: 32,
21722
22002
  character_preview: 41,
22003
+ // Meshy 7 Ultra at 4k; --no-ultra 35, --texture 8k 46
21723
22004
  character_finalize: 29,
21724
22005
  character_animation: 6,
21725
22006
  character_motion: 46,
21726
22007
  // one text-route clip; other routes are priced off their own cost
21727
- character_rerig: 12
22008
+ character_rerig: 12,
22009
+ character_import: 18
22010
+ // Uthana auto-rig of your own GLB; the upload itself is free
21728
22011
  };
21729
22012
 
21730
22013
  // src/index.ts
22014
+ var TEXTURE_FLAG_VALUES = ["standard", "detailed", "none", "2k", "4k", "8k"];
21731
22015
  var MODEL_SUB_SET = new Set(MODEL_SUBCOMMANDS);
21732
22016
  var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "music", "voice", "texture", "image", "video"]);
21733
22017
  var HELP = `${c.bold("genex")} \u2014 set up your project's agent workspace, authorize, and publish 3D games.
@@ -21804,7 +22088,11 @@ ${c.bold("Usage")}
21804
22088
  default, and --no-resellable turns it back off.
21805
22089
  genex model "<prompt>" [options] Generate a 3D model (GLB); prints a public asset URL.
21806
22090
  --image <path|url> builds it FROM a reference
21807
- image instead (prompt optional).
22091
+ image instead (prompt optional). Quality knobs:
22092
+ --texture standard|detailed|none, --geometry
22093
+ detailed, --quad, --low-poly, --parts,
22094
+ --face-limit <n>, --auto-size (each is priced
22095
+ in the quote).
21808
22096
  genex model segment <id> Split a generated model into NAMED parts \u2014
21809
22097
  one GLB, parts addressable by name (doors,
21810
22098
  magazines, turrets, destructibles).
@@ -21814,6 +22102,10 @@ ${c.bold("Usage")}
21814
22102
  avian|serpentine|aquatic). Auto-detects the
21815
22103
  plan; --type picks it explicitly. Unriggable
21816
22104
  meshes are refused and auto-refunded.
22105
+ genex model import <file.glb> Bring in a mesh you already have (a Blender
22106
+ export, a bought asset) as a model of yours \u2014
22107
+ free, \u226464 MB \u2014 so rig/animate/segment, blender
22108
+ import and "Use in game" all work on it.
21817
22109
  genex model animate <rig-id> --preset walk[,run,\u2026]
21818
22110
  Retarget ready-made motion clips onto a
21819
22111
  'model rig' result (billed per clip).
@@ -21853,6 +22145,12 @@ ${c.bold("Usage")}
21853
22145
  can take new animations. With no id it adopts the
21854
22146
  body this game already wears. Free \u2014 it copies,
21855
22147
  it does not generate.
22148
+ genex character import <file.glb> Your own humanoid mesh, auto-rigged by Uthana
22149
+ (finger joints included; --no-fingers skips
22150
+ them; --height <m>). \u226430 MB, biped, T- or
22151
+ A-pose. Then 'character animate <id>
22152
+ --locomotion' \u2014 the Meshy catalog and pack do
22153
+ not apply to a Uthana rig.
21856
22154
  genex creature "<desc>" One-shot rigged enemy/creature (biped-shaped
21857
22155
  bodies only \u2014 Meshy rig limit): model \u2192 rig \u2192
21858
22156
  bind library clips via --animation. No approval
@@ -21914,6 +22212,14 @@ ${c.bold("Options for the generators (`model` `sfx` `music` `voice` `texture` `i
21914
22212
  --image <path|url> (model) build the model FROM this reference image \u2014 a
21915
22213
  local file (\u22644 MB, inlined) or a previous generation's
21916
22214
  asset URL. The prompt becomes optional.
22215
+ --texture <tier> (model) standard | detailed (default) | none \u2014 detailed is
22216
+ +10 credits over standard, none is geometry only.
22217
+ --geometry <tier> (model) standard (default) | detailed (+20 credits, hero pieces).
22218
+ --quad (model) quad-dominant mesh (+5; face limit \u2264150000).
22219
+ --low-poly (model) smart low-poly topology (+10) \u2014 game-ready meshes.
22220
+ --parts (model) separated, named parts at generation (+20).
22221
+ --face-limit <n> (model) cap on the raw mesh, 1000-2000000 (default 150000).
22222
+ --auto-size (model) scale to real-world metres by AI estimate.
21917
22223
  --granularity <g> (model segment) part granularity: simple | balanced |
21918
22224
  detailed (default balanced).
21919
22225
  --type <plan> (model rig) body plan: biped | quadruped | hexapod |
@@ -22060,13 +22366,23 @@ ${c.bold("Options for `character` / `animations search`")}
22060
22366
  ranked action IDs instead of choosing silently.
22061
22367
  --candidate <1|2|3> (character preview) selected concept candidate.
22062
22368
  --user-approved Confirm the user explicitly approved the candidate/preview.
22063
- --approve-remesh 10000
22064
- (character finalize) approve the separate 10,000-face rigging copy.
22065
- --direct-text Compatibility path: skip concept/preview approval and generate
22369
+ --approve-remesh <faces>
22370
+ (character finalize) approve the separate rigging copy at this
22371
+ face budget, 10000-100000 (10000 for a crowd body, 30000+ for a
22372
+ hero). The number IS the approval.
22373
+ --direct-text One-shot path: skip concept/preview approval and generate
22066
22374
  directly from text (defaults to a 10,000-face target).
22067
22375
  --no-controller-pack (character --direct-text only) skip the validated locomotion pack.
22068
22376
  --height <meters> (character) target character height, 0.5-3 meters.
22069
- --polycount <count> (character --direct-text only) target 10000-100000 polygons.
22377
+ --polycount <count> (character --direct-text / creature) target 10000-100000 polygons.
22378
+ --texture <res> (character/creature/preview) texture size 2k | 4k (default) | 8k
22379
+ (+5 credits). Meshy 7 on every lane.
22380
+ --no-ultra (character/creature/preview) skip Meshy Ultra (\u22125 credits, less
22381
+ surface detail).
22382
+ --pose <a-pose|t-pose>
22383
+ (character --direct-text / creature) preferred rest pose; the
22384
+ other stays the QA fallback.
22385
+ --no-fingers (character import) skip finger joints in the Uthana rig.
22070
22386
  --no-wait (character/preview/finalize/animate) enqueue and return a generation id.
22071
22387
  --json Emit one machine-readable workflow result object.
22072
22388
  --action <id|query> (character animate) action to add; repeatable.
@@ -22324,6 +22640,13 @@ function parseArgs(argv) {
22324
22640
  "--image",
22325
22641
  "--granularity",
22326
22642
  "--preset",
22643
+ // Quality knobs (2026-09-06): `--texture` is shared by `model`
22644
+ // (standard|detailed|none) and the character lanes (2k|4k|8k); each command
22645
+ // re-validates its own vocabulary client-side.
22646
+ "--texture",
22647
+ "--geometry",
22648
+ "--face-limit",
22649
+ "--pose",
22327
22650
  // `genex blender` (M1 spike): which shading the contact sheet uses.
22328
22651
  "--mode",
22329
22652
  // `genex ui` string flags (the numeric ones come from UI_NUMBER_FLAGS).
@@ -22407,6 +22730,24 @@ function parseArgs(argv) {
22407
22730
  case "--no-controller-pack":
22408
22731
  parsed.options.controllerPack = false;
22409
22732
  break;
22733
+ case "--no-fingers":
22734
+ parsed.options.fingers = false;
22735
+ break;
22736
+ case "--no-ultra":
22737
+ parsed.options.ultra = false;
22738
+ break;
22739
+ case "--quad":
22740
+ parsed.options.quad = true;
22741
+ break;
22742
+ case "--low-poly":
22743
+ parsed.options.lowPoly = true;
22744
+ break;
22745
+ case "--parts":
22746
+ parsed.options.parts = true;
22747
+ break;
22748
+ case "--auto-size":
22749
+ parsed.options.autoSize = true;
22750
+ break;
22410
22751
  case "--user-approved":
22411
22752
  parsed.options.userApproved = true;
22412
22753
  break;
@@ -22527,7 +22868,9 @@ function parseArgs(argv) {
22527
22868
  parsed.options.conceptId = arg;
22528
22869
  } else if (parsed.options.name === "finalize" && !parsed.options.previewId) {
22529
22870
  parsed.options.previewId = arg;
22530
- } else if (!["animate", "motions", "preview", "finalize", "adopt"].includes(parsed.options.name ?? "")) {
22871
+ } else if (parsed.options.name === "import" && !parsed.options.importPath) {
22872
+ parsed.options.importPath = arg;
22873
+ } else if (!["animate", "motions", "preview", "finalize", "adopt", "import"].includes(parsed.options.name ?? "")) {
22531
22874
  parsed.options.name = `${parsed.options.name} ${arg}`;
22532
22875
  } else {
22533
22876
  parsed.error = `Unexpected argument: ${arg}`;
@@ -22678,8 +23021,8 @@ function applyValueFlag(options, flag, value) {
22678
23021
  }
22679
23022
  case "--approve-remesh": {
22680
23023
  const n = Number(value);
22681
- if (!Number.isInteger(n) || n !== 1e4) {
22682
- throw new Error(`Invalid --approve-remesh value: ${value} (expected exactly 10000)`);
23024
+ if (!Number.isInteger(n) || n < REMESH_TARGET_MIN || n > REMESH_TARGET_MAX) {
23025
+ throw new Error(`Invalid --approve-remesh value: ${value} (expected ${REMESH_TARGET_MIN}-${REMESH_TARGET_MAX} faces)`);
22683
23026
  }
22684
23027
  options.approveRemesh = n;
22685
23028
  break;
@@ -22847,6 +23190,32 @@ function applyValueFlag(options, flag, value) {
22847
23190
  case "--image":
22848
23191
  options.imageUrl = value;
22849
23192
  break;
23193
+ case "--texture":
23194
+ if (!TEXTURE_FLAG_VALUES.includes(value)) {
23195
+ throw new Error(`Invalid --texture value: ${value} (model: standard|detailed|none; character: 2k|4k|8k)`);
23196
+ }
23197
+ options.texture = value;
23198
+ break;
23199
+ case "--geometry":
23200
+ if (value !== "standard" && value !== "detailed") {
23201
+ throw new Error(`Invalid --geometry value: ${value} (expected standard|detailed)`);
23202
+ }
23203
+ options.geometry = value;
23204
+ break;
23205
+ case "--face-limit": {
23206
+ const n = Number(value);
23207
+ if (!Number.isInteger(n) || n < 1e3 || n > 2e6) {
23208
+ throw new Error(`Invalid --face-limit value: ${value} (expected 1000-2000000)`);
23209
+ }
23210
+ options.faceLimit = n;
23211
+ break;
23212
+ }
23213
+ case "--pose":
23214
+ if (value !== "a-pose" && value !== "t-pose") {
23215
+ throw new Error(`Invalid --pose value: ${value} (expected a-pose|t-pose)`);
23216
+ }
23217
+ options.pose = value;
23218
+ break;
22850
23219
  case "--granularity":
22851
23220
  options.granularity = value;
22852
23221
  break;
@@ -22966,6 +23335,7 @@ async function main() {
22966
23335
  if (parsed.command === "model" && MODEL_SUB_SET.has(parsed.options.name ?? "")) {
22967
23336
  if (parsed.options.name === "segment") await runModelSegment(parsed.options);
22968
23337
  else if (parsed.options.name === "rig") await runModelRig(parsed.options);
23338
+ else if (parsed.options.name === "import") await runModelImport(parsed.options);
22969
23339
  else await runModelAnimate(parsed.options);
22970
23340
  return;
22971
23341
  }
@@ -23031,6 +23401,8 @@ async function main() {
23031
23401
  await runCharacterFinalize(parsed.options);
23032
23402
  } else if (parsed.options.name === "adopt") {
23033
23403
  await runCharacterAdopt(parsed.options);
23404
+ } else if (parsed.options.name === "import") {
23405
+ await runCharacterImport(parsed.options);
23034
23406
  } else {
23035
23407
  await runCharacter({ ...parsed.options, prompt: parsed.options.name });
23036
23408
  }