@genex-ai/cli-demo 1.9.2-dev.516 → 1.9.3-dev.518

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
@@ -5859,6 +5859,13 @@ async function recordTerminal(id, status, urls, cwd = process.cwd()) {
5859
5859
  async function countFailed(kind, cwd = process.cwd()) {
5860
5860
  return (await readLedger(cwd)).filter((e) => e.kind === kind && e.status === "failed").length;
5861
5861
  }
5862
+ async function countOutcomes(kind, cwd = process.cwd()) {
5863
+ const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
5864
+ return {
5865
+ failed: rows.filter((e) => e.status === "failed").length,
5866
+ completed: rows.filter((e) => e.status === "completed").length
5867
+ };
5868
+ }
5862
5869
 
5863
5870
  // src/lib/detect-features.ts
5864
5871
  async function detectEmbedSdkVersion(cwd = process.cwd()) {
@@ -5986,7 +5993,8 @@ async function detectSurfaceScan(cwd = process.cwd()) {
5986
5993
  guessedRepeat: [],
5987
5994
  squarePoints: [],
5988
5995
  depthRange: [],
5989
- mediaElementAudio: []
5996
+ mediaElementAudio: [],
5997
+ deferredAudioContext: []
5990
5998
  };
5991
5999
  const srcDir = path18.join(cwd, "src");
5992
6000
  let entries;
@@ -6046,6 +6054,26 @@ async function detectSurfaceScan(cwd = process.cwd()) {
6046
6054
  detail: isCtor ? "new Audio(...)" : 'document.createElement("audio")'
6047
6055
  });
6048
6056
  }
6057
+ const ctxRe = /([A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*)\s*=\s*new\s+(?:\(\s*(?:window\s*\.\s*)?(?:webkit)?AudioContext[^)]*\)|(?:window\s*\.\s*)?(?:webkit)?AudioContext)\s*\(/g;
6058
+ while (m = ctxRe.exec(content)) {
6059
+ const target = m[1].replace(/\s+/g, "");
6060
+ const leaf = target.split(".").pop();
6061
+ const esc = leaf.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6062
+ const startsNull = new RegExp(
6063
+ `\\b${esc}\\s*(?::[^;=
6064
+ ]*\\|\\s*null[^;=
6065
+ ]*)?\\s*=\\s*null\\b|\\b${esc}\\s*:[^;=
6066
+ ]*\\|\\s*null`
6067
+ ).test(content);
6068
+ const guarded = new RegExp(
6069
+ `!\\s*(?:this\\s*\\.\\s*)?${esc}\\b|\\b${esc}\\s*===?\\s*null\\b`
6070
+ ).test(content);
6071
+ if (!startsNull || !guarded) continue;
6072
+ found.deferredAudioContext.push({
6073
+ where: `${rel}:${lineOf(content, m.index)}`,
6074
+ detail: `${target} starts null and is guarded elsewhere in this file`
6075
+ });
6076
+ }
6049
6077
  }
6050
6078
  return found;
6051
6079
  }
@@ -6054,7 +6082,9 @@ var UNPICKED_AFTER_MS = 15 * 60 * 1e3;
6054
6082
  async function detectGenerationAudit(cwd = process.cwd()) {
6055
6083
  const ledger = await readLedger(cwd);
6056
6084
  const audited = ledger.filter((e) => WIRED_BY_URL.has(e.kind));
6057
- if (audited.length === 0) return { unwired: [], unpicked: [], unresolved: [], deadWired: [] };
6085
+ if (audited.length === 0) {
6086
+ return { unwired: [], unpicked: [], unresolved: [], deadWired: [], musicNoSlider: false };
6087
+ }
6058
6088
  let haystack = "";
6059
6089
  const read = async (file) => {
6060
6090
  try {
@@ -6101,7 +6131,10 @@ async function detectGenerationAudit(cwd = process.cwd()) {
6101
6131
  // AFTER the status check converges the ledger to "failed" (otherwise the
6102
6132
  // audit self-erases the moment the agent runs the recommended command and
6103
6133
  // the dead URL ships silently anyway).
6104
- deadWired: audited.filter((e) => e.status === "failed" && haystack.includes(e.id)).map(ref)
6134
+ deadWired: audited.filter((e) => e.status === "failed" && haystack.includes(e.id)).map(ref),
6135
+ // One regex covers the HTML/JSX attribute and the DOM-built input alike,
6136
+ // since `.type = "range"` contains `type = "range"`.
6137
+ musicNoSlider: audited.some((e) => e.kind === "music" && e.status === "completed") && !/type\s*=\s*["'`]range["'`]/.test(haystack)
6105
6138
  };
6106
6139
  }
6107
6140
  async function detectFeatures(log, cwd = process.cwd()) {
@@ -6156,6 +6189,11 @@ function generationNudges(log, g) {
6156
6189
  `${g.unwired.length} finished generation(s) are wired nowhere in the game: ${describe(g.unwired)}. Each was paid for \u2014 wire it in (its URL embeds the id) or tell the user in one plain line why it stays unused.`
6157
6190
  );
6158
6191
  }
6192
+ if (g.musicNoSlider) {
6193
+ log.warn(
6194
+ `This game paid for a music track and has no volume control anywhere in its source. Music with no Music/SFX sliders is the failure genex-ai-music names: two sliders in the settings screen, persisted and applied live (music ~0.30, sfx ~0.7). A player who cannot turn the music down mutes the browser tab instead, which takes every other sound in the game with it.`
6195
+ );
6196
+ }
6159
6197
  }
6160
6198
  function surfaceNudges(log, s) {
6161
6199
  if (s.guessedRepeat.length) {
@@ -6176,6 +6214,12 @@ function surfaceNudges(log, s) {
6176
6214
  `HTML <audio> elements are carrying this game's sound: ${list}. On iPhone an <audio> element is a handle on the operating system's media pipeline in ANOTHER PROCESS: every write to .volume or .playbackRate is a cross-process call, and play() is slow and asynchronous. A sound driven from a live value \u2014 wheels from speed, engine from RPM \u2014 writes 60-120 of those a second and stutters the whole game, while one-shots arrive late. It disguises itself as a graphics problem perfectly: only while moving, identical at every quality setting, desktop fine. THREE.Audio/PositionalAudio on one AudioListener have none of this \u2014 genex-ai-sfx ships the pooled one-shot, the live-value loop, and the decode-during-the-menu preload.`
6177
6215
  );
6178
6216
  }
6217
+ if (s.deferredAudioContext.length) {
6218
+ const list = s.deferredAudioContext.map((d) => `${d.where} \u2014 ${d.detail}`).join("; ");
6219
+ log.warn(
6220
+ `Sound selected before the AudioContext exists is LOST, not queued: ${list}. Building the context inside the first user gesture is correct \u2014 browsers require it \u2014 but the code around it already skips its work while the context is null, and that is the half that goes wrong: a track chosen on a title screen sets the "current track" variable, its play call is skipped, and when the context finally exists the selector already matches, so nothing ever re-fires. The game then runs with permanently silent music while its SFX (wired directly inside the start handler) play fine. Whatever was pending at the moment the context is created must be RE-KICKED right there. If this game already re-kicks, there is nothing to do \u2014 source alone cannot tell the two apart.`
6221
+ );
6222
+ }
6179
6223
  for (const d of s.depthRange) {
6180
6224
  log.warn(
6181
6225
  `Camera depth range ${d.detail} (far/near = ${d.ratio.toExponential(1)}) at ${d.where} is past what a 24-bit depth buffer can serve \u2014 touching surfaces will z-fight and flicker, and it reads fine right up until it doesn't. near is the expensive knob (resolvable depth \u2248 z\xB2 / (near \xD7 2^24)), so raise near to the closest the camera can actually get \u2014 the quality kit's cameraNearFar derives the pair from the world, and spreading depthRendererOptions() into the renderer adds reversed-Z (free precision at distance, works on WebGL and WebGPU alike): genex-threejs-adaptive-quality. If the range is genuinely astronomical, that is what logarithmicDepthBuffer is for (it costs early-Z): genex-threejs-camera-direction.`
@@ -7259,6 +7303,9 @@ async function reportTerminal(kind, view, log, open = false, json = false) {
7259
7303
  if (kind === "video") {
7260
7304
  const failures = await countFailed("video");
7261
7305
  (failures >= 2 ? log.plain : log.dim)(videoFailureAdvice(failures));
7306
+ } else {
7307
+ const advice = laneFailureAdvice(kind, await countOutcomes(kind));
7308
+ if (advice) log.plain(advice);
7262
7309
  }
7263
7310
  }
7264
7311
  process.exitCode = 1;
@@ -7317,6 +7364,10 @@ async function reportTerminal(kind, view, log, open = false, json = false) {
7317
7364
  }
7318
7365
  printHint(kind, view, files, log);
7319
7366
  }
7367
+ function laneFailureAdvice(kind, outcomes) {
7368
+ if (outcomes.failed < 2 || outcomes.completed > 0) return null;
7369
+ 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.`;
7370
+ }
7320
7371
  function videoFailureAdvice(failures) {
7321
7372
  if (failures >= 2) {
7322
7373
  return ` ${c.bold(`\u270B ${failures} failed videos in this project \u2014 don't queue another attempt of the same clip.`)} One retry per clip is the budget. Use its key-art still as the backdrop instead (the genex-ai-menu skill's built-in fallback; a slow CSS pan gives it life), tell the user in one plain line, and spend the minutes in the game.`;
@@ -7436,31 +7487,50 @@ async function reportTextureSeam(url, log) {
7436
7487
  } catch {
7437
7488
  }
7438
7489
  }
7439
- function printHint(kind, view, files, log) {
7440
- const url = files[0]?.url ?? "";
7490
+ var HINT_URL_KINDS = /* @__PURE__ */ new Set([
7491
+ "model",
7492
+ "skybox",
7493
+ "sfx",
7494
+ "music",
7495
+ "voice",
7496
+ "image",
7497
+ "video",
7498
+ "model_segment",
7499
+ "model_rig",
7500
+ "model_animation"
7501
+ ]);
7502
+ function assetHint(kind, view, url) {
7441
7503
  const hint = {
7442
- model: `Standard GLB \u2014 load with GLTFLoader straight from the URL \u2014 see the genex-ai-model skill. url = "${url}"`,
7504
+ model: `Standard GLB \u2014 load with GLTFLoader straight from the URL \u2014 see the genex-ai-model skill.`,
7443
7505
  // Unreachable while the lane is paused (see src/index.ts) — kept so a
7444
7506
  // restore is one deletion. Games that already ship a panorama still load
7445
7507
  // it this way.
7446
- skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment. url = "${url}"`,
7447
- sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. url = "${url}"`,
7448
- music: `Load with AudioLoader into a non-positional THREE.Audio: setLoop(true), setVolume(0.30), start on the first user gesture \u2014 and the settings screen MUST have Music + SFX volume sliders \u2014 see genex-ai-music. url = "${url}"`,
7449
- voice: `Load with AudioLoader into a THREE.Audio (or PositionalAudio at the speaker); voice lines are content \u2014 keep them short, SUBTITLED, and skippable \u2014 see genex-ai-voice. url = "${url}"`,
7508
+ skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment.`,
7509
+ // The boot-order clause is not decoration: a hosted run shipped three paid
7510
+ // music tracks and a silent title screen because the game selected a track
7511
+ // while its AudioContext was still null. NAME the failure pointing at the
7512
+ // skill does not work, because no skill states this rule.
7513
+ sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener). Create the AudioContext inside the first user gesture \u2014 anything you fire before it exists is LOST, not queued \u2014 see genex-ai-sfx.`,
7514
+ music: `Load with AudioLoader into a non-positional THREE.Audio: setLoop(true), setVolume(0.30). Create the AudioContext inside the first user gesture, and RE-KICK whatever track was already selected: a track chosen while the context was null is LOST, not queued, so nothing plays until the selection happens to change again. The settings screen MUST have Music + SFX volume sliders \u2014 see genex-ai-music.`,
7515
+ voice: `Load with AudioLoader into a THREE.Audio (or PositionalAudio at the speaker); voice lines are content \u2014 keep them short, SUBTITLED, and skippable \u2014 see genex-ai-voice.`,
7450
7516
  texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`,
7451
- image: `Load with TextureLoader (set colorSpace = SRGBColorSpace) onto any mesh/plane/sprite \u2014 see genex-ai-image. url = "${url}"`,
7452
- video: `Wire an HTMLVideoElement (crossOrigin="anonymous", muted, loop, playsInline) into a THREE.VideoTexture \u2014 see genex-ai-video. url = "${url}"`,
7517
+ image: `Load with TextureLoader (set colorSpace = SRGBColorSpace) onto any mesh/plane/sprite \u2014 see genex-ai-image.`,
7518
+ video: `Wire an HTMLVideoElement (crossOrigin="anonymous", muted, loop, playsInline) into a THREE.VideoTexture \u2014 see genex-ai-video.`,
7453
7519
  motion: `Raw ARDY takes + verify report \u2014 download the takes, rename them meaningfully (routing is by filename), then: genex motion verify \u2192 compile \u2192 install. Full procedure: the genex-ai-character skill's motion reference.`,
7454
7520
  character_concept: "Show all three concept candidates to the user and wait for an explicit selection.",
7455
7521
  character_preview: "Show all four preview views to the user and wait for explicit approval before finalizing.",
7456
7522
  character: `Install the controller and current manifest with genex controller character --character ${view.id}.`,
7457
7523
  character_animation: "The action is now part of the character's current manifest; refresh the local controller manifest before testing.",
7458
7524
  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.`,
7459
- 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. url = "${url}"`,
7460
- 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. url = "${url}"`,
7461
- model_animation: `Animated GLB \u2014 load with GLTFLoader and play its clips via THREE.AnimationMixer (gltf.animations). url = "${url}"`
7525
+ 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.`,
7526
+ 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.`,
7527
+ model_animation: `Animated GLB \u2014 load with GLTFLoader and play its clips via THREE.AnimationMixer (gltf.animations).`
7462
7528
  };
7463
- log.dim(` ${hint[kind]}`);
7529
+ const base = hint[kind];
7530
+ return url && HINT_URL_KINDS.has(kind) ? `${base} url = "${url}"` : base;
7531
+ }
7532
+ function printHint(kind, view, files, log) {
7533
+ log.dim(` ${assetHint(kind, view, files[0]?.url ?? "")}`);
7464
7534
  log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
7465
7535
  }
7466
7536
  var CONCEPT_STOP = "STOP \u2014 show all three concept images to the user and wait for an explicit selection. Do not create the 3D preview until they choose candidate 1, 2, or 3.";
@@ -7826,8 +7896,11 @@ async function runWaitAll(opts) {
7826
7896
  }
7827
7897
  }
7828
7898
  const rows = [];
7899
+ const landed = [];
7829
7900
  for (const e of ledger) {
7830
- rows.push(await toRow(e, views.get(e.id), cwd));
7901
+ const v = views.get(e.id);
7902
+ if (e.status === "queued" && v?.status === "completed") landed.push({ kind: e.kind, view: v });
7903
+ rows.push(await toRow(e, v, cwd));
7831
7904
  }
7832
7905
  if (opts.json) {
7833
7906
  writeJson({ generations: rows });
@@ -7854,6 +7927,14 @@ async function runWaitAll(opts) {
7854
7927
  if (count("running") + count("queued") > 0) {
7855
7928
  log.dim(` Attach to any single one with: ${c.cyan("npx genex wait <id>")}`);
7856
7929
  }
7930
+ if (landed.length > 0) {
7931
+ log.plain("");
7932
+ log.plain(` ${c.bold("Just landed")} \u2014 how to use each one:`);
7933
+ for (const l of landed) {
7934
+ const hint = assetHint(l.kind, l.view);
7935
+ if (hint) log.dim(` \u2022 ${l.kind}: ${hint}`);
7936
+ }
7937
+ }
7857
7938
  log.plain("");
7858
7939
  log.dim(
7859
7940
  " \u{1F4DC} Resumed or compacted? Re-read AGENTS.md + DESIGN.md (continue from its Now: line) before building on."
@@ -21567,10 +21648,11 @@ async function main() {
21567
21648
  await runCharacterAnimateMotion({ ...parsed.options, creature: true });
21568
21649
  break;
21569
21650
  }
21570
- if (!parsed.options.json && !parsed.options.quiet) {
21571
- log.dim(
21572
- " Biped-shaped creatures only (Meshy rig limit) \u2014 for a true quadruped/flier/serpent, generate it with `npx genex model` and rig it with `npx genex model rig` (7 body plans), or go static + the genex-threejs-creatures procedural recipes."
21573
- );
21651
+ if (!parsed.options.quiet) {
21652
+ const note = " Biped-shaped creatures only (Meshy rig limit) \u2014 for a true quadruped/flier/serpent, generate it with `npx genex model` and rig it with `npx genex model rig` (7 body plans), or go static + the genex-threejs-creatures procedural recipes.";
21653
+ if (parsed.options.json) process.stderr.write(`${note}
21654
+ `);
21655
+ else log.dim(note);
21574
21656
  }
21575
21657
  await runCharacter({
21576
21658
  ...parsed.options,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "1.9.2-dev.516",
3
+ "version": "1.9.3-dev.518",
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": {
@@ -190,8 +190,10 @@ Search the catalog first. A missing verb uses
190
190
  `genex character animate <id> "<verb>"` or the creature equivalent, with the
191
191
  existing free motion plan before paid work, route selection, source/clip
192
192
  validation, locomotion bindings, manifests, controller authority, and runtime
193
- playback unchanged. `$genex-threejs-procedural-animation` remains the
194
- non-biped/prop-motion fallback.
193
+ playback unchanged. A non-biped BODY now has a rigged route —
194
+ `npx genex model` → `model rig --type <plan>` → `model animate` (7 body plans,
195
+ AG-908) — so `$genex-threejs-procedural-animation` is the fallback for shapes
196
+ with no riggable body plan, and it keeps prop/debris motion outright.
195
197
 
196
198
  ### Optimization and mobile
197
199