@genex-ai/cli-demo 0.3.0 → 0.4.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 +9 -4
- package/dist/index.js +99 -61
- package/package.json +1 -1
- package/templates/skills/genex-getting-started/SKILL.md +9 -5
package/README.md
CHANGED
|
@@ -21,9 +21,13 @@ genex texture "<prompt>" # generate a texture → assets/textures/
|
|
|
21
21
|
|
|
22
22
|
`genex init` does four things:
|
|
23
23
|
|
|
24
|
-
1. **
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
1. **Installs the skills for your coding agents** — copies the bundled templates
|
|
25
|
+
into every agent it detects: Claude Code (`~/.claude`, full set incl.
|
|
26
|
+
agents/ + commands/), Codex (`~/.codex/skills`), and Cursor
|
|
27
|
+
(`~/.cursor/skills`). genex-owned skills (`genex-*`) are **always refreshed**
|
|
28
|
+
to the package version so a stale copy can't linger; your own files are never
|
|
29
|
+
overwritten. Pick agents explicitly with `--agents claude,codex,cursor`, or a
|
|
30
|
+
single custom dir with `--dir`.
|
|
27
31
|
2. **Authorizes you** — opens the Genex auth site (web) in your browser. If the
|
|
28
32
|
browser can't open, it prints the URL to open manually.
|
|
29
33
|
3. **Saves your token** — writes `GENEX_TOKEN` to `~/.genex/env` (per-user;
|
|
@@ -107,7 +111,8 @@ npx @genex-ai/cli-demo@latest init
|
|
|
107
111
|
genex init [options]
|
|
108
112
|
|
|
109
113
|
Options
|
|
110
|
-
--
|
|
114
|
+
--agents <list> Agents to install for: claude,codex,cursor (default: auto-detect)
|
|
115
|
+
--dir <path> Single destination workspace (overrides --agents)
|
|
111
116
|
--env <path> Token env file (default: ~/.genex/env)
|
|
112
117
|
--auth-url <url> Override the auth site (default: https://demo-web.glotech.world)
|
|
113
118
|
--no-auth Only scaffold templates; skip authorization
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
9
9
|
import path6 from "path";
|
|
10
10
|
|
|
11
11
|
// src/config.ts
|
|
12
|
+
import fs from "fs";
|
|
12
13
|
import os from "os";
|
|
13
14
|
import path from "path";
|
|
14
15
|
import { fileURLToPath } from "url";
|
|
@@ -42,45 +43,77 @@ function getTemplatesDir() {
|
|
|
42
43
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
43
44
|
return path.resolve(here, "..", "templates");
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
var KNOWN_AGENTS = {
|
|
47
|
+
claude: { label: "Claude Code", dirName: ".claude", full: true },
|
|
48
|
+
codex: { label: "Codex", dirName: ".codex", full: false },
|
|
49
|
+
cursor: { label: "Cursor", dirName: ".cursor", full: false }
|
|
50
|
+
};
|
|
51
|
+
var KNOWN_AGENT_IDS = Object.keys(KNOWN_AGENTS);
|
|
52
|
+
function resolveAgentTargets(opts = {}) {
|
|
53
|
+
if (opts.dir) {
|
|
54
|
+
return [{ id: "custom", label: "workspace", baseDir: path.resolve(opts.dir), full: true }];
|
|
55
|
+
}
|
|
56
|
+
const home = os.homedir();
|
|
57
|
+
let ids;
|
|
58
|
+
if (opts.agents && opts.agents.length > 0) {
|
|
59
|
+
ids = opts.agents.filter((id) => KNOWN_AGENTS[id]);
|
|
60
|
+
} else {
|
|
61
|
+
ids = KNOWN_AGENT_IDS.filter((id) => isDir(path.join(home, KNOWN_AGENTS[id].dirName)));
|
|
62
|
+
if (ids.length === 0) ids = ["claude"];
|
|
63
|
+
}
|
|
64
|
+
return ids.map((id) => {
|
|
65
|
+
const def = KNOWN_AGENTS[id];
|
|
66
|
+
return { id, label: def.label, baseDir: path.join(home, def.dirName), full: def.full };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function isDir(p) {
|
|
70
|
+
try {
|
|
71
|
+
return fs.statSync(p).isDirectory();
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
48
75
|
}
|
|
49
76
|
|
|
50
77
|
// src/lib/copy-templates.ts
|
|
51
|
-
import
|
|
78
|
+
import fs2 from "fs/promises";
|
|
52
79
|
import path2 from "path";
|
|
80
|
+
function isGenexManaged(rel) {
|
|
81
|
+
return rel.split(path2.sep).some((seg) => seg.startsWith("genex"));
|
|
82
|
+
}
|
|
53
83
|
async function copyTemplates(srcDir, destDir, opts = {}) {
|
|
54
|
-
const result = { copied: [], skipped: [] };
|
|
84
|
+
const result = { copied: [], updated: [], skipped: [] };
|
|
55
85
|
await walk(srcDir, srcDir, destDir, opts, result);
|
|
56
86
|
return result;
|
|
57
87
|
}
|
|
58
88
|
async function walk(rootSrc, src, dest, opts, result) {
|
|
59
|
-
const entries = await
|
|
89
|
+
const entries = await fs2.readdir(src, { withFileTypes: true });
|
|
60
90
|
for (const entry of entries) {
|
|
61
91
|
const srcPath = path2.join(src, entry.name);
|
|
62
92
|
const destPath = path2.join(dest, entry.name);
|
|
63
93
|
const rel = path2.relative(rootSrc, srcPath);
|
|
64
94
|
if (entry.isDirectory()) {
|
|
65
|
-
await
|
|
95
|
+
await fs2.mkdir(destPath, { recursive: true });
|
|
66
96
|
await walk(rootSrc, srcPath, destPath, opts, result);
|
|
67
97
|
continue;
|
|
68
98
|
}
|
|
69
99
|
if (!entry.isFile()) {
|
|
70
100
|
continue;
|
|
71
101
|
}
|
|
72
|
-
|
|
102
|
+
const present = await exists(destPath);
|
|
103
|
+
const mayOverwrite = opts.force || isGenexManaged(rel);
|
|
104
|
+
if (present && !mayOverwrite) {
|
|
73
105
|
result.skipped.push(rel);
|
|
74
106
|
continue;
|
|
75
107
|
}
|
|
76
|
-
await
|
|
77
|
-
await
|
|
108
|
+
await fs2.mkdir(path2.dirname(destPath), { recursive: true });
|
|
109
|
+
await fs2.copyFile(srcPath, destPath);
|
|
78
110
|
result.copied.push(rel);
|
|
111
|
+
if (present) result.updated.push(rel);
|
|
79
112
|
}
|
|
80
113
|
}
|
|
81
114
|
async function exists(p) {
|
|
82
115
|
try {
|
|
83
|
-
await
|
|
116
|
+
await fs2.access(p);
|
|
84
117
|
return true;
|
|
85
118
|
} catch {
|
|
86
119
|
return false;
|
|
@@ -416,7 +449,7 @@ function randomSuffix() {
|
|
|
416
449
|
}
|
|
417
450
|
|
|
418
451
|
// src/lib/ssh.ts
|
|
419
|
-
import
|
|
452
|
+
import fs3 from "fs/promises";
|
|
420
453
|
import path3 from "path";
|
|
421
454
|
import { spawn as spawn2 } from "child_process";
|
|
422
455
|
var KEY_NAME = "genex_key";
|
|
@@ -424,7 +457,7 @@ async function generateSshKeypair(dir, log) {
|
|
|
424
457
|
const keyPath = path3.join(dir, KEY_NAME);
|
|
425
458
|
const pubPath = `${keyPath}.pub`;
|
|
426
459
|
try {
|
|
427
|
-
const existing = (await
|
|
460
|
+
const existing = (await fs3.readFile(pubPath, "utf8")).trim();
|
|
428
461
|
if (existing) {
|
|
429
462
|
log.dim(`Reusing existing deploy key (${KEY_NAME}).`);
|
|
430
463
|
return { publicKey: existing };
|
|
@@ -435,12 +468,12 @@ async function generateSshKeypair(dir, log) {
|
|
|
435
468
|
const ok = await runSshKeygen(keyPath, log);
|
|
436
469
|
if (!ok) return null;
|
|
437
470
|
try {
|
|
438
|
-
const pub = (await
|
|
471
|
+
const pub = (await fs3.readFile(pubPath, "utf8")).trim();
|
|
439
472
|
if (!pub) {
|
|
440
473
|
log.warn("ssh-keygen produced no public key.");
|
|
441
474
|
return null;
|
|
442
475
|
}
|
|
443
|
-
await
|
|
476
|
+
await fs3.chmod(keyPath, 384).catch(() => {
|
|
444
477
|
});
|
|
445
478
|
return { publicKey: pub };
|
|
446
479
|
} catch (err) {
|
|
@@ -473,7 +506,7 @@ async function writeGitignore(dir, log) {
|
|
|
473
506
|
const file = path3.join(dir, ".gitignore");
|
|
474
507
|
let content = "";
|
|
475
508
|
try {
|
|
476
|
-
content = await
|
|
509
|
+
content = await fs3.readFile(file, "utf8");
|
|
477
510
|
} catch {
|
|
478
511
|
}
|
|
479
512
|
const present = new Set(content.split("\n").map((l) => l.trim()));
|
|
@@ -483,23 +516,23 @@ async function writeGitignore(dir, log) {
|
|
|
483
516
|
if (next.length > 0 && !next.endsWith("\n")) next += "\n";
|
|
484
517
|
if (!content.trim()) next += "# genex (deploy key + local metadata \u2014 never publish)\n";
|
|
485
518
|
next += toAdd.join("\n") + "\n";
|
|
486
|
-
await
|
|
519
|
+
await fs3.writeFile(file, next);
|
|
487
520
|
log.dim(`Updated .gitignore (${toAdd.join(", ")}).`);
|
|
488
521
|
}
|
|
489
522
|
|
|
490
523
|
// src/lib/store.ts
|
|
491
|
-
import
|
|
524
|
+
import fs5 from "fs/promises";
|
|
492
525
|
import path5 from "path";
|
|
493
526
|
|
|
494
527
|
// src/lib/env.ts
|
|
495
|
-
import
|
|
528
|
+
import fs4 from "fs/promises";
|
|
496
529
|
import path4 from "path";
|
|
497
530
|
import { spawn as spawn3 } from "child_process";
|
|
498
531
|
async function writeEnvVar(envPath, key, value) {
|
|
499
532
|
let content = "";
|
|
500
533
|
let existed = false;
|
|
501
534
|
try {
|
|
502
|
-
content = await
|
|
535
|
+
content = await fs4.readFile(envPath, "utf8");
|
|
503
536
|
existed = true;
|
|
504
537
|
} catch {
|
|
505
538
|
}
|
|
@@ -519,14 +552,14 @@ async function writeEnvVar(envPath, key, value) {
|
|
|
519
552
|
next = prefix + assignment + "\n";
|
|
520
553
|
mode = existed ? "appended" : "created";
|
|
521
554
|
}
|
|
522
|
-
await
|
|
523
|
-
await
|
|
555
|
+
await fs4.mkdir(path4.dirname(envPath), { recursive: true });
|
|
556
|
+
await fs4.writeFile(envPath, next, { mode: 384 });
|
|
524
557
|
await restrictFilePermissions(envPath);
|
|
525
558
|
return { mode, path: envPath };
|
|
526
559
|
}
|
|
527
560
|
async function restrictFilePermissions(filePath) {
|
|
528
561
|
if (process.platform !== "win32") {
|
|
529
|
-
await
|
|
562
|
+
await fs4.chmod(filePath, 384).catch(() => {
|
|
530
563
|
});
|
|
531
564
|
return;
|
|
532
565
|
}
|
|
@@ -575,7 +608,7 @@ async function readUserToken(envPath) {
|
|
|
575
608
|
async function readTokenFromFile(file) {
|
|
576
609
|
let content;
|
|
577
610
|
try {
|
|
578
|
-
content = await
|
|
611
|
+
content = await fs5.readFile(file, "utf8");
|
|
579
612
|
} catch {
|
|
580
613
|
return null;
|
|
581
614
|
}
|
|
@@ -591,7 +624,7 @@ function stripQuotes(v) {
|
|
|
591
624
|
}
|
|
592
625
|
async function readProject(cwd = process.cwd()) {
|
|
593
626
|
try {
|
|
594
|
-
const raw = await
|
|
627
|
+
const raw = await fs5.readFile(getProjectMetadataPath(cwd), "utf8");
|
|
595
628
|
return JSON.parse(raw);
|
|
596
629
|
} catch {
|
|
597
630
|
return null;
|
|
@@ -599,9 +632,9 @@ async function readProject(cwd = process.cwd()) {
|
|
|
599
632
|
}
|
|
600
633
|
async function writeProject(meta, cwd = process.cwd()) {
|
|
601
634
|
const file = getProjectMetadataPath(cwd);
|
|
602
|
-
await
|
|
603
|
-
await
|
|
604
|
-
await
|
|
635
|
+
await fs5.mkdir(path5.dirname(file), { recursive: true });
|
|
636
|
+
await fs5.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
|
|
637
|
+
await fs5.chmod(file, 384).catch(() => {
|
|
605
638
|
});
|
|
606
639
|
return { path: file };
|
|
607
640
|
}
|
|
@@ -631,24 +664,24 @@ async function runInit(opts) {
|
|
|
631
664
|
log.plain(c.bold("genex init"));
|
|
632
665
|
log.plain("");
|
|
633
666
|
const templatesDir = getTemplatesDir();
|
|
634
|
-
const
|
|
635
|
-
log.step(
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
667
|
+
const targets = resolveAgentTargets({ dir: opts.dir, agents: opts.agents });
|
|
668
|
+
log.step(
|
|
669
|
+
`Installing Genex skills for: ${targets.map((t) => c.cyan(t.label)).join(", ")}`
|
|
670
|
+
);
|
|
671
|
+
let totalNew = 0;
|
|
672
|
+
let totalUpdated = 0;
|
|
673
|
+
for (const t of targets) {
|
|
674
|
+
const src = t.full ? templatesDir : path6.join(templatesDir, "skills");
|
|
675
|
+
const dest = t.full ? t.baseDir : path6.join(t.baseDir, "skills");
|
|
676
|
+
const { copied, updated } = await copyTemplates(src, dest, { force: opts.force });
|
|
677
|
+
const added = copied.length - updated.length;
|
|
678
|
+
totalNew += added;
|
|
679
|
+
totalUpdated += updated.length;
|
|
680
|
+
log.dim(` ${t.label}: ${added} added, ${updated.length} refreshed \u2192 ${dest}`);
|
|
681
|
+
}
|
|
682
|
+
log.success(
|
|
683
|
+
`Skills ready (${totalNew} added, ${totalUpdated} refreshed across ${targets.length} agent${targets.length === 1 ? "" : "s"}).`
|
|
684
|
+
);
|
|
652
685
|
log.plain("");
|
|
653
686
|
if (opts.noAuth) {
|
|
654
687
|
log.info("Skipping authorization (--no-auth).");
|
|
@@ -703,7 +736,7 @@ async function runInit(opts) {
|
|
|
703
736
|
|
|
704
737
|
// src/lib/deploy.ts
|
|
705
738
|
import { spawn as spawn4 } from "child_process";
|
|
706
|
-
import
|
|
739
|
+
import fs6 from "fs/promises";
|
|
707
740
|
import os2 from "os";
|
|
708
741
|
import path7 from "path";
|
|
709
742
|
function run(cmd, args, env) {
|
|
@@ -737,24 +770,24 @@ async function deployGame(sshUrl, opts, log) {
|
|
|
737
770
|
log.success("Built.");
|
|
738
771
|
}
|
|
739
772
|
const distDir = path7.join(cwd, "dist");
|
|
740
|
-
const siteDir = await
|
|
773
|
+
const siteDir = await isDir2(distDir) ? distDir : cwd;
|
|
741
774
|
if (siteDir === cwd) {
|
|
742
775
|
await writeGitignore(cwd, log);
|
|
743
776
|
}
|
|
744
777
|
const rel = path7.relative(cwd, siteDir) || ".";
|
|
745
778
|
try {
|
|
746
|
-
await
|
|
779
|
+
await fs6.access(path7.join(siteDir, "index.html"));
|
|
747
780
|
} catch {
|
|
748
781
|
log.warn(`No index.html in ${rel} \u2014 GitHub Pages needs one to serve the game.`);
|
|
749
782
|
}
|
|
750
783
|
await warnIfAbsolutePaths(siteDir, log);
|
|
751
|
-
await
|
|
784
|
+
await fs6.writeFile(path7.join(siteDir, ".nojekyll"), "");
|
|
752
785
|
const keyPath = path7.resolve(cwd, KEY_NAME);
|
|
753
786
|
if (!await fileExists(keyPath)) {
|
|
754
787
|
log.warn(`No deploy key (${KEY_NAME}) here \u2014 run \`genex init\` in this folder first.`);
|
|
755
788
|
return false;
|
|
756
789
|
}
|
|
757
|
-
const gitDir = await
|
|
790
|
+
const gitDir = await fs6.mkdtemp(path7.join(os2.tmpdir(), "genex-deploy-"));
|
|
758
791
|
const gitEnv = { GIT_DIR: gitDir, GIT_WORK_TREE: siteDir };
|
|
759
792
|
try {
|
|
760
793
|
if ((await run("git", ["init", "-q"], gitEnv)).code !== 0) {
|
|
@@ -790,28 +823,28 @@ async function deployGame(sshUrl, opts, log) {
|
|
|
790
823
|
if (tail) log.dim(` ${tail}`);
|
|
791
824
|
return false;
|
|
792
825
|
} finally {
|
|
793
|
-
await
|
|
826
|
+
await fs6.rm(gitDir, { recursive: true, force: true }).catch(() => {
|
|
794
827
|
});
|
|
795
828
|
}
|
|
796
829
|
}
|
|
797
830
|
async function hasBuildScript(cwd) {
|
|
798
831
|
try {
|
|
799
|
-
const pkg = JSON.parse(await
|
|
832
|
+
const pkg = JSON.parse(await fs6.readFile(path7.join(cwd, "package.json"), "utf8"));
|
|
800
833
|
return Boolean(pkg.scripts?.build);
|
|
801
834
|
} catch {
|
|
802
835
|
return false;
|
|
803
836
|
}
|
|
804
837
|
}
|
|
805
|
-
async function
|
|
838
|
+
async function isDir2(p) {
|
|
806
839
|
try {
|
|
807
|
-
return (await
|
|
840
|
+
return (await fs6.stat(p)).isDirectory();
|
|
808
841
|
} catch {
|
|
809
842
|
return false;
|
|
810
843
|
}
|
|
811
844
|
}
|
|
812
845
|
async function fileExists(p) {
|
|
813
846
|
try {
|
|
814
|
-
await
|
|
847
|
+
await fs6.access(p);
|
|
815
848
|
return true;
|
|
816
849
|
} catch {
|
|
817
850
|
return false;
|
|
@@ -820,7 +853,7 @@ async function fileExists(p) {
|
|
|
820
853
|
async function warnIfAbsolutePaths(siteDir, log) {
|
|
821
854
|
let html;
|
|
822
855
|
try {
|
|
823
|
-
html = await
|
|
856
|
+
html = await fs6.readFile(path7.join(siteDir, "index.html"), "utf8");
|
|
824
857
|
} catch {
|
|
825
858
|
return;
|
|
826
859
|
}
|
|
@@ -918,7 +951,7 @@ async function runPreview(opts) {
|
|
|
918
951
|
import path9 from "path";
|
|
919
952
|
|
|
920
953
|
// src/lib/assets.ts
|
|
921
|
-
import
|
|
954
|
+
import fs7 from "fs/promises";
|
|
922
955
|
import path8 from "path";
|
|
923
956
|
function slugify(input) {
|
|
924
957
|
const s = input.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/g, "");
|
|
@@ -928,8 +961,8 @@ async function downloadToFile(url, dest, headers) {
|
|
|
928
961
|
const res = await fetch(url, { headers });
|
|
929
962
|
if (!res.ok) throw new Error(`download failed (HTTP ${res.status}) for ${url}`);
|
|
930
963
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
931
|
-
await
|
|
932
|
-
await
|
|
964
|
+
await fs7.mkdir(path8.dirname(dest), { recursive: true });
|
|
965
|
+
await fs7.writeFile(dest, buf);
|
|
933
966
|
return buf.byteLength;
|
|
934
967
|
}
|
|
935
968
|
|
|
@@ -1099,7 +1132,8 @@ ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture`)")}
|
|
|
1099
1132
|
${c.bold("Options for `init`")}
|
|
1100
1133
|
<name> Project name (positional; default: current directory name).
|
|
1101
1134
|
--name <name> Same as the positional name.
|
|
1102
|
-
--
|
|
1135
|
+
--agents <list> Agents to install skills for (claude,codex,cursor; default: auto-detect).
|
|
1136
|
+
--dir <path> Single destination workspace (overrides --agents).
|
|
1103
1137
|
--env <path> Token env file (default: ~/.genex/env).
|
|
1104
1138
|
--auth-url <url> Override the auth site (default: ${DEFAULT_AUTH_URL}).
|
|
1105
1139
|
--api-url <url> Override the API base URL (default: ${DEFAULT_API_URL}).
|
|
@@ -1151,6 +1185,7 @@ function parseArgs(argv) {
|
|
|
1151
1185
|
"--auth-url",
|
|
1152
1186
|
"--api-url",
|
|
1153
1187
|
"--colyseus-url",
|
|
1188
|
+
"--agents",
|
|
1154
1189
|
"--name",
|
|
1155
1190
|
"--title",
|
|
1156
1191
|
"--description",
|
|
@@ -1233,6 +1268,9 @@ function applyValueFlag(options, flag, value) {
|
|
|
1233
1268
|
case "--colyseus-url":
|
|
1234
1269
|
options.colyseusUrl = value;
|
|
1235
1270
|
break;
|
|
1271
|
+
case "--agents":
|
|
1272
|
+
options.agents = value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
1273
|
+
break;
|
|
1236
1274
|
case "--name":
|
|
1237
1275
|
options.name = value;
|
|
1238
1276
|
break;
|
package/package.json
CHANGED
|
@@ -11,11 +11,15 @@ architecture, and team-ready workflows.
|
|
|
11
11
|
|
|
12
12
|
## What got installed
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
`genex init` installs the Genex skills into **every coding agent it detects** —
|
|
15
|
+
Claude Code (`~/.claude`), Codex (`~/.codex`), and Cursor (`~/.cursor`) — so the
|
|
16
|
+
same skills are available whichever agent you build with. Re-running `init`
|
|
17
|
+
always refreshes the genex-owned skills to the latest version (your own files
|
|
18
|
+
are never touched).
|
|
19
|
+
|
|
20
|
+
- **skills/** - reusable Genex skills for 3D browser-game work (all agents).
|
|
21
|
+
- **agents/** - example subagent definitions (Claude Code).
|
|
22
|
+
- **commands/** - example slash commands (Claude Code).
|
|
19
23
|
|
|
20
24
|
Start with `$genex-threejs-skill-router` for broad game or graphics requests.
|
|
21
25
|
It routes the agent to focused skills for cameras, procedural geometry,
|