@genex-ai/cli-demo 0.30.0 → 0.33.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/dist/index.js +70 -1
- package/package.json +1 -1
- package/templates/skills/genex-ai-model/SKILL.md +9 -0
- package/templates/skills/genex-ai-sfx/SKILL.md +9 -0
- package/templates/skills/genex-ai-skybox/SKILL.md +10 -0
- package/templates/skills/genex-ai-texture/SKILL.md +10 -0
- package/templates/skills/genex-threejs-multiplayer/references/host-physics.md +55 -13
- package/templates/skills/genex-updates/SKILL.md +3 -1
package/dist/index.js
CHANGED
|
@@ -567,6 +567,34 @@ function formatUpdateRequired(body) {
|
|
|
567
567
|
const message = body.message ?? `Genex CLI ${body.clientVersion ?? getCliVersion()} is below the minimum supported version${body.minVersion ? ` ${body.minVersion}` : ""}.`;
|
|
568
568
|
return [`${c.red("\u2717")} ${message}`, ` Update now \u2014 run: ${action} (then re-run this command)`];
|
|
569
569
|
}
|
|
570
|
+
function shortDate(iso) {
|
|
571
|
+
const d = new Date(iso);
|
|
572
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
573
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
574
|
+
}
|
|
575
|
+
function formatInsufficientCredits(body) {
|
|
576
|
+
const message = body.message ?? `this generation costs ${body.price ?? "?"} credits; your balance is ${body.balance ?? 0}.`;
|
|
577
|
+
const lines = [`${c.red("\u2717")} Out of credits \u2014 ${lowerFirst(message)}`];
|
|
578
|
+
if (body.refillAt && body.refillTo) {
|
|
579
|
+
lines.push(` Credits refill to ${body.refillTo} on ${shortDate(body.refillAt)}.`);
|
|
580
|
+
}
|
|
581
|
+
if (body.url) lines.push(` Get more or check your balance: ${body.url}`);
|
|
582
|
+
return lines;
|
|
583
|
+
}
|
|
584
|
+
function formatVerificationRequired(body) {
|
|
585
|
+
const lines = [
|
|
586
|
+
`${c.red("\u2717")} Email not verified \u2014 verify your email to unlock your free generation credits.`
|
|
587
|
+
];
|
|
588
|
+
if (body.url) lines.push(` Verify here: ${body.url} (then re-run this command)`);
|
|
589
|
+
return lines;
|
|
590
|
+
}
|
|
591
|
+
function lowerFirst(s) {
|
|
592
|
+
return s ? s[0].toLowerCase() + s.slice(1) : s;
|
|
593
|
+
}
|
|
594
|
+
var structuredPrinted = /* @__PURE__ */ new WeakSet();
|
|
595
|
+
function printedStructuredError(res) {
|
|
596
|
+
return structuredPrinted.has(res);
|
|
597
|
+
}
|
|
570
598
|
async function apiFetch(url, init = {}) {
|
|
571
599
|
const headers = new Headers(init.headers);
|
|
572
600
|
if (!headers.has(CLI_VERSION_HEADER)) headers.set(CLI_VERSION_HEADER, getCliVersion());
|
|
@@ -580,6 +608,39 @@ async function apiFetch(url, init = {}) {
|
|
|
580
608
|
} catch {
|
|
581
609
|
}
|
|
582
610
|
}
|
|
611
|
+
if (res.status === 402) {
|
|
612
|
+
try {
|
|
613
|
+
const body = await res.clone().json();
|
|
614
|
+
if (body?.error === "insufficient_credits") {
|
|
615
|
+
for (const line of formatInsufficientCredits(body)) process.stderr.write(line + "\n");
|
|
616
|
+
structuredPrinted.add(res);
|
|
617
|
+
}
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (res.status === 403) {
|
|
622
|
+
try {
|
|
623
|
+
const body = await res.clone().json();
|
|
624
|
+
if (body?.error === "email_verification_required") {
|
|
625
|
+
for (const line of formatVerificationRequired(body)) process.stderr.write(line + "\n");
|
|
626
|
+
structuredPrinted.add(res);
|
|
627
|
+
}
|
|
628
|
+
} catch {
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (res.status === 503) {
|
|
632
|
+
try {
|
|
633
|
+
const body = await res.clone().json();
|
|
634
|
+
if (body?.error === "generation_paused") {
|
|
635
|
+
process.stderr.write(
|
|
636
|
+
`${c.red("\u2717")} ${body.message ?? "Generation is temporarily paused platform-wide. Try again later."}
|
|
637
|
+
`
|
|
638
|
+
);
|
|
639
|
+
structuredPrinted.add(res);
|
|
640
|
+
}
|
|
641
|
+
} catch {
|
|
642
|
+
}
|
|
643
|
+
}
|
|
583
644
|
return res;
|
|
584
645
|
}
|
|
585
646
|
|
|
@@ -1299,7 +1360,11 @@ async function pushSource(cwd, ctx, log) {
|
|
|
1299
1360
|
}
|
|
1300
1361
|
const commit = (await run("git", ["commit-tree", tree, "-m", "source"], { ...base, ...ident })).out.trim();
|
|
1301
1362
|
await run("git", ["update-ref", "refs/heads/main", commit], base);
|
|
1302
|
-
const push = await run(
|
|
1363
|
+
const push = await run(
|
|
1364
|
+
"git",
|
|
1365
|
+
["-c", "http.postBuffer=536870912", "push", "-q", pushUrl, "+refs/heads/main:main"],
|
|
1366
|
+
base
|
|
1367
|
+
);
|
|
1303
1368
|
if (push.code === 0) return true;
|
|
1304
1369
|
if (!managed) {
|
|
1305
1370
|
log.error(
|
|
@@ -1660,6 +1725,10 @@ async function runGenerate(kind, opts) {
|
|
|
1660
1725
|
process.exitCode = 1;
|
|
1661
1726
|
return;
|
|
1662
1727
|
}
|
|
1728
|
+
if (printedStructuredError(res)) {
|
|
1729
|
+
process.exitCode = 1;
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1663
1732
|
if (!res.ok) {
|
|
1664
1733
|
log.error(`Generation request failed (HTTP ${res.status}).`);
|
|
1665
1734
|
process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -106,5 +106,14 @@ scene is a ghost: players and objects pass straight through it.
|
|
|
106
106
|
## Troubleshooting
|
|
107
107
|
|
|
108
108
|
- **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
|
|
109
|
+
- **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
|
|
110
|
+
this model generation. Tell the user the facts the CLI printed: their balance, this
|
|
111
|
+
generation's cost, and when their credits refill. Then offer to continue the build
|
|
112
|
+
with a procedurally-coded placeholder (a simple Three.js primitive-based stand-in
|
|
113
|
+
mesh) and mark the spot with `// TODO(genex): regenerate when credits refill` so
|
|
114
|
+
the real asset is one command away later. Do not stop the session over this.
|
|
115
|
+
- **"Email not verified" (`email_verification_required`)** — generation credits
|
|
116
|
+
unlock after the account's email is verified. Give the user the verify link the
|
|
117
|
+
CLI printed, wait for them to confirm, then re-run the command.
|
|
109
118
|
- **The mesh looks low-detail / wrong** — make the prompt more specific
|
|
110
119
|
(materials, style, parts) and regenerate; each run is a fresh asset.
|
|
@@ -73,6 +73,15 @@ Reuse one loaded `buffer` across many plays; create a fresh `Audio`/`PositionalA
|
|
|
73
73
|
## Troubleshooting
|
|
74
74
|
|
|
75
75
|
- **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
|
|
76
|
+
- **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
|
|
77
|
+
this sound generation. Tell the user the facts the CLI printed: their balance, this
|
|
78
|
+
generation's cost, and when their credits refill. Then offer to continue the build
|
|
79
|
+
with a procedurally-coded placeholder (a small WebAudio-synthesized stub sound)
|
|
80
|
+
and mark the spot with `// TODO(genex): regenerate when credits refill` so the
|
|
81
|
+
real asset is one command away later. Do not stop the session over this.
|
|
82
|
+
- **"Email not verified" (`email_verification_required`)** — generation credits
|
|
83
|
+
unlock after the account's email is verified. Give the user the verify link the
|
|
84
|
+
CLI printed, wait for them to confirm, then re-run the command.
|
|
76
85
|
- **No sound** — the `AudioContext` is suspended until a user gesture; trigger the
|
|
77
86
|
first play from a click/keydown. Confirm the camera has an `AudioListener`.
|
|
78
87
|
- **Too quiet/loud** — `sound.setVolume(0..1)`; for positional, tune
|
|
@@ -77,6 +77,16 @@ scene.background = texture; // keep the raw texture for the visible sky
|
|
|
77
77
|
## Troubleshooting
|
|
78
78
|
|
|
79
79
|
- **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
|
|
80
|
+
- **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
|
|
81
|
+
this skybox generation. Tell the user the facts the CLI printed: their balance, this
|
|
82
|
+
generation's cost, and when their credits refill. Then offer to continue the build
|
|
83
|
+
with a procedurally-coded placeholder (a gradient/procedural sky or a plain
|
|
84
|
+
`scene.background` color) and mark the spot with
|
|
85
|
+
`// TODO(genex): regenerate when credits refill` so the real asset is one command
|
|
86
|
+
away later. Do not stop the session over this.
|
|
87
|
+
- **"Email not verified" (`email_verification_required`)** — generation credits
|
|
88
|
+
unlock after the account's email is verified. Give the user the verify link the
|
|
89
|
+
CLI printed, wait for them to confirm, then re-run the command.
|
|
80
90
|
- **Sky looks too dark/bright** — adjust `renderer.toneMappingExposure`, or scale
|
|
81
91
|
`scene.environment` influence via material `envMapIntensity`.
|
|
82
92
|
- **Seam/pole artifacts** — that's inherent to equirect images; keep the camera
|
|
@@ -82,6 +82,16 @@ scene.add(ground);
|
|
|
82
82
|
## Troubleshooting
|
|
83
83
|
|
|
84
84
|
- **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
|
|
85
|
+
- **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
|
|
86
|
+
this texture generation. Tell the user the facts the CLI printed: their balance,
|
|
87
|
+
this generation's cost, and when their credits refill. Then offer to continue the
|
|
88
|
+
build with a procedurally-coded placeholder (a procedural Three.js material in a
|
|
89
|
+
matching color) and mark the spot with
|
|
90
|
+
`// TODO(genex): regenerate when credits refill` so the real asset is one command
|
|
91
|
+
away later. Do not stop the session over this.
|
|
92
|
+
- **"Email not verified" (`email_verification_required`)** — generation credits
|
|
93
|
+
unlock after the account's email is verified. Give the user the verify link the
|
|
94
|
+
CLI printed, wait for them to confirm, then re-run the command.
|
|
85
95
|
- **Visible tiling seams** — use `--terrain` (tuned for seamless edges), lower the
|
|
86
96
|
`repeat`, or blend two textures. Perfect seamlessness is a known v1 limitation.
|
|
87
97
|
- **Colors look washed/dark** — ensure `map.colorSpace = THREE.SRGBColorSpace`.
|
|
@@ -34,24 +34,43 @@ body; when you don't you draw the smoothed stream and park the kinematic body on
|
|
|
34
34
|
|
|
35
35
|
```ts
|
|
36
36
|
import RAPIER from "@dimforge/rapier3d-compat";
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
const r2 = (n: number) => Math.round(n * 100) / 100;
|
|
38
|
+
interface BallState { x: number; y: number; z: number; q?: number[]; vx?: number; vy?: number; vz?: number }
|
|
39
|
+
|
|
40
|
+
// Proxy body: kinematic to start (owned by nobody, or by someone else). `excludeCharacterRay` keeps the
|
|
41
|
+
// character's GROUND query from treating it as walkable, so players can't stand on / ride the ball (the
|
|
42
|
+
// solver collision that drives claim-by-bump + the shove is unaffected).
|
|
43
|
+
const body = physics.createBody({
|
|
44
|
+
type: "kinematicPosition", position: [x, y, z], ccd: true,
|
|
45
|
+
userData: { controller: { excludeCharacterRay: true } },
|
|
46
|
+
});
|
|
39
47
|
ballCollider(physics.world, body, RADIUS, { friction: 0.5, restitution: 0.35, density: 0.6 });
|
|
40
48
|
let wasTouching = false, wasMine = false, claimCd = 0;
|
|
49
|
+
const MAX_SPEED = 24;
|
|
50
|
+
|
|
51
|
+
// The host seeds the canonical spawn ONCE so the object exists on `objects` for late joiners.
|
|
52
|
+
function ensureInit() {
|
|
53
|
+
if (room.isHost && room.objects.get("ball") === undefined) {
|
|
54
|
+
room.objects.claim("ball");
|
|
55
|
+
room.objects.set("ball", { x, y, z, q: [0, 0, 0, 1], vx: 0, vy: 0, vz: 0 });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
41
58
|
|
|
42
59
|
// Per-frame, BEFORE physics.step (so the kinematic proxy is in place for the character to hit it):
|
|
43
60
|
function updateBall(dt: number) {
|
|
44
|
-
let v = room.objects.get("ball");
|
|
61
|
+
let v = room.objects.get<BallState>("ball");
|
|
45
62
|
let mine = !!v?.isMine;
|
|
46
63
|
|
|
47
|
-
// Rising-EDGE claim: sustained contact yields NO new rising edge, so ONE owner holds it
|
|
48
|
-
//
|
|
49
|
-
|
|
64
|
+
// Rising-EDGE claim: sustained contact yields NO new rising edge, so ONE owner holds it (no per-frame
|
|
65
|
+
// ownership ping-pong). A short cooldown throttles a steal-war. Use HORIZONTAL distance — the feet sit
|
|
66
|
+
// at ground level while the ball centre is at y≈RADIUS, so a 3D distance would eat into your reach.
|
|
67
|
+
const dx = ballMesh.position.x - feet.x, dz = ballMesh.position.z - feet.z;
|
|
68
|
+
const touching = onFoot && Math.hypot(dx, dz) < RADIUS + REACH;
|
|
50
69
|
if (claimCd > 0) claimCd -= dt;
|
|
51
70
|
if (touching && !wasTouching && !mine && claimCd <= 0) {
|
|
52
71
|
room.objects.claim("ball"); // optimistic: isMine flips true locally THIS frame
|
|
53
72
|
claimCd = 0.25;
|
|
54
|
-
v = room.objects.get("ball"); mine = !!v?.isMine;
|
|
73
|
+
v = room.objects.get<BallState>("ball"); mine = !!v?.isMine;
|
|
55
74
|
}
|
|
56
75
|
wasTouching = touching;
|
|
57
76
|
|
|
@@ -67,22 +86,37 @@ function updateBall(dt: number) {
|
|
|
67
86
|
wasMine = mine;
|
|
68
87
|
|
|
69
88
|
if (mine) {
|
|
89
|
+
// Cap the owned body's speed: CCD stops single-step tunnelling, the cap stops runaway speed
|
|
90
|
+
// (a hard shove into a wall) from flinging the ball across the map.
|
|
91
|
+
const lv = body.linvel(), sp = Math.hypot(lv.x, lv.y, lv.z);
|
|
92
|
+
if (sp > MAX_SPEED) { const k = MAX_SPEED / sp; body.setLinvel({ x: lv.x * k, y: lv.y * k, z: lv.z * k }, true); }
|
|
70
93
|
ballMesh.position.copy(body.translation()); // draw the dynamic body I simulate
|
|
71
94
|
ballMesh.quaternion.copy(body.rotation());
|
|
72
95
|
} else if (v?.state) {
|
|
73
96
|
ballMesh.position.set(v.state.x, v.state.y, v.state.z); // draw the smoothed stream
|
|
74
97
|
body.setNextKinematicTranslation(ballMesh.position); // keep the proxy on it → I can bump → claim
|
|
98
|
+
if (Array.isArray(v.state.q)) {
|
|
99
|
+
// Rotate the collider too, or a tumbled cube collides as an axis-aligned box and pops on claim.
|
|
100
|
+
ballMesh.quaternion.set(v.state.q[0], v.state.q[1], v.state.q[2], v.state.q[3]);
|
|
101
|
+
body.setNextKinematicRotation({ x: v.state.q[0], y: v.state.q[1], z: v.state.q[2], w: v.state.q[3] });
|
|
102
|
+
}
|
|
75
103
|
}
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
// Owner-gated publish on your fixed tick (30Hz for a fast ball)
|
|
79
|
-
//
|
|
80
|
-
|
|
106
|
+
// Owner-gated publish on your fixed tick (30Hz for a fast ball). Publish MOVED-ONLY + a ~2Hz keepalive —
|
|
107
|
+
// NEVER every tick (a resting owned object republished each tick is the #1 budget-blower; it matters the
|
|
108
|
+
// moment you own more than one prop). Include VELOCITY so the next owner seeds from it without stalling.
|
|
109
|
+
let lastSent = "", lastAt = 0;
|
|
110
|
+
function publishBall() {
|
|
111
|
+
if (!room.objects.get<BallState>("ball")?.isMine) return;
|
|
81
112
|
const t = body.translation(), q = body.rotation(), lv = body.linvel();
|
|
82
|
-
|
|
83
|
-
q: [r2(q.x), r2(q.y), r2(q.z), r2(q.w)], vx: r2(lv.x), vy: r2(lv.y), vz: r2(lv.z) }
|
|
113
|
+
const net = { x: r2(t.x), y: r2(t.y), z: r2(t.z),
|
|
114
|
+
q: [r2(q.x), r2(q.y), r2(q.z), r2(q.w)], vx: r2(lv.x), vy: r2(lv.y), vz: r2(lv.z) };
|
|
115
|
+
const json = JSON.stringify(net), nowMs = Date.now();
|
|
116
|
+
if (json === lastSent && nowMs - lastAt < 500) return; // unchanged + within keepalive window → skip
|
|
117
|
+
lastSent = json; lastAt = nowMs;
|
|
118
|
+
room.objects.set("ball", net);
|
|
84
119
|
}
|
|
85
|
-
const r2 = (v: number) => Math.round(v * 100) / 100;
|
|
86
120
|
```
|
|
87
121
|
|
|
88
122
|
Rules that make it correct:
|
|
@@ -101,6 +135,14 @@ Rules that make it correct:
|
|
|
101
135
|
budget spreads across players instead of funnelling every object through one host's send budget. This
|
|
102
136
|
is why Tier 1 scales to many pushable objects where a host-authoritative fleet (Tier 2) would blow one
|
|
103
137
|
client's rate cap.
|
|
138
|
+
- **Publish moved-only + a low-rate keepalive**, never every tick — a resting owned object republished at
|
|
139
|
+
full rate is the classic budget-blower, and it bites the moment one player owns several props.
|
|
140
|
+
- **Exclude the proxy from the character's ground query** (`userData.controller.excludeCharacterRay`), or
|
|
141
|
+
players stand on the ball/cube and it squirts out from under them the instant they claim it.
|
|
142
|
+
- **Rotate the follower, not just its position** (`setNextKinematicRotation`) — a cube another player
|
|
143
|
+
tumbled otherwise collides as an axis-aligned box (wrong contact) and pops to identity on claim.
|
|
144
|
+
- **Cap the owned body's speed** — CCD stops single-step tunnelling; the cap stops runaway speed (a hard
|
|
145
|
+
shove into a wall) from flinging the object across the map.
|
|
104
146
|
- **Do NOT `registerBody(proxy, mesh)`** — drive the mesh yourself (from the body when you own it, from
|
|
105
147
|
the stream when you don't). Registering it fights the SDK's smoothing on the stream half.
|
|
106
148
|
|
|
@@ -9,13 +9,15 @@ Genex ships improvements continuously. The `genex` CLI tells you when something
|
|
|
9
9
|
is stale; you (the agent) apply the update at the right moment and tell the
|
|
10
10
|
user what happened. The user never manages versions themselves.
|
|
11
11
|
|
|
12
|
-
## The
|
|
12
|
+
## The signals in `genex` output
|
|
13
13
|
|
|
14
14
|
| Line | Meaning | What you do |
|
|
15
15
|
| -- | -- | -- |
|
|
16
16
|
| `🔄 Genex skills updated to X.Y.Z` | Skills were auto-refreshed to match the installed CLI. | Nothing — informational. Skills stay in sync on their own. |
|
|
17
17
|
| `⬆ Genex <package> X.Y.Z available (installed A.B.C) — run: <command>` | A newer npm package exists. | Run the printed command at a **safe moment** (below), then tell the user. |
|
|
18
18
|
| `✗ … below the minimum supported version … Update now — run: <command>` | The API refuses this CLI version (HTTP 426, `cli_update_required`). | Run the printed command **now**, then re-run the refused genex command. |
|
|
19
|
+
| `✗ Out of credits — … costs N credits; your balance is M.` | The account can't cover this generation (HTTP 402, `insufficient_credits`). | Relay the printed facts (balance, cost, refill date, link) to the user; offer a procedural placeholder so the build continues — see the generation skill's Troubleshooting. |
|
|
20
|
+
| `✗ Email not verified — …` | Generation credits are locked until the email is verified (HTTP 403, `email_verification_required`). | Give the user the printed verify link, wait for them to confirm, then re-run the command. |
|
|
19
21
|
|
|
20
22
|
## Safe moments — when to apply a nudge
|
|
21
23
|
|