@genex-ai/cli-demo 1.9.3-dev.519 → 1.10.0-dev.521

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
@@ -219,6 +219,11 @@ var c = {
219
219
 
220
220
  // src/lib/api.ts
221
221
  var CLI_VERSION_HEADER = "x-genex-cli-version";
222
+ var WORKSPACE_HEADER = "x-genex-workspace";
223
+ var workspaceLabel = null;
224
+ function setWorkspaceHeader(label) {
225
+ workspaceLabel = label;
226
+ }
222
227
  function formatUpdateRequired(body) {
223
228
  const action = body.action ?? `npm i -D @genex-ai/cli-demo@${CLI_CHANNEL}`;
224
229
  const message = body.message ?? `Genex CLI ${body.clientVersion ?? getCliVersion()} is below the minimum supported version${body.minVersion ? ` ${body.minVersion}` : ""}.`;
@@ -255,6 +260,9 @@ function printedStructuredError(res) {
255
260
  async function apiFetch(url, init2 = {}) {
256
261
  const headers = new Headers(init2.headers);
257
262
  if (!headers.has(CLI_VERSION_HEADER)) headers.set(CLI_VERSION_HEADER, getCliVersion());
263
+ if (workspaceLabel && !headers.has(WORKSPACE_HEADER)) {
264
+ headers.set(WORKSPACE_HEADER, workspaceLabel);
265
+ }
258
266
  const res = await fetch(url, { ...init2, headers });
259
267
  if (res.status === 426) {
260
268
  try {
@@ -816,6 +824,25 @@ import path4 from "path";
816
824
  function getProjectMetadataPath(cwd = process.cwd()) {
817
825
  return path4.join(cwd, ".genex", "project.json");
818
826
  }
827
+ function getWorkspacePath(cwd = process.cwd()) {
828
+ return path4.join(cwd, ".genex", "workspace.json");
829
+ }
830
+ async function readWorkspace(cwd = process.cwd()) {
831
+ try {
832
+ const raw = await fs4.readFile(getWorkspacePath(cwd), "utf8");
833
+ return JSON.parse(raw);
834
+ } catch {
835
+ return null;
836
+ }
837
+ }
838
+ async function writeWorkspace(meta, cwd = process.cwd()) {
839
+ const file = getWorkspacePath(cwd);
840
+ await fs4.mkdir(path4.dirname(file), { recursive: true });
841
+ await fs4.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
842
+ await fs4.chmod(file, 384).catch(() => {
843
+ });
844
+ return { path: file };
845
+ }
819
846
  async function writeUserToken(token, envPath) {
820
847
  const { path: written } = await writeEnvVar(getGenexEnvPath(envPath), ENV_TOKEN_KEY, token);
821
848
  return { path: written };
@@ -954,6 +981,7 @@ async function walk(rootSrc, src, dest, opts, result) {
954
981
  const destPath = path5.join(dest, entry.name);
955
982
  const rel = path5.relative(rootSrc, srcPath);
956
983
  if (opts.exclude?.includes(rel)) continue;
984
+ if (opts.filter && !opts.filter(rel)) continue;
957
985
  if (entry.isDirectory()) {
958
986
  await fs5.mkdir(destPath, { recursive: true });
959
987
  await walk(rootSrc, srcPath, destPath, opts, result);
@@ -1017,10 +1045,20 @@ Important note: put soul into your creations, with many details and love. Aim to
1017
1045
  19. If the game sells anything, it sells it for coin at a fixed, visible price, and NEVER sells chance. Run this test on any purchasable thing before building it: does the player pay (with coin, or with anything coin bought, directly or indirectly), is the outcome uncertain when they pay, and is there a prize they wanted \u2014 all three yes means it is paid randomness, and you build the deterministic version instead. That rules out loot boxes, gacha, mystery boxes, crates, card packs, prize wheels and raffles; wagering, staking, betting, coinflips and casino or slot mechanics denominated in coin; and donation prompts, tip jars or any player-to-player coin transfer, because coin buys goods and never just moves. Randomness the player EARNS by playing is gameplay, not commerce \u2014 an enemy dropping a random item, a chest found in the level, a procedural layout, a crit roll \u2014 and is completely fine. Every coin price renders its real-money equivalent beside it (the server sends one with every item), item prices sit on the platform's price grid so no player is left holding change they cannot spend, and nothing in a shop carries a countdown, "limited time", or a stock counter. When a request crosses one of these lines, name the mechanic, give the one-sentence reason, propose a specific compliant alternative, and build that \u2014 never the banned version "as an option", never a partial one, and never after asking the player to confirm they want it. Load \`$genex-monetization\` before building a shop.
1018
1046
  ${CONTRACT_END}
1019
1047
  `;
1048
+ var GENEX_TOOLS_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
1049
+ # Genex Tools (always in effect in this folder)
1050
+
1051
+ This folder uses **Genex Tools**: AI asset generation for games, from your terminal. All via \`npx genex \u2026\` \u2014 \`model "<prompt>"\` (plus \`model --image <file>\` from a reference photo, \`model segment <id>\` into named parts, \`model rig <id>\` across 7 body plans, \`model animate <rig-id> --preset walk\`) \xB7 \`image\` (\`--transparent\` \xB7 \`--edit\` \xB7 \`--inpaint\` \xB7 \`--remove-bg\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 \`texture\` \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 rigged \`character\` / \`creature\` and \`character animate <id> "<verb>"\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`doctor\`. Full options with costs: \`npx genex --help\`. Each tool has a skill card (\`genex-tool-\u2026\`) \u2014 load the one for the lane you are using. Whatever this project builds and wherever it ships is the user's own; Genex generates the assets and nothing more.
1052
+
1053
+ 1. **NEVER delete, empty, move, rename, or overwrite anything you did not create yourself.** This is someone's real project: it holds their code, their reference images, their notes, an earlier attempt \u2014 files that may exist nowhere else and have no undo, no trash, no backup. A non-empty folder is NORMAL and is never something to clean up, and "start clean" is never a reason. That rules out \`rm\`/\`rm -rf\`, \`git clean\`, \`git checkout -- .\`, \`git reset --hard\` over their work, deleting to resolve a conflict or a stuck prompt, and every tool's offer to empty a directory (\`--force\`, \`--overwrite\`, "Remove existing files"). You may add files and edit the ones you wrote. If a step genuinely cannot continue without removing something of theirs, STOP and ask, naming the exact files, and wait for a yes \u2014 "it looks like junk" is never that yes.
1054
+ 2. **Generated assets are local files the user owns.** Every generation downloads into \`./assets/\` and the command prints the local path \u2014 wire THAT path into the code, exactly like an asset they drew themselves. The URL printed beside it is provenance, not hosting: never fetch it at runtime, never ship a build that depends on it. Assets are theirs to move, rename, commit, or process further.
1055
+ 3. **Run \`npx genex doctor\` when a command fails, and before promising what you can generate.** It reports sign-in, credit balance, and which generation lanes are actually live on this account \u2014 the only live source for all three. Treat every genex warning line as work, not noise, and never re-run a generation command to "retry" one that is still running: that bills a second asset. Attach to the first with \`npx genex wait <id>\`.
1056
+ ${CONTRACT_END}
1057
+ `;
1020
1058
  var CLAUDE_IMPORT_LINE = "@AGENTS.md";
1021
1059
  var CONTRACT_REGION = /<!-- genex:contract:begin[^\n]*-->[\s\S]*?<!-- genex:contract:end -->/;
1022
- function mergeContractBlock(existing) {
1023
- const block = GENEX_CONTRACT_BLOCK.trimEnd();
1060
+ function mergeContractBlock(existing, contractBlock = GENEX_CONTRACT_BLOCK) {
1061
+ const block = contractBlock.trimEnd();
1024
1062
  if (existing === null || existing.trim() === "") return `${block}
1025
1063
  `;
1026
1064
  if (CONTRACT_REGION.test(existing)) {
@@ -1035,7 +1073,7 @@ function mergeContractBlock(existing) {
1035
1073
  ${block}
1036
1074
  `;
1037
1075
  }
1038
- async function writeAgentsContract(projectDir) {
1076
+ async function writeAgentsContract(projectDir, contractBlock = GENEX_CONTRACT_BLOCK) {
1039
1077
  let changed = false;
1040
1078
  try {
1041
1079
  const agentsPath = path6.join(projectDir, "AGENTS.md");
@@ -1045,7 +1083,7 @@ async function writeAgentsContract(projectDir) {
1045
1083
  } catch {
1046
1084
  existing = null;
1047
1085
  }
1048
- const next = mergeContractBlock(existing);
1086
+ const next = mergeContractBlock(existing, contractBlock);
1049
1087
  if (next !== existing) {
1050
1088
  await fs6.writeFile(agentsPath, next, "utf8");
1051
1089
  changed = true;
@@ -1072,19 +1110,25 @@ ${CLAUDE_IMPORT_LINE}
1072
1110
  }
1073
1111
  return changed;
1074
1112
  }
1113
+ async function writeToolsContract(projectDir) {
1114
+ return writeAgentsContract(projectDir, GENEX_TOOLS_CONTRACT_BLOCK);
1115
+ }
1075
1116
  async function healAncestorContracts(projectDir, stopDir = os4.homedir()) {
1076
1117
  const findings = [];
1077
1118
  const stop = path6.resolve(stopDir);
1078
1119
  let dir = path6.dirname(path6.resolve(projectDir));
1079
1120
  for (let depth = 0; depth < 20; depth++) {
1080
1121
  try {
1081
- let isProject = false;
1082
- try {
1083
- await fs6.access(path6.join(dir, ".genex", "project.json"));
1084
- isProject = true;
1085
- } catch {
1122
+ let isWorkspace = false;
1123
+ for (const marker of ["project.json", "workspace.json"]) {
1124
+ try {
1125
+ await fs6.access(path6.join(dir, ".genex", marker));
1126
+ isWorkspace = true;
1127
+ break;
1128
+ } catch {
1129
+ }
1086
1130
  }
1087
- if (!isProject) {
1131
+ if (!isWorkspace) {
1088
1132
  const agentsPath = path6.join(dir, "AGENTS.md");
1089
1133
  let content = null;
1090
1134
  try {
@@ -1142,6 +1186,45 @@ async function healAncestorContractsAndReport(log, projectDir) {
1142
1186
  import fs7 from "fs/promises";
1143
1187
  import os5 from "os";
1144
1188
  import path7 from "path";
1189
+
1190
+ // src/lib/workspace.ts
1191
+ async function workspaceMode(cwd = process.cwd()) {
1192
+ if (await readProject(cwd)) return "game";
1193
+ const ws = await readWorkspace(cwd);
1194
+ return ws?.mode === "tools" ? "tools" : null;
1195
+ }
1196
+ var REFUSED_IN_TOOLS = /* @__PURE__ */ new Set([
1197
+ "link",
1198
+ "pull",
1199
+ "list",
1200
+ "preview",
1201
+ "promote",
1202
+ "publish",
1203
+ "rollback",
1204
+ "rename",
1205
+ "make-remixable",
1206
+ "shop",
1207
+ "domain",
1208
+ "asset",
1209
+ "controller"
1210
+ ]);
1211
+ function refusedInTools(command) {
1212
+ return command !== void 0 && REFUSED_IN_TOOLS.has(command);
1213
+ }
1214
+ function reportPlatformRefused(log, command) {
1215
+ log.error(`\`genex ${command}\` is not part of Genex Tools.`);
1216
+ log.plain(
1217
+ [
1218
+ " This workspace is the asset toolkit: generation only. Hosting, publishing, multiplayer,",
1219
+ " remixing and custom domains are the Genex platform \u2014 a game built there gets all of it",
1220
+ ` from one prompt: ${c.cyan("https://genex.games")}`,
1221
+ ` ${c.cyan("\u2192")} Everything here still works: ${c.cyan("npx genex model|image|video|texture|sfx|music|voice|character")}.`,
1222
+ ` ${c.cyan("\u2192")} ${c.cyan("npx genex doctor")} lists what this account can generate right now.`
1223
+ ].join("\n")
1224
+ );
1225
+ }
1226
+
1227
+ // src/lib/updates.ts
1145
1228
  function parseSemver(v) {
1146
1229
  const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
1147
1230
  if (!m) return null;
@@ -1181,6 +1264,22 @@ async function hasGenexSkills(skillsDir) {
1181
1264
  return false;
1182
1265
  }
1183
1266
  }
1267
+ var TOOL_SKILL_PREFIX = "genex-tool-";
1268
+ function skillFamilyOf(name) {
1269
+ return name.startsWith(TOOL_SKILL_PREFIX) ? "tools" : "game";
1270
+ }
1271
+ function skillFamilyFilter(mode) {
1272
+ return (rel) => {
1273
+ const segments = rel.split(/[\\/]/);
1274
+ const top = segments[0] ?? "";
1275
+ if (top !== "skills") {
1276
+ return mode === "game";
1277
+ }
1278
+ const skill = segments[1];
1279
+ if (skill === void 0) return true;
1280
+ return skillFamilyOf(skill) === mode;
1281
+ };
1282
+ }
1184
1283
  var REMOVED_SKILLS = [
1185
1284
  "genex-explore",
1186
1285
  "genex-threejs-procedural-materials",
@@ -1232,13 +1331,18 @@ async function pruneRemovedSkills(skillsDir, log) {
1232
1331
  }
1233
1332
  return removed.length > 0;
1234
1333
  }
1235
- async function syncSkillsForTarget(target, templatesDir = getTemplatesDir(), version = getCliVersion(), log) {
1334
+ async function syncSkillsForTarget(target, templatesDir = getTemplatesDir(), version = getCliVersion(), log, family = "game") {
1236
1335
  const skillsDir = path7.join(target.baseDir, "skills");
1237
1336
  if (!await hasGenexSkills(skillsDir)) return false;
1238
1337
  if (await readSkillsMarker(skillsDir) === version) return false;
1239
1338
  const src = target.full ? templatesDir : path7.join(templatesDir, "skills");
1240
1339
  const dest = target.full ? target.baseDir : skillsDir;
1241
- await copyTemplates(src, dest, { exclude: ["controllers", "motion", "asset-viewer"] });
1340
+ const familyFilter = skillFamilyFilter(family);
1341
+ const filter = target.full ? familyFilter : (rel) => familyFilter(`skills/${rel}`);
1342
+ await copyTemplates(src, dest, {
1343
+ exclude: ["controllers", "motion", "asset-viewer"],
1344
+ filter
1345
+ });
1242
1346
  await pruneRemovedSkills(skillsDir, log);
1243
1347
  await writeSkillsMarker(skillsDir, version);
1244
1348
  return true;
@@ -1287,23 +1391,32 @@ async function cleanupLegacyGlobalSkills(log) {
1287
1391
  return false;
1288
1392
  }
1289
1393
  }
1290
- async function syncSkills(log) {
1394
+ async function syncSkills(log, mode) {
1291
1395
  try {
1292
1396
  await cleanupLegacyGlobalSkills(log);
1293
1397
  const version = getCliVersion();
1294
1398
  const templatesDir = getTemplatesDir();
1399
+ const resolved = mode !== void 0 ? mode : await workspaceMode();
1400
+ const family = resolved === "tools" ? "tools" : "game";
1295
1401
  let refreshed = false;
1296
1402
  for (const target of resolveAgentTargets()) {
1297
1403
  try {
1298
- refreshed = await syncSkillsForTarget(target, templatesDir, version, log) || refreshed;
1404
+ refreshed = await syncSkillsForTarget(target, templatesDir, version, log, family) || refreshed;
1299
1405
  } catch {
1300
1406
  }
1301
1407
  }
1302
1408
  if (refreshed) log.plain(`\u{1F504} Genex skills updated to ${version}`);
1303
1409
  try {
1304
- await fs7.access(path7.join(process.cwd(), ".genex", "project.json"));
1305
- if (await writeAgentsContract(process.cwd())) {
1306
- log.plain("\u{1F504} Genex build contract refreshed (AGENTS.md managed block)");
1410
+ if (resolved === "game") {
1411
+ if (await writeAgentsContract(process.cwd())) {
1412
+ log.plain("\u{1F504} Genex build contract refreshed (AGENTS.md managed block)");
1413
+ }
1414
+ } else if (resolved === "tools") {
1415
+ if (await writeToolsContract(process.cwd())) {
1416
+ log.plain("\u{1F504} Genex Tools workspace rules refreshed (AGENTS.md managed block)");
1417
+ }
1418
+ } else {
1419
+ return;
1307
1420
  }
1308
1421
  await healAncestorContractsAndReport(log, process.cwd());
1309
1422
  } catch {
@@ -1706,6 +1819,19 @@ function warnPreexisting(log, entries) {
1706
1819
  }
1707
1820
  async function runInit(opts) {
1708
1821
  const log = createLogger({ quiet: opts.quiet });
1822
+ if ((await readWorkspace())?.mode === "tools") {
1823
+ log.error("This folder is a Genex Tools workspace.");
1824
+ log.plain(
1825
+ [
1826
+ " It has the generation toolkit installed for a game you host yourself \u2014 creating a",
1827
+ " hosted genex game here would install a second, conflicting set of agent instructions.",
1828
+ ` ${c.cyan("\u2192")} Building a genex-hosted game? Run ${c.cyan("npx genex init")} in its own folder.`,
1829
+ ` ${c.cyan("\u2192")} Just want the tools working? ${c.cyan("npx genex doctor")} says what needs fixing.`
1830
+ ].join("\n")
1831
+ );
1832
+ process.exitCode = 1;
1833
+ return;
1834
+ }
1709
1835
  log.plain(c.bold("genex init"));
1710
1836
  log.plain("");
1711
1837
  const preexisting = await listPreexistingEntries(process.cwd());
@@ -1722,7 +1848,8 @@ async function runInit(opts) {
1722
1848
  const dest = t.full ? t.baseDir : path10.join(t.baseDir, "skills");
1723
1849
  const { copied, updated } = await copyTemplates(src, dest, {
1724
1850
  force: opts.force,
1725
- exclude: ["controllers", "motion", "asset-viewer"]
1851
+ exclude: ["controllers", "motion", "asset-viewer"],
1852
+ filter: t.full ? skillFamilyFilter("game") : (rel) => skillFamilyFilter("game")(`skills/${rel}`)
1726
1853
  });
1727
1854
  await pruneRemovedSkills(path10.join(t.baseDir, "skills"), log);
1728
1855
  await writeSkillsMarker(path10.join(t.baseDir, "skills"));
@@ -6685,8 +6812,8 @@ async function runRollback(opts) {
6685
6812
  }
6686
6813
 
6687
6814
  // src/commands/generate.ts
6688
- import fs20 from "fs/promises";
6689
- import path19 from "path";
6815
+ import fs21 from "fs/promises";
6816
+ import path20 from "path";
6690
6817
  import { PNG as PNG4 } from "pngjs";
6691
6818
 
6692
6819
  // src/lib/glass.ts
@@ -6766,6 +6893,96 @@ function solveMagentaGlass(source) {
6766
6893
  return { ...base, png: out, ok: true };
6767
6894
  }
6768
6895
 
6896
+ // src/lib/download-assets.ts
6897
+ import fs20 from "fs/promises";
6898
+ import path19 from "path";
6899
+ var RUNG_ROLE = /@\d+$/;
6900
+ function isPrimaryRole(role) {
6901
+ return !RUNG_ROLE.test(role);
6902
+ }
6903
+ function primaryFiles(files) {
6904
+ return files.filter((f) => isPrimaryRole(f.role));
6905
+ }
6906
+ function localAssetName(kind, prompt, id, file, multi) {
6907
+ const slug = prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "");
6908
+ const base = `${slug || kind}-${id.slice(0, 8)}`;
6909
+ const role = multi ? `-${file.role.replace(/^(texture|image|character|model|model_anim|model-anim)-/, "")}` : "";
6910
+ const ext = file.ext.replace(/^\./, "") || "bin";
6911
+ return `${base}${role}.${ext}`;
6912
+ }
6913
+ async function downloadAssets(files, opts) {
6914
+ const wanted = primaryFiles(files);
6915
+ const result = { saved: [], failures: [] };
6916
+ if (wanted.length === 0) return result;
6917
+ try {
6918
+ await fs20.mkdir(opts.outDir, { recursive: true });
6919
+ } catch (err) {
6920
+ result.failures.push(
6921
+ `couldn't create ${opts.outDir} (${err instanceof Error ? err.message : String(err)})`
6922
+ );
6923
+ return result;
6924
+ }
6925
+ const multi = wanted.length > 1;
6926
+ for (const file of wanted) {
6927
+ const dest = path19.join(opts.outDir, localAssetName(opts.kind, opts.prompt, opts.id, file, multi));
6928
+ try {
6929
+ const res = await fetch(file.url);
6930
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
6931
+ const buf = Buffer.from(await res.arrayBuffer());
6932
+ await fs20.writeFile(dest, buf);
6933
+ result.saved.push({ role: file.role, path: dest, url: file.url, bytes: buf.byteLength });
6934
+ } catch (err) {
6935
+ result.failures.push(
6936
+ `${file.role}: ${err instanceof Error ? err.message : String(err)} \u2014 the asset is still live at ${file.url}`
6937
+ );
6938
+ }
6939
+ }
6940
+ return result;
6941
+ }
6942
+ function formatBytes(bytes) {
6943
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
6944
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
6945
+ return `${bytes} B`;
6946
+ }
6947
+ var DEFAULT_ASSET_DIR = "./assets";
6948
+ function localDeliveryFor(mode, opts, prompt) {
6949
+ if (mode !== "tools" || opts.noDownload) return void 0;
6950
+ return { outDir: opts.outDir ?? DEFAULT_ASSET_DIR, prompt };
6951
+ }
6952
+
6953
+ // src/lib/lanes.ts
6954
+ async function fetchLanes(apiUrl, token, timeoutMs = 4e3) {
6955
+ try {
6956
+ const res = await apiFetch(`${apiUrl}/api/generations/lanes`, {
6957
+ headers: { Authorization: `Bearer ${token}` },
6958
+ signal: AbortSignal.timeout(timeoutMs)
6959
+ });
6960
+ if (!res.ok) return null;
6961
+ const body = await res.json();
6962
+ if (!Array.isArray(body.lanes)) return null;
6963
+ return {
6964
+ lanes: body.lanes.filter(
6965
+ (l) => !!l && typeof l.kind === "string" && typeof l.provider === "string"
6966
+ ),
6967
+ paused: body.paused === true
6968
+ };
6969
+ } catch {
6970
+ return null;
6971
+ }
6972
+ }
6973
+ function laneFor(report, kind) {
6974
+ return report?.lanes.find((l) => l.kind === kind);
6975
+ }
6976
+ function mockLaneRefusal(kind, provider) {
6977
+ return [
6978
+ `The \`${kind}\` lane on this server has no provider key \u2014 it is running in SAMPLE mode (${provider}).`,
6979
+ " A generation would succeed, spend credits, and return a placeholder asset that looks real.",
6980
+ " Refusing before anything is charged.",
6981
+ " \u2192 On genex.games every lane is live; this is a self-hosted or partially configured stand.",
6982
+ " \u2192 `npx genex doctor` lists every lane's status."
6983
+ ];
6984
+ }
6985
+
6769
6986
  // src/lib/open.ts
6770
6987
  import { spawn as spawn5 } from "child_process";
6771
6988
  function tokenize(cmd) {
@@ -6856,7 +7073,7 @@ var isRemoteRef = (value) => /^(https?:\/\/|data:)/i.test(value);
6856
7073
  async function inlineLocalImage(filePath, flag) {
6857
7074
  let bytes;
6858
7075
  try {
6859
- bytes = await fs20.readFile(filePath);
7076
+ bytes = await fs21.readFile(filePath);
6860
7077
  } catch {
6861
7078
  return { ok: false, error: `Couldn't read the ${flag} file at ${filePath}.` };
6862
7079
  }
@@ -6866,7 +7083,7 @@ async function inlineLocalImage(filePath, flag) {
6866
7083
  error: `${flag} file is ${(bytes.length / 1048576).toFixed(1)} MB \u2014 over the ~4 MB inline limit. Downscale/compress it first, or pass an asset URL instead.`
6867
7084
  };
6868
7085
  }
6869
- const mime = IMAGE_MIME_BY_EXT[path19.extname(filePath).toLowerCase()] ?? "image/png";
7086
+ const mime = IMAGE_MIME_BY_EXT[path20.extname(filePath).toLowerCase()] ?? "image/png";
6870
7087
  return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
6871
7088
  }
6872
7089
  var SKYBOX_ENVIRONMENT_SUFFIX = ". The image contains ONLY sky: cloud, atmosphere, light, weather and distant haze at the horizon. Every structure, object, plant and ground surface is outside the frame.";
@@ -7023,7 +7240,7 @@ async function runGenerate(kind, opts) {
7023
7240
  let typedPrompt = opts.prompt?.trim();
7024
7241
  if (!typedPrompt && kind === "model" && opts.imageUrl) {
7025
7242
  const ref = opts.imageUrl.startsWith("data:") ? "local image" : opts.imageUrl;
7026
- typedPrompt = `from image: ${path19.basename(ref).slice(0, 120)}`;
7243
+ typedPrompt = `from image: ${path20.basename(ref).slice(0, 120)}`;
7027
7244
  }
7028
7245
  if (!typedPrompt) {
7029
7246
  log.error(`Missing prompt. Usage: ${c.cyan(`genex ${kind} "<prompt>"`)}`);
@@ -7067,7 +7284,7 @@ async function runGenerate(kind, opts) {
7067
7284
  return;
7068
7285
  }
7069
7286
  try {
7070
- const bytes = await fs20.readFile(opts.inpaintUrl);
7287
+ const bytes = await fs21.readFile(opts.inpaintUrl);
7071
7288
  opts = { ...opts, inpaintUrl: `data:image/png;base64,${bytes.toString("base64")}` };
7072
7289
  } catch {
7073
7290
  log.error(`Couldn't read the --inpaint mask at ${opts.inpaintUrl}.`);
@@ -7115,6 +7332,17 @@ async function runGenerate(kind, opts) {
7115
7332
  }
7116
7333
  const meta = await readProject();
7117
7334
  const apiUrl = getApiUrl(opts.apiUrl ?? meta?.apiUrl);
7335
+ const mode = await workspaceMode();
7336
+ if (mode === "tools") {
7337
+ const lane = laneFor(await fetchLanes(apiUrl, token), kind);
7338
+ if (lane?.mock) {
7339
+ const [headline, ...rest] = mockLaneRefusal(kind, lane.provider);
7340
+ log.error(headline);
7341
+ log.plain(rest.join("\n"));
7342
+ process.exitCode = 1;
7343
+ return;
7344
+ }
7345
+ }
7118
7346
  const options = buildGenOptions(kind, opts);
7119
7347
  log.plain(c.bold(`genex ${kind}`));
7120
7348
  log.dim(` ${prompt}`);
@@ -7187,10 +7415,20 @@ async function runGenerate(kind, opts) {
7187
7415
  process.exitCode = 1;
7188
7416
  return id;
7189
7417
  }
7190
- await reportGlassTerminal(view, opts.outDir ?? ".", log, opts.json ?? false);
7418
+ const glassDir = opts.outDir ?? (mode === "tools" ? DEFAULT_ASSET_DIR : ".");
7419
+ await reportGlassTerminal(view, glassDir, log, opts.json ?? false);
7191
7420
  return id;
7192
7421
  }
7193
- await awaitAndReport(apiUrl, token, id, kind, log, opts.open, opts.json);
7422
+ await awaitAndReport(
7423
+ apiUrl,
7424
+ token,
7425
+ id,
7426
+ kind,
7427
+ log,
7428
+ opts.open,
7429
+ opts.json,
7430
+ localDeliveryFor(mode, opts, prompt)
7431
+ );
7194
7432
  return id;
7195
7433
  }
7196
7434
  async function reportGlassTerminal(view, outDir, log, json) {
@@ -7200,7 +7438,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
7200
7438
  return;
7201
7439
  }
7202
7440
  await recordTerminal(view.id, "completed", files.map((f) => f.url));
7203
- await fs20.mkdir(outDir, { recursive: true });
7441
+ await fs21.mkdir(outDir, { recursive: true });
7204
7442
  const solved = [];
7205
7443
  for (let i = 0; i < files.length; i++) {
7206
7444
  const f = files[i];
@@ -7232,8 +7470,8 @@ async function reportGlassTerminal(view, outDir, log, json) {
7232
7470
  });
7233
7471
  continue;
7234
7472
  }
7235
- const outPath = path19.join(outDir, `glass-${i + 1}.png`);
7236
- await fs20.writeFile(outPath, PNG4.sync.write(r.png));
7473
+ const outPath = path20.join(outDir, `glass-${i + 1}.png`);
7474
+ await fs21.writeFile(outPath, PNG4.sync.write(r.png));
7237
7475
  solved.push({
7238
7476
  path: outPath,
7239
7477
  url: f.url,
@@ -7269,7 +7507,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
7269
7507
  process.exitCode = 1;
7270
7508
  }
7271
7509
  }
7272
- async function awaitAndReport(apiUrl, token, id, kind, log, open = false, json = false) {
7510
+ async function awaitAndReport(apiUrl, token, id, kind, log, open = false, json = false, local) {
7273
7511
  if (!json) log.step("Generating\u2026 (this can take up to a minute)");
7274
7512
  const view = await waitForTerminalView(
7275
7513
  apiUrl,
@@ -7285,7 +7523,7 @@ async function awaitAndReport(apiUrl, token, id, kind, log, open = false, json =
7285
7523
  process.exitCode = 1;
7286
7524
  return;
7287
7525
  }
7288
- await reportTerminal(kind, view, log, open, json);
7526
+ await reportTerminal(kind, view, log, open, json, local);
7289
7527
  }
7290
7528
  async function waitForTerminalView(apiUrl, token, id, kind, onProgress = () => {
7291
7529
  }) {
@@ -7294,7 +7532,7 @@ async function waitForTerminalView(apiUrl, token, id, kind, onProgress = () => {
7294
7532
  const streamed = await waitViaSSE(apiUrl, token, id, onProgress, deadline);
7295
7533
  return streamed === "unsupported" ? poll(apiUrl, token, id, onProgress, timeoutMs) : streamed;
7296
7534
  }
7297
- async function reportTerminal(kind, view, log, open = false, json = false) {
7535
+ async function reportTerminal(kind, view, log, open = false, json = false, local) {
7298
7536
  if (view.status !== "completed") {
7299
7537
  if (json) writeJson({ kind, id: view.id, status: view.status, error: view.error ?? null });
7300
7538
  else log.error(`Generation ${view.status}${view.error ? `: ${view.error}` : ""}.`);
@@ -7319,12 +7557,20 @@ async function reportTerminal(kind, view, log, open = false, json = false) {
7319
7557
  return;
7320
7558
  }
7321
7559
  await recordTerminal(view.id, "completed", files.map((f) => f.url));
7560
+ const delivered = local ? await downloadAssets(files, {
7561
+ kind,
7562
+ prompt: local.prompt,
7563
+ id: view.id,
7564
+ outDir: local.outDir
7565
+ }) : null;
7322
7566
  if (kind === "character_concept") {
7323
7567
  reportCharacterConceptTerminal(view, log, { json, open });
7568
+ if (delivered) reportLocalFiles(delivered, log, json);
7324
7569
  return;
7325
7570
  }
7326
7571
  if (kind === "character_preview") {
7327
7572
  reportCharacterPreviewTerminal(view, log, { json, open });
7573
+ if (delivered) reportLocalFiles(delivered, log, json);
7328
7574
  return;
7329
7575
  }
7330
7576
  if (json) {
@@ -7333,6 +7579,7 @@ async function reportTerminal(kind, view, log, open = false, json = false) {
7333
7579
  id: view.id,
7334
7580
  status: view.status,
7335
7581
  files,
7582
+ ...delivered ? { saved: delivered.saved, downloadFailures: delivered.failures } : {},
7336
7583
  ...kind === "character" ? {
7337
7584
  characterId: view.id,
7338
7585
  nextCommand: `genex controller character --character ${view.id}`
@@ -7341,10 +7588,14 @@ async function reportTerminal(kind, view, log, open = false, json = false) {
7341
7588
  return;
7342
7589
  }
7343
7590
  log.plain("");
7344
- log.success("Done \u2014 live on R2 (nothing downloaded or committed):");
7345
- for (const f of files) {
7346
- const label = files.length > 1 ? `${c.dim(`${f.role.replace(/^texture-/, "")}: `)}` : "";
7347
- log.plain(` ${label}${f.url}`);
7591
+ if (delivered) {
7592
+ reportLocalFiles(delivered, log, false);
7593
+ } else {
7594
+ log.success("Done \u2014 live on R2 (nothing downloaded or committed):");
7595
+ for (const f of files) {
7596
+ const label = files.length > 1 ? `${c.dim(`${f.role.replace(/^texture-/, "")}: `)}` : "";
7597
+ log.plain(` ${label}${f.url}`);
7598
+ }
7348
7599
  }
7349
7600
  log.plain("");
7350
7601
  if (kind === "texture" && files[0]) await reportTextureSeam(files[0].url, log);
@@ -7362,7 +7613,22 @@ async function reportTerminal(kind, view, log, open = false, json = false) {
7362
7613
  log.plain(` ${files[0].url}`);
7363
7614
  log.plain("");
7364
7615
  }
7365
- printHint(kind, view, files, log);
7616
+ printHint(kind, view, files, log, delivered !== null);
7617
+ }
7618
+ function reportLocalFiles(result, log, json) {
7619
+ if (json) return;
7620
+ if (result.saved.length > 0) {
7621
+ log.success(
7622
+ `Done \u2014 ${result.saved.length} file${result.saved.length === 1 ? "" : "s"} saved to your project:`
7623
+ );
7624
+ for (const s of result.saved) {
7625
+ log.plain(` ${s.path} ${c.dim(`(${formatBytes(s.bytes)})`)}`);
7626
+ }
7627
+ log.dim(` source of record (provenance, not hosting): ${result.saved[0].url}`);
7628
+ }
7629
+ for (const failure of result.failures) {
7630
+ log.error(`Couldn't save one file \u2014 ${failure}`);
7631
+ }
7366
7632
  }
7367
7633
  function laneFailureAdvice(kind, outcomes) {
7368
7634
  if (outcomes.failed < 2 || outcomes.completed > 0) return null;
@@ -7529,9 +7795,11 @@ function assetHint(kind, view, url) {
7529
7795
  const base = hint[kind];
7530
7796
  return url && HINT_URL_KINDS.has(kind) ? `${base} url = "${url}"` : base;
7531
7797
  }
7532
- function printHint(kind, view, files, log) {
7533
- log.dim(` ${assetHint(kind, view, files[0]?.url ?? "")}`);
7534
- log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
7798
+ function printHint(kind, view, files, log, local = false) {
7799
+ log.dim(` ${assetHint(kind, view, local ? void 0 : files[0]?.url ?? "")}`);
7800
+ log.dim(
7801
+ local ? " Wire the LOCAL path into your code \u2014 the file is yours. Never fetch the URL at runtime." : " Reference the URL directly in your code \u2014 don't download it into the repo."
7802
+ );
7535
7803
  }
7536
7804
  var CONCEPT_STOP = "STOP \u2014 show all three concept images to the user and wait for an explicit selection. Do not create the 3D preview until they choose candidate 1, 2, or 3.";
7537
7805
  function metadataOf(view) {
@@ -7833,11 +8101,13 @@ async function runWait(opts) {
7833
8101
  return;
7834
8102
  }
7835
8103
  const kind = view.kind ?? "image";
8104
+ const ledgerPrompt = (await readLedger(opts.cwd ?? process.cwd())).find((e) => e.id === id)?.prompt;
8105
+ const local = localDeliveryFor(await workspaceMode(), opts, ledgerPrompt || kind);
7836
8106
  log.plain(c.bold("genex wait"));
7837
8107
  log.dim(` ${kind} ${id}`);
7838
8108
  log.plain("");
7839
8109
  if (TERMINAL2.has(view.status)) {
7840
- await reportTerminal(kind, view, log, opts.open, opts.json);
8110
+ await reportTerminal(kind, view, log, opts.open, opts.json, local);
7841
8111
  return;
7842
8112
  }
7843
8113
  if (kind === "character_concept" || kind === "character_preview" || kind === "character") {
@@ -7845,7 +8115,7 @@ async function runWait(opts) {
7845
8115
  ` \u23F3 Character stages take several minutes server-side \u2014 you don't have to sit here. Keep building and check back with ${c.cyan("genex wait --all")}.`
7846
8116
  );
7847
8117
  }
7848
- await awaitAndReport(apiUrl, token, id, kind, log, opts.open, opts.json);
8118
+ await awaitAndReport(apiUrl, token, id, kind, log, opts.open, opts.json, local);
7849
8119
  }
7850
8120
  async function runWaitAll(opts) {
7851
8121
  const log = createLogger({ quiet: opts.quiet || opts.json });
@@ -7967,8 +8237,8 @@ async function toRow(e, v, cwd) {
7967
8237
  }
7968
8238
 
7969
8239
  // src/commands/controller.ts
7970
- import fs22 from "fs/promises";
7971
- import path21 from "path";
8240
+ import fs23 from "fs/promises";
8241
+ import path22 from "path";
7972
8242
 
7973
8243
  // ../../packages/meshy-animation-catalog/src/index.ts
7974
8244
  import { createHash } from "crypto";
@@ -17098,9 +17368,9 @@ function searchMeshyAnimations(query, options = {}) {
17098
17368
  }
17099
17369
 
17100
17370
  // src/lib/anims.ts
17101
- import fs21 from "fs/promises";
17102
- import path20 from "path";
17103
- var ANIMS_DEST = path20.join("public", "assets", "anims");
17371
+ import fs22 from "fs/promises";
17372
+ import path21 from "path";
17373
+ var ANIMS_DEST = path21.join("public", "assets", "anims");
17104
17374
  var HIDDEN_TAG = "reference";
17105
17375
  async function runAnims(opts) {
17106
17376
  const log = createLogger({ quiet: opts.quiet });
@@ -17116,7 +17386,7 @@ async function runAnims(opts) {
17116
17386
  printCatalog(log, manifest, selectors);
17117
17387
  return;
17118
17388
  }
17119
- const controllerMarker = path20.join(root, "src", "controllers", "character");
17389
+ const controllerMarker = path21.join(root, "src", "controllers", "character");
17120
17390
  if (!await exists2(controllerMarker)) {
17121
17391
  log.error(
17122
17392
  `No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
@@ -17125,11 +17395,11 @@ async function runAnims(opts) {
17125
17395
  process.exitCode = 1;
17126
17396
  return;
17127
17397
  }
17128
- const destDir = path20.join(root, ANIMS_DEST);
17129
- const gameManifestPath = path20.join(destDir, "manifest.json");
17398
+ const destDir = path21.join(root, ANIMS_DEST);
17399
+ const gameManifestPath = path21.join(destDir, "manifest.json");
17130
17400
  if (opts.reset) {
17131
- await fs21.rm(destDir, { recursive: true, force: true });
17132
- log.step(`Cleared ${c.cyan(ANIMS_DEST + path20.sep)} (--reset)`);
17401
+ await fs22.rm(destDir, { recursive: true, force: true });
17402
+ log.step(`Cleared ${c.cyan(ANIMS_DEST + path21.sep)} (--reset)`);
17133
17403
  }
17134
17404
  if (selectors.length === 0) {
17135
17405
  const installed = await readGameManifest(gameManifestPath);
@@ -17167,35 +17437,35 @@ async function runAnims(opts) {
17167
17437
  }
17168
17438
  }
17169
17439
  const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
17170
- const cacheDir = path20.join(
17440
+ const cacheDir = path21.join(
17171
17441
  opts.cacheDir ?? getAnimsCacheDir(),
17172
17442
  `${manifest.library}-v${manifest.version}`
17173
17443
  );
17174
- await fs21.mkdir(cacheDir, { recursive: true });
17175
- await fs21.mkdir(destDir, { recursive: true });
17444
+ await fs22.mkdir(cacheDir, { recursive: true });
17445
+ await fs22.mkdir(destDir, { recursive: true });
17176
17446
  const base = getAnimsBase(opts.animsBase);
17177
17447
  let installedCount = 0;
17178
17448
  let presentCount = 0;
17179
17449
  let addedBytes = 0;
17180
17450
  const failures = [];
17181
17451
  for (const entry of wanted) {
17182
- const dest = path20.join(destDir, entry.file);
17452
+ const dest = path21.join(destDir, entry.file);
17183
17453
  if (await hasSize(dest, entry.bytes)) {
17184
17454
  presentCount++;
17185
17455
  continue;
17186
17456
  }
17187
17457
  try {
17188
- const cached = path20.join(cacheDir, entry.file);
17458
+ const cached = path21.join(cacheDir, entry.file);
17189
17459
  if (!await hasSize(cached, entry.bytes)) {
17190
17460
  const res = await fetch(base + entry.file);
17191
17461
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
17192
17462
  const buf = Buffer.from(await res.arrayBuffer());
17193
- await fs21.writeFile(cached, buf);
17463
+ await fs22.writeFile(cached, buf);
17194
17464
  }
17195
- await fs21.copyFile(cached, dest);
17465
+ await fs22.copyFile(cached, dest);
17196
17466
  installedCount++;
17197
17467
  addedBytes += entry.bytes;
17198
- log.dim(` ${path20.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
17468
+ log.dim(` ${path21.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
17199
17469
  } catch (err) {
17200
17470
  failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
17201
17471
  }
@@ -17211,13 +17481,13 @@ async function runAnims(opts) {
17211
17481
  version: manifest.version,
17212
17482
  clips: [...union].sort((a, b) => a.localeCompare(b))
17213
17483
  };
17214
- await fs21.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
17484
+ await fs22.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
17215
17485
  log.plain("");
17216
17486
  const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
17217
17487
  if (presentCount > 0) parts.push(`${presentCount} already present`);
17218
17488
  if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
17219
17489
  log.success(
17220
- `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path20.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
17490
+ `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path21.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
17221
17491
  );
17222
17492
  for (const [selector, entries] of resolved) {
17223
17493
  const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
@@ -17249,8 +17519,8 @@ async function loadManifest(baseOverride) {
17249
17519
  }
17250
17520
  } catch {
17251
17521
  }
17252
- const snapshotPath = path20.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
17253
- const manifest = JSON.parse(await fs21.readFile(snapshotPath, "utf8"));
17522
+ const snapshotPath = path21.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
17523
+ const manifest = JSON.parse(await fs22.readFile(snapshotPath, "utf8"));
17254
17524
  return { manifest, source: "snapshot" };
17255
17525
  }
17256
17526
  function resolveSelectors(manifest, selectors) {
@@ -17368,21 +17638,21 @@ function printCatalog(log, manifest, selectors) {
17368
17638
  }
17369
17639
  async function readGameManifest(file) {
17370
17640
  try {
17371
- return JSON.parse(await fs21.readFile(file, "utf8"));
17641
+ return JSON.parse(await fs22.readFile(file, "utf8"));
17372
17642
  } catch {
17373
17643
  return null;
17374
17644
  }
17375
17645
  }
17376
17646
  async function hasSize(file, bytes) {
17377
17647
  try {
17378
- return (await fs21.stat(file)).size === bytes;
17648
+ return (await fs22.stat(file)).size === bytes;
17379
17649
  } catch {
17380
17650
  return false;
17381
17651
  }
17382
17652
  }
17383
17653
  async function exists2(p) {
17384
17654
  try {
17385
- await fs21.access(p);
17655
+ await fs22.access(p);
17386
17656
  return true;
17387
17657
  } catch {
17388
17658
  return false;
@@ -17576,8 +17846,8 @@ var CONTROLLER_FILE_SETS = {
17576
17846
  ]
17577
17847
  }
17578
17848
  };
17579
- var CODE_DEST = path21.join("src", "controllers");
17580
- var ASSETS_DEST = path21.join("public", "assets");
17849
+ var CODE_DEST = path22.join("src", "controllers");
17850
+ var ASSETS_DEST = path22.join("public", "assets");
17581
17851
  async function runController(opts) {
17582
17852
  const log = createLogger({ quiet: opts.quiet });
17583
17853
  if (opts.kind?.trim() === "anims") {
@@ -17594,31 +17864,31 @@ async function runController(opts) {
17594
17864
  process.exitCode = 1;
17595
17865
  return;
17596
17866
  }
17597
- const srcDir = path21.join(getTemplatesDir(), "controllers");
17867
+ const srcDir = path22.join(getTemplatesDir(), "controllers");
17598
17868
  const root = opts.cwd ?? process.cwd();
17599
17869
  const set = CONTROLLER_FILE_SETS[kind];
17600
17870
  log.plain(c.bold(`genex controller ${kind}`));
17601
17871
  log.plain("");
17602
- log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path21.sep)}`);
17872
+ log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path22.sep)}`);
17603
17873
  const plan = [
17604
- ...set.code.map((rel) => ({ from: rel, rel: path21.join(CODE_DEST, rel) })),
17874
+ ...set.code.map((rel) => ({ from: rel, rel: path22.join(CODE_DEST, rel) })),
17605
17875
  ...set.assets.map((rel) => ({
17606
17876
  from: rel,
17607
- rel: path21.join(ASSETS_DEST, path21.basename(rel))
17877
+ rel: path22.join(ASSETS_DEST, path22.basename(rel))
17608
17878
  }))
17609
17879
  ];
17610
17880
  let copied = 0;
17611
17881
  let skipped = 0;
17612
17882
  try {
17613
17883
  for (const file of plan) {
17614
- const dest = path21.join(root, file.rel);
17884
+ const dest = path22.join(root, file.rel);
17615
17885
  if (!opts.force && await exists3(dest)) {
17616
17886
  skipped++;
17617
17887
  log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
17618
17888
  continue;
17619
17889
  }
17620
- await fs22.mkdir(path21.dirname(dest), { recursive: true });
17621
- await fs22.copyFile(path21.join(srcDir, file.from), dest);
17890
+ await fs23.mkdir(path22.dirname(dest), { recursive: true });
17891
+ await fs23.copyFile(path22.join(srcDir, file.from), dest);
17622
17892
  copied++;
17623
17893
  log.dim(` ${file.rel}`);
17624
17894
  }
@@ -17671,7 +17941,7 @@ async function runController(opts) {
17671
17941
  for (const line of set.sketch) {
17672
17942
  log.dim(` ${line}`);
17673
17943
  }
17674
- if (kind === "character" && !await exists3(path21.join(root, ASSETS_DEST, "meshy-character.json"))) {
17944
+ if (kind === "character" && !await exists3(path22.join(root, ASSETS_DEST, "meshy-character.json"))) {
17675
17945
  log.plain("");
17676
17946
  log.plain(
17677
17947
  ` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
@@ -17703,9 +17973,9 @@ async function installMeshyCharacterManifest(args) {
17703
17973
  throw new Error("The API returned an invalid Meshy character manifest.");
17704
17974
  }
17705
17975
  assertCompleteMeshyControllerPack(manifest);
17706
- const destination = path21.join(args.root, ASSETS_DEST, "meshy-character.json");
17707
- await fs22.mkdir(path21.dirname(destination), { recursive: true });
17708
- await fs22.writeFile(
17976
+ const destination = path22.join(args.root, ASSETS_DEST, "meshy-character.json");
17977
+ await fs23.mkdir(path22.dirname(destination), { recursive: true });
17978
+ await fs23.writeFile(
17709
17979
  destination,
17710
17980
  `${JSON.stringify(manifest, null, 2)}
17711
17981
  `
@@ -17843,14 +18113,14 @@ function assertCompleteMeshyControllerPack(manifest) {
17843
18113
  }
17844
18114
  async function installFallbackAvatar(args) {
17845
18115
  const { root, srcDir, log } = args;
17846
- const dest = path21.join(root, ASSETS_DEST, "avatar.vrm");
17847
- await fs22.mkdir(path21.dirname(dest), { recursive: true });
17848
- await fs22.copyFile(path21.join(srcDir, "assets", "default-avatar.vrm"), dest);
18116
+ const dest = path22.join(root, ASSETS_DEST, "avatar.vrm");
18117
+ await fs23.mkdir(path22.dirname(dest), { recursive: true });
18118
+ await fs23.copyFile(path22.join(srcDir, "assets", "default-avatar.vrm"), dest);
17849
18119
  log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
17850
18120
  }
17851
18121
  async function exists3(p) {
17852
18122
  try {
17853
- await fs22.access(p);
18123
+ await fs23.access(p);
17854
18124
  return true;
17855
18125
  } catch {
17856
18126
  return false;
@@ -17858,8 +18128,8 @@ async function exists3(p) {
17858
18128
  }
17859
18129
 
17860
18130
  // src/commands/character.ts
17861
- import fs23 from "fs/promises";
17862
- import path22 from "path";
18131
+ import fs24 from "fs/promises";
18132
+ import path23 from "path";
17863
18133
  function exactAnimation(selector) {
17864
18134
  const trimmed = selector.trim();
17865
18135
  if (/^-?\d+$/.test(trimmed)) return animationById(Number(trimmed));
@@ -17939,7 +18209,7 @@ function showAmbiguity(selector, candidates, log, json = false) {
17939
18209
  }
17940
18210
  process.exitCode = 1;
17941
18211
  }
17942
- var INSTALLED_MANIFEST = path22.join("public", "assets", "meshy-character.json");
18212
+ var INSTALLED_MANIFEST = path23.join("public", "assets", "meshy-character.json");
17943
18213
  async function resolveAdoptTarget(selector) {
17944
18214
  const trimmed = selector?.trim();
17945
18215
  if (trimmed && !trimmed.endsWith(".json")) {
@@ -17948,7 +18218,7 @@ async function resolveAdoptTarget(selector) {
17948
18218
  const file = trimmed ?? INSTALLED_MANIFEST;
17949
18219
  let raw;
17950
18220
  try {
17951
- raw = await fs23.readFile(file, "utf8");
18221
+ raw = await fs24.readFile(file, "utf8");
17952
18222
  } catch {
17953
18223
  return {
17954
18224
  ok: false,
@@ -17979,6 +18249,11 @@ async function runCharacterAdopt(opts) {
17979
18249
  fail2(opts, "Not authorized. Run `genex init` first to sign in.", "character");
17980
18250
  return;
17981
18251
  }
18252
+ if (await workspaceMode() === "tools") {
18253
+ reportPlatformRefused(log, "character adopt");
18254
+ process.exitCode = 1;
18255
+ return;
18256
+ }
17982
18257
  const project = await readProject();
17983
18258
  if (!project?.id) {
17984
18259
  fail2(
@@ -18099,7 +18374,7 @@ async function runWorkflow(args) {
18099
18374
  return;
18100
18375
  }
18101
18376
  await recordTerminal(view.id, "completed", (view.files ?? []).map((file) => file.url));
18102
- await args.completed(view);
18377
+ await args.completed(view, localDeliveryFor(await workspaceMode(), args.opts, args.prompt));
18103
18378
  }
18104
18379
  async function runCharacterConcept(opts) {
18105
18380
  const log = createLogger({ quiet: opts.quiet || opts.json });
@@ -18322,7 +18597,7 @@ async function runCharacterFinalize(opts) {
18322
18597
  createPath: basePath,
18323
18598
  body,
18324
18599
  quote: price,
18325
- completed: async (view) => {
18600
+ completed: async (view, local) => {
18326
18601
  if (opts.json) {
18327
18602
  const files = view.files ?? [];
18328
18603
  if (files.length === 0) {
@@ -18348,7 +18623,7 @@ async function runCharacterFinalize(opts) {
18348
18623
  });
18349
18624
  return;
18350
18625
  }
18351
- await reportTerminal("character", view, log);
18626
+ await reportTerminal("character", view, log, false, false, local);
18352
18627
  }
18353
18628
  });
18354
18629
  }
@@ -18426,22 +18701,22 @@ async function context2(opts) {
18426
18701
  const project = await readProject();
18427
18702
  return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
18428
18703
  }
18429
- async function readVideo(path25, log) {
18704
+ async function readVideo(path29, log) {
18430
18705
  let bytes;
18431
18706
  try {
18432
- bytes = await readFile(path25);
18707
+ bytes = await readFile(path29);
18433
18708
  } catch {
18434
- log.error(`Can't read ${path25}.`);
18709
+ log.error(`Can't read ${path29}.`);
18435
18710
  return null;
18436
18711
  }
18437
18712
  if (bytes.byteLength > MAX_VIDEO_BYTES) {
18438
- log.error(`${basename(path25)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
18713
+ log.error(`${basename(path29)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
18439
18714
  return null;
18440
18715
  }
18441
18716
  return bytes;
18442
18717
  }
18443
- async function uploadVideo(apiUrl, token, characterId, path25, bytes, log) {
18444
- const contentType = /\.mov$/i.test(path25) ? "video/quicktime" : "video/mp4";
18718
+ async function uploadVideo(apiUrl, token, characterId, path29, bytes, log) {
18719
+ const contentType = /\.mov$/i.test(path29) ? "video/quicktime" : "video/mp4";
18445
18720
  const minted = await apiFetch(
18446
18721
  `${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
18447
18722
  {
@@ -18456,7 +18731,7 @@ async function uploadVideo(apiUrl, token, characterId, path25, bytes, log) {
18456
18731
  return null;
18457
18732
  }
18458
18733
  const { uploadUrl, videoUrl } = await minted.json();
18459
- log.dim(` uploading ${basename(path25)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
18734
+ log.dim(` uploading ${basename(path29)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
18460
18735
  const put = await fetch(uploadUrl, {
18461
18736
  method: "PUT",
18462
18737
  headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
@@ -18772,8 +19047,8 @@ function rank(items, query) {
18772
19047
  }
18773
19048
 
18774
19049
  // src/commands/motion.ts
18775
- import fs24 from "fs/promises";
18776
- import path23 from "path";
19050
+ import fs25 from "fs/promises";
19051
+ import path24 from "path";
18777
19052
 
18778
19053
  // src/lib/motion/npz.ts
18779
19054
  import zlib from "zlib";
@@ -20024,7 +20299,7 @@ async function motionGen(opts, log) {
20024
20299
  }
20025
20300
  if (opts.constraintsPath !== void 0) {
20026
20301
  try {
20027
- const raw = await fs24.readFile(opts.constraintsPath, "utf8");
20302
+ const raw = await fs25.readFile(opts.constraintsPath, "utf8");
20028
20303
  generationOptions.constraints = JSON.parse(raw);
20029
20304
  } catch {
20030
20305
  log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
@@ -20048,10 +20323,10 @@ async function motionGen(opts, log) {
20048
20323
  async function expandTakes(selectors) {
20049
20324
  const out = [];
20050
20325
  for (const sel of selectors) {
20051
- const st = await fs24.stat(sel).catch(() => null);
20326
+ const st = await fs25.stat(sel).catch(() => null);
20052
20327
  if (st?.isDirectory()) {
20053
- const names = await fs24.readdir(sel);
20054
- for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path23.join(sel, n));
20328
+ const names = await fs25.readdir(sel);
20329
+ for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path24.join(sel, n));
20055
20330
  } else if (st?.isFile()) {
20056
20331
  out.push(sel);
20057
20332
  } else {
@@ -20086,7 +20361,7 @@ async function motionVerify(opts, log) {
20086
20361
  let gates = DEFAULT_GATES;
20087
20362
  if (opts.gatesPath) {
20088
20363
  try {
20089
- gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs24.readFile(opts.gatesPath, "utf8")));
20364
+ gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs25.readFile(opts.gatesPath, "utf8")));
20090
20365
  } catch {
20091
20366
  log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
20092
20367
  process.exitCode = 1;
@@ -20108,9 +20383,9 @@ async function motionVerify(opts, log) {
20108
20383
  }
20109
20384
  const reports = [];
20110
20385
  for (const file of files) {
20111
- const stem = path23.basename(file).replace(/\.npz$/, "");
20386
+ const stem = path24.basename(file).replace(/\.npz$/, "");
20112
20387
  try {
20113
- reports.push(analyzeTake(stem, await fs24.readFile(file), gates));
20388
+ reports.push(analyzeTake(stem, await fs25.readFile(file), gates));
20114
20389
  } catch (err) {
20115
20390
  reports.push({
20116
20391
  take: stem,
@@ -20148,7 +20423,7 @@ async function motionCompile(opts, log) {
20148
20423
  let cfg = DEFAULT_MOTION_CONFIG;
20149
20424
  if (opts.configPath) {
20150
20425
  try {
20151
- const patch = JSON.parse(await fs24.readFile(opts.configPath, "utf8"));
20426
+ const patch = JSON.parse(await fs25.readFile(opts.configPath, "utf8"));
20152
20427
  cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
20153
20428
  } catch {
20154
20429
  log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
@@ -20166,16 +20441,16 @@ async function motionCompile(opts, log) {
20166
20441
  }
20167
20442
  const inputs = [];
20168
20443
  for (const file of files) {
20169
- const stem = path23.basename(file).replace(/\.npz$/, "");
20444
+ const stem = path24.basename(file).replace(/\.npz$/, "");
20170
20445
  try {
20171
- inputs.push({ stem, take: loadTake(await fs24.readFile(file)) });
20446
+ inputs.push({ stem, take: loadTake(await fs25.readFile(file)) });
20172
20447
  } catch (err) {
20173
20448
  log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
20174
20449
  process.exitCode = 1;
20175
20450
  return;
20176
20451
  }
20177
20452
  }
20178
- const setName = opts.set ?? path23.basename(opts.out).replace(/\.json$/, "");
20453
+ const setName = opts.set ?? path24.basename(opts.out).replace(/\.json$/, "");
20179
20454
  let result;
20180
20455
  try {
20181
20456
  result = compileSet(inputs, setName, cfg);
@@ -20190,9 +20465,9 @@ async function motionCompile(opts, log) {
20190
20465
  process.exitCode = 1;
20191
20466
  return;
20192
20467
  }
20193
- await fs24.mkdir(path23.dirname(path23.resolve(opts.out)), { recursive: true });
20468
+ await fs25.mkdir(path24.dirname(path24.resolve(opts.out)), { recursive: true });
20194
20469
  const json = JSON.stringify(result.data);
20195
- await fs24.writeFile(opts.out, json);
20470
+ await fs25.writeFile(opts.out, json);
20196
20471
  if (opts.json) {
20197
20472
  writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
20198
20473
  return;
@@ -20210,9 +20485,9 @@ var MOTION_RUNTIME_FILES = [
20210
20485
  var MOTION_PRESETS = {
20211
20486
  rifle: ["sets/rifle.json", "sets/jumps.json"]
20212
20487
  };
20213
- var MOTION_DEST = path23.join("src", "motion");
20488
+ var MOTION_DEST = path24.join("src", "motion");
20214
20489
  async function motionInstall(opts, log) {
20215
- const srcDir = path23.join(getTemplatesDir(), "motion");
20490
+ const srcDir = path24.join(getTemplatesDir(), "motion");
20216
20491
  const root = opts.cwd ?? process.cwd();
20217
20492
  const preset = opts.set;
20218
20493
  if (preset !== void 0 && !MOTION_PRESETS[preset]) {
@@ -20223,21 +20498,21 @@ async function motionInstall(opts, log) {
20223
20498
  const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
20224
20499
  log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
20225
20500
  log.plain("");
20226
- log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path23.sep)}`);
20501
+ log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path24.sep)}`);
20227
20502
  let copied = 0, skipped = 0;
20228
20503
  try {
20229
20504
  for (const rel of files) {
20230
- const dest = path23.join(root, MOTION_DEST, rel);
20231
- const exists4 = await fs24.access(dest).then(() => true, () => false);
20232
- if (!opts.force && exists4) {
20505
+ const dest = path24.join(root, MOTION_DEST, rel);
20506
+ const exists5 = await fs25.access(dest).then(() => true, () => false);
20507
+ if (!opts.force && exists5) {
20233
20508
  skipped++;
20234
- log.dim(` skipped ${path23.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
20509
+ log.dim(` skipped ${path24.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
20235
20510
  continue;
20236
20511
  }
20237
- await fs24.mkdir(path23.dirname(dest), { recursive: true });
20238
- await fs24.copyFile(path23.join(srcDir, rel), dest);
20512
+ await fs25.mkdir(path24.dirname(dest), { recursive: true });
20513
+ await fs25.copyFile(path24.join(srcDir, rel), dest);
20239
20514
  copied++;
20240
- log.dim(` ${path23.join(MOTION_DEST, rel)}`);
20515
+ log.dim(` ${path24.join(MOTION_DEST, rel)}`);
20241
20516
  }
20242
20517
  } catch (err) {
20243
20518
  log.error(`Copy failed: ${String(err)}`);
@@ -20278,7 +20553,7 @@ async function motionConstraints(opts, log) {
20278
20553
  }
20279
20554
  const doc = directionConstraint(dir, speed, duration);
20280
20555
  const out = opts.out ?? "constraints.json";
20281
- await fs24.writeFile(out, JSON.stringify(doc));
20556
+ await fs25.writeFile(out, JSON.stringify(doc));
20282
20557
  if (opts.json) {
20283
20558
  writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
20284
20559
  return;
@@ -20315,9 +20590,9 @@ async function runMotion(opts) {
20315
20590
  }
20316
20591
 
20317
20592
  // src/commands/asset-new.ts
20318
- import fs25 from "fs";
20593
+ import fs26 from "fs";
20319
20594
  import fsp from "fs/promises";
20320
- import path24 from "path";
20595
+ import path25 from "path";
20321
20596
  import { pathToFileURL } from "url";
20322
20597
  var EXTRA_FILES = [
20323
20598
  "genex-asset.example.json",
@@ -20411,7 +20686,7 @@ Every milestone ends with \`npm run verify\`, then \`npx genex preview --no-buil
20411
20686
  }
20412
20687
  async function runAssetNew(options) {
20413
20688
  const log = createLogger();
20414
- const cwd = options.dir ? path24.resolve(options.dir) : process.cwd();
20689
+ const cwd = options.dir ? path25.resolve(options.dir) : process.cwd();
20415
20690
  const slug = options.assetSlug;
20416
20691
  if (!slug) {
20417
20692
  log.error('Usage: genex asset new <slug> [--title "<Name>"] [--dims WxDxH] [--tri-band LOW-HIGH] [--holder "<your name>"] [--out <dir>]');
@@ -20421,14 +20696,14 @@ async function runAssetNew(options) {
20421
20696
  log.error(`The slug must be kebab-case (letters, digits, dashes), got: ${slug}`);
20422
20697
  return 1;
20423
20698
  }
20424
- const templateDir = path24.join(getTemplatesDir(), "asset-viewer");
20425
- if (!fs25.existsSync(templateDir)) {
20699
+ const templateDir = path25.join(getTemplatesDir(), "asset-viewer");
20700
+ if (!fs26.existsSync(templateDir)) {
20426
20701
  log.error(`Vendored asset-viewer template not found at ${templateDir} \u2014 reinstall the CLI.`);
20427
20702
  return 1;
20428
20703
  }
20429
- const manifestTools = await import(pathToFileURL(path24.join(templateDir, "tools", "emit-manifest.mjs")).href);
20704
+ const manifestTools = await import(pathToFileURL(path25.join(templateDir, "tools", "emit-manifest.mjs")).href);
20430
20705
  const { SHARED_FILES, PARITY_FILENAME, hashSharedFiles, pascalCase } = manifestTools;
20431
- const lockPath = path24.join(templateDir, "shared-files.sha256.json");
20706
+ const lockPath = path25.join(templateDir, "shared-files.sha256.json");
20432
20707
  const lock = JSON.parse(await fsp.readFile(lockPath, "utf8"));
20433
20708
  const actual = hashSharedFiles(templateDir);
20434
20709
  const drifted = SHARED_FILES.filter((rel) => lock.files[rel] !== actual[rel]);
@@ -20441,8 +20716,8 @@ async function runAssetNew(options) {
20441
20716
  const triBand = parseBand(options.triBand ?? "500-8000");
20442
20717
  const holder = options.holder?.trim() || HOLDER_PLACEHOLDER;
20443
20718
  const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
20444
- const outDir = path24.resolve(cwd, options.out ?? slug);
20445
- if (fs25.existsSync(outDir) && fs25.readdirSync(outDir).length > 0 && !options.force) {
20719
+ const outDir = path25.resolve(cwd, options.out ?? slug);
20720
+ if (fs26.existsSync(outDir) && fs26.readdirSync(outDir).length > 0 && !options.force) {
20446
20721
  log.error(`${outDir} already holds files. Nothing was touched \u2014 pass --force to write into it anyway.`);
20447
20722
  return 1;
20448
20723
  }
@@ -20470,24 +20745,24 @@ async function runAssetNew(options) {
20470
20745
  };
20471
20746
  await fsp.mkdir(outDir, { recursive: true });
20472
20747
  for (const rel of [...SHARED_FILES, ...EXTRA_FILES]) {
20473
- const to = path24.join(outDir, rel);
20474
- await fsp.mkdir(path24.dirname(to), { recursive: true });
20475
- await fsp.copyFile(path24.join(templateDir, rel), to);
20748
+ const to = path25.join(outDir, rel);
20749
+ await fsp.mkdir(path25.dirname(to), { recursive: true });
20750
+ await fsp.copyFile(path25.join(templateDir, rel), to);
20476
20751
  }
20477
- const pkg = fillTemplate(await fsp.readFile(path24.join(templateDir, "package.json"), "utf8"), {
20752
+ const pkg = fillTemplate(await fsp.readFile(path25.join(templateDir, "package.json"), "utf8"), {
20478
20753
  slug,
20479
20754
  name,
20480
20755
  version
20481
20756
  });
20482
- await fsp.writeFile(path24.join(outDir, "package.json"), pkg, "utf8");
20483
- await fsp.writeFile(path24.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
20484
- await fsp.writeFile(path24.join(outDir, ".gitignore"), GITIGNORE, "utf8");
20757
+ await fsp.writeFile(path25.join(outDir, "package.json"), pkg, "utf8");
20758
+ await fsp.writeFile(path25.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
20759
+ await fsp.writeFile(path25.join(outDir, ".gitignore"), GITIGNORE, "utf8");
20485
20760
  await fsp.writeFile(
20486
- path24.join(outDir, "DESIGN.md"),
20761
+ path25.join(outDir, "DESIGN.md"),
20487
20762
  designDoc({ name, slug, sizeMeters, triBand, holder }),
20488
20763
  "utf8"
20489
20764
  );
20490
- const placeholder = await fsp.readFile(path24.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
20765
+ const placeholder = await fsp.readFile(path25.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
20491
20766
  const seeded = seedAssetSource(placeholder, {
20492
20767
  slug,
20493
20768
  name,
@@ -20498,8 +20773,8 @@ async function runAssetNew(options) {
20498
20773
  pascalCase
20499
20774
  });
20500
20775
  const assetFilename = `${slug.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase())}.ts`;
20501
- await fsp.mkdir(path24.join(outDir, "src", "asset"), { recursive: true });
20502
- await fsp.writeFile(path24.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
20776
+ await fsp.mkdir(path25.join(outDir, "src", "asset"), { recursive: true });
20777
+ await fsp.writeFile(path25.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
20503
20778
  const copied = hashSharedFiles(outDir);
20504
20779
  const mismatched = SHARED_FILES.filter((rel) => copied[rel] !== lock.files[rel]);
20505
20780
  if (mismatched.length) {
@@ -20507,7 +20782,7 @@ async function runAssetNew(options) {
20507
20782
  return 1;
20508
20783
  }
20509
20784
  await fsp.writeFile(
20510
- path24.join(outDir, PARITY_FILENAME),
20785
+ path25.join(outDir, PARITY_FILENAME),
20511
20786
  JSON.stringify(
20512
20787
  {
20513
20788
  note: "Recorded by `genex asset new`. Gate G13 recomputes these; do not hand-edit the viewer.",
@@ -20550,6 +20825,530 @@ async function runAssetNew(options) {
20550
20825
  return 0;
20551
20826
  }
20552
20827
 
20828
+ // src/commands/tools.ts
20829
+ import path28 from "path";
20830
+
20831
+ // src/lib/telemetry.ts
20832
+ async function reportToolsInit(apiUrl, token) {
20833
+ if (!token || telemetryDisabled()) return;
20834
+ try {
20835
+ await apiFetch(`${apiUrl}/api/cli/events`, {
20836
+ method: "POST",
20837
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
20838
+ body: JSON.stringify({ event: "tools_init" }),
20839
+ signal: AbortSignal.timeout(2e3)
20840
+ });
20841
+ } catch {
20842
+ }
20843
+ }
20844
+
20845
+ // src/lib/local-install.ts
20846
+ import fs27 from "fs/promises";
20847
+ import path26 from "path";
20848
+ import { spawn as spawn6 } from "child_process";
20849
+ var CLI_PACKAGE = "@genex-ai/cli-demo";
20850
+ var FULL_NAME_FALLBACK = `npx ${CLI_PACKAGE}@${CLI_CHANNEL}`;
20851
+ var LOCKFILES = [
20852
+ ["pnpm-lock.yaml", "pnpm"],
20853
+ ["yarn.lock", "yarn"],
20854
+ ["bun.lockb", "bun"],
20855
+ ["bun.lock", "bun"],
20856
+ ["package-lock.json", "npm"]
20857
+ ];
20858
+ async function exists4(p) {
20859
+ try {
20860
+ await fs27.access(p);
20861
+ return true;
20862
+ } catch {
20863
+ return false;
20864
+ }
20865
+ }
20866
+ async function detectPackageManager(cwd) {
20867
+ let dir = path26.resolve(cwd);
20868
+ for (; ; ) {
20869
+ try {
20870
+ const raw = await fs27.readFile(path26.join(dir, "package.json"), "utf8");
20871
+ const pm = JSON.parse(raw).packageManager;
20872
+ if (typeof pm === "string") {
20873
+ const name = pm.split("@")[0];
20874
+ if (name === "pnpm" || name === "yarn" || name === "bun" || name === "npm") return name;
20875
+ }
20876
+ } catch {
20877
+ }
20878
+ for (const [file, pm] of LOCKFILES) {
20879
+ if (await exists4(path26.join(dir, file))) return pm;
20880
+ }
20881
+ const parent = path26.dirname(dir);
20882
+ if (parent === dir) return "npm";
20883
+ dir = parent;
20884
+ }
20885
+ }
20886
+ async function findLocalCli(cwd) {
20887
+ let dir = path26.resolve(cwd);
20888
+ for (; ; ) {
20889
+ if (await exists4(path26.join(dir, "node_modules", CLI_PACKAGE, "package.json"))) return dir;
20890
+ const parent = path26.dirname(dir);
20891
+ if (parent === dir) return null;
20892
+ dir = parent;
20893
+ }
20894
+ }
20895
+ function installArgs(pm, spec) {
20896
+ switch (pm) {
20897
+ case "pnpm":
20898
+ return { command: "pnpm", args: ["add", "-D", spec] };
20899
+ case "yarn":
20900
+ return { command: "yarn", args: ["add", "-D", spec] };
20901
+ case "bun":
20902
+ return { command: "bun", args: ["add", "-d", spec] };
20903
+ case "npm":
20904
+ return { command: "npm", args: ["install", "--save-dev", "--no-audit", "--no-fund", spec] };
20905
+ }
20906
+ }
20907
+ function manifestName(cwd) {
20908
+ const slug = path26.basename(path26.resolve(cwd)).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
20909
+ return slug || "genex-tools-workspace";
20910
+ }
20911
+ function isSourceRun(moduleUrl = import.meta.url) {
20912
+ return moduleUrl.endsWith(".ts");
20913
+ }
20914
+ var INSTALL_TIMEOUT_MS = 18e4;
20915
+ async function spawnInstall(command, args, cwd) {
20916
+ return new Promise((resolve) => {
20917
+ let output = "";
20918
+ let settled = false;
20919
+ const done = (ok, detail) => {
20920
+ if (settled) return;
20921
+ settled = true;
20922
+ resolve({ ok, detail });
20923
+ };
20924
+ try {
20925
+ const child = spawn6(command, args, {
20926
+ cwd,
20927
+ // npm/pnpm/yarn are .cmd shims on Windows; the args carry no user input.
20928
+ shell: process.platform === "win32",
20929
+ stdio: ["ignore", "pipe", "pipe"]
20930
+ });
20931
+ const timer = setTimeout(() => {
20932
+ child.kill();
20933
+ done(false, `timed out after ${INSTALL_TIMEOUT_MS / 1e3}s`);
20934
+ }, INSTALL_TIMEOUT_MS);
20935
+ child.stdout?.on("data", (d) => output += d.toString());
20936
+ child.stderr?.on("data", (d) => output += d.toString());
20937
+ child.on("error", (err) => {
20938
+ clearTimeout(timer);
20939
+ done(false, err.message);
20940
+ });
20941
+ child.on("close", (code2) => {
20942
+ clearTimeout(timer);
20943
+ if (code2 === 0) done(true);
20944
+ else done(false, output.trim().split("\n").slice(-3).join("\n"));
20945
+ });
20946
+ } catch (err) {
20947
+ done(false, err instanceof Error ? err.message : String(err));
20948
+ }
20949
+ });
20950
+ }
20951
+ async function ensureLocalCli(log, cwd = process.cwd(), opts = {}) {
20952
+ if (opts.skip) {
20953
+ log.dim(` --no-install: commands are spelled ${c.cyan(FULL_NAME_FALLBACK)} without it.`);
20954
+ return;
20955
+ }
20956
+ if (!opts.runner && isSourceRun()) {
20957
+ log.dim(" (source run \u2014 skipping the local CLI install)");
20958
+ return;
20959
+ }
20960
+ const already = await findLocalCli(cwd);
20961
+ if (already) {
20962
+ log.dim(` ${CLI_PACKAGE} already resolves from ${c.cyan(already)} \u2014 npx genex works here.`);
20963
+ return;
20964
+ }
20965
+ const pm = await detectPackageManager(cwd);
20966
+ const hadManifest = await exists4(path26.join(cwd, "package.json"));
20967
+ if (!hadManifest) {
20968
+ await fs27.writeFile(
20969
+ path26.join(cwd, "package.json"),
20970
+ JSON.stringify({ name: manifestName(cwd), private: true }, null, 2) + "\n"
20971
+ );
20972
+ await ensureIgnored(cwd, "node_modules/");
20973
+ log.dim(" wrote a minimal package.json (none existed \u2014 the install must land HERE, not in a parent)");
20974
+ }
20975
+ const spec = `${CLI_PACKAGE}@${CLI_CHANNEL}`;
20976
+ const { command, args } = installArgs(pm, spec);
20977
+ log.step(`Adding ${c.cyan(CLI_PACKAGE)} as a devDependency (${pm}) so npx genex resolves here\u2026`);
20978
+ const run2 = opts.runner ?? spawnInstall;
20979
+ const result = await run2(command, args, cwd);
20980
+ if (result.ok) {
20981
+ log.success(`${c.cyan("npx genex")} now resolves in this folder.`);
20982
+ } else {
20983
+ log.warn(`Couldn't install ${CLI_PACKAGE} via ${pm}${result.detail ? ` \u2014 ${result.detail}` : ""}.`);
20984
+ log.dim(` Everything still works with the full name: ${c.cyan(`${FULL_NAME_FALLBACK} doctor`)}`);
20985
+ log.dim(` Or add it yourself: ${c.cyan(`${command} ${args.join(" ")}`)}`);
20986
+ }
20987
+ }
20988
+ async function ensureIgnored(dir, entry) {
20989
+ const file = path26.join(dir, ".gitignore");
20990
+ let content = "";
20991
+ try {
20992
+ content = await fs27.readFile(file, "utf8");
20993
+ } catch {
20994
+ }
20995
+ if (content.split("\n").some((l) => l.trim() === entry)) return;
20996
+ let next = content;
20997
+ if (next.length > 0 && !next.endsWith("\n")) next += "\n";
20998
+ await fs27.writeFile(file, next + entry + "\n");
20999
+ }
21000
+
21001
+ // src/commands/doctor.ts
21002
+ import path27 from "path";
21003
+ var LANE_ORDER = [
21004
+ "model",
21005
+ "image",
21006
+ "video",
21007
+ "texture",
21008
+ "sfx",
21009
+ "music",
21010
+ "voice",
21011
+ "character",
21012
+ "character_motion",
21013
+ "model_segment",
21014
+ "model_rig",
21015
+ "model_animation"
21016
+ ];
21017
+ var NODE_FLOOR = 20;
21018
+ async function runDoctor(opts = {}) {
21019
+ const log = createLogger({ quiet: opts.quiet && !opts.json });
21020
+ const rows = [];
21021
+ const [project, workspace] = await Promise.all([readProject(), readWorkspace()]);
21022
+ const mode = project ? "game" : workspace?.mode === "tools" ? "tools" : null;
21023
+ const synced = await firstSkillsMarker();
21024
+ const syncNote = synced ? ` \xB7 skills ${synced}` : " \xB7 no skills installed here";
21025
+ rows.push(
21026
+ mode === "game" ? { label: "Workspace", value: `genex game "${project.slug}"${syncNote}` } : mode === "tools" ? { label: "Workspace", value: `Genex Tools workspace${syncNote}` } : {
21027
+ label: "Workspace",
21028
+ value: "not a Genex folder",
21029
+ // Never `bad`: doctor is exactly what someone runs BEFORE setting
21030
+ // up, and a red mark there reads as a broken install.
21031
+ fix: "`npx genex tools` for the asset toolkit, or `npx genex init` to build a hosted game here."
21032
+ }
21033
+ );
21034
+ const nodeMajor = Number(process.versions.node.split(".")[0]);
21035
+ const nodeOk = Number.isFinite(nodeMajor) && nodeMajor >= NODE_FLOOR;
21036
+ rows.push({
21037
+ label: "Node",
21038
+ value: `v${process.versions.node}${nodeOk ? "" : ` (needs \u2265 ${NODE_FLOOR})`}`,
21039
+ bad: !nodeOk,
21040
+ fix: nodeOk ? void 0 : "Install Node 20 or newer: nodejs.org/download (or `brew install node`)."
21041
+ });
21042
+ const installed = getCliVersion();
21043
+ const latest = await latestPublished();
21044
+ rows.push({
21045
+ label: "CLI",
21046
+ value: latest && isNewerVersion(latest, installed) ? `${installed} (latest on npm: ${latest})` : `${installed}${latest ? " (up to date)" : ""}`,
21047
+ // An outdated CLI is a nudge, never a failure — it is the update machinery's
21048
+ // job to say so, and failing here would make `doctor` red on every machine
21049
+ // the morning after a release.
21050
+ fix: latest && isNewerVersion(latest, installed) ? "npm i -D @genex-ai/cli-demo@latest (apply at a safe moment, never mid-task)" : void 0
21051
+ });
21052
+ const apiUrl = getApiUrl(opts.apiUrl ?? workspace?.apiUrl ?? project?.apiUrl);
21053
+ const token = await readUserToken(opts.envPath);
21054
+ let email = null;
21055
+ let authState = "none";
21056
+ if (token) {
21057
+ const probe = await probeSession(apiUrl, token);
21058
+ authState = probe.state;
21059
+ email = probe.email;
21060
+ }
21061
+ rows.push(authRow(authState, email, opts.envPath));
21062
+ let credits = null;
21063
+ let lanes = null;
21064
+ if (authState === "ok" && token) {
21065
+ [credits, lanes] = await Promise.all([fetchCredits(apiUrl, token), fetchLanes(apiUrl, token)]);
21066
+ }
21067
+ rows.push(creditsRow(authState, credits));
21068
+ if (opts.json) {
21069
+ process.stdout.write(
21070
+ JSON.stringify(
21071
+ {
21072
+ workspace: mode,
21073
+ node: process.versions.node,
21074
+ cli: { installed, latest },
21075
+ auth: { state: authState, email, apiUrl },
21076
+ credits,
21077
+ lanes: lanes ? { paused: lanes.paused, lanes: lanes.lanes } : null,
21078
+ ok: !rows.some((r) => r.bad)
21079
+ },
21080
+ null,
21081
+ 2
21082
+ ) + "\n"
21083
+ );
21084
+ if (rows.some((r) => r.bad)) process.exitCode = 1;
21085
+ return;
21086
+ }
21087
+ log.plain(c.bold("genex doctor"));
21088
+ log.dim(` ${apiUrl}`);
21089
+ log.plain("");
21090
+ for (const row of rows) {
21091
+ const mark = row.bad ? c.red("\u2717") : c.green("\u2713");
21092
+ log.plain(` ${mark} ${row.label.padEnd(11)}${row.value}`);
21093
+ if (row.fix) log.dim(` ${row.fix}`);
21094
+ }
21095
+ let laneProblem = false;
21096
+ if (authState === "ok") {
21097
+ log.plain("");
21098
+ if (!lanes) {
21099
+ log.plain(` ${c.yellow("?")} Lanes couldn't be read from this server`);
21100
+ log.dim(
21101
+ " An older server has no lane report \u2014 generation still works, and a lane with no"
21102
+ );
21103
+ log.dim(" provider key would return a placeholder asset instead of failing.");
21104
+ } else if (lanes.paused) {
21105
+ laneProblem = true;
21106
+ log.plain(` ${c.red("\u2717")} Lanes generation is PAUSED platform-wide`);
21107
+ log.dim(" The daily spend breaker is tripped \u2014 nothing generates until it resets.");
21108
+ } else {
21109
+ const mocks = lanes.lanes.filter((l) => l.mock);
21110
+ const mark = mocks.length > 0 ? c.yellow("!") : c.green("\u2713");
21111
+ log.plain(
21112
+ ` ${mark} Lanes ${lanes.lanes.length - mocks.length}/${lanes.lanes.length} live${mocks.length > 0 ? ` \xB7 ${mocks.length} in SAMPLE mode` : ""}`
21113
+ );
21114
+ for (const kind of laneOrder(lanes)) {
21115
+ const lane = lanes.lanes.find((l) => l.kind === kind);
21116
+ if (!lane) continue;
21117
+ const price = credits?.prices?.[kind];
21118
+ const cost = typeof price === "number" ? `${price} cr` : "";
21119
+ if (lane.mock) {
21120
+ laneProblem = true;
21121
+ log.plain(
21122
+ ` ${c.yellow("!")} ${kind.padEnd(17)}${"SAMPLE (no provider key)".padEnd(26)}${cost}`
21123
+ );
21124
+ } else {
21125
+ log.dim(` \xB7 ${kind.padEnd(17)}${`live (${lane.provider})`.padEnd(26)}${cost}`);
21126
+ }
21127
+ }
21128
+ if (mocks.length > 0) {
21129
+ log.dim(
21130
+ " A SAMPLE lane returns a placeholder asset and still bills. In a Genex Tools"
21131
+ );
21132
+ log.dim(" workspace those are refused before anything is charged.");
21133
+ }
21134
+ }
21135
+ }
21136
+ const failed = rows.some((r) => r.bad) || laneProblem && mode !== "game";
21137
+ log.plain("");
21138
+ if (failed) {
21139
+ log.plain(` ${c.red("Something above needs fixing.")}`);
21140
+ process.exitCode = 1;
21141
+ } else {
21142
+ log.plain(` ${c.green("All good.")}`);
21143
+ }
21144
+ }
21145
+ function laneOrder(report) {
21146
+ const known = LANE_ORDER.filter((k) => report.lanes.some((l) => l.kind === k));
21147
+ const extra = report.lanes.map((l) => l.kind).filter((k) => !LANE_ORDER.includes(k));
21148
+ return [...known, ...extra.sort()];
21149
+ }
21150
+ function authRow(state, email, envPath) {
21151
+ switch (state) {
21152
+ case "ok":
21153
+ return { label: "Auth", value: `connected as ${email ?? "this account"}` };
21154
+ case "rejected":
21155
+ return {
21156
+ label: "Auth",
21157
+ value: "your saved sign-in was rejected",
21158
+ bad: true,
21159
+ fix: `The credential in ${getGenexEnvPath(envPath)} is expired or revoked \u2014 run \`npx genex auth --force\`.`
21160
+ };
21161
+ case "offline":
21162
+ return {
21163
+ label: "Auth",
21164
+ value: "couldn't reach the API to check",
21165
+ bad: true,
21166
+ fix: "Check the network, then re-run. Nothing is wrong with your credential yet."
21167
+ };
21168
+ default:
21169
+ return {
21170
+ label: "Auth",
21171
+ value: "not signed in",
21172
+ bad: true,
21173
+ fix: "`npx genex auth` \u2014 it prints a short link and a code; approve it on any device."
21174
+ };
21175
+ }
21176
+ }
21177
+ function creditsRow(state, info) {
21178
+ if (state !== "ok") return { label: "Credits", value: "\u2014 (sign in first)" };
21179
+ if (!info) return { label: "Credits", value: "couldn't be read from this server" };
21180
+ if (info.unlimited) return { label: "Credits", value: "unlimited (operator account)" };
21181
+ if (!info.emailVerified) {
21182
+ return {
21183
+ label: "Credits",
21184
+ value: `${info.balance} \xB7 email not verified`,
21185
+ bad: true,
21186
+ fix: "Verify your email to unlock your free credits \u2014 then re-run."
21187
+ };
21188
+ }
21189
+ const refill = info.refillAt ? ` \xB7 refills to ${info.refillTo} on ${shortDate2(info.refillAt)}` : "";
21190
+ return {
21191
+ label: "Credits",
21192
+ value: `${info.balance}${refill}`,
21193
+ bad: info.balance <= 0,
21194
+ fix: info.balance <= 0 ? "Out of credits \u2014 generation is refused until the refill or a top-up." : void 0
21195
+ };
21196
+ }
21197
+ function shortDate2(iso) {
21198
+ const d = new Date(iso);
21199
+ return Number.isNaN(d.getTime()) ? iso : d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
21200
+ }
21201
+ async function probeSession(apiUrl, token) {
21202
+ try {
21203
+ const res = await apiFetch(`${apiUrl}/api/auth/get-session`, {
21204
+ headers: { Authorization: `Bearer ${token}` },
21205
+ signal: AbortSignal.timeout(6e3)
21206
+ });
21207
+ if (res.status === 401 || res.status === 403) return { state: "rejected", email: null };
21208
+ if (!res.ok) return { state: "offline", email: null };
21209
+ const data = await res.json().catch(() => null);
21210
+ if (!data?.user) return { state: "rejected", email: null };
21211
+ return { state: "ok", email: data.user.email ?? null };
21212
+ } catch {
21213
+ return { state: "offline", email: null };
21214
+ }
21215
+ }
21216
+ async function fetchCredits(apiUrl, token) {
21217
+ try {
21218
+ const res = await apiFetch(`${apiUrl}/api/credits/me`, {
21219
+ headers: { Authorization: `Bearer ${token}` },
21220
+ signal: AbortSignal.timeout(6e3)
21221
+ });
21222
+ if (!res.ok) return null;
21223
+ return await res.json();
21224
+ } catch {
21225
+ return null;
21226
+ }
21227
+ }
21228
+ async function firstSkillsMarker() {
21229
+ for (const target of resolveAgentTargets()) {
21230
+ const marker = await readSkillsMarker(path27.join(target.baseDir, "skills"));
21231
+ if (marker) return marker;
21232
+ }
21233
+ return null;
21234
+ }
21235
+ async function latestPublished() {
21236
+ try {
21237
+ const cache = await startUpdateCheck();
21238
+ return cache?.latest["@genex-ai/cli-demo"] ?? null;
21239
+ } catch {
21240
+ return null;
21241
+ }
21242
+ }
21243
+
21244
+ // src/commands/tools.ts
21245
+ async function runTools(opts) {
21246
+ const log = createLogger({ quiet: opts.quiet });
21247
+ const project = await readProject();
21248
+ if (project) {
21249
+ log.error("This folder is already a Genex game project.");
21250
+ log.plain(
21251
+ [
21252
+ ` Its skills already cover every generation tool \u2014 ${c.cyan("npx genex --help")} lists them,`,
21253
+ " and it can publish, host and go multiplayer on top. Nothing to set up.",
21254
+ " Genex Tools is for a game you host yourself; run it in that project's folder."
21255
+ ].join("\n")
21256
+ );
21257
+ process.exitCode = 1;
21258
+ return;
21259
+ }
21260
+ log.plain(c.bold("genex tools"));
21261
+ log.plain("");
21262
+ await cleanupLegacyGlobalSkills(log);
21263
+ const templatesDir = getTemplatesDir();
21264
+ const targets = resolveAgentTargets({ dir: opts.dir, agents: opts.agents });
21265
+ log.step(`Installing Genex tool skills for: ${targets.map((t) => c.cyan(t.label)).join(", ")}`);
21266
+ let totalNew = 0;
21267
+ let totalUpdated = 0;
21268
+ for (const t of targets) {
21269
+ const dest = path28.join(t.baseDir, "skills");
21270
+ const { copied, updated } = await copyTemplates(path28.join(templatesDir, "skills"), dest, {
21271
+ filter: (rel) => skillFamilyFilter("tools")(`skills/${rel}`)
21272
+ });
21273
+ await pruneRemovedSkills(dest, log);
21274
+ await writeSkillsMarker(dest);
21275
+ const added = copied.length - updated.length;
21276
+ totalNew += added;
21277
+ totalUpdated += updated.length;
21278
+ log.dim(` ${t.label}: ${added} added, ${updated.length} refreshed \u2192 ${dest}`);
21279
+ }
21280
+ log.success(
21281
+ `Tool skills ready (${totalNew} added, ${totalUpdated} refreshed across ${targets.length} agent${targets.length === 1 ? "" : "s"}).`
21282
+ );
21283
+ const apiUrl = getApiUrl(opts.apiUrl);
21284
+ const { path: markerPath } = await writeWorkspace({
21285
+ mode: "tools",
21286
+ apiUrl,
21287
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
21288
+ });
21289
+ log.dim(` saved ${c.cyan(markerPath)}`);
21290
+ await writeGitignore(process.cwd(), log);
21291
+ if (await writeToolsContract(process.cwd())) {
21292
+ log.dim(" wrote the Genex Tools rules into AGENTS.md (managed block) + CLAUDE.md stub");
21293
+ }
21294
+ await healAncestorContractsAndReport(log, process.cwd());
21295
+ await ensureLocalCli(log, process.cwd(), { skip: opts.noInstall });
21296
+ log.plain("");
21297
+ const authBaseUrl = getAuthUrl(opts.authUrl);
21298
+ let token = await readUserToken(opts.envPath);
21299
+ if (!token) {
21300
+ try {
21301
+ token = await authorize(apiUrl, authBaseUrl, {
21302
+ log,
21303
+ inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
21304
+ });
21305
+ } catch (err) {
21306
+ if (err instanceof AuthPendingError) {
21307
+ printAuthHandoff(log, err);
21308
+ log.dim(
21309
+ "Everything else is set up \u2014 `genex auth` finishes the sign-in, then you can generate."
21310
+ );
21311
+ return;
21312
+ }
21313
+ log.error(err instanceof Error ? err.message : String(err));
21314
+ log.dim("The workspace is set up. Re-run `genex tools` to finish authorizing.");
21315
+ throw markPrinted(err);
21316
+ }
21317
+ const { path: tokenPath } = await writeUserToken(token, opts.envPath);
21318
+ log.success(`Authorized. Saved your token to ${c.cyan(tokenPath)} (${ENV_TOKEN_KEY}).`);
21319
+ log.plain("");
21320
+ }
21321
+ await reportToolsInit(apiUrl, token);
21322
+ await runDoctor({ apiUrl: opts.apiUrl, envPath: opts.envPath, quiet: opts.quiet });
21323
+ log.plain("");
21324
+ log.success("Genex Tools is ready. \u{1F9F0}");
21325
+ log.plain(` Try it: ${c.cyan('npx genex image "weathered treasure map, hand-inked" --transparent')}`);
21326
+ log.dim(" Assets land in ./assets/ as files you own \u2014 wire the local path into your game.");
21327
+ }
21328
+
21329
+ // src/lib/costs.ts
21330
+ var TYPICAL_CREDITS = {
21331
+ model: 25,
21332
+ image: 8,
21333
+ texture: 3,
21334
+ sfx: 5,
21335
+ music: 40,
21336
+ voice: 10,
21337
+ video: 20,
21338
+ model_segment: 30,
21339
+ model_rig: 20,
21340
+ model_animation: 8,
21341
+ // PER CLIP — `--preset walk,run` bills twice
21342
+ character: 50,
21343
+ character_concept: 15,
21344
+ character_preview: 20,
21345
+ character_finalize: 30,
21346
+ character_animation: 5,
21347
+ character_motion: 26,
21348
+ // PER CLIP on the text route; other routes scale from it
21349
+ character_rerig: 10
21350
+ };
21351
+
20553
21352
  // src/index.ts
20554
21353
  var MODEL_SUB_SET = new Set(MODEL_SUBCOMMANDS);
20555
21354
  var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "music", "voice", "texture", "image", "video"]);
@@ -20557,6 +21356,17 @@ var HELP = `${c.bold("genex")} \u2014 set up your project's agent workspace, aut
20557
21356
 
20558
21357
  ${c.bold("Usage")}
20559
21358
  genex init [<name>] [options] Scaffold + authorize + create the draft project.
21359
+ genex tools [options] Set this folder up as a Genex Tools workspace: the
21360
+ asset generators alone (models, images, video,
21361
+ textures, audio, characters) for a game you host
21362
+ yourself. Installs the tool skill cards, signs you
21363
+ in, and downloads every asset into ./assets.
21364
+ Creates nothing on the platform.
21365
+ genex doctor [--json] Check everything a generation needs: sign-in,
21366
+ credit balance, Node + CLI version, and which
21367
+ generation lanes are live. Exit 0 all clear,
21368
+ 1 if something needs fixing \u2014 run it first when
21369
+ a command fails.
20560
21370
  genex auth [options] Connect this machine to your Genex account, or
20561
21371
  finish a sign-in another command started \u2014 it
20562
21372
  resumes the same code, so an interrupted
@@ -20731,6 +21541,9 @@ ${c.bold("Options for the generators (`model` `sfx` `music` `voice` `texture` `i
20731
21541
  multi-beat move room to land instead of rushing it.
20732
21542
  (music: 10-300, default 90 \u2014 a looping gameplay track).
20733
21543
  --transparent (image) transparent background (alpha) \u2014 for decals/stickers.
21544
+ Takes --quality and --size; without --quality it runs a
21545
+ mid tier. Not with --candidates (a cutout is billed per
21546
+ image \u2014 pick a candidate first, then --clean it).
20734
21547
  --glass (image) magenta-key glass lane: generate the panel on a
20735
21548
  flat #FF00FF screen, then locally solve REAL per-pixel
20736
21549
  translucency and write RGBA files (glass-1.png\u2026) into
@@ -20770,6 +21583,10 @@ ${c.bold("Options for the generators (`model` `sfx` `music` `voice` `texture` `i
20770
21583
  --last-frame <url> (video) \u2026 and its end frame.
20771
21584
  --no-wait Enqueue only; don't block waiting for the asset. Prints
20772
21585
  the id that "genex wait <id>" picks the result up with.
21586
+ --no-download (Genex Tools workspaces) print the asset URL only instead
21587
+ of saving the file into ./assets.
21588
+ --out-dir <dir> (Genex Tools workspaces) where saved assets land
21589
+ (default ./assets). Also --glass's output directory.
20773
21590
  --api-url <url> Override the API base URL.
20774
21591
  --env <path> Token env file (default: ~/.genex/env).
20775
21592
 
@@ -20935,6 +21752,86 @@ ${c.bold("Examples")}
20935
21752
  genex controller chat
20936
21753
  genex controller voice
20937
21754
  `;
21755
+ var cr = (kind) => `${String(TYPICAL_CREDITS[kind] ?? "?").padStart(3)} cr`;
21756
+ var HELP_TOOLS = `${c.bold("genex")} \u2014 the gamedev AI asset toolkit. One sign-in, one meter, no provider accounts.
21757
+
21758
+ This folder is a ${c.bold("Genex Tools")} workspace: generation only. Assets download into
21759
+ ${c.cyan("./assets")} as files you own \u2014 wire the local path into your game, whatever engine
21760
+ it uses and wherever you host it.
21761
+
21762
+ ${c.bold("Generate")} typical cost
21763
+ genex model "<prompt>" A 3D model (GLB). ${cr("model")}
21764
+ --image <path|url> \u2026built from a reference image instead.
21765
+ genex model segment <id> Split a model into NAMED parts. ${cr("model_segment")}
21766
+ genex model rig <id> Give any mesh a skeleton (7 body ${cr("model_rig")}
21767
+ plans: biped|quadruped|hexapod|
21768
+ octopod|avian|serpentine|aquatic).
21769
+ genex model animate <rig-id> --preset walk[,run,\u2026]
21770
+ Retarget ready-made clips onto a rig. ${cr("model_animation")} each
21771
+ genex image "<prompt>" An image (PNG/JPEG). ${cr("image")}
21772
+ --transparent \u2026with a real alpha channel (sprites, icons, logos).
21773
+ --edit <path|url> \u2026edit an existing image with the prompt.
21774
+ --remove-bg / --bg-mode <m> \u2026cut the background (sprite|glyph|sheet|matte).
21775
+ genex video "<prompt>" A video (mp4). --loop, --frame <img>. ${cr("video")}
21776
+ genex texture "<prompt>" A tiling PBR texture set. --terrain. ${cr("texture")}
21777
+ genex sfx "<prompt>" A sound effect (mp3). --duration. ${cr("sfx")}
21778
+ genex music "<prompt>" A looping instrumental track (mp3). ${cr("music")}
21779
+ --duration 10-300s (default 90).
21780
+ genex voice "<line>" A spoken line (mp3). --voice ${cr("voice")}
21781
+ narrator|heroine|gruff|elder|robot|imp.
21782
+
21783
+ ${c.bold("Characters")}
21784
+ genex character "<brief>" Three concept candidates to choose from. ${cr("character_concept")}
21785
+ genex character preview <concept-id> --candidate <1|2|3> --user-approved
21786
+ A 3D preview of the chosen one. ${cr("character_preview")}
21787
+ genex character finalize <preview-id> --user-approved --approve-remesh 10000
21788
+ The rigged, game-ready character. ${cr("character_finalize")}
21789
+ genex character "<brief>" --direct-text
21790
+ One shot, no approval steps. ${cr("character")}
21791
+ genex creature "<desc>" A rigged enemy (biped-shaped bodies). ${cr("character")}
21792
+ genex character animate <id> "<verb>" \u2026
21793
+ Any move, in plain words. --locomotion ${cr("character_motion")} each
21794
+ for the 8-way walk+run set; --video
21795
+ ./clip.mp4 to use your own footage.
21796
+ genex character motions <id> List the clips installed on a character.
21797
+ genex animations search "<intent>" Search the ready-made animation library. free
21798
+
21799
+ ${c.bold("Workflow")}
21800
+ genex doctor [--json] Sign-in, credits, Node/CLI version, and which
21801
+ lanes are live. Run it when anything fails.
21802
+ genex wait <id> Pick up a generation enqueued with --no-wait.
21803
+ Never bills \u2014 re-running a generate command does.
21804
+ genex wait --all One status line per generation from this folder.
21805
+ genex auth [--force] Connect this machine (or switch accounts).
21806
+ genex tools Re-run setup here (refresh skills + rules).
21807
+
21808
+ ${c.bold("Options")}
21809
+ --no-wait Enqueue and return; pick it up with "genex wait <id>".
21810
+ --no-download Print the asset URL only; save nothing.
21811
+ --no-install (tools setup) Don't add the CLI to devDependencies.
21812
+ --out-dir <dir> Where saved assets land (default ./assets).
21813
+ --json Machine-readable output.
21814
+ --quiet Reduce output.
21815
+ --api-url <url> Override the API base URL.
21816
+ --env <path> Token env file (default: ~/.genex/env).
21817
+ -h, --help Show this help. -v, --version Show the version.
21818
+
21819
+ ${c.bold("Costs")}
21820
+ Prices above are typical. Live prices and your balance: ${c.cyan("genex doctor")}.
21821
+ A failed generation is refunded automatically. Re-running a generate command
21822
+ starts (and bills) a NEW asset \u2014 attach to a running one with ${c.cyan("genex wait <id>")}.
21823
+
21824
+ ${c.bold("Examples")}
21825
+ genex image "weathered treasure map, hand-inked" --transparent
21826
+ genex model "low-poly rusted fuel drum" --no-wait
21827
+ genex texture "mossy cracked cobblestone" --terrain
21828
+ genex character "stylized sci-fi courier, practical clothing" --direct-text
21829
+ genex character animate <character-id> --locomotion --no-wait
21830
+ genex wait --all
21831
+
21832
+ Hosting, publishing, multiplayer, remixing and custom domains are the Genex
21833
+ platform \u2014 build a game there from one prompt: ${c.cyan("https://genex.games")}
21834
+ `;
20938
21835
  function parseArgs(argv) {
20939
21836
  const parsed = {
20940
21837
  command: void 0,
@@ -21048,6 +21945,12 @@ function parseArgs(argv) {
21048
21945
  case "--no-wait":
21049
21946
  parsed.options.noWait = true;
21050
21947
  break;
21948
+ case "--no-download":
21949
+ parsed.options.noDownload = true;
21950
+ break;
21951
+ case "--no-install":
21952
+ parsed.options.noInstall = true;
21953
+ break;
21051
21954
  case "--no-controller-pack":
21052
21955
  parsed.options.controllerPack = false;
21053
21956
  break;
@@ -21545,13 +22448,17 @@ async function main() {
21545
22448
  log.plain(getCliVersion());
21546
22449
  return;
21547
22450
  }
22451
+ const mode = await workspaceMode();
22452
+ setWorkspaceHeader(mode === "tools" ? "tools" : null);
21548
22453
  if (parsed.help || parsed.command === void 0) {
21549
- log.plain(HELP);
22454
+ log.plain(mode === "tools" ? HELP_TOOLS : HELP);
21550
22455
  return;
21551
22456
  }
21552
22457
  const quietUpdates = Boolean(parsed.options.quiet || parsed.options.json);
21553
22458
  const updateLog = createLogger({ quiet: quietUpdates });
21554
- if (parsed.command !== "init") await syncSkills(updateLog);
22459
+ if (parsed.command !== "init" && parsed.command !== "tools") {
22460
+ await syncSkills(updateLog, mode);
22461
+ }
21555
22462
  const updateCheck = startUpdateCheck();
21556
22463
  Sentry2.setTag("command", parsed.command);
21557
22464
  Sentry2.setTag("cli.version", getCliVersion());
@@ -21565,6 +22472,11 @@ async function main() {
21565
22472
  ci: !!process.env.CI
21566
22473
  });
21567
22474
  try {
22475
+ if (mode === "tools" && refusedInTools(parsed.command)) {
22476
+ reportPlatformRefused(log, parsed.command);
22477
+ process.exitCode = 1;
22478
+ return;
22479
+ }
21568
22480
  if (parsed.command === "skybox") {
21569
22481
  log.error("The generated-sky lane is paused \u2014 `genex skybox` is not available.");
21570
22482
  log.plain(
@@ -21597,6 +22509,16 @@ async function main() {
21597
22509
  case "init":
21598
22510
  await runInit(parsed.options);
21599
22511
  break;
22512
+ // AG-911 — the OTHER door into this CLI: mark the folder a Genex Tools
22513
+ // workspace (generation only, assets downloaded locally, nothing created
22514
+ // on the platform). Bare word on purpose: it is the first command a
22515
+ // person runs, from a prompt they pasted.
22516
+ case "tools":
22517
+ await runTools(parsed.options);
22518
+ break;
22519
+ case "doctor":
22520
+ await runDoctor(parsed.options);
22521
+ break;
21600
22522
  case "auth":
21601
22523
  await runAuth(parsed.options);
21602
22524
  break;