@genex-ai/cli-demo 1.25.0-dev.616 → 1.25.1-dev.617

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
@@ -5774,11 +5774,87 @@ async function promoteBuild(apiUrl, projectId, token, log) {
5774
5774
  }
5775
5775
 
5776
5776
  // src/lib/detect-features.ts
5777
+ import fs16 from "fs/promises";
5778
+ import path15 from "path";
5779
+
5780
+ // src/lib/download-assets.ts
5777
5781
  import fs15 from "fs/promises";
5778
5782
  import path14 from "path";
5783
+ var RUNG_ROLE = /@\d+$/;
5784
+ function isPrimaryRole(role) {
5785
+ return !RUNG_ROLE.test(role);
5786
+ }
5787
+ function primaryFiles(files) {
5788
+ return files.filter((f) => isPrimaryRole(f.role));
5789
+ }
5790
+ function localAssetStem(kind, prompt, id) {
5791
+ const slug = prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
5792
+ return `${slug || kind}-${id.slice(0, 8)}`;
5793
+ }
5794
+ function localAssetName(kind, prompt, id, file, multi) {
5795
+ const base = localAssetStem(kind, prompt, id);
5796
+ const role = multi ? `-${file.role.replace(/^(texture|image|character|model|model_anim|model-anim)-/, "")}` : "";
5797
+ const ext = file.ext.replace(/^\./, "") || "bin";
5798
+ return `${base}${role}.${ext}`;
5799
+ }
5800
+ async function downloadAssets(files, opts) {
5801
+ const wanted = primaryFiles(files);
5802
+ const result = { saved: [], failures: [] };
5803
+ if (wanted.length === 0) return result;
5804
+ try {
5805
+ await fs15.mkdir(opts.outDir, { recursive: true });
5806
+ } catch (err) {
5807
+ result.failures.push(
5808
+ `couldn't create ${opts.outDir} (${err instanceof Error ? err.message : String(err)})`
5809
+ );
5810
+ return result;
5811
+ }
5812
+ const multi = wanted.length > 1;
5813
+ for (const file of wanted) {
5814
+ const dest = path14.join(opts.outDir, localAssetName(opts.kind, opts.prompt, opts.id, file, multi));
5815
+ try {
5816
+ const res = await fetch(file.url);
5817
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
5818
+ const buf = Buffer.from(await res.arrayBuffer());
5819
+ await fs15.writeFile(dest, buf);
5820
+ result.saved.push({ role: file.role, path: dest, url: file.url, bytes: buf.byteLength });
5821
+ } catch (err) {
5822
+ result.failures.push(
5823
+ `${file.role}: ${err instanceof Error ? err.message : String(err)} \u2014 the asset is still live at ${file.url}`
5824
+ );
5825
+ }
5826
+ }
5827
+ return result;
5828
+ }
5829
+ function formatBytes(bytes) {
5830
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
5831
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
5832
+ return `${bytes} B`;
5833
+ }
5834
+ var DEFAULT_ASSET_DIR = "./assets";
5835
+ function localDeliveryFor(mode, opts, prompt) {
5836
+ if (mode !== "tools" || opts.noDownload) return void 0;
5837
+ return { outDir: opts.outDir ?? DEFAULT_ASSET_DIR, prompt };
5838
+ }
5839
+ async function undeliveredFiles(files, opts) {
5840
+ const wanted = primaryFiles(files);
5841
+ const multi = wanted.length > 1;
5842
+ const missing = [];
5843
+ for (const file of wanted) {
5844
+ const dest = path14.join(opts.outDir, localAssetName(opts.kind, opts.prompt, opts.id, file, multi));
5845
+ try {
5846
+ await fs15.access(dest);
5847
+ } catch {
5848
+ missing.push(file);
5849
+ }
5850
+ }
5851
+ return missing;
5852
+ }
5853
+
5854
+ // src/lib/detect-features.ts
5779
5855
  async function detectEmbedSdkVersion(cwd = process.cwd()) {
5780
5856
  try {
5781
- const raw = await fs15.readFile(path14.join(cwd, "package.json"), "utf8");
5857
+ const raw = await fs16.readFile(path15.join(cwd, "package.json"), "utf8");
5782
5858
  const pkg = JSON.parse(raw);
5783
5859
  const version = pkg.dependencies?.["@genex-ai/embed-sdk"] ?? pkg.devDependencies?.["@genex-ai/embed-sdk"];
5784
5860
  return typeof version === "string" && version ? version : null;
@@ -5788,7 +5864,7 @@ async function detectEmbedSdkVersion(cwd = process.cwd()) {
5788
5864
  }
5789
5865
  async function detectMultiplayer(cwd = process.cwd()) {
5790
5866
  try {
5791
- const raw = await fs15.readFile(path14.join(cwd, "package.json"), "utf8");
5867
+ const raw = await fs16.readFile(path15.join(cwd, "package.json"), "utf8");
5792
5868
  const pkg = JSON.parse(raw);
5793
5869
  return Boolean(
5794
5870
  pkg.dependencies?.["@genex-ai/multiplayer"] ?? pkg.devDependencies?.["@genex-ai/multiplayer"]
@@ -5800,7 +5876,7 @@ async function detectMultiplayer(cwd = process.cwd()) {
5800
5876
  async function detectMatchmaking(log, cwd = process.cwd()) {
5801
5877
  let pkg;
5802
5878
  try {
5803
- pkg = JSON.parse(await fs15.readFile(path14.join(cwd, "package.json"), "utf8"));
5879
+ pkg = JSON.parse(await fs16.readFile(path15.join(cwd, "package.json"), "utf8"));
5804
5880
  } catch (err) {
5805
5881
  log.dim(` (skipping matchmaking \u2014 couldn't read package.json: ${String(err)})`);
5806
5882
  return null;
@@ -5818,15 +5894,15 @@ async function detectMatchmaking(log, cwd = process.cwd()) {
5818
5894
  var TOUCH_KIT_MARKERS = /controllers\/touch\/|controllers\/character\/touch-joystick|TouchJoystick|VirtualButton|DragZone|RotateOverlay/;
5819
5895
  async function detectMobileControls(cwd = process.cwd()) {
5820
5896
  try {
5821
- const raw = await fs15.readFile(path14.join(cwd, "package.json"), "utf8");
5897
+ const raw = await fs16.readFile(path15.join(cwd, "package.json"), "utf8");
5822
5898
  const pkg = JSON.parse(raw);
5823
5899
  if (pkg.genex?.mobileControls === true) return true;
5824
5900
  } catch {
5825
5901
  }
5826
- const srcDir = path14.join(cwd, "src");
5902
+ const srcDir = path15.join(cwd, "src");
5827
5903
  let entries;
5828
5904
  try {
5829
- entries = await fs15.readdir(srcDir, { recursive: true });
5905
+ entries = await fs16.readdir(srcDir, { recursive: true });
5830
5906
  } catch {
5831
5907
  return false;
5832
5908
  }
@@ -5834,7 +5910,7 @@ async function detectMobileControls(cwd = process.cwd()) {
5834
5910
  if (rel.includes("node_modules")) continue;
5835
5911
  if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
5836
5912
  try {
5837
- const content = await fs15.readFile(path14.join(srcDir, rel), "utf8");
5913
+ const content = await fs16.readFile(path15.join(srcDir, rel), "utf8");
5838
5914
  if (TOUCH_KIT_MARKERS.test(content)) return true;
5839
5915
  } catch {
5840
5916
  }
@@ -5843,10 +5919,10 @@ async function detectMobileControls(cwd = process.cwd()) {
5843
5919
  }
5844
5920
  var GAME_STATE_CALLS = /savePlayerState|saveWorldState|submitScore/;
5845
5921
  async function detectGameStateUsage(cwd = process.cwd()) {
5846
- const srcDir = path14.join(cwd, "src");
5922
+ const srcDir = path15.join(cwd, "src");
5847
5923
  let entries;
5848
5924
  try {
5849
- entries = await fs15.readdir(srcDir, { recursive: true });
5925
+ entries = await fs16.readdir(srcDir, { recursive: true });
5850
5926
  } catch {
5851
5927
  return false;
5852
5928
  }
@@ -5854,7 +5930,7 @@ async function detectGameStateUsage(cwd = process.cwd()) {
5854
5930
  if (rel.includes("node_modules")) continue;
5855
5931
  if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
5856
5932
  try {
5857
- const content = await fs15.readFile(path14.join(srcDir, rel), "utf8");
5933
+ const content = await fs16.readFile(path15.join(srcDir, rel), "utf8");
5858
5934
  if (GAME_STATE_CALLS.test(content)) return true;
5859
5935
  } catch {
5860
5936
  }
@@ -5904,20 +5980,20 @@ var LOOP_START = /\b(?:requestAnimationFrame|setAnimationLoop)\s*\((?!\s*null\b)
5904
5980
  var INIT_EMBED_CALL = /\binitEmbed\s*\(/;
5905
5981
  async function detectEmbedBoot(cwd = process.cwd()) {
5906
5982
  const found = { awaitsIdentity: [], callsInitEmbed: false, rendererAfterAwait: [], loopAfterAwait: [] };
5907
- const srcDir = path14.join(cwd, "src");
5983
+ const srcDir = path15.join(cwd, "src");
5908
5984
  let entries;
5909
5985
  try {
5910
- entries = await fs15.readdir(srcDir, { recursive: true });
5986
+ entries = await fs16.readdir(srcDir, { recursive: true });
5911
5987
  } catch {
5912
5988
  return found;
5913
5989
  }
5914
5990
  for (const nativeRel of entries) {
5915
5991
  if (nativeRel.includes("node_modules")) continue;
5916
- if (nativeRel.split(path14.sep)[0] === "controllers") continue;
5992
+ if (nativeRel.split(path15.sep)[0] === "controllers") continue;
5917
5993
  if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(nativeRel)) continue;
5918
- const raw = await fs15.readFile(path14.join(srcDir, nativeRel), "utf8").catch(() => "");
5994
+ const raw = await fs16.readFile(path15.join(srcDir, nativeRel), "utf8").catch(() => "");
5919
5995
  if (!raw) continue;
5920
- const rel = nativeRel.split(path14.sep).join("/");
5996
+ const rel = nativeRel.split(path15.sep).join("/");
5921
5997
  const content = blankComments(raw);
5922
5998
  if (INIT_EMBED_CALL.test(content)) found.callsInitEmbed = true;
5923
5999
  const awaits = [];
@@ -6012,19 +6088,19 @@ async function detectSurfaceScan(cwd = process.cwd()) {
6012
6088
  deferredAudioContext: [],
6013
6089
  usesThree: false
6014
6090
  };
6015
- const srcDir = path14.join(cwd, "src");
6091
+ const srcDir = path15.join(cwd, "src");
6016
6092
  let entries;
6017
6093
  try {
6018
- entries = await fs15.readdir(srcDir, { recursive: true });
6094
+ entries = await fs16.readdir(srcDir, { recursive: true });
6019
6095
  } catch {
6020
6096
  return found;
6021
6097
  }
6022
6098
  for (const nativeRel of entries) {
6023
6099
  if (nativeRel.includes("node_modules")) continue;
6024
6100
  if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(nativeRel)) continue;
6025
- const raw = await fs15.readFile(path14.join(srcDir, nativeRel), "utf8").catch(() => "");
6101
+ const raw = await fs16.readFile(path15.join(srcDir, nativeRel), "utf8").catch(() => "");
6026
6102
  if (!raw) continue;
6027
- const rel = nativeRel.split(path14.sep).join("/");
6103
+ const rel = nativeRel.split(path15.sep).join("/");
6028
6104
  const content = blankComments(raw);
6029
6105
  const repeatRe = /\.repeat\.set\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g;
6030
6106
  let m;
@@ -6116,28 +6192,28 @@ async function detectGenerationAudit(cwd = process.cwd()) {
6116
6192
  let haystack = "";
6117
6193
  const read = async (file) => {
6118
6194
  try {
6119
- const raw = await fs15.readFile(file, "utf8");
6120
- const ext = path14.extname(file).toLowerCase();
6195
+ const raw = await fs16.readFile(file, "utf8");
6196
+ const ext = path15.extname(file).toLowerCase();
6121
6197
  if (ext === ".txt") return;
6122
6198
  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;
6123
6199
  } catch {
6124
6200
  }
6125
6201
  };
6126
6202
  try {
6127
- for (const entry of await fs15.readdir(cwd, { withFileTypes: true })) {
6203
+ for (const entry of await fs16.readdir(cwd, { withFileTypes: true })) {
6128
6204
  if (entry.isFile() && /\.(ts|tsx|js|jsx|css|html|json)$/.test(entry.name)) {
6129
- await read(path14.join(cwd, entry.name));
6205
+ await read(path15.join(cwd, entry.name));
6130
6206
  }
6131
6207
  }
6132
6208
  } catch {
6133
6209
  }
6134
6210
  for (const sub of ["src", "public"]) {
6135
6211
  try {
6136
- const entries = await fs15.readdir(path14.join(cwd, sub), { recursive: true });
6212
+ const entries = await fs16.readdir(path15.join(cwd, sub), { recursive: true });
6137
6213
  for (const rel of entries) {
6138
6214
  if (rel.includes("node_modules")) continue;
6139
6215
  if (!/\.(ts|tsx|js|jsx|css|html|json|txt)$/.test(rel)) continue;
6140
- await read(path14.join(cwd, sub, rel));
6216
+ await read(path15.join(cwd, sub, rel));
6141
6217
  }
6142
6218
  } catch {
6143
6219
  }
@@ -6149,20 +6225,21 @@ async function detectGenerationAudit(cwd = process.cwd()) {
6149
6225
  });
6150
6226
  const now = Date.now();
6151
6227
  const stale = (e) => e.status === "queued" && now - Date.parse(e.queuedAt) > UNPICKED_AFTER_MS;
6228
+ const wired = (e) => haystack.includes(e.id) || haystack.includes(localAssetStem(e.kind, e.prompt, e.id));
6152
6229
  return {
6153
- unwired: audited.filter((e) => e.status === "completed" && !haystack.includes(e.id)).map(ref),
6154
- unpicked: audited.filter((e) => stale(e) && !haystack.includes(e.id)).map(ref),
6230
+ unwired: audited.filter((e) => e.status === "completed" && !wired(e)).map(ref),
6231
+ unpicked: audited.filter((e) => stale(e) && !wired(e)).map(ref),
6155
6232
  // Wired in but never picked up: the deterministic URL let the agent wire
6156
6233
  // the asset before it finished, so nobody ever learned whether it DID
6157
6234
  // finish — a provider-side failure (credits, moderation) leaves a dead URL
6158
6235
  // that renders as a silently missing asset. Kept separate from `unpicked`
6159
6236
  // because the fix is a status check, not wiring.
6160
- unresolved: audited.filter((e) => stale(e) && haystack.includes(e.id)).map(ref),
6237
+ unresolved: audited.filter((e) => stale(e) && wired(e)).map(ref),
6161
6238
  // Known-failed AND still wired: the worst case, and it must keep firing
6162
6239
  // AFTER the status check converges the ledger to "failed" (otherwise the
6163
6240
  // audit self-erases the moment the agent runs the recommended command and
6164
6241
  // the dead URL ships silently anyway).
6165
- deadWired: audited.filter((e) => e.status === "failed" && haystack.includes(e.id)).map(ref),
6242
+ deadWired: audited.filter((e) => e.status === "failed" && wired(e)).map(ref),
6166
6243
  // One regex covers the HTML/JSX attribute and the DOM-built input alike,
6167
6244
  // since `.type = "range"` contains `type = "range"`.
6168
6245
  musicNoSlider: audited.some((e) => e.kind === "music" && e.status === "completed") && !/type\s*=\s*["'`]range["'`]/.test(haystack)
@@ -6307,7 +6384,7 @@ async function borrowEvidence(meta, cwd) {
6307
6384
  } catch {
6308
6385
  return true;
6309
6386
  }
6310
- const gitConfig = await fs15.readFile(path14.join(cwd, ".git", "config"), "utf8").catch(() => "");
6387
+ const gitConfig = await fs16.readFile(path15.join(cwd, ".git", "config"), "utf8").catch(() => "");
6311
6388
  for (const m of gitConfig.matchAll(/url\s*=\s*(\S+)/g)) {
6312
6389
  try {
6313
6390
  const u = new URL(m[1]);
@@ -6315,7 +6392,7 @@ async function borrowEvidence(meta, cwd) {
6315
6392
  } catch {
6316
6393
  }
6317
6394
  }
6318
- const readme = await fs15.readFile(path14.join(cwd, "README.md"), "utf8").catch(() => "");
6395
+ const readme = await fs16.readFile(path15.join(cwd, "README.md"), "utf8").catch(() => "");
6319
6396
  return readme.includes(host) || /\b(upstream|originally by|ported from|borrowed from)\b/i.test(readme);
6320
6397
  }
6321
6398
 
@@ -6964,63 +7041,6 @@ function ceilingVerdict(input) {
6964
7041
  return { allow: true };
6965
7042
  }
6966
7043
 
6967
- // src/lib/download-assets.ts
6968
- import fs16 from "fs/promises";
6969
- import path15 from "path";
6970
- var RUNG_ROLE = /@\d+$/;
6971
- function isPrimaryRole(role) {
6972
- return !RUNG_ROLE.test(role);
6973
- }
6974
- function primaryFiles(files) {
6975
- return files.filter((f) => isPrimaryRole(f.role));
6976
- }
6977
- function localAssetName(kind, prompt, id, file, multi) {
6978
- const slug = prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
6979
- const base = `${slug || kind}-${id.slice(0, 8)}`;
6980
- const role = multi ? `-${file.role.replace(/^(texture|image|character|model|model_anim|model-anim)-/, "")}` : "";
6981
- const ext = file.ext.replace(/^\./, "") || "bin";
6982
- return `${base}${role}.${ext}`;
6983
- }
6984
- async function downloadAssets(files, opts) {
6985
- const wanted = primaryFiles(files);
6986
- const result = { saved: [], failures: [] };
6987
- if (wanted.length === 0) return result;
6988
- try {
6989
- await fs16.mkdir(opts.outDir, { recursive: true });
6990
- } catch (err) {
6991
- result.failures.push(
6992
- `couldn't create ${opts.outDir} (${err instanceof Error ? err.message : String(err)})`
6993
- );
6994
- return result;
6995
- }
6996
- const multi = wanted.length > 1;
6997
- for (const file of wanted) {
6998
- const dest = path15.join(opts.outDir, localAssetName(opts.kind, opts.prompt, opts.id, file, multi));
6999
- try {
7000
- const res = await fetch(file.url);
7001
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
7002
- const buf = Buffer.from(await res.arrayBuffer());
7003
- await fs16.writeFile(dest, buf);
7004
- result.saved.push({ role: file.role, path: dest, url: file.url, bytes: buf.byteLength });
7005
- } catch (err) {
7006
- result.failures.push(
7007
- `${file.role}: ${err instanceof Error ? err.message : String(err)} \u2014 the asset is still live at ${file.url}`
7008
- );
7009
- }
7010
- }
7011
- return result;
7012
- }
7013
- function formatBytes(bytes) {
7014
- if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
7015
- if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
7016
- return `${bytes} B`;
7017
- }
7018
- var DEFAULT_ASSET_DIR = "./assets";
7019
- function localDeliveryFor(mode, opts, prompt) {
7020
- if (mode !== "tools" || opts.noDownload) return void 0;
7021
- return { outDir: opts.outDir ?? DEFAULT_ASSET_DIR, prompt };
7022
- }
7023
-
7024
7044
  // src/lib/lanes.ts
7025
7045
  var LANE_CREDITS = /* @__PURE__ */ new Set(["ok", "exhausted", "unknown"]);
7026
7046
  async function fetchLanes(apiUrl, token, timeoutMs = 4e3) {
@@ -7276,6 +7296,15 @@ function applySkyboxGuard(prompt, raw) {
7276
7296
  return { prompt: guarded, guarded: true, nouns };
7277
7297
  }
7278
7298
  var MODEL_TEXTURE_TIERS = ["standard", "detailed", "none"];
7299
+ var LOW_POLY_FACE_LIMIT = { min: 1e3, max: 2e4 };
7300
+ var LOW_POLY_QUAD_FACE_LIMIT = { min: 500, max: 1e4 };
7301
+ var QUAD_FACE_LIMIT_MAX = 15e4;
7302
+ function faceLimitBand(o) {
7303
+ if (o.lowPoly && o.quad) return { ...LOW_POLY_QUAD_FACE_LIMIT, why: "for --low-poly with --quad" };
7304
+ if (o.lowPoly) return { ...LOW_POLY_FACE_LIMIT, why: "for --low-poly" };
7305
+ if (o.quad) return { min: 1e3, max: QUAD_FACE_LIMIT_MAX, why: "for --quad" };
7306
+ return { min: 1e3, max: 2e6, why: "for a raw mesh" };
7307
+ }
7279
7308
  function buildGenOptions(kind, opts) {
7280
7309
  const options = { ...opts.generationOptions };
7281
7310
  if (kind === "model" && opts.imageUrl) options.imageUrl = opts.imageUrl;
@@ -7286,7 +7315,15 @@ function buildGenOptions(kind, opts) {
7286
7315
  if (opts.quad) options.quad = true;
7287
7316
  if (opts.lowPoly) options.smartLowPoly = true;
7288
7317
  if (opts.parts) options.generateParts = true;
7289
- if (opts.faceLimit !== void 0) options.faceLimit = opts.faceLimit;
7318
+ if (opts.faceLimit !== void 0) {
7319
+ const band = faceLimitBand({ lowPoly: !!opts.lowPoly, quad: !!opts.quad });
7320
+ if (opts.faceLimit < band.min || opts.faceLimit > band.max) {
7321
+ throw new Error(
7322
+ `--face-limit ${opts.faceLimit} is outside the ${band.min}-${band.max} band ${band.why} \u2014 Tripo refuses anything else for that topology. Drop --face-limit to take ${band.max}, or name a value in the band.`
7323
+ );
7324
+ }
7325
+ options.faceLimit = opts.faceLimit;
7326
+ }
7290
7327
  if (opts.autoSize) options.autoSize = true;
7291
7328
  }
7292
7329
  if (kind === "texture" && opts.terrain) options.terrain = true;
@@ -7481,7 +7518,7 @@ async function runGenerate(kind, opts) {
7481
7518
  log.dim(` ${prompt}`);
7482
7519
  if (skyboxGuard?.guarded) log.dim(" \u21B3 environment-only guard applied (--raw to disable)");
7483
7520
  log.plain("");
7484
- {
7521
+ if (mode !== "tools") {
7485
7522
  const counts = unshippedCounts(await readLedgerRows());
7486
7523
  const verdict = ceilingVerdict({ kind, counts, config: ceilingConfig() });
7487
7524
  if (!verdict.allow) {
@@ -8326,6 +8363,7 @@ async function runModelAnimate(opts) {
8326
8363
  }
8327
8364
 
8328
8365
  // src/commands/wait.ts
8366
+ import path18 from "path";
8329
8367
  var TERMINAL2 = /* @__PURE__ */ new Set(["completed", "failed"]);
8330
8368
  async function runWait(opts) {
8331
8369
  if (opts.all) return runWaitAll(opts);
@@ -8448,8 +8486,26 @@ async function runWaitAll(opts) {
8448
8486
  if (e.status === "queued" && v?.status === "completed") landed.push({ kind: e.kind, view: v });
8449
8487
  rows.push(await toRow(e, v, cwd));
8450
8488
  }
8489
+ const delivered = [];
8490
+ if (!opts.noDownload && await workspaceMode(cwd) === "tools") {
8491
+ for (const e of ledger) {
8492
+ const v = views.get(e.id);
8493
+ if (v?.status !== "completed" || !v.files?.length) continue;
8494
+ const local = localDeliveryFor("tools", opts, e.prompt || e.kind);
8495
+ if (!local) continue;
8496
+ const outDir = path18.join(path18.relative(process.cwd(), cwd) || ".", local.outDir);
8497
+ const target = { kind: e.kind, prompt: local.prompt, id: e.id, outDir };
8498
+ const missing = await undeliveredFiles(v.files, target);
8499
+ if (missing.length === 0) continue;
8500
+ delivered.push({ kind: e.kind, id: e.id, result: await downloadAssets(missing, target) });
8501
+ }
8502
+ }
8451
8503
  if (opts.json) {
8452
- writeJson({ generations: rows });
8504
+ writeJson({
8505
+ generations: rows,
8506
+ // Additive: what THIS refresh wrote to disk. Absent when nothing was.
8507
+ ...delivered.length > 0 ? { saved: delivered.flatMap((d) => d.result.saved.map((f) => ({ id: d.id, kind: d.kind, ...f }))) } : {}
8508
+ });
8453
8509
  return;
8454
8510
  }
8455
8511
  log.plain(c.bold("genex wait --all"));
@@ -8484,6 +8540,14 @@ async function runWaitAll(opts) {
8484
8540
  if (count("running") + count("queued") > 0) {
8485
8541
  log.dim(` Attach to any single one with: ${c.cyan("npx genex wait <id>")}`);
8486
8542
  }
8543
+ if (delivered.length > 0) {
8544
+ log.plain("");
8545
+ log.plain(` ${c.bold("Picked up")} \u2014 finished generations that were not in your project yet:`);
8546
+ for (const d of delivered) {
8547
+ log.plain(` ${c.dim(`${d.kind} ${d.id}`)}`);
8548
+ reportLocalFiles(d.result, log, false);
8549
+ }
8550
+ }
8487
8551
  if (landed.length > 0) {
8488
8552
  log.plain("");
8489
8553
  log.plain(` ${c.bold("Just landed")} \u2014 how to use each one:`);
@@ -8525,7 +8589,7 @@ async function toRow(e, v, cwd) {
8525
8589
 
8526
8590
  // src/commands/controller.ts
8527
8591
  import fs20 from "fs/promises";
8528
- import path19 from "path";
8592
+ import path20 from "path";
8529
8593
 
8530
8594
  // ../../packages/meshy-animation-catalog/src/index.ts
8531
8595
  import { createHash } from "crypto";
@@ -17656,8 +17720,8 @@ function searchMeshyAnimations(query, options = {}) {
17656
17720
 
17657
17721
  // src/lib/anims.ts
17658
17722
  import fs19 from "fs/promises";
17659
- import path18 from "path";
17660
- var ANIMS_DEST = path18.join("public", "assets", "anims");
17723
+ import path19 from "path";
17724
+ var ANIMS_DEST = path19.join("public", "assets", "anims");
17661
17725
  var HIDDEN_TAG = "reference";
17662
17726
  async function runAnims(opts) {
17663
17727
  const log = createLogger({ quiet: opts.quiet });
@@ -17673,7 +17737,7 @@ async function runAnims(opts) {
17673
17737
  printCatalog(log, manifest, selectors);
17674
17738
  return;
17675
17739
  }
17676
- const controllerMarker = path18.join(root, "src", "controllers", "character");
17740
+ const controllerMarker = path19.join(root, "src", "controllers", "character");
17677
17741
  if (!await exists2(controllerMarker)) {
17678
17742
  log.error(
17679
17743
  `No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
@@ -17682,11 +17746,11 @@ async function runAnims(opts) {
17682
17746
  process.exitCode = 1;
17683
17747
  return;
17684
17748
  }
17685
- const destDir = path18.join(root, ANIMS_DEST);
17686
- const gameManifestPath = path18.join(destDir, "manifest.json");
17749
+ const destDir = path19.join(root, ANIMS_DEST);
17750
+ const gameManifestPath = path19.join(destDir, "manifest.json");
17687
17751
  if (opts.reset) {
17688
17752
  await fs19.rm(destDir, { recursive: true, force: true });
17689
- log.step(`Cleared ${c.cyan(ANIMS_DEST + path18.sep)} (--reset)`);
17753
+ log.step(`Cleared ${c.cyan(ANIMS_DEST + path19.sep)} (--reset)`);
17690
17754
  }
17691
17755
  if (selectors.length === 0) {
17692
17756
  const installed = await readGameManifest(gameManifestPath);
@@ -17724,7 +17788,7 @@ async function runAnims(opts) {
17724
17788
  }
17725
17789
  }
17726
17790
  const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
17727
- const cacheDir = path18.join(
17791
+ const cacheDir = path19.join(
17728
17792
  opts.cacheDir ?? getAnimsCacheDir(),
17729
17793
  `${manifest.library}-v${manifest.version}`
17730
17794
  );
@@ -17736,13 +17800,13 @@ async function runAnims(opts) {
17736
17800
  let addedBytes = 0;
17737
17801
  const failures = [];
17738
17802
  for (const entry of wanted) {
17739
- const dest = path18.join(destDir, entry.file);
17803
+ const dest = path19.join(destDir, entry.file);
17740
17804
  if (await hasSize(dest, entry.bytes)) {
17741
17805
  presentCount++;
17742
17806
  continue;
17743
17807
  }
17744
17808
  try {
17745
- const cached = path18.join(cacheDir, entry.file);
17809
+ const cached = path19.join(cacheDir, entry.file);
17746
17810
  if (!await hasSize(cached, entry.bytes)) {
17747
17811
  const res = await fetch(base + entry.file);
17748
17812
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
@@ -17752,7 +17816,7 @@ async function runAnims(opts) {
17752
17816
  await fs19.copyFile(cached, dest);
17753
17817
  installedCount++;
17754
17818
  addedBytes += entry.bytes;
17755
- log.dim(` ${path18.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
17819
+ log.dim(` ${path19.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
17756
17820
  } catch (err) {
17757
17821
  failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
17758
17822
  }
@@ -17774,7 +17838,7 @@ async function runAnims(opts) {
17774
17838
  if (presentCount > 0) parts.push(`${presentCount} already present`);
17775
17839
  if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
17776
17840
  log.success(
17777
- `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path18.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
17841
+ `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path19.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
17778
17842
  );
17779
17843
  for (const [selector, entries] of resolved) {
17780
17844
  const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
@@ -17806,7 +17870,7 @@ async function loadManifest(baseOverride) {
17806
17870
  }
17807
17871
  } catch {
17808
17872
  }
17809
- const snapshotPath = path18.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
17873
+ const snapshotPath = path19.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
17810
17874
  const manifest = JSON.parse(await fs19.readFile(snapshotPath, "utf8"));
17811
17875
  return { manifest, source: "snapshot" };
17812
17876
  }
@@ -18140,8 +18204,8 @@ var CONTROLLER_FILE_SETS = {
18140
18204
  ]
18141
18205
  }
18142
18206
  };
18143
- var CODE_DEST = path19.join("src", "controllers");
18144
- var ASSETS_DEST = path19.join("public", "assets");
18207
+ var CODE_DEST = path20.join("src", "controllers");
18208
+ var ASSETS_DEST = path20.join("public", "assets");
18145
18209
  async function runController(opts) {
18146
18210
  const log = createLogger({ quiet: opts.quiet });
18147
18211
  if (opts.kind?.trim() === "anims") {
@@ -18158,31 +18222,31 @@ async function runController(opts) {
18158
18222
  process.exitCode = 1;
18159
18223
  return;
18160
18224
  }
18161
- const srcDir = path19.join(getTemplatesDir(), "controllers");
18225
+ const srcDir = path20.join(getTemplatesDir(), "controllers");
18162
18226
  const root = opts.cwd ?? process.cwd();
18163
18227
  const set = CONTROLLER_FILE_SETS[kind];
18164
18228
  log.plain(c.bold(`genex controller ${kind}`));
18165
18229
  log.plain("");
18166
- log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path19.sep)}`);
18230
+ log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path20.sep)}`);
18167
18231
  const plan = [
18168
- ...set.code.map((rel) => ({ from: rel, rel: path19.join(CODE_DEST, rel) })),
18232
+ ...set.code.map((rel) => ({ from: rel, rel: path20.join(CODE_DEST, rel) })),
18169
18233
  ...set.assets.map((rel) => ({
18170
18234
  from: rel,
18171
- rel: path19.join(ASSETS_DEST, path19.basename(rel))
18235
+ rel: path20.join(ASSETS_DEST, path20.basename(rel))
18172
18236
  }))
18173
18237
  ];
18174
18238
  let copied = 0;
18175
18239
  let skipped = 0;
18176
18240
  try {
18177
18241
  for (const file of plan) {
18178
- const dest = path19.join(root, file.rel);
18242
+ const dest = path20.join(root, file.rel);
18179
18243
  if (!opts.force && await exists3(dest)) {
18180
18244
  skipped++;
18181
18245
  log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
18182
18246
  continue;
18183
18247
  }
18184
- await fs20.mkdir(path19.dirname(dest), { recursive: true });
18185
- await fs20.copyFile(path19.join(srcDir, file.from), dest);
18248
+ await fs20.mkdir(path20.dirname(dest), { recursive: true });
18249
+ await fs20.copyFile(path20.join(srcDir, file.from), dest);
18186
18250
  copied++;
18187
18251
  log.dim(` ${file.rel}`);
18188
18252
  }
@@ -18235,7 +18299,7 @@ async function runController(opts) {
18235
18299
  for (const line of set.sketch) {
18236
18300
  log.dim(` ${line}`);
18237
18301
  }
18238
- if (kind === "character" && !await exists3(path19.join(root, ASSETS_DEST, "meshy-character.json"))) {
18302
+ if (kind === "character" && !await exists3(path20.join(root, ASSETS_DEST, "meshy-character.json"))) {
18239
18303
  log.plain("");
18240
18304
  log.plain(
18241
18305
  ` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
@@ -18267,8 +18331,8 @@ async function installMeshyCharacterManifest(args) {
18267
18331
  throw new Error("The API returned an invalid Meshy character manifest.");
18268
18332
  }
18269
18333
  assertCompleteMeshyControllerPack(manifest);
18270
- const destination = path19.join(args.root, ASSETS_DEST, "meshy-character.json");
18271
- await fs20.mkdir(path19.dirname(destination), { recursive: true });
18334
+ const destination = path20.join(args.root, ASSETS_DEST, "meshy-character.json");
18335
+ await fs20.mkdir(path20.dirname(destination), { recursive: true });
18272
18336
  await fs20.writeFile(
18273
18337
  destination,
18274
18338
  `${JSON.stringify(manifest, null, 2)}
@@ -18414,9 +18478,9 @@ function assertCompleteMeshyControllerPack(manifest) {
18414
18478
  }
18415
18479
  async function installFallbackAvatar(args) {
18416
18480
  const { root, srcDir, log } = args;
18417
- const dest = path19.join(root, ASSETS_DEST, "avatar.vrm");
18418
- await fs20.mkdir(path19.dirname(dest), { recursive: true });
18419
- await fs20.copyFile(path19.join(srcDir, "assets", "default-avatar.vrm"), dest);
18481
+ const dest = path20.join(root, ASSETS_DEST, "avatar.vrm");
18482
+ await fs20.mkdir(path20.dirname(dest), { recursive: true });
18483
+ await fs20.copyFile(path20.join(srcDir, "assets", "default-avatar.vrm"), dest);
18420
18484
  log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
18421
18485
  }
18422
18486
  async function exists3(p) {
@@ -18430,7 +18494,7 @@ async function exists3(p) {
18430
18494
 
18431
18495
  // src/commands/character.ts
18432
18496
  import fs21 from "fs/promises";
18433
- import path20 from "path";
18497
+ import path21 from "path";
18434
18498
  function exactAnimation(selector) {
18435
18499
  const trimmed = selector.trim();
18436
18500
  if (/^-?\d+$/.test(trimmed)) return animationById(Number(trimmed));
@@ -18510,7 +18574,7 @@ function showAmbiguity(selector, candidates, log, json = false) {
18510
18574
  }
18511
18575
  process.exitCode = 1;
18512
18576
  }
18513
- var INSTALLED_MANIFEST = path20.join("public", "assets", "meshy-character.json");
18577
+ var INSTALLED_MANIFEST = path21.join("public", "assets", "meshy-character.json");
18514
18578
  async function resolveAdoptTarget(selector) {
18515
18579
  const trimmed = selector?.trim();
18516
18580
  if (trimmed && !trimmed.endsWith(".json")) {
@@ -18639,7 +18703,7 @@ async function runCharacterImport(opts) {
18639
18703
  opts,
18640
18704
  ctx,
18641
18705
  kind: "character",
18642
- prompt: `Import ${path20.basename(filePath)} as a rigged character`,
18706
+ prompt: `Import ${path21.basename(filePath)} as a rigged character`,
18643
18707
  createPath: "/api/characters/import",
18644
18708
  body,
18645
18709
  quote: price,
@@ -19119,22 +19183,22 @@ async function context2(opts) {
19119
19183
  const project = await readProject();
19120
19184
  return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
19121
19185
  }
19122
- async function readVideo(path27, log) {
19186
+ async function readVideo(path28, log) {
19123
19187
  let bytes;
19124
19188
  try {
19125
- bytes = await readFile(path27);
19189
+ bytes = await readFile(path28);
19126
19190
  } catch {
19127
- log.error(`Can't read ${path27}.`);
19191
+ log.error(`Can't read ${path28}.`);
19128
19192
  return null;
19129
19193
  }
19130
19194
  if (bytes.byteLength > MAX_VIDEO_BYTES) {
19131
- log.error(`${basename(path27)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
19195
+ log.error(`${basename(path28)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
19132
19196
  return null;
19133
19197
  }
19134
19198
  return bytes;
19135
19199
  }
19136
- async function uploadVideo(apiUrl, token, characterId, path27, bytes, log) {
19137
- const contentType = /\.mov$/i.test(path27) ? "video/quicktime" : "video/mp4";
19200
+ async function uploadVideo(apiUrl, token, characterId, path28, bytes, log) {
19201
+ const contentType = /\.mov$/i.test(path28) ? "video/quicktime" : "video/mp4";
19138
19202
  const minted = await apiFetch(
19139
19203
  `${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
19140
19204
  {
@@ -19149,7 +19213,7 @@ async function uploadVideo(apiUrl, token, characterId, path27, bytes, log) {
19149
19213
  return null;
19150
19214
  }
19151
19215
  const { uploadUrl, videoUrl } = await minted.json();
19152
- log.dim(` uploading ${basename(path27)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
19216
+ log.dim(` uploading ${basename(path28)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
19153
19217
  const put = await fetch(uploadUrl, {
19154
19218
  method: "PUT",
19155
19219
  headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
@@ -19466,7 +19530,7 @@ function rank(items, query) {
19466
19530
 
19467
19531
  // src/commands/motion.ts
19468
19532
  import fs22 from "fs/promises";
19469
- import path21 from "path";
19533
+ import path22 from "path";
19470
19534
 
19471
19535
  // src/lib/motion/npz.ts
19472
19536
  import zlib from "zlib";
@@ -20744,7 +20808,7 @@ async function expandTakes(selectors) {
20744
20808
  const st = await fs22.stat(sel).catch(() => null);
20745
20809
  if (st?.isDirectory()) {
20746
20810
  const names = await fs22.readdir(sel);
20747
- for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path21.join(sel, n));
20811
+ for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path22.join(sel, n));
20748
20812
  } else if (st?.isFile()) {
20749
20813
  out.push(sel);
20750
20814
  } else {
@@ -20801,7 +20865,7 @@ async function motionVerify(opts, log) {
20801
20865
  }
20802
20866
  const reports = [];
20803
20867
  for (const file of files) {
20804
- const stem = path21.basename(file).replace(/\.npz$/, "");
20868
+ const stem = path22.basename(file).replace(/\.npz$/, "");
20805
20869
  try {
20806
20870
  reports.push(analyzeTake(stem, await fs22.readFile(file), gates));
20807
20871
  } catch (err) {
@@ -20859,7 +20923,7 @@ async function motionCompile(opts, log) {
20859
20923
  }
20860
20924
  const inputs = [];
20861
20925
  for (const file of files) {
20862
- const stem = path21.basename(file).replace(/\.npz$/, "");
20926
+ const stem = path22.basename(file).replace(/\.npz$/, "");
20863
20927
  try {
20864
20928
  inputs.push({ stem, take: loadTake(await fs22.readFile(file)) });
20865
20929
  } catch (err) {
@@ -20868,7 +20932,7 @@ async function motionCompile(opts, log) {
20868
20932
  return;
20869
20933
  }
20870
20934
  }
20871
- const setName = opts.set ?? path21.basename(opts.out).replace(/\.json$/, "");
20935
+ const setName = opts.set ?? path22.basename(opts.out).replace(/\.json$/, "");
20872
20936
  let result;
20873
20937
  try {
20874
20938
  result = compileSet(inputs, setName, cfg);
@@ -20883,7 +20947,7 @@ async function motionCompile(opts, log) {
20883
20947
  process.exitCode = 1;
20884
20948
  return;
20885
20949
  }
20886
- await fs22.mkdir(path21.dirname(path21.resolve(opts.out)), { recursive: true });
20950
+ await fs22.mkdir(path22.dirname(path22.resolve(opts.out)), { recursive: true });
20887
20951
  const json = JSON.stringify(result.data);
20888
20952
  await fs22.writeFile(opts.out, json);
20889
20953
  if (opts.json) {
@@ -20903,9 +20967,9 @@ var MOTION_RUNTIME_FILES = [
20903
20967
  var MOTION_PRESETS = {
20904
20968
  rifle: ["sets/rifle.json", "sets/jumps.json"]
20905
20969
  };
20906
- var MOTION_DEST = path21.join("src", "motion");
20970
+ var MOTION_DEST = path22.join("src", "motion");
20907
20971
  async function motionInstall(opts, log) {
20908
- const srcDir = path21.join(getTemplatesDir(), "motion");
20972
+ const srcDir = path22.join(getTemplatesDir(), "motion");
20909
20973
  const root = opts.cwd ?? process.cwd();
20910
20974
  const preset = opts.set;
20911
20975
  if (preset !== void 0 && !MOTION_PRESETS[preset]) {
@@ -20916,21 +20980,21 @@ async function motionInstall(opts, log) {
20916
20980
  const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
20917
20981
  log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
20918
20982
  log.plain("");
20919
- log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path21.sep)}`);
20983
+ log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path22.sep)}`);
20920
20984
  let copied = 0, skipped = 0;
20921
20985
  try {
20922
20986
  for (const rel of files) {
20923
- const dest = path21.join(root, MOTION_DEST, rel);
20987
+ const dest = path22.join(root, MOTION_DEST, rel);
20924
20988
  const exists5 = await fs22.access(dest).then(() => true, () => false);
20925
20989
  if (!opts.force && exists5) {
20926
20990
  skipped++;
20927
- log.dim(` skipped ${path21.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
20991
+ log.dim(` skipped ${path22.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
20928
20992
  continue;
20929
20993
  }
20930
- await fs22.mkdir(path21.dirname(dest), { recursive: true });
20931
- await fs22.copyFile(path21.join(srcDir, rel), dest);
20994
+ await fs22.mkdir(path22.dirname(dest), { recursive: true });
20995
+ await fs22.copyFile(path22.join(srcDir, rel), dest);
20932
20996
  copied++;
20933
- log.dim(` ${path21.join(MOTION_DEST, rel)}`);
20997
+ log.dim(` ${path22.join(MOTION_DEST, rel)}`);
20934
20998
  }
20935
20999
  } catch (err) {
20936
21000
  log.error(`Copy failed: ${String(err)}`);
@@ -21009,12 +21073,12 @@ async function runMotion(opts) {
21009
21073
 
21010
21074
  // src/commands/blender.ts
21011
21075
  import fs23 from "fs/promises";
21012
- import path22 from "path";
21076
+ import path23 from "path";
21013
21077
  var SUBS2 = ["demo", "exec", "snap", "scene", "import", "export", "reset", "mcp", "serve", "seat", "release"];
21014
21078
  var DEFAULT_OUT_DIR = "assets/blender";
21015
21079
  async function writeB64(dir, name, b64) {
21016
21080
  await fs23.mkdir(dir, { recursive: true });
21017
- const p = path22.join(dir, name);
21081
+ const p = path23.join(dir, name);
21018
21082
  await fs23.writeFile(p, Buffer.from(b64, "base64"));
21019
21083
  return p;
21020
21084
  }
@@ -21099,7 +21163,7 @@ async function runBlender(opts) {
21099
21163
  log.plain(rest.join("\n"));
21100
21164
  return 1;
21101
21165
  }
21102
- const outDir = path22.resolve(opts.outDir ?? DEFAULT_OUT_DIR);
21166
+ const outDir = path23.resolve(opts.outDir ?? DEFAULT_OUT_DIR);
21103
21167
  const mode = opts.mode;
21104
21168
  if (mode !== void 0 && !isRenderMode(mode)) {
21105
21169
  log.error(`Unknown --mode ${mode}. Use one of: ${RENDER_MODES.join(", ")}.`);
@@ -21139,13 +21203,13 @@ async function runBlender(opts) {
21139
21203
  return 0;
21140
21204
  }
21141
21205
  case "export": {
21142
- const target = opts.out ?? path22.join(outDir, "scene.glb");
21206
+ const target = opts.out ?? path23.join(outDir, "scene.glb");
21143
21207
  const r = await blenderCall(base, "/export", { path: "/tmp/genex-export.glb" });
21144
21208
  if (!r.glbBase64) {
21145
21209
  log.error(`/export answered with no GLB bytes${r.uploaded ? " (it was uploaded, not inlined)" : ""}`);
21146
21210
  return 1;
21147
21211
  }
21148
- await fs23.mkdir(path22.dirname(target), { recursive: true });
21212
+ await fs23.mkdir(path23.dirname(target), { recursive: true });
21149
21213
  await fs23.writeFile(target, Buffer.from(r.glbBase64, "base64"));
21150
21214
  log.success(`Exported ${r.bytes ?? 0} bytes`);
21151
21215
  log.plain(` ${c.cyan(target)}`);
@@ -21184,7 +21248,7 @@ async function runBlender(opts) {
21184
21248
  log.error(`Can't read ${opts.input}`);
21185
21249
  return 1;
21186
21250
  }
21187
- label = path22.basename(opts.input);
21251
+ label = path23.basename(opts.input);
21188
21252
  }
21189
21253
  const r = await blenderCall(base, "/exec", { script, ...mode ? { mode } : {}, sheet: { formats: SHEET_FORMATS } });
21190
21254
  if (r.stdout?.trim()) log.plain(r.stdout.trimEnd());
@@ -21305,7 +21369,7 @@ print(f"castle: {n} objects")
21305
21369
  // src/commands/asset-new.ts
21306
21370
  import fs24 from "fs";
21307
21371
  import fsp from "fs/promises";
21308
- import path23 from "path";
21372
+ import path24 from "path";
21309
21373
  import { pathToFileURL } from "url";
21310
21374
  var EXTRA_FILES = [
21311
21375
  "genex-asset.example.json",
@@ -21399,7 +21463,7 @@ Every milestone ends with \`npm run verify\`, then \`npx genex preview --no-buil
21399
21463
  }
21400
21464
  async function runAssetNew(options) {
21401
21465
  const log = createLogger();
21402
- const cwd = options.dir ? path23.resolve(options.dir) : process.cwd();
21466
+ const cwd = options.dir ? path24.resolve(options.dir) : process.cwd();
21403
21467
  const slug = options.assetSlug;
21404
21468
  if (!slug) {
21405
21469
  log.error('Usage: genex asset new <slug> [--title "<Name>"] [--dims WxDxH] [--tri-band LOW-HIGH] [--holder "<your name>"] [--out <dir>]');
@@ -21409,14 +21473,14 @@ async function runAssetNew(options) {
21409
21473
  log.error(`The slug must be kebab-case (letters, digits, dashes), got: ${slug}`);
21410
21474
  return 1;
21411
21475
  }
21412
- const templateDir = path23.join(getTemplatesDir(), "asset-viewer");
21476
+ const templateDir = path24.join(getTemplatesDir(), "asset-viewer");
21413
21477
  if (!fs24.existsSync(templateDir)) {
21414
21478
  log.error(`Vendored asset-viewer template not found at ${templateDir} \u2014 reinstall the CLI.`);
21415
21479
  return 1;
21416
21480
  }
21417
- const manifestTools = await import(pathToFileURL(path23.join(templateDir, "tools", "emit-manifest.mjs")).href);
21481
+ const manifestTools = await import(pathToFileURL(path24.join(templateDir, "tools", "emit-manifest.mjs")).href);
21418
21482
  const { SHARED_FILES, PARITY_FILENAME, hashSharedFiles, pascalCase } = manifestTools;
21419
- const lockPath = path23.join(templateDir, "shared-files.sha256.json");
21483
+ const lockPath = path24.join(templateDir, "shared-files.sha256.json");
21420
21484
  const lock = JSON.parse(await fsp.readFile(lockPath, "utf8"));
21421
21485
  const actual = hashSharedFiles(templateDir);
21422
21486
  const drifted = SHARED_FILES.filter((rel) => lock.files[rel] !== actual[rel]);
@@ -21429,7 +21493,7 @@ async function runAssetNew(options) {
21429
21493
  const triBand = parseBand(options.triBand ?? "500-8000");
21430
21494
  const holder = options.holder?.trim() || HOLDER_PLACEHOLDER;
21431
21495
  const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
21432
- const outDir = path23.resolve(cwd, options.out ?? slug);
21496
+ const outDir = path24.resolve(cwd, options.out ?? slug);
21433
21497
  if (fs24.existsSync(outDir) && fs24.readdirSync(outDir).length > 0 && !options.force) {
21434
21498
  log.error(`${outDir} already holds files. Nothing was touched \u2014 pass --force to write into it anyway.`);
21435
21499
  return 1;
@@ -21458,24 +21522,24 @@ async function runAssetNew(options) {
21458
21522
  };
21459
21523
  await fsp.mkdir(outDir, { recursive: true });
21460
21524
  for (const rel of [...SHARED_FILES, ...EXTRA_FILES]) {
21461
- const to = path23.join(outDir, rel);
21462
- await fsp.mkdir(path23.dirname(to), { recursive: true });
21463
- await fsp.copyFile(path23.join(templateDir, rel), to);
21525
+ const to = path24.join(outDir, rel);
21526
+ await fsp.mkdir(path24.dirname(to), { recursive: true });
21527
+ await fsp.copyFile(path24.join(templateDir, rel), to);
21464
21528
  }
21465
- const pkg = fillTemplate(await fsp.readFile(path23.join(templateDir, "package.json"), "utf8"), {
21529
+ const pkg = fillTemplate(await fsp.readFile(path24.join(templateDir, "package.json"), "utf8"), {
21466
21530
  slug,
21467
21531
  name,
21468
21532
  version
21469
21533
  });
21470
- await fsp.writeFile(path23.join(outDir, "package.json"), pkg, "utf8");
21471
- await fsp.writeFile(path23.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
21472
- await fsp.writeFile(path23.join(outDir, ".gitignore"), GITIGNORE, "utf8");
21534
+ await fsp.writeFile(path24.join(outDir, "package.json"), pkg, "utf8");
21535
+ await fsp.writeFile(path24.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
21536
+ await fsp.writeFile(path24.join(outDir, ".gitignore"), GITIGNORE, "utf8");
21473
21537
  await fsp.writeFile(
21474
- path23.join(outDir, "DESIGN.md"),
21538
+ path24.join(outDir, "DESIGN.md"),
21475
21539
  designDoc({ name, slug, sizeMeters, triBand, holder }),
21476
21540
  "utf8"
21477
21541
  );
21478
- const placeholder = await fsp.readFile(path23.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
21542
+ const placeholder = await fsp.readFile(path24.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
21479
21543
  const seeded = seedAssetSource(placeholder, {
21480
21544
  slug,
21481
21545
  name,
@@ -21486,8 +21550,8 @@ async function runAssetNew(options) {
21486
21550
  pascalCase
21487
21551
  });
21488
21552
  const assetFilename = `${slug.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase())}.ts`;
21489
- await fsp.mkdir(path23.join(outDir, "src", "asset"), { recursive: true });
21490
- await fsp.writeFile(path23.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
21553
+ await fsp.mkdir(path24.join(outDir, "src", "asset"), { recursive: true });
21554
+ await fsp.writeFile(path24.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
21491
21555
  const copied = hashSharedFiles(outDir);
21492
21556
  const mismatched = SHARED_FILES.filter((rel) => copied[rel] !== lock.files[rel]);
21493
21557
  if (mismatched.length) {
@@ -21495,7 +21559,7 @@ async function runAssetNew(options) {
21495
21559
  return 1;
21496
21560
  }
21497
21561
  await fsp.writeFile(
21498
- path23.join(outDir, PARITY_FILENAME),
21562
+ path24.join(outDir, PARITY_FILENAME),
21499
21563
  JSON.stringify(
21500
21564
  {
21501
21565
  note: "Recorded by `genex asset new`. Gate G13 recomputes these; do not hand-edit the viewer.",
@@ -21539,11 +21603,11 @@ async function runAssetNew(options) {
21539
21603
  }
21540
21604
 
21541
21605
  // src/commands/tools.ts
21542
- import path26 from "path";
21606
+ import path27 from "path";
21543
21607
 
21544
21608
  // src/lib/local-install.ts
21545
21609
  import fs25 from "fs/promises";
21546
- import path24 from "path";
21610
+ import path25 from "path";
21547
21611
  import { spawn as spawn4 } from "child_process";
21548
21612
  var CLI_PACKAGE = "@genex-ai/cli-demo";
21549
21613
  var FULL_NAME_FALLBACK = `npx ${CLI_PACKAGE}@${CLI_CHANNEL}`;
@@ -21563,10 +21627,10 @@ async function exists4(p) {
21563
21627
  }
21564
21628
  }
21565
21629
  async function detectPackageManager(cwd) {
21566
- let dir = path24.resolve(cwd);
21630
+ let dir = path25.resolve(cwd);
21567
21631
  for (; ; ) {
21568
21632
  try {
21569
- const raw = await fs25.readFile(path24.join(dir, "package.json"), "utf8");
21633
+ const raw = await fs25.readFile(path25.join(dir, "package.json"), "utf8");
21570
21634
  const pm = JSON.parse(raw).packageManager;
21571
21635
  if (typeof pm === "string") {
21572
21636
  const name = pm.split("@")[0];
@@ -21575,18 +21639,18 @@ async function detectPackageManager(cwd) {
21575
21639
  } catch {
21576
21640
  }
21577
21641
  for (const [file, pm] of LOCKFILES) {
21578
- if (await exists4(path24.join(dir, file))) return pm;
21642
+ if (await exists4(path25.join(dir, file))) return pm;
21579
21643
  }
21580
- const parent = path24.dirname(dir);
21644
+ const parent = path25.dirname(dir);
21581
21645
  if (parent === dir) return "npm";
21582
21646
  dir = parent;
21583
21647
  }
21584
21648
  }
21585
21649
  async function findLocalCli(cwd) {
21586
- let dir = path24.resolve(cwd);
21650
+ let dir = path25.resolve(cwd);
21587
21651
  for (; ; ) {
21588
- if (await exists4(path24.join(dir, "node_modules", CLI_PACKAGE, "package.json"))) return dir;
21589
- const parent = path24.dirname(dir);
21652
+ if (await exists4(path25.join(dir, "node_modules", CLI_PACKAGE, "package.json"))) return dir;
21653
+ const parent = path25.dirname(dir);
21590
21654
  if (parent === dir) return null;
21591
21655
  dir = parent;
21592
21656
  }
@@ -21604,7 +21668,7 @@ function installArgs(pm, spec) {
21604
21668
  }
21605
21669
  }
21606
21670
  function manifestName(cwd) {
21607
- const slug = path24.basename(path24.resolve(cwd)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
21671
+ const slug = path25.basename(path25.resolve(cwd)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
21608
21672
  return slug || "genex-tools-workspace";
21609
21673
  }
21610
21674
  function isSourceRun(moduleUrl = import.meta.url) {
@@ -21662,10 +21726,10 @@ async function ensureLocalCli(log, cwd = process.cwd(), opts = {}) {
21662
21726
  return;
21663
21727
  }
21664
21728
  const pm = await detectPackageManager(cwd);
21665
- const hadManifest = await exists4(path24.join(cwd, "package.json"));
21729
+ const hadManifest = await exists4(path25.join(cwd, "package.json"));
21666
21730
  if (!hadManifest) {
21667
21731
  await fs25.writeFile(
21668
- path24.join(cwd, "package.json"),
21732
+ path25.join(cwd, "package.json"),
21669
21733
  JSON.stringify({ name: manifestName(cwd), private: true }, null, 2) + "\n"
21670
21734
  );
21671
21735
  await ensureIgnored(cwd, "node_modules/");
@@ -21685,7 +21749,7 @@ async function ensureLocalCli(log, cwd = process.cwd(), opts = {}) {
21685
21749
  }
21686
21750
  }
21687
21751
  async function ensureIgnored(dir, entry) {
21688
- const file = path24.join(dir, ".gitignore");
21752
+ const file = path25.join(dir, ".gitignore");
21689
21753
  let content = "";
21690
21754
  try {
21691
21755
  content = await fs25.readFile(file, "utf8");
@@ -21698,7 +21762,7 @@ async function ensureIgnored(dir, entry) {
21698
21762
  }
21699
21763
 
21700
21764
  // src/commands/doctor.ts
21701
- import path25 from "path";
21765
+ import path26 from "path";
21702
21766
  var LANE_ORDER = [
21703
21767
  "model",
21704
21768
  "image",
@@ -21979,7 +22043,7 @@ async function fetchLegalStatus(apiUrl, token) {
21979
22043
  }
21980
22044
  async function firstSkillsMarker() {
21981
22045
  for (const target of resolveAgentTargets()) {
21982
- const marker = await readSkillsMarker(path25.join(target.baseDir, "skills"));
22046
+ const marker = await readSkillsMarker(path26.join(target.baseDir, "skills"));
21983
22047
  if (marker) return marker;
21984
22048
  }
21985
22049
  return null;
@@ -22018,8 +22082,8 @@ async function runTools(opts) {
22018
22082
  let totalNew = 0;
22019
22083
  let totalUpdated = 0;
22020
22084
  for (const t of targets) {
22021
- const dest = path26.join(t.baseDir, "skills");
22022
- const { copied, updated } = await copyTemplates(path26.join(templatesDir, "skills"), dest, {
22085
+ const dest = path27.join(t.baseDir, "skills");
22086
+ const { copied, updated } = await copyTemplates(path27.join(templatesDir, "skills"), dest, {
22023
22087
  filter: (rel) => skillFamilyFilter("tools")(`skills/${rel}`)
22024
22088
  });
22025
22089
  await pruneRemovedSkills(dest, log);
@@ -22316,8 +22380,12 @@ ${c.bold("Options for the generators (`model` `sfx` `music` `voice` `texture` `i
22316
22380
  --geometry <tier> (model) standard (default) | detailed (+20 credits, hero pieces).
22317
22381
  --quad (model) quad-dominant mesh (+5; face limit \u2264150000).
22318
22382
  --low-poly (model) smart low-poly topology (+10) \u2014 game-ready meshes.
22383
+ Holds --face-limit to 1000-20000 (500-10000 with --quad);
22384
+ omit it to take 20000. Runs a post-process after the mesh:
22385
+ allow up to 30 minutes.
22319
22386
  --parts (model) separated, named parts at generation (+20).
22320
- --face-limit <n> (model) cap on the raw mesh, 1000-2000000 (default 150000).
22387
+ --face-limit <n> (model) cap on the raw mesh, 1000-2000000 (default 150000;
22388
+ see --low-poly for its band).
22321
22389
  --auto-size (model) scale to real-world metres by AI estimate.
22322
22390
  --granularity <g> (model segment) part granularity: simple | balanced |
22323
22391
  detailed (default balanced).
@@ -23303,8 +23371,8 @@ function applyValueFlag(options, flag, value) {
23303
23371
  break;
23304
23372
  case "--face-limit": {
23305
23373
  const n = Number(value);
23306
- if (!Number.isInteger(n) || n < 1e3 || n > 2e6) {
23307
- throw new Error(`Invalid --face-limit value: ${value} (expected 1000-2000000)`);
23374
+ if (!Number.isInteger(n) || n < 500 || n > 2e6) {
23375
+ throw new Error(`Invalid --face-limit value: ${value} (expected 1000-2000000; 500-10000 with --low-poly --quad)`);
23308
23376
  }
23309
23377
  options.faceLimit = n;
23310
23378
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "1.25.0-dev.616",
3
+ "version": "1.25.1-dev.617",
4
4
  "description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -283,7 +283,9 @@ scene is a ghost: players and objects pass straight through it.
283
283
  the camera sits on; never for a crate), `--quad` (+5, quad-dominant mesh
284
284
  for anything you will deform or edit further; face limit ≤150000),
285
285
  `--low-poly` (+10, smart low-poly topology — the game-ready choice for
286
- props that appear in numbers), `--parts` (+20, separated named parts at
286
+ props that appear in numbers; it holds `--face-limit` to 1000-20000,
287
+ 500-10000 with `--quad` — omit the flag to take 20000 — and runs a
288
+ post-process after the mesh, so allow up to 30 minutes), `--parts` (+20, separated named parts at
287
289
  generation — cheaper than `model segment` when you know up front you need
288
290
  doors, wheels, magazines), `--face-limit <n>` (1000-2000000, default
289
291
  150000; the raw cap — the game still loads the @2048/@1024 rungs),
@@ -42,14 +42,14 @@ npx genex model rig <model-id> --type quadruped
42
42
  npx genex model animate <rig-id> --preset walk,run
43
43
  ```
44
44
 
45
- - **`segment`** — one GLB whose parts carry NAMES, so you can address them: doors, turrets, magazines, destructibles. `--granularity simple|balanced|detailed`.
45
+ - **`segment`** — one GLB split into addressable parts: doors, turrets, magazines, destructibles. `--granularity simple|balanced|detailed`. The parts are named by index (`tripo_part_0`, `tripo_part_1`, …), not by what they are — find the one you want by its bounds (the sails are the part with the widest extent, the door the lowest thin one), then keep that index in your manifest.
46
46
  - **`rig`** — a skeleton for any mesh, across 7 body plans: `biped`, `quadruped`, `hexapod`, `octopod`, `avian`, `serpentine`, `aquatic`. The plan is auto-detected; `--type` picks it. A mesh that cannot be rigged is refused and refunded before the paid step.
47
47
  - **`animate`** — retarget ready-made clips onto a rig, billed per clip. biped: `idle|walk|run|dive|climb|jump|slash|shoot|hurt|fall|turn`; quadruped/hexapod/octopod: `walk`; serpentine/aquatic: `march`.
48
48
 
49
49
  ## Options
50
50
 
51
51
  - `--image <path|url>` — build the model from a reference image (local file ≤ 4 MB, or a previous generation's URL). The prompt becomes optional.
52
- - **Quality knobs** (Tripo H3.1, each priced in the quote — pick per asset, say it in one line): `--texture standard|detailed|none` (detailed default, +10 over standard; none = geometry only), `--geometry detailed` (+20, hero pieces only), `--quad` (+5, for meshes you will edit; face limit ≤150000), `--low-poly` (+10, game-ready topology for props in numbers), `--parts` (+20, named parts at generation), `--face-limit <n>` (1000-2000000, default 150000), `--auto-size` (real-world metres).
52
+ - **Quality knobs** (Tripo H3.1, each priced in the quote — pick per asset, say it in one line): `--texture standard|detailed|none` (detailed default, +10 over standard; none = geometry only), `--geometry detailed` (+20, hero pieces only), `--quad` (+5, for meshes you will edit; face limit ≤150000), `--low-poly` (+10, game-ready topology for props in numbers — it holds `--face-limit` to 1000-20000, 500-10000 with `--quad`; omit the flag to take 20000; it runs a post-process after the mesh, so allow up to 30 minutes), `--parts` (+20, named parts at generation), `--face-limit <n>` (1000-2000000, default 150000; see `--low-poly` for its band), `--auto-size` (real-world metres).
53
53
  - `--out-dir <dir>` — where the file lands (default `./assets`).
54
54
  - `--no-download` — print the URL only.
55
55
  - `--no-wait` — enqueue and return; pick it up with `npx genex wait <id>`.
@@ -65,9 +65,21 @@ Live prices and your balance: `npx genex doctor`.
65
65
 
66
66
  Models take the longest of any lane. `--no-wait` is the normal way to run
67
67
  several at once: enqueue them all, keep building, then `npx genex wait --all`
68
- for one status line each, and `npx genex wait <id>` to pick one up.
69
- **Re-running the model command bills a NEW model** never use it as a status
70
- check.
68
+ for one status line each in a tools workspace it also downloads every
69
+ finished model that is not in `./assets` yet, so one call after a break (or a
70
+ restarted session) delivers the whole batch. `npx genex wait <id>` picks one
71
+ up. **Re-running the model command bills a NEW model** — never use it as a
72
+ status check. Five or so in flight at a time is the provider's comfortable
73
+ concurrency; a burst beyond that waits on the server side rather than failing.
74
+
75
+ ## Placing a model
76
+
77
+ A generated GLB has no shared "front": one building's door faces −x, the next
78
+ one's +z. Do not guess and do not spend a render per side — read the mesh once
79
+ on load (bounding box, and where the detail is: the door, the counter, the
80
+ opening) or check it in a viewer, then record a per-model `front` (a yaw in
81
+ your manifest) beside its path and apply it when you place it. Ask for a facing
82
+ in the prompt too (`"…front toward +Z"`) — it helps, it does not guarantee.
71
83
 
72
84
  ## Troubleshooting
73
85