@genex-ai/cli-demo 1.4.1-dev.381 → 1.4.2

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/README.md CHANGED
@@ -11,7 +11,6 @@ genex preview # build + push to the hosted draft URL (unlisted)
11
11
  genex publish # build + push, then list the game in the gallery
12
12
  genex make-remixable # make this game remixable — migrate a private source to a public repo
13
13
  genex model "<prompt>" # generate a 3D model → prints an asset URL
14
- genex skybox "<prompt>" # generate a 360° sky → prints an asset URL
15
14
  genex sfx "<prompt>" # generate a sound fx → prints an asset URL
16
15
  genex texture "<prompt>" # generate a texture → prints an asset URL
17
16
  genex image "<prompt>" # generate an image → prints an asset URL
@@ -88,7 +87,7 @@ baked at build time via `GENEX_CLI_CHANNEL` (see `src/config.ts`).
88
87
 
89
88
  ## Generating assets
90
89
 
91
- `genex model | skybox | sfx | texture | image | video "<prompt>"` turn a prompt into a
90
+ `genex model | sfx | texture | image | video "<prompt>"` turn a prompt into a
92
91
  real, game-ready asset stored in R2. The command blocks until the asset is ready (live SSE
93
92
  stream), then prints its public URL — the game loads it directly, nothing is
94
93
  downloaded or committed:
@@ -96,7 +95,6 @@ downloaded or committed:
96
95
  | Command | Provider | Prints (role) |
97
96
  | --- | --- | --- |
98
97
  | `genex model "<prompt>"` | Tripo | `…/generations/<id>/model-glb` |
99
- | `genex skybox "<prompt>"` | Blockade Labs | `…/generations/<id>/skybox-equirect` |
100
98
  | `genex sfx "<prompt>" [--duration <s>]` | ElevenLabs | `…/generations/<id>/audio-sfx` |
101
99
  | `genex texture "<prompt>" [--terrain]` | Gemini | `…/generations/<id>/texture-basecolor` |
102
100
  | `genex image "<prompt>" [--transparent] [--aspect <ratio>]` | fal.ai | `…/generations/<id>/image-main` |
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import fs from "fs";
8
8
  import os from "os";
9
9
  import path from "path";
10
10
  import { fileURLToPath } from "url";
11
- var RAW_CHANNEL = "dev";
11
+ var RAW_CHANNEL = "latest";
12
12
  var CLI_CHANNEL = RAW_CHANNEL === "dev" ? "dev" : "latest";
13
13
  var STANDS = {
14
14
  prod: { api: "https://api.genex.games", dashboard: "https://genex.games" },
@@ -991,7 +991,7 @@ var CONTRACT_END = "<!-- genex:contract:end -->";
991
991
  var GENEX_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
992
992
  # Genex build contract (always in effect for this game)
993
993
 
994
- Your agentic capabilities that can boost your creation (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skybox\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality|chat\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\` \xB7 \`rename <name>\` (move the game to a new address \u2014 keeps its plays and likes, kills the old link, needs \`--yes\` once published, and the game must be rebuilt after). Full options: \`npx genex --help\`. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 re-load it whenever you're unsure which lane owns a task.
994
+ Your agentic capabilities that can boost your creation (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality|chat\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\` \xB7 \`rename <name>\` (move the game to a new address \u2014 keeps its plays and likes, kills the old link, needs \`--yes\` once published, and the game must be rebuilt after). Full options: \`npx genex --help\`. There is NO sky generator: every sky, horizon, time of day and weather mood is built in code \u2014 your call how, from a background colour and fog to an atmospheric sky shader with a real sun. It costs nothing, weighs nothing on phones, and keeps the sun in sync with the scene's own lighting, which a painted panorama never can. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 re-load it whenever you're unsure which lane owns a task.
995
995
 
996
996
  Important note: put soul into your creations, with many details and love. Aim to make them realistic and feel real. Use genex capabilities as an extension, but never limit your imagination \u2014 you are a powerful agent. Use your built-in sub-agents and iteration loops (\`/loop\` or your platform's equivalent) where you need them to reach outstanding results. Verify yourself. Anything you put in front of the player should already feel alive and impressive \u2014 never a bare scene waiting for "later". Build what's still missing before polishing what already works.
997
997
 
@@ -1118,6 +1118,11 @@ var REMOVED_SKILLS = [
1118
1118
  "genex-explore",
1119
1119
  "genex-threejs-procedural-materials",
1120
1120
  // folded into genex-threejs-procedural-assets (AG-881)
1121
+ // 2026-08-12: the generated-panorama lane is paused at the provider, and
1122
+ // every sky is built in code now. Leaving this SKILL.md
1123
+ // installed would keep pointing agents at `npx genex skybox`, which the CLI
1124
+ // refuses.
1125
+ "genex-ai-skybox",
1121
1126
  "genex-threejs-skill-router",
1122
1127
  "genex-threejs-bloom",
1123
1128
  "genex-threejs-screen-space-ambient-occlusion",
@@ -4890,157 +4895,6 @@ async function runMakeRemixable(opts) {
4890
4895
  }
4891
4896
  }
4892
4897
 
4893
- // src/commands/domain.ts
4894
- var SUBCOMMANDS = ["add", "list", "verify", "remove"];
4895
- function statusWord(d) {
4896
- if (d.status === "active") return c.green("live");
4897
- if (d.status === "failed") return c.red("stopped");
4898
- return c.dim("waiting for DNS");
4899
- }
4900
- async function runDomain(opts) {
4901
- const log = createLogger({ quiet: opts.quiet });
4902
- const sub = (opts.name ?? "list").trim();
4903
- if (!SUBCOMMANDS.includes(sub)) {
4904
- log.error(`Unknown subcommand \`${sub}\`. Use: ${SUBCOMMANDS.join(", ")}.`);
4905
- process.exitCode = 1;
4906
- return;
4907
- }
4908
- const needsHost = sub !== "list";
4909
- const hostname = opts.hostname?.trim();
4910
- if (needsHost && !hostname) {
4911
- log.error(`\`genex domain ${sub}\` needs a hostname, e.g. ${c.cyan(`genex domain ${sub} play.yourdomain.com`)}.`);
4912
- process.exitCode = 1;
4913
- return;
4914
- }
4915
- const meta = await readProject();
4916
- if (!meta?.id) {
4917
- log.error("This folder isn't linked to a game.");
4918
- log.dim(` Run ${c.cyan("genex link")} (or ${c.cyan("genex list")} to find the slug) first.`);
4919
- process.exitCode = 1;
4920
- return;
4921
- }
4922
- const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
4923
- let token = opts.token ?? await readUserToken(opts.envPath);
4924
- if (!token) {
4925
- if (opts.noAuth) {
4926
- log.error("Not signed in. Re-run without --no-auth to connect.");
4927
- process.exitCode = 1;
4928
- return;
4929
- }
4930
- log.plain("Not signed in \u2014 connecting\u2026");
4931
- try {
4932
- token = await authorize(apiUrl, getAuthUrl(opts.authUrl), {
4933
- log,
4934
- inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
4935
- });
4936
- } catch (err) {
4937
- if (err instanceof AuthPendingError) {
4938
- printAuthHandoff(log, err);
4939
- log.dim(`Then re-run ${c.cyan(`genex domain ${sub}`)}.`);
4940
- return;
4941
- }
4942
- log.error(`Sign-in didn't complete: ${err instanceof Error ? err.message : String(err)}`);
4943
- process.exitCode = 1;
4944
- return;
4945
- }
4946
- await writeUserToken(token, opts.envPath);
4947
- }
4948
- const base = `${apiUrl}/api/projects/${encodeURIComponent(meta.id)}/domains`;
4949
- const auth = { Authorization: `Bearer ${token}` };
4950
- const call = (url, init2) => apiFetch(url, { ...init2, headers: { ...auth, "Content-Type": "application/json", ...init2?.headers ?? {} } });
4951
- let res;
4952
- try {
4953
- if (sub === "list") res = await call(base);
4954
- else if (sub === "add") res = await call(base, { method: "POST", body: JSON.stringify({ hostname }) });
4955
- else if (sub === "verify")
4956
- res = await call(`${base}/${encodeURIComponent(hostname)}/verify`, { method: "POST" });
4957
- else res = await call(`${base}/${encodeURIComponent(hostname)}`, { method: "DELETE" });
4958
- } catch (err) {
4959
- log.error(`Couldn't reach the API at ${apiUrl}.`);
4960
- log.dim(` ${String(err)}`);
4961
- process.exitCode = 1;
4962
- return;
4963
- }
4964
- if (!res.ok) {
4965
- if (await printedStructuredError(res)) {
4966
- process.exitCode = 1;
4967
- return;
4968
- }
4969
- const body = await res.json().catch(() => null);
4970
- if (res.status === 503) {
4971
- log.error("Custom domains aren't available on this Genex environment yet.");
4972
- } else if (res.status === 404) {
4973
- log.error(sub === "list" || sub === "add" ? "That game wasn't found on this account." : `${hostname} isn't connected to this game.`);
4974
- } else {
4975
- log.error(body?.message ?? body?.error ?? `Request failed (${res.status}).`);
4976
- }
4977
- process.exitCode = 1;
4978
- return;
4979
- }
4980
- const data = await res.json();
4981
- if (opts.json) {
4982
- log.plain(JSON.stringify(data, null, 2));
4983
- return;
4984
- }
4985
- if (sub === "list") {
4986
- const rows = data.domains ?? [];
4987
- if (rows.length === 0) {
4988
- log.plain("No domains connected to this game.");
4989
- log.dim(` ${c.cyan("genex domain add play.yourdomain.com")} to connect one.`);
4990
- return;
4991
- }
4992
- for (const d of rows) log.plain(` ${d.hostname.padEnd(34)} ${statusWord(d)}`);
4993
- return;
4994
- }
4995
- if (sub === "add") {
4996
- if (data.supported === false) {
4997
- log.plain(String(data.message ?? "That domain's DNS host doesn't support one-click setup."));
4998
- const manual = data.manual;
4999
- const records = Array.isArray(manual?.records) ? manual.records : [];
5000
- if (records.length > 0) {
5001
- log.plain("");
5002
- log.plain(" Add these two records at your DNS host:");
5003
- log.plain("");
5004
- for (const r of records) {
5005
- log.plain(` ${c.cyan(String(r.type ?? ""))} ${String(r.name ?? "")}`);
5006
- log.plain(` ${String(r.value ?? "")}`);
5007
- }
5008
- log.plain("");
5009
- if (manual?.apexHint) {
5010
- log.dim(" At a root domain your host may call the first one ALIAS or ANAME.");
5011
- }
5012
- log.dim(` Then run: genex domain verify ${String(data.hostname ?? "")}`);
5013
- log.dim(" DNS can take a few minutes to spread.");
5014
- return;
5015
- }
5016
- log.dim(" Your game stays reachable at its usual address.");
5017
- return;
5018
- }
5019
- const applyUrl = String(data.applyUrl ?? "");
5020
- const providerName = String(data.providerName ?? "your DNS host");
5021
- log.success(`${data.hostname} can be connected through ${providerName}.`);
5022
- log.plain("");
5023
- log.plain(` Approve the DNS change here: ${c.cyan(applyUrl)}`);
5024
- log.plain("");
5025
- log.dim(" One click writes the records. Nothing to copy or paste.");
5026
- if (!applyUrl.startsWith("https://")) {
5027
- log.warn("That DNS host returned a setup link we could not verify \u2014 not opening it.");
5028
- } else if (!opts.noOpen) {
5029
- openBrowser(applyUrl, () => log.dim(" (Couldn't open a browser \u2014 use the link above.)"));
5030
- }
5031
- log.dim(` Then ${c.cyan(`genex domain verify ${String(data.hostname)}`)} once you have approved it.`);
5032
- return;
5033
- }
5034
- if (sub === "verify") {
5035
- const status = String(data.status ?? "");
5036
- if (status === "active") log.success(`${hostname} is live.`);
5037
- else if (status === "failed") log.error(`${hostname} is stopped \u2014 its verification record is missing.`);
5038
- else log.plain(`${hostname} isn't verified yet \u2014 DNS changes can take a few minutes to spread.`);
5039
- return;
5040
- }
5041
- log.success(`${hostname} disconnected.`);
5042
- }
5043
-
5044
4898
  // src/lib/promote.ts
5045
4899
  async function promoteBuild(apiUrl, projectId, token, log) {
5046
4900
  let res;
@@ -6529,7 +6383,10 @@ function printHint(kind, view, files, log) {
6529
6383
  const url = files[0]?.url ?? "";
6530
6384
  const hint = {
6531
6385
  model: `Standard GLB \u2014 load with GLTFLoader straight from the URL \u2014 see the genex-ai-model skill. url = "${url}"`,
6532
- skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. url = "${url}"`,
6386
+ // Unreachable while the lane is paused (see src/index.ts) kept so a
6387
+ // restore is one deletion. Games that already ship a panorama still load
6388
+ // it this way.
6389
+ skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment. url = "${url}"`,
6533
6390
  sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. url = "${url}"`,
6534
6391
  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}"`,
6535
6392
  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}"`,
@@ -16766,6 +16623,8 @@ async function exists3(p) {
16766
16623
  }
16767
16624
 
16768
16625
  // src/commands/character.ts
16626
+ import fs21 from "fs/promises";
16627
+ import path20 from "path";
16769
16628
  function exactAnimation(selector) {
16770
16629
  const trimmed = selector.trim();
16771
16630
  if (/^-?\d+$/.test(trimmed)) return animationById(Number(trimmed));
@@ -16845,6 +16704,86 @@ function showAmbiguity(selector, candidates, log, json = false) {
16845
16704
  }
16846
16705
  process.exitCode = 1;
16847
16706
  }
16707
+ var INSTALLED_MANIFEST = path20.join("public", "assets", "meshy-character.json");
16708
+ async function resolveAdoptTarget(selector) {
16709
+ const trimmed = selector?.trim();
16710
+ if (trimmed && !trimmed.endsWith(".json")) {
16711
+ return { ok: true, characterId: trimmed, from: "the id you gave" };
16712
+ }
16713
+ const file = trimmed ?? INSTALLED_MANIFEST;
16714
+ let raw;
16715
+ try {
16716
+ raw = await fs21.readFile(file, "utf8");
16717
+ } catch {
16718
+ return {
16719
+ ok: false,
16720
+ message: trimmed ? `Could not read ${file}.` : `No character id given, and this game has no installed character manifest at ${INSTALLED_MANIFEST}. Pass the id from the game you remixed: genex character adopt <character-id>.`
16721
+ };
16722
+ }
16723
+ let parsed;
16724
+ try {
16725
+ parsed = JSON.parse(raw);
16726
+ } catch {
16727
+ return { ok: false, message: `${file} is not valid JSON.` };
16728
+ }
16729
+ const characterId = parsed?.characterId;
16730
+ if (typeof characterId !== "string" || characterId.length === 0) {
16731
+ return { ok: false, message: `${file} carries no characterId.` };
16732
+ }
16733
+ return { ok: true, characterId, from: file };
16734
+ }
16735
+ async function runCharacterAdopt(opts) {
16736
+ const log = createLogger({ quiet: opts.quiet || opts.json });
16737
+ const target = await resolveAdoptTarget(opts.characterId);
16738
+ if (!target.ok) {
16739
+ fail2(opts, target.message, "character");
16740
+ return;
16741
+ }
16742
+ const ctx = await context(opts);
16743
+ if (!ctx) {
16744
+ fail2(opts, "Not authorized. Run `genex init` first to sign in.", "character");
16745
+ return;
16746
+ }
16747
+ const project = await readProject();
16748
+ if (!project?.id) {
16749
+ fail2(
16750
+ opts,
16751
+ "No linked game here. Run this from the game folder that remixed the original (`genex link` if it is not linked yet).",
16752
+ "character"
16753
+ );
16754
+ return;
16755
+ }
16756
+ if (!opts.json) {
16757
+ log.plain(c.bold("Adopting character"));
16758
+ log.plain(` ${target.characterId} \xB7 from ${target.from}`);
16759
+ log.dim(" Copies the body and its clips into your account. No credits \u2014 it generates nothing.");
16760
+ log.plain("");
16761
+ }
16762
+ await runWorkflow({
16763
+ opts,
16764
+ ctx,
16765
+ kind: "character",
16766
+ prompt: `Adopt character ${target.characterId}`,
16767
+ createPath: `/api/characters/${encodeURIComponent(target.characterId)}/adopt`,
16768
+ body: { projectId: project.id },
16769
+ quote: { credits: 0 },
16770
+ completed: (view) => {
16771
+ if (opts.json) {
16772
+ writeJson({
16773
+ kind: "character",
16774
+ status: view.status,
16775
+ characterId: view.id,
16776
+ adoptedFrom: target.characterId,
16777
+ nextCommand: `genex controller character --character ${view.id}`
16778
+ });
16779
+ return;
16780
+ }
16781
+ log.success(`Adopted. This character is yours now: ${c.cyan(view.id)}`);
16782
+ log.plain(` Point the game at it: ${c.cyan(`genex controller character --character ${view.id}`)}`);
16783
+ log.plain(` Then animate it: ${c.cyan(`genex character animate ${view.id} "<what it should do>"`)}`);
16784
+ }
16785
+ });
16786
+ }
16848
16787
  async function runCharacter(opts) {
16849
16788
  if (opts.directText) {
16850
16789
  await runDirectTextCharacter(opts);
@@ -17252,22 +17191,22 @@ async function context2(opts) {
17252
17191
  const project = await readProject();
17253
17192
  return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
17254
17193
  }
17255
- async function readVideo(path21, log) {
17194
+ async function readVideo(path22, log) {
17256
17195
  let bytes;
17257
17196
  try {
17258
- bytes = await readFile(path21);
17197
+ bytes = await readFile(path22);
17259
17198
  } catch {
17260
- log.error(`Can't read ${path21}.`);
17199
+ log.error(`Can't read ${path22}.`);
17261
17200
  return null;
17262
17201
  }
17263
17202
  if (bytes.byteLength > MAX_VIDEO_BYTES) {
17264
- log.error(`${basename(path21)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
17203
+ log.error(`${basename(path22)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
17265
17204
  return null;
17266
17205
  }
17267
17206
  return bytes;
17268
17207
  }
17269
- async function uploadVideo(apiUrl, token, characterId, path21, bytes, log) {
17270
- const contentType = /\.mov$/i.test(path21) ? "video/quicktime" : "video/mp4";
17208
+ async function uploadVideo(apiUrl, token, characterId, path22, bytes, log) {
17209
+ const contentType = /\.mov$/i.test(path22) ? "video/quicktime" : "video/mp4";
17271
17210
  const minted = await apiFetch(
17272
17211
  `${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
17273
17212
  {
@@ -17282,7 +17221,7 @@ async function uploadVideo(apiUrl, token, characterId, path21, bytes, log) {
17282
17221
  return null;
17283
17222
  }
17284
17223
  const { uploadUrl, videoUrl } = await minted.json();
17285
- log.dim(` uploading ${basename(path21)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
17224
+ log.dim(` uploading ${basename(path22)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
17286
17225
  const put = await fetch(uploadUrl, {
17287
17226
  method: "PUT",
17288
17227
  headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
@@ -17598,8 +17537,8 @@ function rank(items, query) {
17598
17537
  }
17599
17538
 
17600
17539
  // src/commands/motion.ts
17601
- import fs21 from "fs/promises";
17602
- import path20 from "path";
17540
+ import fs22 from "fs/promises";
17541
+ import path21 from "path";
17603
17542
 
17604
17543
  // src/lib/motion/npz.ts
17605
17544
  import zlib from "zlib";
@@ -18850,7 +18789,7 @@ async function motionGen(opts, log) {
18850
18789
  }
18851
18790
  if (opts.constraintsPath !== void 0) {
18852
18791
  try {
18853
- const raw = await fs21.readFile(opts.constraintsPath, "utf8");
18792
+ const raw = await fs22.readFile(opts.constraintsPath, "utf8");
18854
18793
  generationOptions.constraints = JSON.parse(raw);
18855
18794
  } catch {
18856
18795
  log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
@@ -18874,10 +18813,10 @@ async function motionGen(opts, log) {
18874
18813
  async function expandTakes(selectors) {
18875
18814
  const out = [];
18876
18815
  for (const sel of selectors) {
18877
- const st = await fs21.stat(sel).catch(() => null);
18816
+ const st = await fs22.stat(sel).catch(() => null);
18878
18817
  if (st?.isDirectory()) {
18879
- const names = await fs21.readdir(sel);
18880
- for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path20.join(sel, n));
18818
+ const names = await fs22.readdir(sel);
18819
+ for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path21.join(sel, n));
18881
18820
  } else if (st?.isFile()) {
18882
18821
  out.push(sel);
18883
18822
  } else {
@@ -18912,7 +18851,7 @@ async function motionVerify(opts, log) {
18912
18851
  let gates = DEFAULT_GATES;
18913
18852
  if (opts.gatesPath) {
18914
18853
  try {
18915
- gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs21.readFile(opts.gatesPath, "utf8")));
18854
+ gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs22.readFile(opts.gatesPath, "utf8")));
18916
18855
  } catch {
18917
18856
  log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
18918
18857
  process.exitCode = 1;
@@ -18934,9 +18873,9 @@ async function motionVerify(opts, log) {
18934
18873
  }
18935
18874
  const reports = [];
18936
18875
  for (const file of files) {
18937
- const stem = path20.basename(file).replace(/\.npz$/, "");
18876
+ const stem = path21.basename(file).replace(/\.npz$/, "");
18938
18877
  try {
18939
- reports.push(analyzeTake(stem, await fs21.readFile(file), gates));
18878
+ reports.push(analyzeTake(stem, await fs22.readFile(file), gates));
18940
18879
  } catch (err) {
18941
18880
  reports.push({
18942
18881
  take: stem,
@@ -18974,7 +18913,7 @@ async function motionCompile(opts, log) {
18974
18913
  let cfg = DEFAULT_MOTION_CONFIG;
18975
18914
  if (opts.configPath) {
18976
18915
  try {
18977
- const patch = JSON.parse(await fs21.readFile(opts.configPath, "utf8"));
18916
+ const patch = JSON.parse(await fs22.readFile(opts.configPath, "utf8"));
18978
18917
  cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
18979
18918
  } catch {
18980
18919
  log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
@@ -18992,16 +18931,16 @@ async function motionCompile(opts, log) {
18992
18931
  }
18993
18932
  const inputs = [];
18994
18933
  for (const file of files) {
18995
- const stem = path20.basename(file).replace(/\.npz$/, "");
18934
+ const stem = path21.basename(file).replace(/\.npz$/, "");
18996
18935
  try {
18997
- inputs.push({ stem, take: loadTake(await fs21.readFile(file)) });
18936
+ inputs.push({ stem, take: loadTake(await fs22.readFile(file)) });
18998
18937
  } catch (err) {
18999
18938
  log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
19000
18939
  process.exitCode = 1;
19001
18940
  return;
19002
18941
  }
19003
18942
  }
19004
- const setName = opts.set ?? path20.basename(opts.out).replace(/\.json$/, "");
18943
+ const setName = opts.set ?? path21.basename(opts.out).replace(/\.json$/, "");
19005
18944
  let result;
19006
18945
  try {
19007
18946
  result = compileSet(inputs, setName, cfg);
@@ -19016,9 +18955,9 @@ async function motionCompile(opts, log) {
19016
18955
  process.exitCode = 1;
19017
18956
  return;
19018
18957
  }
19019
- await fs21.mkdir(path20.dirname(path20.resolve(opts.out)), { recursive: true });
18958
+ await fs22.mkdir(path21.dirname(path21.resolve(opts.out)), { recursive: true });
19020
18959
  const json = JSON.stringify(result.data);
19021
- await fs21.writeFile(opts.out, json);
18960
+ await fs22.writeFile(opts.out, json);
19022
18961
  if (opts.json) {
19023
18962
  writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
19024
18963
  return;
@@ -19036,9 +18975,9 @@ var MOTION_RUNTIME_FILES = [
19036
18975
  var MOTION_PRESETS = {
19037
18976
  rifle: ["sets/rifle.json", "sets/jumps.json"]
19038
18977
  };
19039
- var MOTION_DEST = path20.join("src", "motion");
18978
+ var MOTION_DEST = path21.join("src", "motion");
19040
18979
  async function motionInstall(opts, log) {
19041
- const srcDir = path20.join(getTemplatesDir(), "motion");
18980
+ const srcDir = path21.join(getTemplatesDir(), "motion");
19042
18981
  const root = opts.cwd ?? process.cwd();
19043
18982
  const preset = opts.set;
19044
18983
  if (preset !== void 0 && !MOTION_PRESETS[preset]) {
@@ -19049,21 +18988,21 @@ async function motionInstall(opts, log) {
19049
18988
  const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
19050
18989
  log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
19051
18990
  log.plain("");
19052
- log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path20.sep)}`);
18991
+ log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path21.sep)}`);
19053
18992
  let copied = 0, skipped = 0;
19054
18993
  try {
19055
18994
  for (const rel of files) {
19056
- const dest = path20.join(root, MOTION_DEST, rel);
19057
- const exists4 = await fs21.access(dest).then(() => true, () => false);
18995
+ const dest = path21.join(root, MOTION_DEST, rel);
18996
+ const exists4 = await fs22.access(dest).then(() => true, () => false);
19058
18997
  if (!opts.force && exists4) {
19059
18998
  skipped++;
19060
- log.dim(` skipped ${path20.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
18999
+ log.dim(` skipped ${path21.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
19061
19000
  continue;
19062
19001
  }
19063
- await fs21.mkdir(path20.dirname(dest), { recursive: true });
19064
- await fs21.copyFile(path20.join(srcDir, rel), dest);
19002
+ await fs22.mkdir(path21.dirname(dest), { recursive: true });
19003
+ await fs22.copyFile(path21.join(srcDir, rel), dest);
19065
19004
  copied++;
19066
- log.dim(` ${path20.join(MOTION_DEST, rel)}`);
19005
+ log.dim(` ${path21.join(MOTION_DEST, rel)}`);
19067
19006
  }
19068
19007
  } catch (err) {
19069
19008
  log.error(`Copy failed: ${String(err)}`);
@@ -19104,7 +19043,7 @@ async function motionConstraints(opts, log) {
19104
19043
  }
19105
19044
  const doc = directionConstraint(dir, speed, duration);
19106
19045
  const out = opts.out ?? "constraints.json";
19107
- await fs21.writeFile(out, JSON.stringify(doc));
19046
+ await fs22.writeFile(out, JSON.stringify(doc));
19108
19047
  if (opts.json) {
19109
19048
  writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
19110
19049
  return;
@@ -19169,12 +19108,7 @@ ${c.bold("Usage")}
19169
19108
  genex publish [options] Build + push + make live, then list it in the gallery.
19170
19109
  genex make-remixable [options] Make THIS game remixable by everyone \u2014 migrates a
19171
19110
  private source onto a public managed genex repo.
19172
- genex domain <sub> [host] Play this game on a domain you own:
19173
- add | list | verify | remove. "add" opens a
19174
- one-click approval at your DNS host \u2014 no
19175
- records to copy or paste.
19176
19111
  genex model "<prompt>" [options] Generate a 3D model (GLB); prints a public asset URL.
19177
- genex skybox "<prompt>" [options] Generate a skybox (equirect); prints a public asset URL.
19178
19112
  genex sfx "<prompt>" [options] Generate a sound effect (mp3); prints a public asset URL.
19179
19113
  genex music "<prompt>" [options] Generate an instrumental music track (mp3); prints a
19180
19114
  public asset URL. --duration 10-300s (default 90).
@@ -19188,7 +19122,7 @@ ${c.bold("Usage")}
19188
19122
  genex video "<prompt>" [options] Generate a video (mp4); prints a public asset URL.
19189
19123
  genex character "<brief>" Generate 3 character concepts for user selection.
19190
19124
  genex character preview <concept-id>
19191
- Generate an unremeshed Meshy 6 preview of one
19125
+ Generate an unremeshed Meshy 7 preview of one
19192
19126
  explicitly user-approved concept candidate.
19193
19127
  genex character finalize <preview-id>
19194
19128
  Finalize an approved preview as a 10,000-face
@@ -19204,6 +19138,10 @@ ${c.bold("Usage")}
19204
19138
  multi-beat moves; --action <id-or-query> still
19205
19139
  picks a catalog clip.
19206
19140
  genex character motions <id> List the clips installed on that character.
19141
+ genex character adopt [<id>] Claim a character from a game you remixed, so it
19142
+ can take new animations. With no id it adopts the
19143
+ body this game already wears. Free \u2014 it copies,
19144
+ it does not generate.
19207
19145
  genex creature "<desc>" One-shot rigged enemy/creature (biped-shaped
19208
19146
  bodies only \u2014 Meshy rig limit): model \u2192 rig \u2192
19209
19147
  bind library clips via --animation. No approval
@@ -19245,12 +19183,8 @@ ${c.bold("Usage")}
19245
19183
  Pure local work (no API); the PNG tools read
19246
19184
  a file or URL, audit scans the project.
19247
19185
 
19248
- ${c.bold("Options for the generators (`model` `skybox` `sfx` `music` `voice` `texture` `image` `video`)")}
19186
+ ${c.bold("Options for the generators (`model` `sfx` `music` `voice` `texture` `image` `video`)")}
19249
19187
  --terrain (texture) seamless tiling surface for terrain/ground.
19250
- --raw (skybox) send the prompt exactly as written \u2014 skip the
19251
- appended environment-only clause AND the refusal on
19252
- prompts that name structures/objects (a sky is sky only;
19253
- landforms and weather always pass).
19254
19188
  --duration <sec> (sfx, video, music) target clip length in seconds;
19255
19189
  (character/creature animate) 3-6 s of reference video for
19256
19190
  verbs that route to AI video (default 3) \u2014 longer gives a
@@ -19414,11 +19348,8 @@ ${c.bold("Examples")}
19414
19348
  genex publish
19415
19349
  genex publish --categories games,vfx
19416
19350
  genex make-remixable
19417
- genex domain add play.mygame.com
19418
- genex domain list
19419
19351
  genex publish --no-push --title "My Game"
19420
19352
  genex model "weathered wooden barrel with iron bands"
19421
- genex skybox "golden hour over a misty mountain range"
19422
19353
  genex sfx "punchy laser zap" --duration 2
19423
19354
  genex music "brooding orchestral battle loop, seamless loop, no intro or outro" --no-wait
19424
19355
  genex voice "You shall not pass!" --voice gruff
@@ -19539,9 +19470,6 @@ function parseArgs(argv) {
19539
19470
  case "--no-auth":
19540
19471
  parsed.options.noAuth = true;
19541
19472
  break;
19542
- case "--no-open":
19543
- parsed.options.noOpen = true;
19544
- break;
19545
19473
  case "--no-push":
19546
19474
  parsed.options.noPush = true;
19547
19475
  break;
@@ -19649,7 +19577,7 @@ function parseArgs(argv) {
19649
19577
  } else if (parsed.command === "controller") {
19650
19578
  (parsed.options.selectors ??= []).push(arg);
19651
19579
  } else if (parsed.command === "character") {
19652
- if ((parsed.options.name === "animate" || parsed.options.name === "motions") && !parsed.options.characterId) {
19580
+ if ((parsed.options.name === "animate" || parsed.options.name === "motions" || parsed.options.name === "adopt") && !parsed.options.characterId) {
19653
19581
  parsed.options.characterId = arg;
19654
19582
  } else if (parsed.options.name === "animate") {
19655
19583
  (parsed.options.verbs ??= []).push(arg);
@@ -19657,7 +19585,7 @@ function parseArgs(argv) {
19657
19585
  parsed.options.conceptId = arg;
19658
19586
  } else if (parsed.options.name === "finalize" && !parsed.options.previewId) {
19659
19587
  parsed.options.previewId = arg;
19660
- } else if (!["animate", "motions", "preview", "finalize"].includes(parsed.options.name ?? "")) {
19588
+ } else if (!["animate", "motions", "preview", "finalize", "adopt"].includes(parsed.options.name ?? "")) {
19661
19589
  parsed.options.name = `${parsed.options.name} ${arg}`;
19662
19590
  } else {
19663
19591
  parsed.error = `Unexpected argument: ${arg}`;
@@ -19676,12 +19604,6 @@ function parseArgs(argv) {
19676
19604
  } else {
19677
19605
  (parsed.options.selectors ??= []).push(arg);
19678
19606
  }
19679
- } else if (parsed.command === "domain") {
19680
- if (!parsed.options.hostname) parsed.options.hostname = arg;
19681
- else {
19682
- parsed.error = `Unexpected argument: ${arg}`;
19683
- return parsed;
19684
- }
19685
19607
  } else if (parsed.command === "animations") {
19686
19608
  parsed.options.query = parsed.options.query ? `${parsed.options.query} ${arg}` : arg;
19687
19609
  } else {
@@ -19991,6 +19913,21 @@ async function main() {
19991
19913
  ci: !!process.env.CI
19992
19914
  });
19993
19915
  try {
19916
+ if (parsed.command === "skybox") {
19917
+ log.error("The generated-sky lane is paused \u2014 `genex skybox` is not available.");
19918
+ log.plain(
19919
+ [
19920
+ ` ${c.cyan("\u2192")} Build the sky in code instead \u2014 your call how: a background colour with`,
19921
+ ` fog, a gradient dome, or three.js's own ${c.cyan("Sky")} shader with a real sun. It costs`,
19922
+ " no credits, no download and no phone memory, and the sun stays in sync with",
19923
+ " your lighting.",
19924
+ ` ${c.cyan("\u2192")} A distant skyline or landmark was never a sky: build it as geometry, or`,
19925
+ ` generate it with ${c.cyan("npx genex model")}.`
19926
+ ].join("\n")
19927
+ );
19928
+ process.exitCode = 1;
19929
+ return;
19930
+ }
19994
19931
  if (GEN_KINDS.has(parsed.command)) {
19995
19932
  await runGenerate(parsed.command, {
19996
19933
  ...parsed.options,
@@ -20017,9 +19954,6 @@ async function main() {
20017
19954
  case "make-remixable":
20018
19955
  await runMakeRemixable(parsed.options);
20019
19956
  break;
20020
- case "domain":
20021
- await runDomain(parsed.options);
20022
- break;
20023
19957
  case "controller":
20024
19958
  await runController({ ...parsed.options, kind: parsed.options.name });
20025
19959
  break;
@@ -20036,6 +19970,8 @@ async function main() {
20036
19970
  await runCharacterPreview(parsed.options);
20037
19971
  } else if (parsed.options.name === "finalize") {
20038
19972
  await runCharacterFinalize(parsed.options);
19973
+ } else if (parsed.options.name === "adopt") {
19974
+ await runCharacterAdopt(parsed.options);
20039
19975
  } else {
20040
19976
  await runCharacter({ ...parsed.options, prompt: parsed.options.name });
20041
19977
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "1.4.1-dev.381",
3
+ "version": "1.4.2",
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": {
@@ -32,7 +32,10 @@ export interface MeshyCharacterManifest {
32
32
  };
33
33
  provenance?: {
34
34
  provider: "meshy";
35
- aiModel: "meshy-6";
35
+ /** Open on purpose. This type is vendored into a player's game and frozen
36
+ * there at install time, so a future model would make an older copy of
37
+ * this file describe its own manifest as invalid. */
38
+ aiModel: string;
36
39
  apiVersion?: string;
37
40
  meshyApiVersion?: string;
38
41
  poseMode: "a-pose" | "t-pose";
@@ -225,7 +225,7 @@ first one is the one a screenshot of the whole arena will not show you.
225
225
 
226
226
  ## Troubleshooting
227
227
 
228
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
228
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
229
229
  - **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
230
230
  this texture generation. Tell the user the facts the CLI printed: their balance,
231
231
  this generation's cost, and when their credits refill. Then offer to continue the
@@ -151,7 +151,7 @@ set belongs to `$genex-ai-hud` — both build on `npx genex image`/`video`.
151
151
 
152
152
  ## Troubleshooting
153
153
 
154
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
154
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
155
155
  - **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
156
156
  This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
157
157
  - **Nothing plays / black surface** — the first `video.play()` must run inside a user
@@ -112,8 +112,8 @@ Genex asset and runtime lanes are:
112
112
 
113
113
  - `npx genex model "<prompt>"`—a static GLB → `$genex-ai-model`
114
114
  - `npx genex texture "<prompt>"`—a generated surface → `$genex-ai-texture`
115
- - `npx genex skybox "<prompt>"`—a 360° environment-only sky
116
- `$genex-ai-skybox`
115
+ - sky, horizon, time of day, weather mood—built in code, never generated; no
116
+ command and no skill owns it, so pick what this game needs
117
117
  - `npx genex sfx "<prompt>"` / `music` / `voice` →
118
118
  `$genex-ai-sfx`, `$genex-ai-music`, `$genex-ai-voice`
119
119
  - `npx genex image "<prompt>"` / `video`—2D and motion art →
@@ -146,9 +146,10 @@ Assets row.
146
146
  The request sets the floor for the asset plan, never the ceiling: a world
147
147
  should feel dressed and alive, so plan the detail its genre implies — and pick
148
148
  the more efficient engine per object. Paid generation buys hero pieces (the
149
- player's character, key props, the skybox, music); procedural code is the
149
+ player's character, key props, music); procedural code is the
150
150
  unlimited detail engine for structures, buildings, modular kits, and repeated
151
- or varied dressing (`$genex-threejs-procedural-assets` owns that lane). Mixing
151
+ or varied dressing (`$genex-threejs-procedural-assets` owns that lane) — and
152
+ for the sky, which is never generated at all. Mixing
152
153
  both in one scene is the normal way to build, never a fallback. In focused
153
154
  work, stay inside the touched scope. Run independent planned generations with
154
155
  `--no-wait`, scaffold while they land, and preserve their IDs, URLs, and
@@ -54,7 +54,7 @@ copy demo architecture.
54
54
  | fixed-view screenshots, input direction, facing, temporal and budget evidence | `$genex-threejs-visual-validation` |
55
55
  | a static generated GLB for a concrete prop, vehicle, building, or non-rigged object | `$genex-ai-model` |
56
56
  | generated surface or terrain texture with real-world UV scale | `$genex-ai-texture` |
57
- | environment-only 360° sky | `$genex-ai-skybox` |
57
+ | sky, skybox, horizon, time of day, weather mood, night or space backdrop | build it in code in the scene—there is no sky command and no owning skill, so pick the technique this game needs |
58
58
  | poster, sign, sprite, decal, reference sheet, or other 2D art | `$genex-ai-image` |
59
59
  | in-world motion art or another requested video | `$genex-ai-video` |
60
60
  | sound effect, one looping music bed, or a short spoken line | `$genex-ai-sfx`, `$genex-ai-music`, or `$genex-ai-voice` |
@@ -40,7 +40,6 @@ the asset lives in Genex storage (R2), not your repo, so there's nothing to comm
40
40
 
41
41
  ```bash
42
42
  npx genex model "weathered wooden barrel" # a 3D mesh (GLB)
43
- npx genex skybox "golden hour over mountains" # a 360° sky + lighting
44
43
  npx genex sfx "punchy laser zap" --duration 2 # a sound effect (mp3)
45
44
  npx genex texture "mossy cobblestone" --terrain # a tiling surface texture
46
45
  npx genex image "vintage travel poster" # a picture (poster/sign/sprite/decal)
@@ -51,8 +50,11 @@ npx genex video "waterfall mist drifting" --loop # a video clip (screen/bac
51
50
  `npx genex` resolve to the right CLI.)
52
51
 
53
52
  Each has a focused skill with the exact loader code — `$genex-ai-model`,
54
- `$genex-ai-skybox`, `$genex-ai-sfx`, `$genex-ai-texture`, `$genex-ai-image`,
55
- `$genex-ai-video`. Two more workflow skills draw UI art on the same commands,
53
+ `$genex-ai-sfx`, `$genex-ai-texture`, `$genex-ai-image`,
54
+ `$genex-ai-video`. The sky is not on that list: there is no sky command, so
55
+ every sky, horizon, and time of day is built in code — your call how, from a
56
+ background colour and fog to an atmospheric sky shader with a real sun. Two
57
+ more workflow skills draw UI art on the same commands,
56
58
  for games whose style wants it: `$genex-ai-menu` (a cinematic menu — looping
57
59
  video backdrop + DOM buttons) and `$genex-ai-hud` (a HUD sprite set with
58
60
  masked fills). A restrained interface in clean CSS is a finished UI too —
@@ -159,7 +161,7 @@ and re-link the clone to the same live game:
159
161
  ```bash
160
162
  git clone <the game's repo url> my-game && cd my-game
161
163
  npm install
162
- npx @genex-ai/cli-demo@dev link <slug> # slug = the name in the play URL
164
+ npx @genex-ai/cli-demo@latest link <slug> # slug = the name in the play URL
163
165
  ```
164
166
 
165
167
  Don't know the slug? **`npx genex list`** prints every game on your account —
@@ -186,7 +188,7 @@ Safe to run any time — genex-owned skills are refreshed to the latest version,
186
188
  and your own files are never touched:
187
189
 
188
190
  ```bash
189
- npx @genex-ai/cli-demo@dev init
191
+ npx @genex-ai/cli-demo@latest init
190
192
  ```
191
193
 
192
194
  Use `--force` only if you intentionally want your own existing files overwritten
@@ -7,7 +7,7 @@ description: Make a Genex Three.js game phone-survivable with the vendored adapt
7
7
 
8
8
  Phones enforce a hard GPU-memory ceiling desktops don't have: iOS silently
9
9
  kills the page when a game allocates too much, and the kill arrives at BOOT —
10
- exactly when a skybox, models, and the post stack all decode at once. This
10
+ exactly when textures, models, and the post stack all decode at once. This
11
11
  skill wires the vendored quality kit so the game boots conservatively on
12
12
  phones, steps quality UP when the device proves smooth, and never gets uglier
13
13
  on desktop. **You can recover from ugly; you cannot recover from a killed
@@ -115,22 +115,27 @@ reads; pause your own loop and audio on `visibilitychange` too.
115
115
 
116
116
  ## Generated assets: load through the rungs
117
117
 
118
- Generated skyboxes are 8192×4096 about 178 MB decoded, over half a phone's
119
- whole budget in one texture. Every generated image asset ships with downscale
120
- rungs; phones must load through them:
118
+ A generated texture original can be several thousand pixels square a handful
119
+ of them decode into more memory than a phone has. Every generated image asset
120
+ ships with downscale rungs; phones must load through them:
121
121
 
122
122
  ```ts
123
123
  import { detectTier } from "./controllers/quality/tier.ts";
124
124
  import { loadTextureWithFallback } from "./controllers/quality/pick-asset.ts";
125
125
 
126
- const texture = await loadTextureWithFallback(SKYBOX_URL, tier, (u) =>
126
+ const texture = await loadTextureWithFallback(TEXTURE_URL, tier, (u) =>
127
127
  new THREE.TextureLoader().loadAsync(u),
128
128
  );
129
129
  ```
130
130
 
131
- Desktop loads the original, phones the `@2048` rung (~11 MB), and a missing
132
- rung falls back to the original — never a broken boot. The `$genex-ai-skybox`
133
- and `$genex-ai-texture` skills show the wiring in place.
131
+ Desktop loads the original, phones the small rung, and a missing rung falls
132
+ back to the original — never a broken boot. The `$genex-ai-texture` skill
133
+ shows the wiring in place.
134
+
135
+ The sky costs nothing here: it is built in code, so there
136
+ is no panorama to download, no rung ladder, and no boot-time decode. Games
137
+ carrying a generated equirect sky predate that and still load through the
138
+ `skybox-equirect` rungs the kit resolves for them.
134
139
 
135
140
  ## Generated models: load through the rungs
136
141
 
@@ -170,7 +175,7 @@ source, fetched only as the fallback of last resort. Meshy characters take
170
175
  the same ladder through
171
176
  `loadMeshyCharacter(manifestUrl, { loader: gltfLoader.loader, modelUrlCandidates: (u) => [pickModel(u, tier, { ktx2: gltfLoader.ktx2 }), pickModel(u, tier), u] })`.
172
177
  KTX2-capable games can also pass `ktx2Load` to `loadTextureWithFallback` so
173
- skyboxes/textures use their `.ktx2` variants.
178
+ textures use their `.ktx2` variants.
174
179
 
175
180
  ## Quality picker in settings
176
181
 
@@ -219,9 +224,10 @@ runtime).
219
224
  (antialias) are locked wrong for the session. Tier first, renderer second.
220
225
  - Governor wired to context-creation flags → no-op at best. Runtime knobs
221
226
  only: DPR, post toggles, distances, frame cap.
222
- - Skybox loaded with a bare `TextureLoader.loadAsync(SKYBOX_URL)` on a game
223
- that targets phones → ~178 MB decoded; route it through
224
- `loadTextureWithFallback`.
227
+ - A generated texture loaded with a bare `TextureLoader.loadAsync(url)` on a
228
+ game that targets phones → the full-size original decoded; route it through
229
+ `loadTextureWithFallback`. (Same for the equirect sky of a game that predates
230
+ the procedural sky lane — the `skybox-equirect` rungs still resolve.)
225
231
  - Quality stepping on every spike → shader compiles read as slowness. The
226
232
  governor requires SUSTAINED slow windows; do not shorten them.
227
233
  - Testing quality tiers by resizing the desktop window → tiers key off touch +
@@ -7,7 +7,7 @@ when, or teaching the governor a game-specific step.
7
7
 
8
8
  The phone budget is a hard ceiling that includes GPU memory (textures,
9
9
  framebuffers), and the OS kill arrives with no catchable event. Boot is the
10
- danger window: skybox + models + post targets decode together. So phone tiers
10
+ danger window: big textures + models + post targets decode together. So phone tiers
11
11
  START one notch below what the heuristics suggest and the governor steps UP
12
12
  after ~20 smooth seconds. The cost of guessing low is moments of softness; the
13
13
  cost of guessing high is a dead page.
@@ -67,7 +67,7 @@ type.
67
67
 
68
68
  **3. Never stall the build on art.** Scaffold, boot wiring, the core loop,
69
69
  and the gameplay-logic modules keep moving in parallel whatever the UI is
70
- doing — and the core asset set (hero model, ground texture, skybox, sfx) is
70
+ doing — and the core asset set (hero model, ground texture, sky, sfx) is
71
71
  prompted from the game IDEA, so it can launch up front behind placeholders.
72
72
  Nothing ready to build waits on an art answer: decide, say the decision in
73
73
  one line, keep going.
@@ -134,13 +134,9 @@ everything twice.
134
134
  visible; the lock may only ever engage from the Play/Resume click or a
135
135
  gameplay canvas click (the phase binding `setPaused(phase !== "playing")` is
136
136
  what guarantees this — check it rides `setPhase`, not the render loop).
137
- Headless caveat, measured on Chromium 151: `requestPointerLock` does NOT
138
- throw it locks, with or without a user gesture, so the lock and the
139
- unlocked cue ARE yours to assert headless. What does not survive is the
140
- both-axes look check: synthesised mouse movement cancels to a net zero
141
- delta, so turning right then left proves nothing about direction. Assert the
142
- wiring and the cue in a screenshot, and say plainly that confirming which way
143
- the view turns needs one manual pass with a real mouse.
137
+ Headless caveat: `requestPointerLock` throws in headless Chromium —
138
+ assert the wiring and the unlocked cue in a screenshot, and say plainly that
139
+ the lock itself needs one manual click (do the both-axes look check there).
144
140
  7. **Ask the scene the three things the screenshot cannot answer** (below). Run it
145
141
  once, in the same browser you already have open.
146
142
 
@@ -38,7 +38,7 @@ update, so update immediately.)
38
38
  Run exactly the command the nudge printed, from the game project root:
39
39
 
40
40
  ```bash
41
- npm i -D @genex-ai/cli-demo@dev # the genex CLI (a dev dependency)
41
+ npm i -D @genex-ai/cli-demo@latest # the genex CLI (a dev dependency)
42
42
  npm i @genex-ai/embed-sdk@latest # identity/saves SDK (ships inside the game)
43
43
  npm i @genex-ai/multiplayer@latest # multiplayer SDK (only if the game uses it)
44
44
  ```
@@ -1,133 +0,0 @@
1
- ---
2
- name: genex-ai-skybox
3
- description: Generate a real 360° skybox (equirectangular panorama) from a text prompt with `npx genex skybox`, then load it as the scene background + environment lighting in Three.js. Use when the user wants a specific photoreal/painted sky or backdrop ("sunset over the ocean", "alien purple nebula", "foggy pine forest") as an image, rather than a procedural/shader sky.
4
- ---
5
-
6
- # Genex AI · Skybox
7
-
8
- Turn a prompt into a 360° **equirectangular** panorama that wraps the scene and
9
- also lights it (image-based lighting).
10
-
11
- ## When to use this vs. a procedural sky
12
-
13
- - **Use `npx genex skybox`** for a specific, describable ENVIRONMENT — "golden
14
- hour over misty mountains", "rolling thunderstorm, towering cumulonimbus",
15
- "alien purple nebula", "hazy dusk sky over a distant sea horizon". You get a
16
- real image.
17
- - Build a procedural, animated, time-of-day sky directly in the game's existing
18
- scene/runtime only when that is the requested deliverable; this image lane
19
- owns static generated panoramas.
20
-
21
- **A skybox is environment only — never content.** Buildings, trees, ruins,
22
- towers, or any object painted into the sky render at infinite distance: they
23
- never get closer as the player moves, they sit at the wrong parallax against
24
- the real 3D world, and they read as broken the moment the camera strafes.
25
- Structures belong in the scene as geometry (`npx genex model`); the sky
26
- carries atmosphere — light, weather, clouds, haze, stars.
27
-
28
- **Writing "no cathedral" does not rescue a prompt that also says "cathedral".**
29
- Image models are steered by the nouns they ARE given; a ban loses to any
30
- positive mention of the same thing in the same prompt. A real pilot asked for
31
- a *"cinematic dark anime gothic atmosphere, ruined cathedral courtyard mood,
32
- **no buildings or structures**"* — and got a full gothic cathedral with a
33
- courtyard balustrade and a baked ground plane. Five negations in that prompt,
34
- zero effect. So never name a structure in a skybox prompt **even to exclude
35
- it**: describe only light, colour, cloud, weather and time of day.
36
-
37
- The CLI enforces this both ways. It appends a closed-positive environment
38
- clause to every prompt (you'll see `↳ environment-only guard applied`; the
39
- full final prompt is stored with the generation), and it **refuses outright,
40
- before spending any credits**, when the prompt itself names a structure or
41
- object. Landforms are fine — "golden hour over misty mountains", "a distant
42
- sea horizon", "towering cumulonimbus" all pass. If you genuinely need content
43
- baked into the sky — e.g. a space station panorama for a scene with no world
44
- geometry — pass `--raw` to send your prompt exactly as written.
45
-
46
- ## Run
47
-
48
- ```bash
49
- npx genex skybox "<prompt>"
50
- ```
51
-
52
- Blocks until ready, then prints its public URL:
53
-
54
- ```
55
- https://assets.genex.technology/generations/<id>/skybox-equirect
56
- ```
57
-
58
- The image lives in Genex storage (R2) and loads straight from that URL — you don't
59
- download it and nothing is committed to your repo. The URL is permanent (works the same
60
- in local dev, the published game, and remixes).
61
-
62
- ## Load it as background + environment
63
-
64
- Load the equirect JPG through the quality tier's rung ladder, mark it
65
- equirectangular, and use it for both the visible background and the lighting.
66
- The bare URL is an 8192×4096 original — ~178 MB decoded, over half a phone's
67
- GPU budget in one texture — so phones must load the downscale rung the platform
68
- stores next to every skybox (`$genex-threejs-adaptive-quality`):
69
-
70
- ```ts
71
- import * as THREE from "three";
72
- import { detectTier } from "./controllers/quality/tier.ts";
73
- import { loadTextureWithFallback } from "./controllers/quality/pick-asset.ts";
74
-
75
- // the URL `npx genex skybox` printed (R2 sends CORS headers, so cross-origin works):
76
- const SKYBOX_URL = "https://assets.genex.technology/generations/<id>/skybox-equirect";
77
- const tier = detectTier(); // reuse the boot tier if you already have it
78
- const texture = await loadTextureWithFallback(SKYBOX_URL, tier, (u) =>
79
- new THREE.TextureLoader().loadAsync(u),
80
- );
81
- texture.mapping = THREE.EquirectangularReflectionMapping;
82
- texture.colorSpace = THREE.SRGBColorSpace;
83
-
84
- scene.background = texture; // visible sky
85
- scene.environment = texture; // image-based lighting on PBR materials
86
- ```
87
-
88
- Desktop gets the original; phones get the `@2048`/`@4096` rung; a missing rung
89
- falls back to the original automatically — never a broken boot.
90
-
91
- For sharper reflections/lighting, pre-filter it with `PMREMGenerator`:
92
-
93
- ```ts
94
- const pmrem = new THREE.PMREMGenerator(renderer);
95
- const envMap = pmrem.fromEquirectangular(texture).texture;
96
- scene.environment = envMap;
97
- scene.background = texture; // keep the raw texture for the visible sky
98
- // Do NOT dispose `texture` here — it IS the visible background. Dispose it only
99
- // if you later stop using it as the sky.
100
- ```
101
-
102
- ## Publish checklist
103
-
104
- - Load it from the **URL** the command printed — absolute and permanent, so it resolves
105
- the same in local dev, the published game, and remixes. Nothing to commit.
106
- - Don't copy the image into `public/assets/` — generated assets live in R2, not the repo.
107
-
108
- ## Options
109
-
110
- - `--raw` — send the prompt exactly as written, skipping the appended
111
- environment-only guard (for the rare sky that must carry content).
112
- - `--no-wait` — enqueue and return immediately with the generation id; pick
113
- the result up later with `npx genex wait <id>` (safe to re-run — it attaches
114
- to the SAME generation).
115
- - `--api-url <url>` — override the API base (local dev).
116
-
117
- ## Troubleshooting
118
-
119
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
120
- - **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
121
- this skybox generation. Tell the user the facts the CLI printed: their balance, this
122
- generation's cost, and when their credits refill. Then offer to continue the build
123
- with a procedurally-coded placeholder (a gradient/procedural sky or a plain
124
- `scene.background` color) and mark the spot with
125
- `// TODO(genex): regenerate when credits refill` so the real asset is one command
126
- away later. Do not stop the session over this.
127
- - **"Email not verified" (`email_verification_required`)** — generation credits
128
- unlock after the account's email is verified. Give the user the verify link the
129
- CLI printed, wait for them to confirm, then re-run the command.
130
- - **Sky looks too dark/bright** — adjust `renderer.toneMappingExposure`, or scale
131
- `scene.environment` influence via material `envMapIntensity`.
132
- - **Seam/pole artifacts** — that's inherent to equirect images; keep the camera
133
- away from looking straight up/down, or use PMREM for the lighting.