@genex-ai/cli-demo 0.1.1 → 0.2.0
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 +53 -2
- package/dist/index.js +212 -14
- package/package.json +2 -2
- package/templates/skills/genex-ai-model/SKILL.md +70 -0
- package/templates/skills/genex-ai-sfx/SKILL.md +65 -0
- package/templates/skills/genex-ai-skybox/SKILL.md +73 -0
- package/templates/skills/genex-ai-texture/SKILL.md +76 -0
- package/templates/skills/genex-getting-started/SKILL.md +15 -0
- package/templates/skills/genex-threejs-skill-router/SKILL.md +19 -0
package/README.md
CHANGED
|
@@ -4,8 +4,12 @@ Set up your `~/.claude` workspace, authorize, create a game project, and publish
|
|
|
4
4
|
This is the `cli` app of the [genex monorepo](../../README.md).
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
|
-
genex init <name>
|
|
8
|
-
genex publish
|
|
7
|
+
genex init <name> # authorize + create the draft project
|
|
8
|
+
genex publish # push the built game + list it in the gallery
|
|
9
|
+
genex model "<prompt>" # generate a 3D model → assets/models/
|
|
10
|
+
genex skybox "<prompt>" # generate a 360° sky → assets/skybox/
|
|
11
|
+
genex sfx "<prompt>" # generate a sound fx → assets/sfx/
|
|
12
|
+
genex texture "<prompt>" # generate a texture → assets/textures/
|
|
9
13
|
```
|
|
10
14
|
|
|
11
15
|
`genex init` does four things:
|
|
@@ -31,6 +35,31 @@ push. Defaults: API `https://demo-api.glotech.world`, auth site
|
|
|
31
35
|
`https://demo-web.glotech.world` — override with `--api-url` / `--auth-url` (or
|
|
32
36
|
`GENEX_API_URL` / `GENEX_AUTH_URL`) for local dev.
|
|
33
37
|
|
|
38
|
+
## Generating assets
|
|
39
|
+
|
|
40
|
+
`genex model | skybox | sfx | texture "<prompt>"` turn a prompt into a real,
|
|
41
|
+
game-ready asset and download it into the project's `./assets/<kind>/` folder. The
|
|
42
|
+
command blocks until the asset is ready (poll loop), then writes the file(s):
|
|
43
|
+
|
|
44
|
+
| Command | Provider | Writes |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| `genex model "<prompt>"` | Tripo | `assets/models/<slug>.glb` |
|
|
47
|
+
| `genex skybox "<prompt>"` | Blockade Labs | `assets/skybox/<slug>.jpg` |
|
|
48
|
+
| `genex sfx "<prompt>" [--duration <s>]` | ElevenLabs | `assets/sfx/<slug>.mp3` |
|
|
49
|
+
| `genex texture "<prompt>" [--terrain]` | Gemini | `assets/textures/<slug>/basecolor.<ext>` |
|
|
50
|
+
|
|
51
|
+
Because the file lands in the project and `genex publish` commits the whole working
|
|
52
|
+
directory, generated assets **ship inside the published GitHub Pages build** — load
|
|
53
|
+
them by **relative** path (`./assets/...`) and players get them from the same origin
|
|
54
|
+
(no CORS, no runtime dependency on this API). Each kind has a scaffolded
|
|
55
|
+
`genex-ai-<kind>` skill with the exact Three.js loader code.
|
|
56
|
+
|
|
57
|
+
Auth reuses the existing `GENEX_TOKEN` (run `genex init` first). Server-side, each
|
|
58
|
+
provider is keyed by an env var (`TRIPO_API_KEY`, `BLOCKADE_LABS_API_KEY`,
|
|
59
|
+
`ELEVENLABS_API_KEY`, `GEMINI_API_KEY`); when a key is unset that kind falls back to
|
|
60
|
+
a built-in **mock** provider that returns sample assets, so the flow runs keyless.
|
|
61
|
+
`--no-wait` enqueues without downloading.
|
|
62
|
+
|
|
34
63
|
## Install / run
|
|
35
64
|
|
|
36
65
|
From the monorepo root (the CLI runs directly via Node — no build step):
|
|
@@ -165,6 +194,28 @@ pnpm test # node --test test/*.test.ts
|
|
|
165
194
|
pnpm start init --no-auth --dir ./tmp-claude
|
|
166
195
|
```
|
|
167
196
|
|
|
197
|
+
### Testing a command like an end user
|
|
198
|
+
|
|
199
|
+
`pnpm start <cmd>` runs a single command from source — fine for quick checks. To
|
|
200
|
+
exercise the CLI as a *global* command (the way users invoke `genex`), symlink a
|
|
201
|
+
`genexd` shim onto your PATH pointing at the source entrypoint. No build step —
|
|
202
|
+
it runs the TypeScript directly and picks up edits instantly:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# one-time, from this package directory: put `genexd` on PATH
|
|
206
|
+
ln -sf "$PWD/src/index.ts" "$(dirname "$(which node)")/genexd"
|
|
207
|
+
|
|
208
|
+
genexd --help # works from any directory
|
|
209
|
+
genexd init --no-auth --dir ./tmp-claude
|
|
210
|
+
|
|
211
|
+
rm "$(dirname "$(which node)")/genexd" # remove it when done
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Use `genexd` (not `genex`) so the local shim never shadows a globally installed
|
|
215
|
+
published build — this is the same shim the local scaffold flow uses (see
|
|
216
|
+
`scaffold_prompt.local.md`). `npm link` is unnecessary here: it would require a
|
|
217
|
+
`dist/` build, whereas `genexd` / `pnpm start` run the source as-is.
|
|
218
|
+
|
|
168
219
|
Relative imports use explicit `.ts` extensions and only erasable TypeScript
|
|
169
220
|
syntax is allowed — both are monorepo-wide rules (see the root README).
|
|
170
221
|
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { readFileSync } from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path10 from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/commands/init.ts
|
|
@@ -739,10 +739,11 @@ async function runPublish(opts) {
|
|
|
739
739
|
return;
|
|
740
740
|
}
|
|
741
741
|
const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
|
|
742
|
+
let pushed = null;
|
|
742
743
|
if (opts.noPush) {
|
|
743
744
|
log.info("Skipping git push (--no-push).");
|
|
744
745
|
} else {
|
|
745
|
-
await pushGame(meta.sshUrl, log);
|
|
746
|
+
pushed = await pushGame(meta.sshUrl, log);
|
|
746
747
|
}
|
|
747
748
|
log.step("Publishing to the gallery\u2026");
|
|
748
749
|
let res;
|
|
@@ -766,10 +767,17 @@ async function runPublish(opts) {
|
|
|
766
767
|
return;
|
|
767
768
|
}
|
|
768
769
|
log.plain("");
|
|
769
|
-
|
|
770
|
+
if (pushed === false) {
|
|
771
|
+
log.warn(
|
|
772
|
+
"Listed in the gallery, but the game wasn't pushed \u2014 the play URL won't work until the push succeeds."
|
|
773
|
+
);
|
|
774
|
+
log.dim(" Fix the push error above and re-run `genex publish`.");
|
|
775
|
+
} else {
|
|
776
|
+
log.success("Published. \u{1F389}");
|
|
777
|
+
}
|
|
770
778
|
if (meta.playUrl) {
|
|
771
779
|
log.dim(` play: ${meta.playUrl}`);
|
|
772
|
-
log.dim(" (GitHub Pages rebuilds ~30\u201390s after a push.)");
|
|
780
|
+
if (pushed === true) log.dim(" (GitHub Pages rebuilds ~30\u201390s after a push.)");
|
|
773
781
|
}
|
|
774
782
|
}
|
|
775
783
|
async function pushGame(sshUrl, log) {
|
|
@@ -782,8 +790,8 @@ async function pushGame(sshUrl, log) {
|
|
|
782
790
|
if (!isRepo) {
|
|
783
791
|
log.step("Initializing git\u2026");
|
|
784
792
|
if ((await run("git", ["init"])).code !== 0) {
|
|
785
|
-
log.warn("git init failed \u2014
|
|
786
|
-
return;
|
|
793
|
+
log.warn("git init failed \u2014 the game was not pushed (use `genex publish --no-push` to silence).");
|
|
794
|
+
return false;
|
|
787
795
|
}
|
|
788
796
|
}
|
|
789
797
|
await run("git", ["add", "-A"]);
|
|
@@ -792,24 +800,177 @@ async function pushGame(sshUrl, log) {
|
|
|
792
800
|
log.dim(" (no new commit to push)");
|
|
793
801
|
}
|
|
794
802
|
log.step("Pushing your game over SSH\u2026");
|
|
795
|
-
const push = await run("git", ["push", sshUrl, "HEAD:main"], {
|
|
803
|
+
const push = await run("git", ["push", sshUrl, "+HEAD:main"], {
|
|
796
804
|
GIT_SSH_COMMAND: `ssh -i ./${KEY_NAME} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new`
|
|
797
805
|
});
|
|
798
806
|
if (push.code === 0) {
|
|
799
807
|
log.success("Pushed to main.");
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
log.warn("git push failed \u2014 your live game was NOT updated.");
|
|
811
|
+
const tail = push.err.trim().split("\n").slice(-2).join(" ");
|
|
812
|
+
if (tail) log.dim(` ${tail}`);
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// src/commands/generate.ts
|
|
817
|
+
import path9 from "path";
|
|
818
|
+
|
|
819
|
+
// src/lib/assets.ts
|
|
820
|
+
import fs6 from "fs/promises";
|
|
821
|
+
import path8 from "path";
|
|
822
|
+
function slugify(input) {
|
|
823
|
+
const s = input.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/g, "");
|
|
824
|
+
return s || "asset";
|
|
825
|
+
}
|
|
826
|
+
async function downloadToFile(url, dest, headers) {
|
|
827
|
+
const res = await fetch(url, { headers });
|
|
828
|
+
if (!res.ok) throw new Error(`download failed (HTTP ${res.status}) for ${url}`);
|
|
829
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
830
|
+
await fs6.mkdir(path8.dirname(dest), { recursive: true });
|
|
831
|
+
await fs6.writeFile(dest, buf);
|
|
832
|
+
return buf.byteLength;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// src/commands/generate.ts
|
|
836
|
+
var KIND_DIR = {
|
|
837
|
+
model: "assets/models",
|
|
838
|
+
skybox: "assets/skybox",
|
|
839
|
+
sfx: "assets/sfx",
|
|
840
|
+
texture: "assets/textures"
|
|
841
|
+
};
|
|
842
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
843
|
+
function fileDest(kind, slug, f) {
|
|
844
|
+
if (kind === "texture") {
|
|
845
|
+
const name = f.role.replace(/^texture-/, "");
|
|
846
|
+
return `assets/textures/${slug}/${name}.${f.ext}`;
|
|
847
|
+
}
|
|
848
|
+
return `${KIND_DIR[kind]}/${slug}.${f.ext}`;
|
|
849
|
+
}
|
|
850
|
+
async function runGenerate(kind, opts) {
|
|
851
|
+
const log = createLogger({ quiet: opts.quiet });
|
|
852
|
+
const prompt = opts.prompt?.trim();
|
|
853
|
+
if (!prompt) {
|
|
854
|
+
log.error(`Missing prompt. Usage: ${c.cyan(`genex ${kind} "<prompt>"`)}`);
|
|
855
|
+
process.exitCode = 1;
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const token = opts.token ?? await readUserToken(opts.envPath);
|
|
859
|
+
if (!token) {
|
|
860
|
+
log.error("Not authorized. Run `genex init` first to sign in.");
|
|
861
|
+
process.exitCode = 1;
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const meta = await readProject();
|
|
865
|
+
const apiUrl = getApiUrl(opts.apiUrl ?? meta?.apiUrl);
|
|
866
|
+
const options = {};
|
|
867
|
+
if (kind === "texture" && opts.terrain) options.terrain = true;
|
|
868
|
+
if (kind === "sfx" && opts.duration) options.durationSeconds = opts.duration;
|
|
869
|
+
log.plain(c.bold(`genex ${kind}`));
|
|
870
|
+
log.dim(` ${prompt}`);
|
|
871
|
+
log.plain("");
|
|
872
|
+
let id;
|
|
873
|
+
try {
|
|
874
|
+
const res = await fetch(`${apiUrl}/api/generations`, {
|
|
875
|
+
method: "POST",
|
|
876
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
877
|
+
body: JSON.stringify({ kind, prompt, options })
|
|
878
|
+
});
|
|
879
|
+
if (res.status === 401) {
|
|
880
|
+
log.error("Unauthorized \u2014 your token may have expired. Re-run `genex init`.");
|
|
881
|
+
process.exitCode = 1;
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (!res.ok) {
|
|
885
|
+
log.error(`Generation request failed (HTTP ${res.status}).`);
|
|
886
|
+
process.exitCode = 1;
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
({ id } = await res.json());
|
|
890
|
+
} catch (err) {
|
|
891
|
+
log.error(`Couldn't reach the API at ${apiUrl}: ${String(err)}`);
|
|
892
|
+
process.exitCode = 1;
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (opts.noWait) {
|
|
896
|
+
log.success(`Queued (${id}). Re-run without --no-wait to fetch the finished asset.`);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
log.step("Generating\u2026 (this can take up to a minute)");
|
|
900
|
+
const view = await poll(apiUrl, token, id, (p) => log.dim(` ${p}%`));
|
|
901
|
+
if (!view) {
|
|
902
|
+
log.error("Timed out waiting for the generation.");
|
|
903
|
+
process.exitCode = 1;
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (view.status !== "completed") {
|
|
907
|
+
log.error(`Generation ${view.status}${view.error ? `: ${view.error}` : ""}.`);
|
|
908
|
+
process.exitCode = 1;
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const slug = slugify(prompt);
|
|
912
|
+
const written = [];
|
|
913
|
+
try {
|
|
914
|
+
for (const f of view.files ?? []) {
|
|
915
|
+
const rel = fileDest(kind, slug, f);
|
|
916
|
+
const url = `${apiUrl}/api/generations/${id}/asset/${f.role}`;
|
|
917
|
+
const bytes = await downloadToFile(url, path9.join(process.cwd(), rel), {
|
|
918
|
+
Authorization: `Bearer ${token}`
|
|
919
|
+
});
|
|
920
|
+
written.push(rel);
|
|
921
|
+
log.success(`${rel} ${c.dim(`(${(bytes / 1024).toFixed(0)} KB)`)}`);
|
|
922
|
+
}
|
|
923
|
+
} catch (err) {
|
|
924
|
+
log.error(`Download failed: ${String(err)}`);
|
|
925
|
+
process.exitCode = 1;
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
log.plain("");
|
|
929
|
+
printHint(kind, written, log);
|
|
930
|
+
}
|
|
931
|
+
async function poll(apiUrl, token, id, onProgress) {
|
|
932
|
+
const deadline = Date.now() + 10 * 60 * 1e3;
|
|
933
|
+
let last = -1;
|
|
934
|
+
while (Date.now() < deadline) {
|
|
935
|
+
try {
|
|
936
|
+
const res = await fetch(`${apiUrl}/api/generations/${id}`, {
|
|
937
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
938
|
+
});
|
|
939
|
+
if (res.ok) {
|
|
940
|
+
const { generation } = await res.json();
|
|
941
|
+
if (generation.progress !== last) {
|
|
942
|
+
last = generation.progress;
|
|
943
|
+
onProgress(generation.progress);
|
|
944
|
+
}
|
|
945
|
+
if (generation.status === "completed" || generation.status === "failed") {
|
|
946
|
+
return generation;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
} catch {
|
|
950
|
+
}
|
|
951
|
+
await sleep(3e3);
|
|
804
952
|
}
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
function printHint(kind, written, log) {
|
|
956
|
+
const primary = written[0] ?? "";
|
|
957
|
+
const hint = {
|
|
958
|
+
model: `Load with GLTFLoader and add to the scene \u2014 see the genex-ai-model skill. Use the relative path "./${primary}".`,
|
|
959
|
+
skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. Path "./${primary}".`,
|
|
960
|
+
sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. Path "./${primary}".`,
|
|
961
|
+
texture: `Load with TextureLoader, set RepeatWrapping, build a MeshStandardMaterial \u2014 see genex-ai-texture. Path "./${primary}".`
|
|
962
|
+
};
|
|
963
|
+
log.success("Done. Asset saved into ./assets (commit it \u2014 `genex publish` ships it with the game).");
|
|
964
|
+
log.dim(` ${hint[kind]}`);
|
|
805
965
|
}
|
|
806
966
|
|
|
807
967
|
// src/index.ts
|
|
968
|
+
var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture"]);
|
|
808
969
|
function getVersion() {
|
|
809
970
|
try {
|
|
810
|
-
const here =
|
|
971
|
+
const here = path10.dirname(fileURLToPath2(import.meta.url));
|
|
811
972
|
const pkg = JSON.parse(
|
|
812
|
-
readFileSync(
|
|
973
|
+
readFileSync(path10.resolve(here, "..", "package.json"), "utf8")
|
|
813
974
|
);
|
|
814
975
|
return pkg.version ?? "0.0.0";
|
|
815
976
|
} catch {
|
|
@@ -821,6 +982,17 @@ var HELP = `${c.bold("genex")} \u2014 set up your ~/.claude workspace, authorize
|
|
|
821
982
|
${c.bold("Usage")}
|
|
822
983
|
genex init [<name>] [options] Scaffold + authorize + create the draft project.
|
|
823
984
|
genex publish [options] Push the built game and list it in the gallery.
|
|
985
|
+
genex model "<prompt>" [options] Generate a 3D model (GLB) into ./assets/models.
|
|
986
|
+
genex skybox "<prompt>" [options] Generate a skybox (equirect) into ./assets/skybox.
|
|
987
|
+
genex sfx "<prompt>" [options] Generate a sound effect (mp3) into ./assets/sfx.
|
|
988
|
+
genex texture "<prompt>" [options] Generate a PBR texture into ./assets/textures.
|
|
989
|
+
|
|
990
|
+
${c.bold("Options for the generators (`model` `skybox` `sfx` `texture`)")}
|
|
991
|
+
--terrain (texture) seamless tiling surface for terrain/ground.
|
|
992
|
+
--duration <sec> (sfx) target clip length in seconds.
|
|
993
|
+
--no-wait Enqueue only; don't block waiting for the asset.
|
|
994
|
+
--api-url <url> Override the API base URL.
|
|
995
|
+
--env <path> Token env file (default: ~/.genex/env).
|
|
824
996
|
|
|
825
997
|
${c.bold("Options for `init`")}
|
|
826
998
|
<name> Project name (positional; default: current directory name).
|
|
@@ -857,6 +1029,10 @@ ${c.bold("Examples")}
|
|
|
857
1029
|
genex init my-game --api-url http://localhost:3000 --auth-url http://localhost:5173
|
|
858
1030
|
genex publish
|
|
859
1031
|
genex publish --no-push --title "My Game"
|
|
1032
|
+
genex model "weathered wooden barrel with iron bands"
|
|
1033
|
+
genex skybox "golden hour over a misty mountain range"
|
|
1034
|
+
genex sfx "punchy laser zap" --duration 2
|
|
1035
|
+
genex texture "mossy cracked cobblestone" --terrain
|
|
860
1036
|
`;
|
|
861
1037
|
function parseArgs(argv) {
|
|
862
1038
|
const parsed = {
|
|
@@ -874,7 +1050,8 @@ function parseArgs(argv) {
|
|
|
874
1050
|
"--name",
|
|
875
1051
|
"--title",
|
|
876
1052
|
"--description",
|
|
877
|
-
"--timeout"
|
|
1053
|
+
"--timeout",
|
|
1054
|
+
"--duration"
|
|
878
1055
|
]);
|
|
879
1056
|
let i = 0;
|
|
880
1057
|
while (i < argv.length) {
|
|
@@ -895,6 +1072,12 @@ function parseArgs(argv) {
|
|
|
895
1072
|
case "--no-push":
|
|
896
1073
|
parsed.options.noPush = true;
|
|
897
1074
|
break;
|
|
1075
|
+
case "--no-wait":
|
|
1076
|
+
parsed.options.noWait = true;
|
|
1077
|
+
break;
|
|
1078
|
+
case "--terrain":
|
|
1079
|
+
parsed.options.terrain = true;
|
|
1080
|
+
break;
|
|
898
1081
|
case "--force":
|
|
899
1082
|
parsed.options.force = true;
|
|
900
1083
|
break;
|
|
@@ -960,6 +1143,14 @@ function applyValueFlag(options, flag, value) {
|
|
|
960
1143
|
options.timeoutSec = n;
|
|
961
1144
|
break;
|
|
962
1145
|
}
|
|
1146
|
+
case "--duration": {
|
|
1147
|
+
const n = Number(value);
|
|
1148
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
1149
|
+
throw new Error(`Invalid --duration value: ${value}`);
|
|
1150
|
+
}
|
|
1151
|
+
options.duration = n;
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
963
1154
|
}
|
|
964
1155
|
}
|
|
965
1156
|
async function main() {
|
|
@@ -986,6 +1177,13 @@ async function main() {
|
|
|
986
1177
|
log.plain(HELP);
|
|
987
1178
|
return;
|
|
988
1179
|
}
|
|
1180
|
+
if (GEN_KINDS.has(parsed.command)) {
|
|
1181
|
+
await runGenerate(parsed.command, {
|
|
1182
|
+
...parsed.options,
|
|
1183
|
+
prompt: parsed.options.name
|
|
1184
|
+
});
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
989
1187
|
switch (parsed.command) {
|
|
990
1188
|
case "init":
|
|
991
1189
|
await runInit(parsed.options);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Set up your ~/.claude workspace, authorize, create a game project, and publish (genex CLI).",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Set up your ~/.claude workspace, authorize, create a game project, generate AI assets, and publish (genex CLI).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"genex": "./dist/index.js"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-ai-model
|
|
3
|
+
description: Generate a real 3D model (GLB mesh) from a text prompt with `genex model`, then load it into the Three.js scene. Use when the user wants a specific prop, item, character, vehicle, building, or any concrete object as an actual mesh ("add a wooden barrel", "I need a spaceship", "generate a sword") rather than a procedurally-coded shape.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex AI · Model
|
|
7
|
+
|
|
8
|
+
Turn a text prompt into a real, game-ready **GLB** and drop it into the project.
|
|
9
|
+
|
|
10
|
+
## When to use this vs. procedural geometry
|
|
11
|
+
|
|
12
|
+
- **Use `genex model`** for a specific, recognizable object — a barrel, a chair, a
|
|
13
|
+
sword, a spaceship, an animal. You get a real textured mesh.
|
|
14
|
+
- **Use `$genex-threejs-procedural-geometry`** for parametric/abstract shapes,
|
|
15
|
+
terrain, or anything you want to generate in code (infinite variations, no files).
|
|
16
|
+
|
|
17
|
+
## Run
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
genex model "<prompt>"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Write a specific prompt — "weathered wooden barrel with rusted iron bands" beats
|
|
24
|
+
"barrel". The command blocks until the mesh is ready (up to ~a minute), then saves:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
assets/models/<slug>.glb
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`<slug>` is derived from the prompt (e.g. `weathered-wooden-barrel.glb`). The file
|
|
31
|
+
is **committed by `genex publish`**, so it ships inside your published game.
|
|
32
|
+
|
|
33
|
+
## Load it into the scene
|
|
34
|
+
|
|
35
|
+
Use Three.js `GLTFLoader` with the **relative** path (so it works under the
|
|
36
|
+
published subpath `https://<user>.github.io/<slug>/`):
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
40
|
+
|
|
41
|
+
const loader = new GLTFLoader();
|
|
42
|
+
const gltf = await loader.loadAsync("./assets/models/weathered-wooden-barrel.glb");
|
|
43
|
+
const model = gltf.scene;
|
|
44
|
+
model.scale.setScalar(1); // tune to taste
|
|
45
|
+
model.position.set(0, 0, 0);
|
|
46
|
+
scene.add(model);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
To place many copies, `model.clone()` per instance. For animated GLBs, drive
|
|
50
|
+
`gltf.animations` with a `THREE.AnimationMixer`.
|
|
51
|
+
|
|
52
|
+
## Publish checklist (so players see the model)
|
|
53
|
+
|
|
54
|
+
- Reference assets with **relative** paths (`./assets/...`), never absolute
|
|
55
|
+
(`/assets/...`) — GitHub Pages serves the game at a subpath.
|
|
56
|
+
- Set `base: "./"` in `vite.config.ts` before `npm run build` (see the scaffold
|
|
57
|
+
prompt's publish step).
|
|
58
|
+
- Commit the `assets/` folder — `genex publish` pushes it with the game.
|
|
59
|
+
|
|
60
|
+
## Options
|
|
61
|
+
|
|
62
|
+
- `--no-wait` — enqueue and return immediately (the file won't be downloaded;
|
|
63
|
+
re-run without `--no-wait` to fetch it).
|
|
64
|
+
- `--api-url <url>` — override the API base (local dev).
|
|
65
|
+
|
|
66
|
+
## Troubleshooting
|
|
67
|
+
|
|
68
|
+
- **"Not authorized"** — run `genex init` first (it writes your `GENEX_TOKEN`).
|
|
69
|
+
- **The mesh looks low-detail / wrong** — make the prompt more specific
|
|
70
|
+
(materials, style, parts) and regenerate; each run is a fresh asset.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-ai-sfx
|
|
3
|
+
description: Generate a real sound effect (mp3) from a text prompt with `genex sfx`, then play it in Three.js (positional or global audio). Use when the user wants a specific sound — a gunshot, footstep, pickup chime, explosion, engine hum, UI click, whoosh — triggered on a game event.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex AI · SFX
|
|
7
|
+
|
|
8
|
+
Turn a prompt into a real **mp3** sound effect and wire it to a game event.
|
|
9
|
+
|
|
10
|
+
## Run
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
genex sfx "<prompt>"
|
|
14
|
+
genex sfx "punchy laser zap, short and dry" --duration 2
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`--duration <sec>` (0.5–22) sets a target length; omit it to let the model pick.
|
|
18
|
+
Blocks until ready, then saves:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
assets/sfx/<slug>.mp3
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Committed by `genex publish`, so it ships with your game.
|
|
25
|
+
|
|
26
|
+
## Play it in Three.js
|
|
27
|
+
|
|
28
|
+
Audio needs **one `AudioListener` on the camera**. Then load the clip and play it
|
|
29
|
+
on an event. Browsers block autoplay — start audio after a user gesture (click/keydown).
|
|
30
|
+
|
|
31
|
+
**Positional** (3D, attenuates with distance — attach to an object):
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import * as THREE from "three";
|
|
35
|
+
|
|
36
|
+
const listener = new THREE.AudioListener();
|
|
37
|
+
camera.add(listener);
|
|
38
|
+
|
|
39
|
+
const buffer = await new THREE.AudioLoader().loadAsync("./assets/sfx/laser-zap.mp3");
|
|
40
|
+
|
|
41
|
+
const sound = new THREE.PositionalAudio(listener);
|
|
42
|
+
sound.setBuffer(buffer);
|
|
43
|
+
sound.setRefDistance(5);
|
|
44
|
+
laserMesh.add(sound); // follows the object
|
|
45
|
+
|
|
46
|
+
// on fire:
|
|
47
|
+
if (!sound.isPlaying) sound.play();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Global** (UI/non-spatial — same volume everywhere): use `new THREE.Audio(listener)`
|
|
51
|
+
instead of `PositionalAudio` and don't attach it to a mesh.
|
|
52
|
+
|
|
53
|
+
Reuse one loaded `buffer` across many plays; create a fresh `Audio`/`PositionalAudio`
|
|
54
|
+
(or call `sound.play()` again once stopped) per trigger.
|
|
55
|
+
|
|
56
|
+
## Publish checklist
|
|
57
|
+
|
|
58
|
+
- Relative path `./assets/sfx/...`; `base: "./"` in `vite.config.ts`; commit `assets/`.
|
|
59
|
+
|
|
60
|
+
## Troubleshooting
|
|
61
|
+
|
|
62
|
+
- **No sound** — the `AudioContext` is suspended until a user gesture; trigger the
|
|
63
|
+
first play from a click/keydown. Confirm the camera has an `AudioListener`.
|
|
64
|
+
- **Too quiet/loud** — `sound.setVolume(0..1)`; for positional, tune
|
|
65
|
+
`setRefDistance` / `setRolloffFactor`.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-ai-skybox
|
|
3
|
+
description: Generate a real 360° skybox (equirectangular panorama) from a text prompt with `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 `genex skybox`** for a specific, recognizable sky/backdrop you can
|
|
14
|
+
describe — "golden hour over misty mountains", "stormy alien sky", "city at
|
|
15
|
+
night". You get a real image.
|
|
16
|
+
- **Use `$genex-threejs-atmosphere-aerial-perspective`** (or
|
|
17
|
+
`$genex-threejs-volumetric-clouds`) for a fully procedural, animated,
|
|
18
|
+
time-of-day sky generated in shaders.
|
|
19
|
+
|
|
20
|
+
## Run
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
genex skybox "<prompt>"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Blocks until ready, then saves:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
assets/skybox/<slug>.jpg
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The JPG is **committed by `genex publish`**, so it ships with your game.
|
|
33
|
+
|
|
34
|
+
## Load it as background + environment
|
|
35
|
+
|
|
36
|
+
Load the equirect JPG, mark it equirectangular, and use it for both the visible
|
|
37
|
+
background and the lighting:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import * as THREE from "three";
|
|
41
|
+
|
|
42
|
+
const texture = await new THREE.TextureLoader().loadAsync(
|
|
43
|
+
"./assets/skybox/golden-hour-over-misty-mountains.jpg",
|
|
44
|
+
);
|
|
45
|
+
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
46
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
47
|
+
|
|
48
|
+
scene.background = texture; // visible sky
|
|
49
|
+
scene.environment = texture; // image-based lighting on PBR materials
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For sharper reflections/lighting, pre-filter it with `PMREMGenerator`:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const pmrem = new THREE.PMREMGenerator(renderer);
|
|
56
|
+
const envMap = pmrem.fromEquirectangular(texture).texture;
|
|
57
|
+
scene.environment = envMap;
|
|
58
|
+
scene.background = texture; // keep the raw texture for the visible sky
|
|
59
|
+
texture.dispose; // (dispose the PMREM source later if you stop using it)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Publish checklist
|
|
63
|
+
|
|
64
|
+
- Use the **relative** path `./assets/skybox/...` (GitHub Pages serves at a subpath).
|
|
65
|
+
- `base: "./"` in `vite.config.ts` before `npm run build`.
|
|
66
|
+
- Commit `assets/` — `genex publish` pushes it with the game.
|
|
67
|
+
|
|
68
|
+
## Troubleshooting
|
|
69
|
+
|
|
70
|
+
- **Sky looks too dark/bright** — adjust `renderer.toneMappingExposure`, or scale
|
|
71
|
+
`scene.environment` influence via material `envMapIntensity`.
|
|
72
|
+
- **Seam/pole artifacts** — that's inherent to equirect images; keep the camera
|
|
73
|
+
away from looking straight up/down, or use PMREM for the lighting.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-ai-texture
|
|
3
|
+
description: Generate a real photoreal surface texture (PBR base-color image) from a text prompt with `genex texture`, then apply it as a tiling material on meshes or terrain in Three.js. Use `--terrain` for seamless ground. Use when the user wants a specific photoreal material on a surface ("rusty metal floor", "mossy cobblestone path", "grassy terrain") rather than a procedural/shader material.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex AI · Texture
|
|
7
|
+
|
|
8
|
+
Turn a prompt into a real, tileable **base-color** texture image and apply it as a
|
|
9
|
+
material on a primitive, a mesh, or terrain.
|
|
10
|
+
|
|
11
|
+
## When to use this vs. procedural materials
|
|
12
|
+
|
|
13
|
+
- **Use `genex texture`** for a specific photoreal surface you can describe —
|
|
14
|
+
"weathered Roman cobblestone", "cracked desert clay", "oak planks". You get a
|
|
15
|
+
real raster image.
|
|
16
|
+
- **Use `$genex-threejs-procedural-materials`** for stylized/abstract or
|
|
17
|
+
fully-parametric materials authored in shaders (instant, perfectly tiling, no
|
|
18
|
+
files). The two are complementary.
|
|
19
|
+
|
|
20
|
+
## Run
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
genex texture "<prompt>"
|
|
24
|
+
genex texture "lush green grass" --terrain # seamless tiling for ground/terrain
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Blocks until ready, then saves:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
assets/textures/<slug>/basecolor.<ext>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Committed by `genex publish`, so it ships with your game.
|
|
34
|
+
|
|
35
|
+
> **Scope:** v1 generates the **base-color (albedo)** map only. Normal / roughness
|
|
36
|
+
> / AO are a planned follow-up — for now set sensible `roughness`/`metalness`
|
|
37
|
+
> constants on the material.
|
|
38
|
+
|
|
39
|
+
## Apply it (tiling material)
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import * as THREE from "three";
|
|
43
|
+
|
|
44
|
+
const map = await new THREE.TextureLoader().loadAsync(
|
|
45
|
+
"./assets/textures/lush-green-grass/basecolor.jpg",
|
|
46
|
+
);
|
|
47
|
+
map.colorSpace = THREE.SRGBColorSpace;
|
|
48
|
+
map.wrapS = map.wrapT = THREE.RepeatWrapping;
|
|
49
|
+
map.repeat.set(8, 8); // tile count — raise for large surfaces
|
|
50
|
+
map.anisotropy = renderer.capabilities.getMaxAnisotropy();
|
|
51
|
+
|
|
52
|
+
const material = new THREE.MeshStandardMaterial({ map, roughness: 0.9, metalness: 0 });
|
|
53
|
+
groundMesh.material = material; // e.g. a PlaneGeometry ground
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Terrain
|
|
57
|
+
|
|
58
|
+
Generate with `--terrain`, lay a large `PlaneGeometry` (rotated flat), and raise
|
|
59
|
+
`repeat` so the texture tiles densely across it:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const ground = new THREE.Mesh(new THREE.PlaneGeometry(200, 200), material);
|
|
63
|
+
ground.rotation.x = -Math.PI / 2;
|
|
64
|
+
map.repeat.set(64, 64);
|
|
65
|
+
scene.add(ground);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Publish checklist
|
|
69
|
+
|
|
70
|
+
- Relative path `./assets/textures/...`; `base: "./"` in `vite.config.ts`; commit `assets/`.
|
|
71
|
+
|
|
72
|
+
## Troubleshooting
|
|
73
|
+
|
|
74
|
+
- **Visible tiling seams** — use `--terrain` (tuned for seamless edges), lower the
|
|
75
|
+
`repeat`, or blend two textures. Perfect seamlessness is a known v1 limitation.
|
|
76
|
+
- **Colors look washed/dark** — ensure `map.colorSpace = THREE.SRGBColorSpace`.
|
|
@@ -21,6 +21,21 @@ Start with `$genex-threejs-skill-router` for broad game or graphics requests.
|
|
|
21
21
|
It routes the agent to focused skills for cameras, procedural geometry,
|
|
22
22
|
materials, atmosphere, water, VFX, post-processing, and visual validation.
|
|
23
23
|
|
|
24
|
+
## Generating real assets
|
|
25
|
+
|
|
26
|
+
Beyond procedural code, Genex can generate **real, AI-made assets** from a prompt
|
|
27
|
+
and drop them into `./assets/` (committed when you publish):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
genex model "weathered wooden barrel" # a 3D mesh (GLB)
|
|
31
|
+
genex skybox "golden hour over mountains" # a 360° sky + lighting
|
|
32
|
+
genex sfx "punchy laser zap" --duration 2 # a sound effect (mp3)
|
|
33
|
+
genex texture "mossy cobblestone" --terrain # a tiling surface texture
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Each has a focused skill with the exact loader code — `$genex-ai-model`,
|
|
37
|
+
`$genex-ai-skybox`, `$genex-ai-sfx`, `$genex-ai-texture`.
|
|
38
|
+
|
|
24
39
|
Your existing files were left untouched. `genex init` only adds what is missing.
|
|
25
40
|
|
|
26
41
|
## Re-running setup
|
|
@@ -37,6 +37,25 @@ map, execution order, and acceptance gate.
|
|
|
37
37
|
| render-target ownership, pass ordering, depth/normal/history signals | `$genex-threejs-image-pipeline` |
|
|
38
38
|
| fixed-view captures, seed sweeps, browser and GPU evidence | `$genex-threejs-visual-validation` |
|
|
39
39
|
|
|
40
|
+
## Real (AI-generated) assets — `genex` commands
|
|
41
|
+
|
|
42
|
+
When the user wants a **specific, recognizable asset** (a named object, a described
|
|
43
|
+
sky, a particular sound or surface) rather than something authored in code, generate
|
|
44
|
+
it with a `genex` command. Each drops a real file into `./assets/` (committed by
|
|
45
|
+
`genex publish`), and its skill has the exact Three.js loader code.
|
|
46
|
+
|
|
47
|
+
| Work needed | Generate with | Skill |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| a specific prop / item / character / vehicle as a real mesh | `genex model "<prompt>"` | `$genex-ai-model` |
|
|
50
|
+
| a described 360° sky / backdrop + image-based lighting | `genex skybox "<prompt>"` | `$genex-ai-skybox` |
|
|
51
|
+
| a specific sound effect tied to an event | `genex sfx "<prompt>"` | `$genex-ai-sfx` |
|
|
52
|
+
| a photoreal surface/material on a mesh or terrain | `genex texture "<prompt>" [--terrain]` | `$genex-ai-texture` |
|
|
53
|
+
|
|
54
|
+
Prefer the **procedural** skills above for abstract/parametric/animated systems
|
|
55
|
+
(geometry, materials, sky, water, VFX) — no files, infinite variation. Prefer the
|
|
56
|
+
**`genex` generators** for concrete, describable, photoreal assets. They complement
|
|
57
|
+
each other.
|
|
58
|
+
|
|
40
59
|
## Routing rules
|
|
41
60
|
|
|
42
61
|
- Start from the playable game target: player verb, scene scale, camera distance,
|