@genex-ai/cli-demo 0.37.0 → 0.39.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/commands/init.ts
4
- import path8 from "path";
3
+ // src/instrument.ts
4
+ import * as Sentry from "@sentry/node";
5
5
 
6
6
  // src/config.ts
7
7
  import fs from "fs";
@@ -11,6 +11,15 @@ import { fileURLToPath } from "url";
11
11
  var DEFAULT_AUTH_URL = "https://demo-web.glotech.world";
12
12
  var DEFAULT_API_URL = "https://demo-api.glotech.world";
13
13
  var DEFAULT_COLYSEUS_URL = "wss://demo-colyseus.glotech.world";
14
+ var DEFAULT_ANIMS_BASE = "https://cdn.genex.technology/anims/ual1/v1/";
15
+ var ANIMS_BASE_ENV = "GENEX_ANIMS_BASE";
16
+ function getAnimsBase(override) {
17
+ const raw = override || process.env[ANIMS_BASE_ENV] || DEFAULT_ANIMS_BASE;
18
+ return raw.replace(/\/+$/, "") + "/";
19
+ }
20
+ function getAnimsCacheDir() {
21
+ return path.join(getGenexDir(), "cache", "anims");
22
+ }
14
23
  var ENV_TOKEN_KEY = "GENEX_TOKEN";
15
24
  var AUTH_URL_ENV = "GENEX_AUTH_URL";
16
25
  var API_URL_ENV = "GENEX_API_URL";
@@ -80,6 +89,107 @@ function isDir(p) {
80
89
  }
81
90
  }
82
91
 
92
+ // src/lib/sentry-scrub.ts
93
+ import os2 from "os";
94
+ var BEARER = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
95
+ var URL_CRED = /(https?:\/\/)[^/@\s:]+(?::[^/@\s]*)?@/gi;
96
+ var TOKENISH_KV = /((?:token|secret|password|api[_-]?key|authorization)=)[^&#\s"']+/gi;
97
+ var SENSITIVE_KEY = /token|secret|password|api[_-]?key|authorization|bearer/i;
98
+ function scrubString(s) {
99
+ return s.replace(URL_CRED, "$1[redacted]@").replace(BEARER, "Bearer [redacted]").replace(TOKENISH_KV, "$1[redacted]");
100
+ }
101
+ function redactHome(s, home = os2.homedir()) {
102
+ if (!home) return s;
103
+ return s.split(home).join("~");
104
+ }
105
+ var clean = (s) => redactHome(scrubString(s));
106
+ function scrubEvent(event) {
107
+ delete event.user;
108
+ delete event.server_name;
109
+ if (typeof event.message === "string") event.message = clean(event.message);
110
+ for (const ex of event.exception?.values ?? []) {
111
+ if (typeof ex.value === "string") ex.value = clean(ex.value);
112
+ for (const f of ex.stacktrace?.frames ?? []) {
113
+ if (typeof f.filename === "string") f.filename = redactHome(f.filename);
114
+ if (typeof f.abs_path === "string") f.abs_path = redactHome(f.abs_path);
115
+ delete f.vars;
116
+ }
117
+ }
118
+ if (event.request && typeof event.request.url === "string") {
119
+ event.request.url = clean(event.request.url);
120
+ }
121
+ for (const b of event.breadcrumbs ?? []) {
122
+ if (typeof b.message === "string") b.message = clean(b.message);
123
+ const d = b.data;
124
+ if (d) {
125
+ for (const k of Object.keys(d)) {
126
+ if (typeof d[k] === "string") d[k] = clean(d[k]);
127
+ }
128
+ }
129
+ }
130
+ for (const bag of [event.extra, event.tags]) {
131
+ if (!bag) continue;
132
+ for (const k of Object.keys(bag)) {
133
+ if (SENSITIVE_KEY.test(k)) bag[k] = "[Filtered]";
134
+ else if (typeof bag[k] === "string") bag[k] = clean(bag[k]);
135
+ }
136
+ }
137
+ return event;
138
+ }
139
+
140
+ // src/instrument.ts
141
+ var GENEX_CLI_DSN = "https://43efc6f7d16c3e67cad6c60fa8175c20@o4511115493900288.ingest.us.sentry.io/4511706579599360";
142
+ function bakedDsn() {
143
+ return import.meta.url.includes("/dist/") ? GENEX_CLI_DSN : "";
144
+ }
145
+ function resolveDsn() {
146
+ return (process.env.GENEX_SENTRY_DSN || bakedDsn()).trim();
147
+ }
148
+ function isTruthy(v) {
149
+ return v !== void 0 && v.trim() !== "" && !/^(0|false|off|no)$/i.test(v.trim());
150
+ }
151
+ function telemetryDisabled() {
152
+ if (isTruthy(process.env.DO_NOT_TRACK)) return true;
153
+ if (process.env.GENEX_TELEMETRY !== void 0 && !isTruthy(process.env.GENEX_TELEMETRY)) return true;
154
+ if (isTruthy(process.env.GENEX_DISABLE_SENTRY)) return true;
155
+ return false;
156
+ }
157
+ var dsn = resolveDsn();
158
+ var sentryEnabled = !!dsn && !telemetryDisabled();
159
+ if (sentryEnabled) {
160
+ Sentry.init({
161
+ dsn,
162
+ // release aligns with the AG-757 `x-genex-cli-version` telemetry stream so
163
+ // the two correlate; environment is prod unless pointed at a non-default API.
164
+ release: `@genex-ai/cli-demo@${getCliVersion()}`,
165
+ environment: getApiUrl() === DEFAULT_API_URL ? "production" : "development",
166
+ // A short-lived CLI has no meaningful tracing workload — errors only.
167
+ tracesSampleRate: 0,
168
+ // No IP / user auto-collection (opposite of the server apps' `userInfo: true`).
169
+ dataCollection: { userInfo: false },
170
+ // Keep @sentry/node's default onUncaughtException + onUnhandledRejection
171
+ // integrations (auto-registered) — they catch the fire-and-forget async
172
+ // paths in lib/updates.ts / lib/deploy.ts that otherwise swallow errors.
173
+ beforeSend(event) {
174
+ scrubEvent(event);
175
+ return event;
176
+ }
177
+ });
178
+ }
179
+ async function flushSentry(timeoutMs = 2e3) {
180
+ if (!sentryEnabled) return;
181
+ try {
182
+ await Sentry.flush(timeoutMs);
183
+ } catch {
184
+ }
185
+ }
186
+
187
+ // src/index.ts
188
+ import * as Sentry2 from "@sentry/node";
189
+
190
+ // src/commands/init.ts
191
+ import path8 from "path";
192
+
83
193
  // src/lib/copy-templates.ts
84
194
  import fs2 from "fs/promises";
85
195
  import path2 from "path";
@@ -595,10 +705,10 @@ var structuredPrinted = /* @__PURE__ */ new WeakSet();
595
705
  function printedStructuredError(res) {
596
706
  return structuredPrinted.has(res);
597
707
  }
598
- async function apiFetch(url, init = {}) {
599
- const headers = new Headers(init.headers);
708
+ async function apiFetch(url, init2 = {}) {
709
+ const headers = new Headers(init2.headers);
600
710
  if (!headers.has(CLI_VERSION_HEADER)) headers.set(CLI_VERSION_HEADER, getCliVersion());
601
- const res = await fetch(url, { ...init, headers });
711
+ const res = await fetch(url, { ...init2, headers });
602
712
  if (res.status === 426) {
603
713
  try {
604
714
  const body = await res.clone().json();
@@ -1148,7 +1258,7 @@ async function listOwnSlugs(apiUrl, token, log) {
1148
1258
  import { spawn as spawn3 } from "child_process";
1149
1259
  import crypto3 from "crypto";
1150
1260
  import fs9 from "fs/promises";
1151
- import os2 from "os";
1261
+ import os3 from "os";
1152
1262
  import path10 from "path";
1153
1263
  function run(cmd, args, env) {
1154
1264
  return new Promise((resolve) => {
@@ -1345,7 +1455,7 @@ async function pushWorktree(cwd, pushUrl, managed, log) {
1345
1455
  log.error("Couldn't save your game's source \u2014 please try again.");
1346
1456
  return false;
1347
1457
  };
1348
- const gitDir = await fs9.mkdtemp(path10.join(os2.tmpdir(), "genex-source-"));
1458
+ const gitDir = await fs9.mkdtemp(path10.join(os3.tmpdir(), "genex-source-"));
1349
1459
  const base = { GIT_DIR: gitDir };
1350
1460
  const ident = {
1351
1461
  GIT_AUTHOR_NAME: "genex",
@@ -1956,8 +2066,305 @@ function printHint(kind, files, log) {
1956
2066
  }
1957
2067
 
1958
2068
  // src/commands/controller.ts
2069
+ import fs12 from "fs/promises";
2070
+ import path13 from "path";
2071
+
2072
+ // src/lib/anims.ts
1959
2073
  import fs11 from "fs/promises";
1960
2074
  import path12 from "path";
2075
+ var ANIMS_DEST = path12.join("public", "assets", "anims");
2076
+ var HIDDEN_TAG = "reference";
2077
+ async function runAnims(opts) {
2078
+ const log = createLogger({ quiet: opts.quiet });
2079
+ const root = opts.cwd ?? process.cwd();
2080
+ const selectors = opts.selectors ?? [];
2081
+ log.plain(c.bold("genex controller anims"));
2082
+ log.plain("");
2083
+ const { manifest, source } = await loadManifest(opts.animsBase);
2084
+ if (source === "snapshot") {
2085
+ log.dim(" (offline or CDN unreachable \u2014 using the bundled catalog snapshot)");
2086
+ }
2087
+ if (opts.list) {
2088
+ printCatalog(log, manifest, selectors);
2089
+ return;
2090
+ }
2091
+ const controllerMarker = path12.join(root, "src", "controllers", "character");
2092
+ if (!await exists2(controllerMarker)) {
2093
+ log.error(
2094
+ `No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
2095
+ );
2096
+ log.plain(` Run ${c.cyan("genex controller character")} first, then re-run this command.`);
2097
+ process.exitCode = 1;
2098
+ return;
2099
+ }
2100
+ const destDir = path12.join(root, ANIMS_DEST);
2101
+ const gameManifestPath = path12.join(destDir, "manifest.json");
2102
+ if (opts.reset) {
2103
+ await fs11.rm(destDir, { recursive: true, force: true });
2104
+ log.step(`Cleared ${c.cyan(ANIMS_DEST + path12.sep)} (--reset)`);
2105
+ }
2106
+ if (selectors.length === 0) {
2107
+ const installed = await readGameManifest(gameManifestPath);
2108
+ if (installed === null || installed.clips.length === 0) {
2109
+ log.plain(" No animation packs installed yet.");
2110
+ } else {
2111
+ log.plain(` Installed (${installed.clips.length} clips): ${installed.clips.join(", ")}`);
2112
+ }
2113
+ log.plain("");
2114
+ log.plain(
2115
+ ` Install with ${c.cyan("genex controller anims <tag|clip \u2026>")}; browse with ${c.cyan(
2116
+ "genex controller anims --list"
2117
+ )}.`
2118
+ );
2119
+ return;
2120
+ }
2121
+ let resolved;
2122
+ try {
2123
+ resolved = resolveSelectors(manifest, selectors);
2124
+ } catch (err) {
2125
+ log.error(err instanceof Error ? err.message : String(err));
2126
+ process.exitCode = 1;
2127
+ return;
2128
+ }
2129
+ const coreNames = new Set(manifest.core);
2130
+ const byName = /* @__PURE__ */ new Map();
2131
+ let bundledSkips = 0;
2132
+ for (const entries of resolved.values()) {
2133
+ for (const entry of entries) {
2134
+ if (coreNames.has(entry.name)) {
2135
+ bundledSkips++;
2136
+ continue;
2137
+ }
2138
+ byName.set(entry.name, entry);
2139
+ }
2140
+ }
2141
+ const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
2142
+ const cacheDir = path12.join(
2143
+ opts.cacheDir ?? getAnimsCacheDir(),
2144
+ `${manifest.library}-v${manifest.version}`
2145
+ );
2146
+ await fs11.mkdir(cacheDir, { recursive: true });
2147
+ await fs11.mkdir(destDir, { recursive: true });
2148
+ const base = getAnimsBase(opts.animsBase);
2149
+ let installedCount = 0;
2150
+ let presentCount = 0;
2151
+ let addedBytes = 0;
2152
+ const failures = [];
2153
+ for (const entry of wanted) {
2154
+ const dest = path12.join(destDir, entry.file);
2155
+ if (await hasSize(dest, entry.bytes)) {
2156
+ presentCount++;
2157
+ continue;
2158
+ }
2159
+ try {
2160
+ const cached = path12.join(cacheDir, entry.file);
2161
+ if (!await hasSize(cached, entry.bytes)) {
2162
+ const res = await fetch(base + entry.file);
2163
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2164
+ const buf = Buffer.from(await res.arrayBuffer());
2165
+ await fs11.writeFile(cached, buf);
2166
+ }
2167
+ await fs11.copyFile(cached, dest);
2168
+ installedCount++;
2169
+ addedBytes += entry.bytes;
2170
+ log.dim(` ${path12.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
2171
+ } catch (err) {
2172
+ failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
2173
+ }
2174
+ }
2175
+ const previous = await readGameManifest(gameManifestPath);
2176
+ const union = new Set(previous?.clips ?? []);
2177
+ for (const entry of wanted) {
2178
+ if (!failures.some((f) => f.startsWith(`${entry.name} (`))) union.add(entry.name);
2179
+ }
2180
+ const gameManifest = {
2181
+ schema: 1,
2182
+ library: manifest.library,
2183
+ version: manifest.version,
2184
+ clips: [...union].sort((a, b) => a.localeCompare(b))
2185
+ };
2186
+ await fs11.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
2187
+ log.plain("");
2188
+ const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
2189
+ if (presentCount > 0) parts.push(`${presentCount} already present`);
2190
+ if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
2191
+ log.success(
2192
+ `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path12.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
2193
+ );
2194
+ for (const [selector, entries] of resolved) {
2195
+ const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
2196
+ if (names.length > 0) log.dim(` ${selector}: ${names.join(", ")}`);
2197
+ }
2198
+ log.info(
2199
+ `Wiring: load the ${c.cyan("genex-threejs-character-controller")} skill \u2192 "Animation packs" (loadCharacterClips picks these up automatically).`
2200
+ );
2201
+ if (failures.length > 0) {
2202
+ log.plain("");
2203
+ log.error(
2204
+ `${failures.length} clip${failures.length === 1 ? "" : "s"} failed to download: ${failures.join(", ")}`
2205
+ );
2206
+ log.plain(
2207
+ " Each clip needs the network once per machine \u2014 check your connection and re-run the same command (already-installed clips are skipped)."
2208
+ );
2209
+ process.exitCode = 1;
2210
+ }
2211
+ }
2212
+ async function loadManifest(baseOverride) {
2213
+ const base = getAnimsBase(baseOverride);
2214
+ try {
2215
+ const res = await fetch(base + "manifest.json", { signal: AbortSignal.timeout(5e3) });
2216
+ if (res.ok) {
2217
+ const manifest2 = await res.json();
2218
+ if (manifest2.schema === 1 && Array.isArray(manifest2.clips)) {
2219
+ return { manifest: manifest2, source: "cdn" };
2220
+ }
2221
+ }
2222
+ } catch {
2223
+ }
2224
+ const snapshotPath = path12.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
2225
+ const manifest = JSON.parse(await fs11.readFile(snapshotPath, "utf8"));
2226
+ return { manifest, source: "snapshot" };
2227
+ }
2228
+ function resolveSelectors(manifest, selectors) {
2229
+ const byName = new Map(manifest.clips.map((entry) => [entry.name, entry]));
2230
+ const byLowerName = new Map(manifest.clips.map((entry) => [entry.name.toLowerCase(), entry]));
2231
+ const tags = /* @__PURE__ */ new Map();
2232
+ for (const entry of manifest.clips) {
2233
+ for (const tag of entry.tags) {
2234
+ const list = tags.get(tag) ?? [];
2235
+ list.push(entry);
2236
+ tags.set(tag, list);
2237
+ }
2238
+ }
2239
+ const out = /* @__PURE__ */ new Map();
2240
+ for (const selector of selectors) {
2241
+ const exact = byName.get(selector) ?? byLowerName.get(selector.toLowerCase());
2242
+ if (exact) {
2243
+ out.set(selector, [exact]);
2244
+ continue;
2245
+ }
2246
+ const tagHit = tags.get(selector) ?? tags.get(selector.toLowerCase());
2247
+ if (tagHit) {
2248
+ out.set(selector, tagHit);
2249
+ continue;
2250
+ }
2251
+ const candidates = [...tags.keys(), ...byName.keys()];
2252
+ const close = suggest(selector, candidates);
2253
+ throw new Error(
2254
+ `unknown clip/tag "${selector}"${close.length > 0 ? ` \u2014 closest: ${close.join(", ")}` : ""}. Run ${c.cyan(
2255
+ "genex controller anims --list"
2256
+ )} for the catalog.`
2257
+ );
2258
+ }
2259
+ return out;
2260
+ }
2261
+ function suggest(input, candidates) {
2262
+ const lower = input.toLowerCase();
2263
+ const scored = [];
2264
+ for (const candidate of candidates) {
2265
+ const candidateLower = candidate.toLowerCase();
2266
+ if (candidateLower.includes(lower) || lower.includes(candidateLower)) {
2267
+ scored.push({ name: candidate, score: 0 });
2268
+ continue;
2269
+ }
2270
+ const distance = levenshtein(lower, candidateLower, 2);
2271
+ if (distance <= 2) scored.push({ name: candidate, score: distance });
2272
+ }
2273
+ scored.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name));
2274
+ return scored.slice(0, 4).map((s) => s.name);
2275
+ }
2276
+ function levenshtein(a, b, max) {
2277
+ if (Math.abs(a.length - b.length) > max) return max + 1;
2278
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
2279
+ for (let i = 1; i <= a.length; i++) {
2280
+ const curr = [i];
2281
+ let rowMin = i;
2282
+ for (let j = 1; j <= b.length; j++) {
2283
+ curr[j] = Math.min(
2284
+ prev[j] + 1,
2285
+ curr[j - 1] + 1,
2286
+ prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
2287
+ );
2288
+ if (curr[j] < rowMin) rowMin = curr[j];
2289
+ }
2290
+ if (rowMin > max) return max + 1;
2291
+ prev = curr;
2292
+ }
2293
+ return prev[b.length];
2294
+ }
2295
+ function printCatalog(log, manifest, selectors) {
2296
+ const coreNames = new Set(manifest.core);
2297
+ if (selectors.length > 0) {
2298
+ let resolved;
2299
+ try {
2300
+ resolved = resolveSelectors(manifest, selectors);
2301
+ } catch (err) {
2302
+ log.error(err instanceof Error ? err.message : String(err));
2303
+ process.exitCode = 1;
2304
+ return;
2305
+ }
2306
+ for (const [selector, entries] of resolved) {
2307
+ log.plain(c.bold(selector));
2308
+ for (const entry of entries) {
2309
+ const bundled = coreNames.has(entry.name) ? " (bundled)" : "";
2310
+ log.plain(
2311
+ ` ${entry.name.padEnd(26)} ${entry.duration.toFixed(1)}s ${formatMb(entry.bytes)}${bundled} ${c.dim(entry.desc)}`
2312
+ );
2313
+ }
2314
+ }
2315
+ return;
2316
+ }
2317
+ const tags = /* @__PURE__ */ new Map();
2318
+ for (const entry of manifest.clips) {
2319
+ for (const tag of entry.tags) {
2320
+ if (tag === HIDDEN_TAG) continue;
2321
+ const list = tags.get(tag) ?? [];
2322
+ list.push(entry);
2323
+ tags.set(tag, list);
2324
+ }
2325
+ }
2326
+ log.plain(
2327
+ `${c.bold(`Animation packs`)} (${manifest.library} v${manifest.version}, ${manifest.clips.length} clips)`
2328
+ );
2329
+ log.plain(
2330
+ ` Install: ${c.cyan("genex controller anims <tag|clip \u2026>")} Details: ${c.cyan(
2331
+ "genex controller anims --list <tag>"
2332
+ )}`
2333
+ );
2334
+ log.plain("");
2335
+ for (const [tag, entries] of [...tags.entries()].sort(([a], [b]) => a.localeCompare(b))) {
2336
+ const bytes = entries.reduce((sum, entry) => sum + entry.bytes, 0);
2337
+ const suffix = tag === "core" ? " \u2014 bundled in animation-library.glb" : ` (${formatMb(bytes)})`;
2338
+ log.plain(` ${c.bold(tag.padEnd(18))} ${entries.map((e) => e.name).join(", ")}${suffix}`);
2339
+ }
2340
+ }
2341
+ async function readGameManifest(file) {
2342
+ try {
2343
+ return JSON.parse(await fs11.readFile(file, "utf8"));
2344
+ } catch {
2345
+ return null;
2346
+ }
2347
+ }
2348
+ async function hasSize(file, bytes) {
2349
+ try {
2350
+ return (await fs11.stat(file)).size === bytes;
2351
+ } catch {
2352
+ return false;
2353
+ }
2354
+ }
2355
+ async function exists2(p) {
2356
+ try {
2357
+ await fs11.access(p);
2358
+ return true;
2359
+ } catch {
2360
+ return false;
2361
+ }
2362
+ }
2363
+ function formatMb(bytes) {
2364
+ return bytes >= 1e6 ? `${(bytes / 1e6).toFixed(1)} MB` : `${Math.round(bytes / 1e3)} KB`;
2365
+ }
2366
+
2367
+ // src/commands/controller.ts
1961
2368
  var CONTROLLER_KINDS = ["character", "car", "drone"];
1962
2369
  var SHARED = [
1963
2370
  "shared/math.ts",
@@ -1976,6 +2383,7 @@ var CONTROLLER_FILE_SETS = {
1976
2383
  ...SHARED,
1977
2384
  "character/character-controller.ts",
1978
2385
  "character/character-animations.ts",
2386
+ "character/animation-packs.ts",
1979
2387
  "character/presets.ts",
1980
2388
  // VRM avatar support (three-vrm): load + retarget the UAL clips + auto-fit
1981
2389
  // the capsule + optional foot IK. Owner's avatar replaces the old mannequin.
@@ -1988,15 +2396,16 @@ var CONTROLLER_FILE_SETS = {
1988
2396
  ],
1989
2397
  // The player's VRM is written to public/assets/avatar.vrm at install time by
1990
2398
  // installOwnerAvatar (owner's avatar, or the bundled default) — so it is NOT
1991
- // a static manifest asset. animation-library.glb (46 clips) still is.
2399
+ // a static manifest asset. animation-library.glb (the 12-clip core) still is;
2400
+ // extra clips arrive via `genex controller anims` into public/assets/anims/.
1992
2401
  assets: ["assets/animation-library.glb"],
1993
2402
  skill: "genex-threejs-character-controller",
1994
2403
  sketch: [
1995
2404
  `const physics = await PhysicsWorld.create();`,
1996
2405
  `const { scene, vrm } = await loadVrm("./assets/avatar.vrm");`,
1997
- `const lib = await new GLTFLoader().loadAsync("./assets/animation-library.glb");`,
2406
+ `const clips = await loadCharacterClips(vrm); // core library + every genex-controller-anims pack`,
1998
2407
  `const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...capsuleFromModel(scene), position: { x: 0, y: 2, z: 0 } });`,
1999
- `character.root.add(scene); const anims = new CharacterAnimations(scene, retargetClips(vrm, lib.scene, lib.animations));`,
2408
+ `character.root.add(scene); const anims = new CharacterAnimations(scene, clips);`,
2000
2409
  `addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); vrm.update(dt);`
2001
2410
  ]
2002
2411
  },
@@ -2036,45 +2445,49 @@ var CONTROLLER_FILE_SETS = {
2036
2445
  ]
2037
2446
  }
2038
2447
  };
2039
- var CODE_DEST = path12.join("src", "controllers");
2040
- var ASSETS_DEST = path12.join("public", "assets");
2448
+ var CODE_DEST = path13.join("src", "controllers");
2449
+ var ASSETS_DEST = path13.join("public", "assets");
2041
2450
  async function runController(opts) {
2042
2451
  const log = createLogger({ quiet: opts.quiet });
2452
+ if (opts.kind?.trim() === "anims") {
2453
+ await runAnims(opts);
2454
+ return;
2455
+ }
2043
2456
  const kind = opts.kind?.trim();
2044
2457
  if (!kind || !CONTROLLER_KINDS.includes(kind)) {
2045
2458
  log.error(
2046
2459
  `Missing or unknown controller type${kind ? ` "${kind}"` : ""}. Usage: ${c.cyan(
2047
2460
  "genex controller <character|car|drone> [--force]"
2048
- )}`
2461
+ )} or ${c.cyan("genex controller anims <tag|clip \u2026>")}`
2049
2462
  );
2050
2463
  process.exitCode = 1;
2051
2464
  return;
2052
2465
  }
2053
- const srcDir = path12.join(getTemplatesDir(), "controllers");
2466
+ const srcDir = path13.join(getTemplatesDir(), "controllers");
2054
2467
  const root = opts.cwd ?? process.cwd();
2055
2468
  const set = CONTROLLER_FILE_SETS[kind];
2056
2469
  log.plain(c.bold(`genex controller ${kind}`));
2057
2470
  log.plain("");
2058
- log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path12.sep)}`);
2471
+ log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path13.sep)}`);
2059
2472
  const plan = [
2060
- ...set.code.map((rel) => ({ from: rel, rel: path12.join(CODE_DEST, rel) })),
2473
+ ...set.code.map((rel) => ({ from: rel, rel: path13.join(CODE_DEST, rel) })),
2061
2474
  ...set.assets.map((rel) => ({
2062
2475
  from: rel,
2063
- rel: path12.join(ASSETS_DEST, path12.basename(rel))
2476
+ rel: path13.join(ASSETS_DEST, path13.basename(rel))
2064
2477
  }))
2065
2478
  ];
2066
2479
  let copied = 0;
2067
2480
  let skipped = 0;
2068
2481
  try {
2069
2482
  for (const file of plan) {
2070
- const dest = path12.join(root, file.rel);
2071
- if (!opts.force && await exists2(dest)) {
2483
+ const dest = path13.join(root, file.rel);
2484
+ if (!opts.force && await exists3(dest)) {
2072
2485
  skipped++;
2073
2486
  log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
2074
2487
  continue;
2075
2488
  }
2076
- await fs11.mkdir(path12.dirname(dest), { recursive: true });
2077
- await fs11.copyFile(path12.join(srcDir, file.from), dest);
2489
+ await fs12.mkdir(path13.dirname(dest), { recursive: true });
2490
+ await fs12.copyFile(path13.join(srcDir, file.from), dest);
2078
2491
  copied++;
2079
2492
  log.dim(` ${file.rel}`);
2080
2493
  }
@@ -2106,8 +2519,8 @@ async function runController(opts) {
2106
2519
  }
2107
2520
  async function installOwnerAvatar(args) {
2108
2521
  const { root, srcDir, apiUrl, token, log } = args;
2109
- const dest = path12.join(root, ASSETS_DEST, "avatar.vrm");
2110
- await fs11.mkdir(path12.dirname(dest), { recursive: true });
2522
+ const dest = path13.join(root, ASSETS_DEST, "avatar.vrm");
2523
+ await fs12.mkdir(path13.dirname(dest), { recursive: true });
2111
2524
  if (token) {
2112
2525
  try {
2113
2526
  const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
@@ -2119,7 +2532,7 @@ async function installOwnerAvatar(args) {
2119
2532
  const vrmRes = await fetch(me.vrmUrl);
2120
2533
  if (vrmRes.ok) {
2121
2534
  const buf = Buffer.from(await vrmRes.arrayBuffer());
2122
- await fs11.writeFile(dest, buf);
2535
+ await fs12.writeFile(dest, buf);
2123
2536
  log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
2124
2537
  return;
2125
2538
  }
@@ -2130,14 +2543,14 @@ async function installOwnerAvatar(args) {
2130
2543
  log.dim(" avatar fetch failed (offline?); using the bundled default.");
2131
2544
  }
2132
2545
  }
2133
- await fs11.copyFile(path12.join(srcDir, "assets", "default-avatar.vrm"), dest);
2546
+ await fs12.copyFile(path13.join(srcDir, "assets", "default-avatar.vrm"), dest);
2134
2547
  log.dim(
2135
2548
  token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
2136
2549
  );
2137
2550
  }
2138
- async function exists2(p) {
2551
+ async function exists3(p) {
2139
2552
  try {
2140
- await fs11.access(p);
2553
+ await fs12.access(p);
2141
2554
  return true;
2142
2555
  } catch {
2143
2556
  return false;
@@ -2230,6 +2643,10 @@ ${c.bold("Usage")}
2230
2643
  genex video "<prompt>" [options] Generate a video (mp4); prints a public asset URL.
2231
2644
  genex controller <type> [--force] Install a physics controller (character|car|drone)
2232
2645
  into src/controllers (+ assets into public/assets).
2646
+ genex controller anims <sel \u2026> Download extra character animation clips by tag or
2647
+ exact name (sword, stealth, Celebration, \u2026) into
2648
+ public/assets/anims/. --list shows the catalog;
2649
+ --reset forgets the previous selection first.
2233
2650
  genex explore ["<query>"] [options] Search the curated community gallery \u2014 proven
2234
2651
  Three.js systems you can clone or borrow parts
2235
2652
  from. No query lists the whole catalog.
@@ -2303,6 +2720,8 @@ ${c.bold("Environment")}
2303
2720
  GENEX_API_URL Overrides the default API base URL.
2304
2721
  GENEX_COLYSEUS_URL Overrides the default multiplayer URL.
2305
2722
  GENEX_BROWSER Command used to open the browser (falls back to BROWSER).
2723
+ GENEX_TELEMETRY Set to 0 to disable anonymous crash reporting (Sentry).
2724
+ DO_NOT_TRACK Standard opt-out; any value disables crash reporting.
2306
2725
 
2307
2726
  ${c.bold("Examples")}
2308
2727
  genex init my-game
@@ -2390,6 +2809,12 @@ function parseArgs(argv) {
2390
2809
  case "--force":
2391
2810
  parsed.options.force = true;
2392
2811
  break;
2812
+ case "--list":
2813
+ parsed.options.list = true;
2814
+ break;
2815
+ case "--reset":
2816
+ parsed.options.reset = true;
2817
+ break;
2393
2818
  case "--regenerate-cover":
2394
2819
  parsed.options.regenerateCover = true;
2395
2820
  break;
@@ -2419,6 +2844,8 @@ function parseArgs(argv) {
2419
2844
  parsed.options.name = arg;
2420
2845
  } else if (parsed.command === "explore") {
2421
2846
  parsed.options.name = `${parsed.options.name} ${arg}`;
2847
+ } else if (parsed.command === "controller") {
2848
+ (parsed.options.selectors ??= []).push(arg);
2422
2849
  } else {
2423
2850
  parsed.error = `Unexpected argument: ${arg}`;
2424
2851
  return parsed;
@@ -2530,6 +2957,17 @@ async function main() {
2530
2957
  const updateLog = createLogger({ quiet: parsed.options.quiet });
2531
2958
  if (parsed.command !== "init") await syncSkills(updateLog);
2532
2959
  const updateCheck = startUpdateCheck();
2960
+ Sentry2.setTag("command", parsed.command);
2961
+ Sentry2.setTag("cli.version", getCliVersion());
2962
+ Sentry2.setTag("node.version", process.versions.node);
2963
+ Sentry2.setTag("os.platform", process.platform);
2964
+ Sentry2.setContext("runtime", {
2965
+ cliVersion: getCliVersion(),
2966
+ node: process.version,
2967
+ platform: process.platform,
2968
+ arch: process.arch,
2969
+ ci: !!process.env.CI
2970
+ });
2533
2971
  try {
2534
2972
  if (GEN_KINDS.has(parsed.command)) {
2535
2973
  await runGenerate(parsed.command, {
@@ -2567,10 +3005,13 @@ async function main() {
2567
3005
  }
2568
3006
  } finally {
2569
3007
  await reportUpdateNudges(updateCheck, updateLog);
3008
+ await flushSentry();
2570
3009
  }
2571
3010
  }
2572
- main().catch((err) => {
3011
+ main().catch(async (err) => {
3012
+ Sentry2.captureException(err);
2573
3013
  const log = createLogger();
2574
3014
  log.error(err instanceof Error ? err.message : String(err));
3015
+ await flushSentry();
2575
3016
  process.exitCode = 1;
2576
3017
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.37.0",
3
+ "version": "0.39.0",
4
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": {
@@ -35,6 +35,9 @@
35
35
  "publishConfig": {
36
36
  "access": "public"
37
37
  },
38
+ "dependencies": {
39
+ "@sentry/node": "^10.63.0"
40
+ },
38
41
  "devDependencies": {
39
42
  "@dimforge/rapier3d-compat": "^0.19.3",
40
43
  "@pixiv/three-vrm": "^3.5.4",