@genex-ai/cli-demo 1.32.2 → 1.32.3-dev.679

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
@@ -42,7 +42,7 @@ import {
42
42
  writeProject,
43
43
  writeUserToken,
44
44
  writeWorkspace
45
- } from "./chunk-OJAXYQ5L.js";
45
+ } from "./chunk-2COG4P3T.js";
46
46
  import {
47
47
  CLI_CHANNEL,
48
48
  DEFAULT_API_URL,
@@ -59,7 +59,7 @@ import {
59
59
  getGenexEnvPath,
60
60
  getTemplatesDir,
61
61
  resolveAgentTargets
62
- } from "./chunk-5FA2WLM7.js";
62
+ } from "./chunk-HYCSNWYX.js";
63
63
 
64
64
  // src/instrument.ts
65
65
  import * as Sentry from "@sentry/node";
@@ -1940,14 +1940,16 @@ async function readLedger(cwd = process.cwd()) {
1940
1940
  }
1941
1941
  return [...entries.values()];
1942
1942
  }
1943
- async function recordQueued(id, kind, prompt, cwd = process.cwd()) {
1943
+ async function recordQueued(id, kind, prompt, cwd = process.cwd(), extra = {}) {
1944
+ const credits = extra.credits;
1944
1945
  await append(cwd, {
1945
1946
  t: "q",
1946
1947
  id,
1947
1948
  kind,
1948
1949
  prompt: prompt.slice(0, 120),
1949
1950
  ...prompt.length > 120 ? { tail: prompt.slice(-120) } : {},
1950
- at: (/* @__PURE__ */ new Date()).toISOString()
1951
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1952
+ ...typeof credits === "number" && Number.isFinite(credits) && credits >= 0 ? { credits } : {}
1951
1953
  });
1952
1954
  }
1953
1955
  async function recordTerminal(id, status, urls, cwd = process.cwd()) {
@@ -7879,8 +7881,8 @@ async function runRollback(opts) {
7879
7881
  }
7880
7882
 
7881
7883
  // src/commands/generate.ts
7882
- import fs23 from "fs/promises";
7883
- import path22 from "path";
7884
+ import fs24 from "fs/promises";
7885
+ import path23 from "path";
7884
7886
  import { PNG as PNG4 } from "pngjs";
7885
7887
 
7886
7888
  // src/lib/glass.ts
@@ -8047,6 +8049,159 @@ function ceilingVerdict(input) {
8047
8049
  return { allow: true };
8048
8050
  }
8049
8051
 
8052
+ // src/lib/asset-budget.ts
8053
+ import fs23 from "fs/promises";
8054
+ import path22 from "path";
8055
+ var DEFAULT_ASSET_BUDGET_SHARE = 0.5;
8056
+ var DEFAULT_ASSET_BUDGET_CAP = 500;
8057
+ function envShare(raw, fallback) {
8058
+ if (raw === void 0 || raw.trim() === "") return fallback;
8059
+ const n = Number(raw);
8060
+ return Number.isFinite(n) && n >= 0 && n <= 1 ? n : fallback;
8061
+ }
8062
+ function envCredits(raw, fallback) {
8063
+ if (raw === void 0 || raw.trim() === "") return fallback;
8064
+ const n = Number(raw);
8065
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
8066
+ }
8067
+ function budgetConfig(env = process.env) {
8068
+ return {
8069
+ share: envShare(env.GENEX_ASSET_BUDGET_SHARE, DEFAULT_ASSET_BUDGET_SHARE),
8070
+ cap: envCredits(env.GENEX_ASSET_BUDGET_CAP, DEFAULT_ASSET_BUDGET_CAP)
8071
+ };
8072
+ }
8073
+ function budgetDisabled(config) {
8074
+ return config.cap <= 0;
8075
+ }
8076
+ function allowanceFor(input) {
8077
+ const { spendable, config } = input;
8078
+ if (!Number.isFinite(spendable) || spendable <= 0) return 0;
8079
+ return Math.max(0, Math.min(Math.floor(spendable * config.share), config.cap));
8080
+ }
8081
+ var storePath = (cwd) => path22.join(cwd, ".genex", "asset-budget.json");
8082
+ async function readBudgetStore(cwd = process.cwd()) {
8083
+ try {
8084
+ const raw = await fs23.readFile(storePath(cwd), "utf8");
8085
+ const parsed = JSON.parse(raw);
8086
+ if (!parsed || parsed.v !== 1 || typeof parsed.allowanceCredits !== "number" || !Number.isFinite(parsed.allowanceCredits) || parsed.setBy !== "default" && parsed.setBy !== "user") {
8087
+ return null;
8088
+ }
8089
+ return {
8090
+ v: 1,
8091
+ allowanceCredits: Math.max(0, Math.floor(parsed.allowanceCredits)),
8092
+ setBy: parsed.setBy,
8093
+ setAt: typeof parsed.setAt === "string" ? parsed.setAt : "",
8094
+ spendableAtStart: typeof parsed.spendableAtStart === "number" ? parsed.spendableAtStart : 0,
8095
+ balanceAtStart: typeof parsed.balanceAtStart === "number" ? parsed.balanceAtStart : 0
8096
+ };
8097
+ } catch {
8098
+ return null;
8099
+ }
8100
+ }
8101
+ async function writeBudgetStore(cwd, store) {
8102
+ try {
8103
+ await fs23.access(path22.join(cwd, ".genex"));
8104
+ await fs23.writeFile(storePath(cwd), `${JSON.stringify(store, null, 2)}
8105
+ `, "utf8");
8106
+ return true;
8107
+ } catch {
8108
+ return false;
8109
+ }
8110
+ }
8111
+ async function ensureBudgetStore(cwd, input) {
8112
+ const existing = await readBudgetStore(cwd);
8113
+ if (existing) return existing;
8114
+ const store = {
8115
+ v: 1,
8116
+ allowanceCredits: allowanceFor({ spendable: input.spendable, config: input.config }),
8117
+ setBy: "default",
8118
+ setAt: (/* @__PURE__ */ new Date()).toISOString(),
8119
+ spendableAtStart: input.spendable,
8120
+ balanceAtStart: input.balance
8121
+ };
8122
+ await writeBudgetStore(cwd, store);
8123
+ return store;
8124
+ }
8125
+ function isFailureRow(row) {
8126
+ return row.t === "f" || row.t === "done" && row.status === "failed";
8127
+ }
8128
+ function spentCredits(rows) {
8129
+ const quoted = /* @__PURE__ */ new Map();
8130
+ for (const row of rows) {
8131
+ if (typeof row.id !== "string") continue;
8132
+ if (row.t === "q") {
8133
+ if (quoted.has(row.id)) continue;
8134
+ const credits = typeof row.credits === "number" && Number.isFinite(row.credits) && row.credits > 0 ? row.credits : 0;
8135
+ quoted.set(row.id, credits);
8136
+ } else if (isFailureRow(row) && quoted.has(row.id)) {
8137
+ quoted.set(row.id, 0);
8138
+ }
8139
+ }
8140
+ let total = 0;
8141
+ for (const credits of quoted.values()) total += credits;
8142
+ return total;
8143
+ }
8144
+ function budgetVerdict(input) {
8145
+ const { kind, price, spent, allowance } = input;
8146
+ const config = input.config ?? budgetConfig();
8147
+ if (budgetDisabled(config)) return { allow: true };
8148
+ if (spent + price <= allowance) return { allow: true };
8149
+ return {
8150
+ allow: false,
8151
+ reason: `This ${kind} is quoted at ${price} credits and this build has spent ${spent} of its ${allowance}-credit asset allowance. Refused so one build cannot drain the wallet. If the player wants more of this game generated, ask them \u2014 then re-run after \`npx genex budget --assets <credits> --user-approved\`. Otherwise build it in code and say why. (See the numbers any time with \`npx genex budget\`.)`
8152
+ };
8153
+ }
8154
+ async function fetchCreditsSnapshot(apiUrl, token) {
8155
+ try {
8156
+ const res = await apiFetch(`${apiUrl}/api/credits/me`, {
8157
+ headers: { Authorization: `Bearer ${token}` },
8158
+ signal: AbortSignal.timeout(6e3)
8159
+ });
8160
+ if (!res.ok) return null;
8161
+ const body = await res.json();
8162
+ if (!body || typeof body.balance !== "number" || typeof body.prices !== "object" || body.prices === null) {
8163
+ return null;
8164
+ }
8165
+ const balance = body.balance;
8166
+ const reserved = typeof body.reserved === "number" ? body.reserved : 0;
8167
+ return {
8168
+ balance,
8169
+ reserved,
8170
+ spendable: typeof body.spendable === "number" ? body.spendable : Math.max(0, balance - reserved),
8171
+ unlimited: body.unlimited === true,
8172
+ ...typeof body.emailVerified === "boolean" ? { emailVerified: body.emailVerified } : {},
8173
+ ...body.refillAt !== void 0 ? { refillAt: body.refillAt } : {},
8174
+ ...typeof body.refillTo === "number" ? { refillTo: body.refillTo } : {},
8175
+ prices: body.prices
8176
+ };
8177
+ } catch {
8178
+ return null;
8179
+ }
8180
+ }
8181
+ async function assetBudgetGate(input) {
8182
+ const config = input.config ?? budgetConfig();
8183
+ if (budgetDisabled(config)) return { allow: true };
8184
+ const cwd = input.cwd ?? process.cwd();
8185
+ const snapshot = await fetchCreditsSnapshot(input.apiUrl, input.token);
8186
+ if (!snapshot) {
8187
+ input.log.dim(" Asset allowance not checked (couldn't read /api/credits/me) \u2014 the API still refuses on insufficient credits.");
8188
+ return { allow: true };
8189
+ }
8190
+ if (snapshot.unlimited) return { allow: true };
8191
+ const price = typeof input.quotedCredits === "number" && Number.isFinite(input.quotedCredits) ? input.quotedCredits : snapshot.prices[input.kind];
8192
+ if (typeof price !== "number" || !Number.isFinite(price)) {
8193
+ input.log.dim(` Asset allowance not checked (no live price for ${input.kind}).`);
8194
+ return { allow: true };
8195
+ }
8196
+ const store = await ensureBudgetStore(cwd, {
8197
+ balance: snapshot.balance,
8198
+ spendable: snapshot.spendable,
8199
+ config
8200
+ });
8201
+ const spent = spentCredits(await readLedgerRows(cwd));
8202
+ return budgetVerdict({ kind: input.kind, price, spent, allowance: store.allowanceCredits, config });
8203
+ }
8204
+
8050
8205
  // src/lib/lanes.ts
8051
8206
  var LANE_CREDITS = /* @__PURE__ */ new Set(["ok", "exhausted", "unknown"]);
8052
8207
  async function fetchLanes(apiUrl, token, timeoutMs = 4e3) {
@@ -8184,7 +8339,7 @@ var isRemoteRef = (value) => /^(https?:\/\/|data:)/i.test(value);
8184
8339
  async function inlineLocalImage(filePath, flag) {
8185
8340
  let bytes;
8186
8341
  try {
8187
- bytes = await fs23.readFile(filePath);
8342
+ bytes = await fs24.readFile(filePath);
8188
8343
  } catch {
8189
8344
  return { ok: false, error: `Couldn't read the ${flag} file at ${filePath}.` };
8190
8345
  }
@@ -8194,7 +8349,7 @@ async function inlineLocalImage(filePath, flag) {
8194
8349
  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.`
8195
8350
  };
8196
8351
  }
8197
- const mime = IMAGE_MIME_BY_EXT[path22.extname(filePath).toLowerCase()] ?? "image/png";
8352
+ const mime = IMAGE_MIME_BY_EXT[path23.extname(filePath).toLowerCase()] ?? "image/png";
8198
8353
  return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
8199
8354
  }
8200
8355
  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.";
@@ -8380,7 +8535,7 @@ async function runGenerate(kind, opts) {
8380
8535
  let typedPrompt = opts.prompt?.trim();
8381
8536
  if (!typedPrompt && kind === "model" && opts.imageUrl) {
8382
8537
  const ref = opts.imageUrl.startsWith("data:") ? "local image" : opts.imageUrl;
8383
- typedPrompt = `from image: ${path22.basename(ref).slice(0, 120)}`;
8538
+ typedPrompt = `from image: ${path23.basename(ref).slice(0, 120)}`;
8384
8539
  }
8385
8540
  if (kind === "model" && opts.texture !== void 0 && !MODEL_TEXTURE_TIERS.includes(opts.texture)) {
8386
8541
  log.error(`--texture ${opts.texture} is a character texture size. \`genex model\` takes a texture TIER: ${MODEL_TEXTURE_TIERS.join("|")} (default detailed).`);
@@ -8429,7 +8584,7 @@ async function runGenerate(kind, opts) {
8429
8584
  return;
8430
8585
  }
8431
8586
  try {
8432
- const bytes = await fs23.readFile(opts.inpaintUrl);
8587
+ const bytes = await fs24.readFile(opts.inpaintUrl);
8433
8588
  opts = { ...opts, inpaintUrl: `data:image/png;base64,${bytes.toString("base64")}` };
8434
8589
  } catch {
8435
8590
  log.error(`Couldn't read the --inpaint mask at ${opts.inpaintUrl}.`);
@@ -8533,7 +8688,16 @@ async function runGenerate(kind, opts) {
8533
8688
  return;
8534
8689
  }
8535
8690
  }
8691
+ {
8692
+ const budget = await assetBudgetGate({ apiUrl, token, kind, quotedCredits: opts.quotedCredits, log });
8693
+ if (!budget.allow) {
8694
+ log.error(budget.reason);
8695
+ process.exitCode = 1;
8696
+ return;
8697
+ }
8698
+ }
8536
8699
  let id;
8700
+ let creditsQuoted;
8537
8701
  try {
8538
8702
  const res = await apiFetch(`${apiUrl}/api/generations`, {
8539
8703
  method: "POST",
@@ -8562,13 +8726,17 @@ async function runGenerate(kind, opts) {
8562
8726
  process.exitCode = 1;
8563
8727
  return;
8564
8728
  }
8565
- ({ id } = await res.json());
8729
+ const created = await res.json();
8730
+ id = created.id;
8731
+ if (typeof created.creditsQuoted === "number" && Number.isFinite(created.creditsQuoted)) {
8732
+ creditsQuoted = created.creditsQuoted;
8733
+ }
8566
8734
  } catch (err) {
8567
8735
  log.error(`Couldn't reach the API at ${apiUrl}: ${String(err)}`);
8568
8736
  process.exitCode = 1;
8569
8737
  return;
8570
8738
  }
8571
- await recordQueued(id, kind, prompt);
8739
+ await recordQueued(id, kind, prompt, void 0, { credits: creditsQuoted ?? opts.quotedCredits });
8572
8740
  if (opts.noWait) {
8573
8741
  if (opts.json) {
8574
8742
  writeJson({
@@ -8623,7 +8791,7 @@ async function reportGlassTerminal(view, outDir, log, json) {
8623
8791
  return;
8624
8792
  }
8625
8793
  await recordTerminal(view.id, "completed", files.map((f) => f.url));
8626
- await fs23.mkdir(outDir, { recursive: true });
8794
+ await fs24.mkdir(outDir, { recursive: true });
8627
8795
  const solved = [];
8628
8796
  for (let i = 0; i < files.length; i++) {
8629
8797
  const f = files[i];
@@ -8655,8 +8823,8 @@ async function reportGlassTerminal(view, outDir, log, json) {
8655
8823
  });
8656
8824
  continue;
8657
8825
  }
8658
- const outPath = path22.join(outDir, `glass-${i + 1}.png`);
8659
- await fs23.writeFile(outPath, PNG4.sync.write(r.png));
8826
+ const outPath = path23.join(outDir, `glass-${i + 1}.png`);
8827
+ await fs24.writeFile(outPath, PNG4.sync.write(r.png));
8660
8828
  solved.push({
8661
8829
  path: outPath,
8662
8830
  url: f.url,
@@ -9174,8 +9342,8 @@ function writeJson(value) {
9174
9342
  }
9175
9343
 
9176
9344
  // src/commands/model-sub.ts
9177
- import fs24 from "fs/promises";
9178
- import path23 from "path";
9345
+ import fs25 from "fs/promises";
9346
+ import path24 from "path";
9179
9347
  var MODEL_SUBCOMMANDS = ["segment", "rig", "animate", "import"];
9180
9348
  function apiErrorMessage(data, fallback) {
9181
9349
  if (typeof data !== "object" || data === null) return fallback;
@@ -9190,29 +9358,29 @@ async function importModelFile(args) {
9190
9358
  const { log } = args;
9191
9359
  const filePath = args.filePath.trim();
9192
9360
  if (!/\.glb$/i.test(filePath)) {
9193
- log.error(`\`import\` takes a .glb file (binary glTF 2.0). Export ${path23.basename(filePath) || "the model"} as GLB first \u2014 Blender: File \u2192 Export \u2192 glTF 2.0, format "glTF Binary".`);
9361
+ log.error(`\`import\` takes a .glb file (binary glTF 2.0). Export ${path24.basename(filePath) || "the model"} as GLB first \u2014 Blender: File \u2192 Export \u2192 glTF 2.0, format "glTF Binary".`);
9194
9362
  return null;
9195
9363
  }
9196
9364
  let bytes;
9197
9365
  try {
9198
- bytes = await fs24.readFile(filePath);
9366
+ bytes = await fs25.readFile(filePath);
9199
9367
  } catch {
9200
9368
  log.error(`Couldn't read ${filePath}.`);
9201
9369
  return null;
9202
9370
  }
9203
9371
  if (bytes.byteLength > MODEL_IMPORT_MAX_BYTES) {
9204
- log.error(`${path23.basename(filePath)} is ${(bytes.byteLength / 1e6).toFixed(1)} MB; imports are capped at ${MODEL_IMPORT_MAX_BYTES / 1024 / 1024} MB. Shrink its textures (they are usually the bulk) and try again.`);
9372
+ log.error(`${path24.basename(filePath)} is ${(bytes.byteLength / 1e6).toFixed(1)} MB; imports are capped at ${MODEL_IMPORT_MAX_BYTES / 1024 / 1024} MB. Shrink its textures (they are usually the bulk) and try again.`);
9205
9373
  return null;
9206
9374
  }
9207
9375
  if (bytes.byteLength < 12 || bytes.readUInt32LE(0) !== GLB_MAGIC) {
9208
- log.error(`${path23.basename(filePath)} is not a GLB (no glTF magic). A .gltf + .bin pair must be exported as one binary .glb.`);
9376
+ log.error(`${path24.basename(filePath)} is not a GLB (no glTF magic). A .gltf + .bin pair must be exported as one binary .glb.`);
9209
9377
  return null;
9210
9378
  }
9211
9379
  const headers = { "Content-Type": "application/json", Authorization: `Bearer ${args.token}` };
9212
9380
  const minted = await apiFetch(`${args.apiUrl}/api/generations/import`, {
9213
9381
  method: "POST",
9214
9382
  headers,
9215
- body: JSON.stringify({ filename: path23.basename(filePath), bytes: bytes.byteLength, contentType: "model/gltf-binary" })
9383
+ body: JSON.stringify({ filename: path24.basename(filePath), bytes: bytes.byteLength, contentType: "model/gltf-binary" })
9216
9384
  });
9217
9385
  if (printedStructuredError(minted)) return null;
9218
9386
  if (!minted.ok) {
@@ -9221,7 +9389,7 @@ async function importModelFile(args) {
9221
9389
  return null;
9222
9390
  }
9223
9391
  const { id, uploadUrl, url } = await minted.json();
9224
- log.dim(` uploading ${path23.basename(filePath)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
9392
+ log.dim(` uploading ${path24.basename(filePath)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
9225
9393
  const put = await fetch(uploadUrl, {
9226
9394
  method: "PUT",
9227
9395
  headers: { "Content-Type": "model/gltf-binary", "Content-Length": String(bytes.byteLength) },
@@ -9369,7 +9537,7 @@ async function runModelAnimate(opts) {
9369
9537
  }
9370
9538
 
9371
9539
  // src/commands/wait.ts
9372
- import path24 from "path";
9540
+ import path25 from "path";
9373
9541
  var TERMINAL2 = /* @__PURE__ */ new Set(["completed", "failed"]);
9374
9542
  async function runWait(opts) {
9375
9543
  if (opts.all) return runWaitAll(opts);
@@ -9500,7 +9668,7 @@ async function runWaitAll(opts) {
9500
9668
  if (v?.status !== "completed" || !v.files?.length) continue;
9501
9669
  const local = localDeliveryFor("tools", opts, e.prompt || e.kind);
9502
9670
  if (!local) continue;
9503
- const outDir = path24.join(path24.relative(process.cwd(), cwd) || ".", local.outDir);
9671
+ const outDir = path25.join(path25.relative(process.cwd(), cwd) || ".", local.outDir);
9504
9672
  const target = { kind: e.kind, prompt: local.prompt, id: e.id, outDir };
9505
9673
  const missing = await undeliveredFiles(v.files, target);
9506
9674
  if (missing.length === 0) continue;
@@ -9595,8 +9763,8 @@ async function toRow(e, v, cwd) {
9595
9763
  }
9596
9764
 
9597
9765
  // src/commands/controller.ts
9598
- import fs26 from "fs/promises";
9599
- import path26 from "path";
9766
+ import fs27 from "fs/promises";
9767
+ import path27 from "path";
9600
9768
 
9601
9769
  // ../../packages/meshy-animation-catalog/src/index.ts
9602
9770
  import { createHash } from "crypto";
@@ -18726,9 +18894,9 @@ function searchMeshyAnimations(query, options = {}) {
18726
18894
  }
18727
18895
 
18728
18896
  // src/lib/anims.ts
18729
- import fs25 from "fs/promises";
18730
- import path25 from "path";
18731
- var ANIMS_DEST = path25.join("public", "assets", "anims");
18897
+ import fs26 from "fs/promises";
18898
+ import path26 from "path";
18899
+ var ANIMS_DEST = path26.join("public", "assets", "anims");
18732
18900
  var HIDDEN_TAG = "reference";
18733
18901
  async function runAnims(opts) {
18734
18902
  const log = createLogger({ quiet: opts.quiet });
@@ -18744,7 +18912,7 @@ async function runAnims(opts) {
18744
18912
  printCatalog(log, manifest, selectors);
18745
18913
  return;
18746
18914
  }
18747
- const controllerMarker = path25.join(root, "src", "controllers", "character");
18915
+ const controllerMarker = path26.join(root, "src", "controllers", "character");
18748
18916
  if (!await exists3(controllerMarker)) {
18749
18917
  log.error(
18750
18918
  `No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
@@ -18753,11 +18921,11 @@ async function runAnims(opts) {
18753
18921
  process.exitCode = 1;
18754
18922
  return;
18755
18923
  }
18756
- const destDir = path25.join(root, ANIMS_DEST);
18757
- const gameManifestPath = path25.join(destDir, "manifest.json");
18924
+ const destDir = path26.join(root, ANIMS_DEST);
18925
+ const gameManifestPath = path26.join(destDir, "manifest.json");
18758
18926
  if (opts.reset) {
18759
- await fs25.rm(destDir, { recursive: true, force: true });
18760
- log.step(`Cleared ${c.cyan(ANIMS_DEST + path25.sep)} (--reset)`);
18927
+ await fs26.rm(destDir, { recursive: true, force: true });
18928
+ log.step(`Cleared ${c.cyan(ANIMS_DEST + path26.sep)} (--reset)`);
18761
18929
  }
18762
18930
  if (selectors.length === 0) {
18763
18931
  const installed = await readGameManifest(gameManifestPath);
@@ -18795,35 +18963,35 @@ async function runAnims(opts) {
18795
18963
  }
18796
18964
  }
18797
18965
  const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
18798
- const cacheDir = path25.join(
18966
+ const cacheDir = path26.join(
18799
18967
  opts.cacheDir ?? getAnimsCacheDir(),
18800
18968
  `${manifest.library}-v${manifest.version}`
18801
18969
  );
18802
- await fs25.mkdir(cacheDir, { recursive: true });
18803
- await fs25.mkdir(destDir, { recursive: true });
18970
+ await fs26.mkdir(cacheDir, { recursive: true });
18971
+ await fs26.mkdir(destDir, { recursive: true });
18804
18972
  const base = getAnimsBase(opts.animsBase);
18805
18973
  let installedCount = 0;
18806
18974
  let presentCount = 0;
18807
18975
  let addedBytes = 0;
18808
18976
  const failures = [];
18809
18977
  for (const entry of wanted) {
18810
- const dest = path25.join(destDir, entry.file);
18978
+ const dest = path26.join(destDir, entry.file);
18811
18979
  if (await hasSize(dest, entry.bytes)) {
18812
18980
  presentCount++;
18813
18981
  continue;
18814
18982
  }
18815
18983
  try {
18816
- const cached = path25.join(cacheDir, entry.file);
18984
+ const cached = path26.join(cacheDir, entry.file);
18817
18985
  if (!await hasSize(cached, entry.bytes)) {
18818
18986
  const res = await fetch(base + entry.file);
18819
18987
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
18820
18988
  const buf = Buffer.from(await res.arrayBuffer());
18821
- await fs25.writeFile(cached, buf);
18989
+ await fs26.writeFile(cached, buf);
18822
18990
  }
18823
- await fs25.copyFile(cached, dest);
18991
+ await fs26.copyFile(cached, dest);
18824
18992
  installedCount++;
18825
18993
  addedBytes += entry.bytes;
18826
- log.dim(` ${path25.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
18994
+ log.dim(` ${path26.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
18827
18995
  } catch (err) {
18828
18996
  failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
18829
18997
  }
@@ -18839,13 +19007,13 @@ async function runAnims(opts) {
18839
19007
  version: manifest.version,
18840
19008
  clips: [...union].sort((a, b) => a.localeCompare(b))
18841
19009
  };
18842
- await fs25.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
19010
+ await fs26.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
18843
19011
  log.plain("");
18844
19012
  const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
18845
19013
  if (presentCount > 0) parts.push(`${presentCount} already present`);
18846
19014
  if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
18847
19015
  log.success(
18848
- `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path25.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
19016
+ `${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path26.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
18849
19017
  );
18850
19018
  for (const [selector, entries] of resolved) {
18851
19019
  const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
@@ -18877,8 +19045,8 @@ async function loadManifest(baseOverride) {
18877
19045
  }
18878
19046
  } catch {
18879
19047
  }
18880
- const snapshotPath = path25.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
18881
- const manifest = JSON.parse(await fs25.readFile(snapshotPath, "utf8"));
19048
+ const snapshotPath = path26.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
19049
+ const manifest = JSON.parse(await fs26.readFile(snapshotPath, "utf8"));
18882
19050
  return { manifest, source: "snapshot" };
18883
19051
  }
18884
19052
  function resolveSelectors(manifest, selectors) {
@@ -18996,21 +19164,21 @@ function printCatalog(log, manifest, selectors) {
18996
19164
  }
18997
19165
  async function readGameManifest(file) {
18998
19166
  try {
18999
- return JSON.parse(await fs25.readFile(file, "utf8"));
19167
+ return JSON.parse(await fs26.readFile(file, "utf8"));
19000
19168
  } catch {
19001
19169
  return null;
19002
19170
  }
19003
19171
  }
19004
19172
  async function hasSize(file, bytes) {
19005
19173
  try {
19006
- return (await fs25.stat(file)).size === bytes;
19174
+ return (await fs26.stat(file)).size === bytes;
19007
19175
  } catch {
19008
19176
  return false;
19009
19177
  }
19010
19178
  }
19011
19179
  async function exists3(p) {
19012
19180
  try {
19013
- await fs25.access(p);
19181
+ await fs26.access(p);
19014
19182
  return true;
19015
19183
  } catch {
19016
19184
  return false;
@@ -19211,8 +19379,8 @@ var CONTROLLER_FILE_SETS = {
19211
19379
  ]
19212
19380
  }
19213
19381
  };
19214
- var CODE_DEST = path26.join("src", "controllers");
19215
- var ASSETS_DEST = path26.join("public", "assets");
19382
+ var CODE_DEST = path27.join("src", "controllers");
19383
+ var ASSETS_DEST = path27.join("public", "assets");
19216
19384
  async function runController(opts) {
19217
19385
  const log = createLogger({ quiet: opts.quiet });
19218
19386
  if (opts.kind?.trim() === "anims") {
@@ -19229,31 +19397,31 @@ async function runController(opts) {
19229
19397
  process.exitCode = 1;
19230
19398
  return;
19231
19399
  }
19232
- const srcDir = path26.join(getTemplatesDir(), "controllers");
19400
+ const srcDir = path27.join(getTemplatesDir(), "controllers");
19233
19401
  const root = opts.cwd ?? process.cwd();
19234
19402
  const set = CONTROLLER_FILE_SETS[kind];
19235
19403
  log.plain(c.bold(`genex controller ${kind}`));
19236
19404
  log.plain("");
19237
- log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path26.sep)}`);
19405
+ log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path27.sep)}`);
19238
19406
  const plan = [
19239
- ...set.code.map((rel) => ({ from: rel, rel: path26.join(CODE_DEST, rel) })),
19407
+ ...set.code.map((rel) => ({ from: rel, rel: path27.join(CODE_DEST, rel) })),
19240
19408
  ...set.assets.map((rel) => ({
19241
19409
  from: rel,
19242
- rel: path26.join(ASSETS_DEST, path26.basename(rel))
19410
+ rel: path27.join(ASSETS_DEST, path27.basename(rel))
19243
19411
  }))
19244
19412
  ];
19245
19413
  let copied = 0;
19246
19414
  let skipped = 0;
19247
19415
  try {
19248
19416
  for (const file of plan) {
19249
- const dest = path26.join(root, file.rel);
19417
+ const dest = path27.join(root, file.rel);
19250
19418
  if (!opts.force && await exists4(dest)) {
19251
19419
  skipped++;
19252
19420
  log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
19253
19421
  continue;
19254
19422
  }
19255
- await fs26.mkdir(path26.dirname(dest), { recursive: true });
19256
- await fs26.copyFile(path26.join(srcDir, file.from), dest);
19423
+ await fs27.mkdir(path27.dirname(dest), { recursive: true });
19424
+ await fs27.copyFile(path27.join(srcDir, file.from), dest);
19257
19425
  copied++;
19258
19426
  log.dim(` ${file.rel}`);
19259
19427
  }
@@ -19306,7 +19474,7 @@ async function runController(opts) {
19306
19474
  for (const line of set.sketch) {
19307
19475
  log.dim(` ${line}`);
19308
19476
  }
19309
- if (kind === "character" && !await exists4(path26.join(root, ASSETS_DEST, "meshy-character.json"))) {
19477
+ if (kind === "character" && !await exists4(path27.join(root, ASSETS_DEST, "meshy-character.json"))) {
19310
19478
  log.plain("");
19311
19479
  log.plain(
19312
19480
  ` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
@@ -19338,9 +19506,9 @@ async function installMeshyCharacterManifest(args) {
19338
19506
  throw new Error("The API returned an invalid Meshy character manifest.");
19339
19507
  }
19340
19508
  assertCompleteMeshyControllerPack(manifest);
19341
- const destination = path26.join(args.root, ASSETS_DEST, "meshy-character.json");
19342
- await fs26.mkdir(path26.dirname(destination), { recursive: true });
19343
- await fs26.writeFile(
19509
+ const destination = path27.join(args.root, ASSETS_DEST, "meshy-character.json");
19510
+ await fs27.mkdir(path27.dirname(destination), { recursive: true });
19511
+ await fs27.writeFile(
19344
19512
  destination,
19345
19513
  `${JSON.stringify(manifest, null, 2)}
19346
19514
  `
@@ -19485,14 +19653,14 @@ function assertCompleteMeshyControllerPack(manifest) {
19485
19653
  }
19486
19654
  async function installFallbackAvatar(args) {
19487
19655
  const { root, srcDir, log } = args;
19488
- const dest = path26.join(root, ASSETS_DEST, "avatar.vrm");
19489
- await fs26.mkdir(path26.dirname(dest), { recursive: true });
19490
- await fs26.copyFile(path26.join(srcDir, "assets", "default-avatar.vrm"), dest);
19656
+ const dest = path27.join(root, ASSETS_DEST, "avatar.vrm");
19657
+ await fs27.mkdir(path27.dirname(dest), { recursive: true });
19658
+ await fs27.copyFile(path27.join(srcDir, "assets", "default-avatar.vrm"), dest);
19491
19659
  log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
19492
19660
  }
19493
19661
  async function exists4(p) {
19494
19662
  try {
19495
- await fs26.access(p);
19663
+ await fs27.access(p);
19496
19664
  return true;
19497
19665
  } catch {
19498
19666
  return false;
@@ -19500,8 +19668,8 @@ async function exists4(p) {
19500
19668
  }
19501
19669
 
19502
19670
  // src/commands/character.ts
19503
- import fs27 from "fs/promises";
19504
- import path27 from "path";
19671
+ import fs28 from "fs/promises";
19672
+ import path28 from "path";
19505
19673
  function exactAnimation(selector) {
19506
19674
  const trimmed = selector.trim();
19507
19675
  if (/^-?\d+$/.test(trimmed)) return animationById(Number(trimmed));
@@ -19581,7 +19749,7 @@ function showAmbiguity(selector, candidates, log, json = false) {
19581
19749
  }
19582
19750
  process.exitCode = 1;
19583
19751
  }
19584
- var INSTALLED_MANIFEST = path27.join("public", "assets", "meshy-character.json");
19752
+ var INSTALLED_MANIFEST = path28.join("public", "assets", "meshy-character.json");
19585
19753
  async function resolveAdoptTarget(selector) {
19586
19754
  const trimmed = selector?.trim();
19587
19755
  if (trimmed && !trimmed.endsWith(".json")) {
@@ -19590,7 +19758,7 @@ async function resolveAdoptTarget(selector) {
19590
19758
  const file = trimmed ?? INSTALLED_MANIFEST;
19591
19759
  let raw;
19592
19760
  try {
19593
- raw = await fs27.readFile(file, "utf8");
19761
+ raw = await fs28.readFile(file, "utf8");
19594
19762
  } catch {
19595
19763
  return {
19596
19764
  ok: false,
@@ -19650,6 +19818,8 @@ async function runCharacterAdopt(opts) {
19650
19818
  createPath: `/api/characters/${encodeURIComponent(target.characterId)}/adopt`,
19651
19819
  body: { projectId: project.id },
19652
19820
  quote: { credits: 0 },
19821
+ // A copy, not a generation: it costs nothing, and the allowance sees 0.
19822
+ quotedCredits: 0,
19653
19823
  completed: (view) => {
19654
19824
  if (opts.json) {
19655
19825
  writeJson({
@@ -19711,10 +19881,11 @@ async function runCharacterImport(opts) {
19711
19881
  opts,
19712
19882
  ctx,
19713
19883
  kind: "character",
19714
- prompt: `Import ${path27.basename(filePath)} as a rigged character`,
19884
+ prompt: `Import ${path28.basename(filePath)} as a rigged character`,
19715
19885
  createPath: "/api/characters/import",
19716
19886
  body,
19717
19887
  quote: price,
19888
+ quotedCredits: price.credits,
19718
19889
  completed: (view) => {
19719
19890
  const confidence = typeof view.metadata?.autoRigConfidence === "number" ? view.metadata.autoRigConfidence : null;
19720
19891
  const next = {
@@ -19789,12 +19960,28 @@ async function runWorkflow(args) {
19789
19960
  return;
19790
19961
  }
19791
19962
  }
19963
+ {
19964
+ const budget = await assetBudgetGate({
19965
+ apiUrl: args.ctx.apiUrl,
19966
+ token: args.ctx.token,
19967
+ kind: args.kind,
19968
+ quotedCredits: args.quotedCredits,
19969
+ log
19970
+ });
19971
+ if (!budget.allow) {
19972
+ log.error(budget.reason);
19973
+ process.exitCode = 1;
19974
+ return;
19975
+ }
19976
+ }
19792
19977
  const created = await postWorkflow(`${args.ctx.apiUrl}${args.createPath}`, args.ctx.token, args.body);
19793
19978
  if (!created) {
19794
19979
  process.exitCode = 1;
19795
19980
  return;
19796
19981
  }
19797
- await recordQueued(created.id, args.kind, args.prompt);
19982
+ await recordQueued(created.id, args.kind, args.prompt, void 0, {
19983
+ credits: created.creditsQuoted ?? args.quotedCredits
19984
+ });
19798
19985
  if (args.opts.noWait) {
19799
19986
  const nextCommand = `genex wait ${created.id}${args.opts.json ? " --json" : ""}`;
19800
19987
  if (args.opts.json) {
@@ -19894,6 +20081,7 @@ async function runCharacterConcept(opts) {
19894
20081
  createPath: "/api/characters/concepts",
19895
20082
  body: request,
19896
20083
  quote: price,
20084
+ quotedCredits: price.credits,
19897
20085
  completed: (view) => reportCharacterConceptTerminal(view, log, {
19898
20086
  json: opts.json,
19899
20087
  open: opts.open,
@@ -19945,6 +20133,7 @@ async function runDirectTextCharacter(opts) {
19945
20133
  prompt,
19946
20134
  token: ctx.token,
19947
20135
  apiUrl: ctx.apiUrl,
20136
+ quotedCredits: price.credits,
19948
20137
  generationOptions: {
19949
20138
  actionIds: resolved.actionIds,
19950
20139
  controllerPack,
@@ -20014,6 +20203,7 @@ async function runCharacterPreview(opts) {
20014
20203
  createPath: basePath,
20015
20204
  body,
20016
20205
  quote: price,
20206
+ quotedCredits: price.credits,
20017
20207
  completed: (view) => reportCharacterPreviewTerminal(view, log, {
20018
20208
  json: opts.json,
20019
20209
  open: opts.open,
@@ -20087,6 +20277,7 @@ async function runCharacterFinalize(opts) {
20087
20277
  createPath: basePath,
20088
20278
  body,
20089
20279
  quote: price,
20280
+ quotedCredits: price.credits,
20090
20281
  completed: async (view, local) => {
20091
20282
  if (opts.json) {
20092
20283
  const files = view.files ?? [];
@@ -20176,6 +20367,7 @@ async function runCharacterAnimate(opts) {
20176
20367
  prompt: `Attach Meshy actions ${resolved.actionIds.join(", ")} to character ${characterId}`,
20177
20368
  token: ctx.token,
20178
20369
  apiUrl: ctx.apiUrl,
20370
+ quotedCredits: price.credits,
20179
20371
  generationOptions: { characterId, actionIds: resolved.actionIds }
20180
20372
  });
20181
20373
  }
@@ -20191,22 +20383,22 @@ async function context2(opts) {
20191
20383
  const project = await readProject();
20192
20384
  return { token, apiUrl: getApiUrl(opts.apiUrl ?? project?.apiUrl) };
20193
20385
  }
20194
- async function readVideo(path33, log) {
20386
+ async function readVideo(path35, log) {
20195
20387
  let bytes;
20196
20388
  try {
20197
- bytes = await readFile(path33);
20389
+ bytes = await readFile(path35);
20198
20390
  } catch {
20199
- log.error(`Can't read ${path33}.`);
20391
+ log.error(`Can't read ${path35}.`);
20200
20392
  return null;
20201
20393
  }
20202
20394
  if (bytes.byteLength > MAX_VIDEO_BYTES) {
20203
- log.error(`${basename(path33)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
20395
+ log.error(`${basename(path35)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
20204
20396
  return null;
20205
20397
  }
20206
20398
  return bytes;
20207
20399
  }
20208
- async function uploadVideo(apiUrl, token, characterId, path33, bytes, log) {
20209
- const contentType = /\.mov$/i.test(path33) ? "video/quicktime" : "video/mp4";
20400
+ async function uploadVideo(apiUrl, token, characterId, path35, bytes, log) {
20401
+ const contentType = /\.mov$/i.test(path35) ? "video/quicktime" : "video/mp4";
20210
20402
  const minted = await apiFetch(
20211
20403
  `${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
20212
20404
  {
@@ -20221,7 +20413,7 @@ async function uploadVideo(apiUrl, token, characterId, path33, bytes, log) {
20221
20413
  return null;
20222
20414
  }
20223
20415
  const { uploadUrl, videoUrl } = await minted.json();
20224
- log.dim(` uploading ${basename(path33)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
20416
+ log.dim(` uploading ${basename(path35)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
20225
20417
  const put = await fetch(uploadUrl, {
20226
20418
  method: "PUT",
20227
20419
  headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
@@ -20537,8 +20729,8 @@ function rank(items, query) {
20537
20729
  }
20538
20730
 
20539
20731
  // src/commands/motion.ts
20540
- import fs28 from "fs/promises";
20541
- import path28 from "path";
20732
+ import fs29 from "fs/promises";
20733
+ import path29 from "path";
20542
20734
 
20543
20735
  // src/lib/motion/npz.ts
20544
20736
  import zlib from "zlib";
@@ -21789,7 +21981,7 @@ async function motionGen(opts, log) {
21789
21981
  }
21790
21982
  if (opts.constraintsPath !== void 0) {
21791
21983
  try {
21792
- const raw = await fs28.readFile(opts.constraintsPath, "utf8");
21984
+ const raw = await fs29.readFile(opts.constraintsPath, "utf8");
21793
21985
  generationOptions.constraints = JSON.parse(raw);
21794
21986
  } catch {
21795
21987
  log.error(`Couldn't read the --constraints JSON at ${opts.constraintsPath}.`);
@@ -21813,10 +22005,10 @@ async function motionGen(opts, log) {
21813
22005
  async function expandTakes(selectors) {
21814
22006
  const out = [];
21815
22007
  for (const sel of selectors) {
21816
- const st = await fs28.stat(sel).catch(() => null);
22008
+ const st = await fs29.stat(sel).catch(() => null);
21817
22009
  if (st?.isDirectory()) {
21818
- const names = await fs28.readdir(sel);
21819
- for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path28.join(sel, n));
22010
+ const names = await fs29.readdir(sel);
22011
+ for (const n of names.sort()) if (n.endsWith(".npz")) out.push(path29.join(sel, n));
21820
22012
  } else if (st?.isFile()) {
21821
22013
  out.push(sel);
21822
22014
  } else {
@@ -21851,7 +22043,7 @@ async function motionVerify(opts, log) {
21851
22043
  let gates = DEFAULT_GATES;
21852
22044
  if (opts.gatesPath) {
21853
22045
  try {
21854
- gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs28.readFile(opts.gatesPath, "utf8")));
22046
+ gates = mergeGates(DEFAULT_GATES, JSON.parse(await fs29.readFile(opts.gatesPath, "utf8")));
21855
22047
  } catch {
21856
22048
  log.error(`Couldn't read the --gates JSON at ${opts.gatesPath}.`);
21857
22049
  process.exitCode = 1;
@@ -21873,9 +22065,9 @@ async function motionVerify(opts, log) {
21873
22065
  }
21874
22066
  const reports = [];
21875
22067
  for (const file of files) {
21876
- const stem = path28.basename(file).replace(/\.npz$/, "");
22068
+ const stem = path29.basename(file).replace(/\.npz$/, "");
21877
22069
  try {
21878
- reports.push(analyzeTake(stem, await fs28.readFile(file), gates));
22070
+ reports.push(analyzeTake(stem, await fs29.readFile(file), gates));
21879
22071
  } catch (err) {
21880
22072
  reports.push({
21881
22073
  take: stem,
@@ -21913,7 +22105,7 @@ async function motionCompile(opts, log) {
21913
22105
  let cfg = DEFAULT_MOTION_CONFIG;
21914
22106
  if (opts.configPath) {
21915
22107
  try {
21916
- const patch = JSON.parse(await fs28.readFile(opts.configPath, "utf8"));
22108
+ const patch = JSON.parse(await fs29.readFile(opts.configPath, "utf8"));
21917
22109
  cfg = { ...DEFAULT_MOTION_CONFIG, ...patch, idleLoop: { ...DEFAULT_MOTION_CONFIG.idleLoop, ...patch.idleLoop } };
21918
22110
  } catch {
21919
22111
  log.error(`Couldn't read the --config JSON at ${opts.configPath}.`);
@@ -21931,16 +22123,16 @@ async function motionCompile(opts, log) {
21931
22123
  }
21932
22124
  const inputs = [];
21933
22125
  for (const file of files) {
21934
- const stem = path28.basename(file).replace(/\.npz$/, "");
22126
+ const stem = path29.basename(file).replace(/\.npz$/, "");
21935
22127
  try {
21936
- inputs.push({ stem, take: loadTake(await fs28.readFile(file)) });
22128
+ inputs.push({ stem, take: loadTake(await fs29.readFile(file)) });
21937
22129
  } catch (err) {
21938
22130
  log.error(`${stem}: ${err instanceof Error ? err.message : String(err)}`);
21939
22131
  process.exitCode = 1;
21940
22132
  return;
21941
22133
  }
21942
22134
  }
21943
- const setName = opts.set ?? path28.basename(opts.out).replace(/\.json$/, "");
22135
+ const setName = opts.set ?? path29.basename(opts.out).replace(/\.json$/, "");
21944
22136
  let result;
21945
22137
  try {
21946
22138
  result = compileSet(inputs, setName, cfg);
@@ -21955,9 +22147,9 @@ async function motionCompile(opts, log) {
21955
22147
  process.exitCode = 1;
21956
22148
  return;
21957
22149
  }
21958
- await fs28.mkdir(path28.dirname(path28.resolve(opts.out)), { recursive: true });
22150
+ await fs29.mkdir(path29.dirname(path29.resolve(opts.out)), { recursive: true });
21959
22151
  const json = JSON.stringify(result.data);
21960
- await fs28.writeFile(opts.out, json);
22152
+ await fs29.writeFile(opts.out, json);
21961
22153
  if (opts.json) {
21962
22154
  writeJson({ out: opts.out, set: setName, tracks: Object.keys(result.data.gaits), bytes: json.length });
21963
22155
  return;
@@ -21975,9 +22167,9 @@ var MOTION_RUNTIME_FILES = [
21975
22167
  var MOTION_PRESETS = {
21976
22168
  rifle: ["sets/rifle.json", "sets/jumps.json"]
21977
22169
  };
21978
- var MOTION_DEST = path28.join("src", "motion");
22170
+ var MOTION_DEST = path29.join("src", "motion");
21979
22171
  async function motionInstall(opts, log) {
21980
- const srcDir = path28.join(getTemplatesDir(), "motion");
22172
+ const srcDir = path29.join(getTemplatesDir(), "motion");
21981
22173
  const root = opts.cwd ?? process.cwd();
21982
22174
  const preset = opts.set;
21983
22175
  if (preset !== void 0 && !MOTION_PRESETS[preset]) {
@@ -21988,21 +22180,21 @@ async function motionInstall(opts, log) {
21988
22180
  const files = [...MOTION_RUNTIME_FILES, ...preset ? MOTION_PRESETS[preset] : []];
21989
22181
  log.plain(c.bold(`genex motion install${preset ? ` --set ${preset}` : ""}`));
21990
22182
  log.plain("");
21991
- log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path28.sep)}`);
22183
+ log.step(`Vendoring the motion runtime into ${c.cyan(MOTION_DEST + path29.sep)}`);
21992
22184
  let copied = 0, skipped = 0;
21993
22185
  try {
21994
22186
  for (const rel of files) {
21995
- const dest = path28.join(root, MOTION_DEST, rel);
21996
- const exists5 = await fs28.access(dest).then(() => true, () => false);
22187
+ const dest = path29.join(root, MOTION_DEST, rel);
22188
+ const exists5 = await fs29.access(dest).then(() => true, () => false);
21997
22189
  if (!opts.force && exists5) {
21998
22190
  skipped++;
21999
- log.dim(` skipped ${path28.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
22191
+ log.dim(` skipped ${path29.join(MOTION_DEST, rel)} (exists \u2014 use --force to overwrite)`);
22000
22192
  continue;
22001
22193
  }
22002
- await fs28.mkdir(path28.dirname(dest), { recursive: true });
22003
- await fs28.copyFile(path28.join(srcDir, rel), dest);
22194
+ await fs29.mkdir(path29.dirname(dest), { recursive: true });
22195
+ await fs29.copyFile(path29.join(srcDir, rel), dest);
22004
22196
  copied++;
22005
- log.dim(` ${path28.join(MOTION_DEST, rel)}`);
22197
+ log.dim(` ${path29.join(MOTION_DEST, rel)}`);
22006
22198
  }
22007
22199
  } catch (err) {
22008
22200
  log.error(`Copy failed: ${String(err)}`);
@@ -22043,7 +22235,7 @@ async function motionConstraints(opts, log) {
22043
22235
  }
22044
22236
  const doc = directionConstraint(dir, speed, duration);
22045
22237
  const out = opts.out ?? "constraints.json";
22046
- await fs28.writeFile(out, JSON.stringify(doc));
22238
+ await fs29.writeFile(out, JSON.stringify(doc));
22047
22239
  if (opts.json) {
22048
22240
  writeJson({ out, dir: dirName, speed, duration, waypoints: doc[0].frame_indices.length });
22049
22241
  return;
@@ -22080,14 +22272,14 @@ async function runMotion(opts) {
22080
22272
  }
22081
22273
 
22082
22274
  // src/commands/blender.ts
22083
- import fs29 from "fs/promises";
22084
- import path29 from "path";
22275
+ import fs30 from "fs/promises";
22276
+ import path30 from "path";
22085
22277
  var SUBS2 = ["demo", "exec", "snap", "scene", "import", "export", "reset", "mcp", "serve", "seat", "release"];
22086
22278
  var DEFAULT_OUT_DIR = "assets/blender";
22087
22279
  async function writeB64(dir, name, b64) {
22088
- await fs29.mkdir(dir, { recursive: true });
22089
- const p = path29.join(dir, name);
22090
- await fs29.writeFile(p, Buffer.from(b64, "base64"));
22280
+ await fs30.mkdir(dir, { recursive: true });
22281
+ const p = path30.join(dir, name);
22282
+ await fs30.writeFile(p, Buffer.from(b64, "base64"));
22091
22283
  return p;
22092
22284
  }
22093
22285
  function reportScene(log, s) {
@@ -22104,7 +22296,7 @@ async function runBlender(opts) {
22104
22296
  return 1;
22105
22297
  }
22106
22298
  if (sub === "serve") {
22107
- const { serveLocalBlender } = await import("./blender-serve-PVVCH3S6.js");
22299
+ const { serveLocalBlender } = await import("./blender-serve-BF4FZ55Z.js");
22108
22300
  const port = Number(process.env.GENEX_BLENDER_PORT ?? 8088);
22109
22301
  log.step(`Starting a local Blender service on port ${port}`);
22110
22302
  log.plain(
@@ -22113,7 +22305,7 @@ async function runBlender(opts) {
22113
22305
  return serveLocalBlender({ port, log });
22114
22306
  }
22115
22307
  if (sub === "mcp") {
22116
- const { runBlenderMcp } = await import("./blender-mcp-X66ZZ4TN.js");
22308
+ const { runBlenderMcp } = await import("./blender-mcp-Q6PSFYSE.js");
22117
22309
  return runBlenderMcp();
22118
22310
  }
22119
22311
  if (sub === "seat") {
@@ -22171,7 +22363,7 @@ async function runBlender(opts) {
22171
22363
  log.plain(rest.join("\n"));
22172
22364
  return 1;
22173
22365
  }
22174
- const outDir = path29.resolve(opts.outDir ?? DEFAULT_OUT_DIR);
22366
+ const outDir = path30.resolve(opts.outDir ?? DEFAULT_OUT_DIR);
22175
22367
  const mode = opts.mode;
22176
22368
  if (mode !== void 0 && !isRenderMode(mode)) {
22177
22369
  log.error(`Unknown --mode ${mode}. Use one of: ${RENDER_MODES.join(", ")}.`);
@@ -22211,14 +22403,14 @@ async function runBlender(opts) {
22211
22403
  return 0;
22212
22404
  }
22213
22405
  case "export": {
22214
- const target = opts.out ?? path29.join(outDir, "scene.glb");
22406
+ const target = opts.out ?? path30.join(outDir, "scene.glb");
22215
22407
  const r = await blenderCall(base, "/export", { path: "/tmp/genex-export.glb" });
22216
22408
  if (!r.glbBase64) {
22217
22409
  log.error(`/export answered with no GLB bytes${r.uploaded ? " (it was uploaded, not inlined)" : ""}`);
22218
22410
  return 1;
22219
22411
  }
22220
- await fs29.mkdir(path29.dirname(target), { recursive: true });
22221
- await fs29.writeFile(target, Buffer.from(r.glbBase64, "base64"));
22412
+ await fs30.mkdir(path30.dirname(target), { recursive: true });
22413
+ await fs30.writeFile(target, Buffer.from(r.glbBase64, "base64"));
22222
22414
  log.success(`Exported ${r.bytes ?? 0} bytes`);
22223
22415
  log.plain(` ${c.cyan(target)}`);
22224
22416
  return 0;
@@ -22251,12 +22443,12 @@ async function runBlender(opts) {
22251
22443
  return 1;
22252
22444
  }
22253
22445
  try {
22254
- script = await fs29.readFile(opts.input, "utf8");
22446
+ script = await fs30.readFile(opts.input, "utf8");
22255
22447
  } catch {
22256
22448
  log.error(`Can't read ${opts.input}`);
22257
22449
  return 1;
22258
22450
  }
22259
- label = path29.basename(opts.input);
22451
+ label = path30.basename(opts.input);
22260
22452
  }
22261
22453
  const r = await blenderCall(base, "/exec", { script, ...mode ? { mode } : {}, sheet: { formats: SHEET_FORMATS } });
22262
22454
  if (r.stdout?.trim()) log.plain(r.stdout.trimEnd());
@@ -22375,9 +22567,9 @@ print(f"castle: {n} objects")
22375
22567
  `;
22376
22568
 
22377
22569
  // src/commands/asset-new.ts
22378
- import fs30 from "fs";
22570
+ import fs31 from "fs";
22379
22571
  import fsp from "fs/promises";
22380
- import path30 from "path";
22572
+ import path31 from "path";
22381
22573
  import { pathToFileURL } from "url";
22382
22574
  var EXTRA_FILES = [
22383
22575
  "genex-asset.example.json",
@@ -22471,7 +22663,7 @@ Every milestone ends with \`npm run verify\`, then \`npx genex preview --no-buil
22471
22663
  }
22472
22664
  async function runAssetNew(options) {
22473
22665
  const log = createLogger();
22474
- const cwd = options.dir ? path30.resolve(options.dir) : process.cwd();
22666
+ const cwd = options.dir ? path31.resolve(options.dir) : process.cwd();
22475
22667
  const slug = options.assetSlug;
22476
22668
  if (!slug) {
22477
22669
  log.error('Usage: genex asset new <slug> [--title "<Name>"] [--dims WxDxH] [--tri-band LOW-HIGH] [--holder "<your name>"] [--out <dir>]');
@@ -22481,14 +22673,14 @@ async function runAssetNew(options) {
22481
22673
  log.error(`The slug must be kebab-case (letters, digits, dashes), got: ${slug}`);
22482
22674
  return 1;
22483
22675
  }
22484
- const templateDir = path30.join(getTemplatesDir(), "asset-viewer");
22485
- if (!fs30.existsSync(templateDir)) {
22676
+ const templateDir = path31.join(getTemplatesDir(), "asset-viewer");
22677
+ if (!fs31.existsSync(templateDir)) {
22486
22678
  log.error(`Vendored asset-viewer template not found at ${templateDir} \u2014 reinstall the CLI.`);
22487
22679
  return 1;
22488
22680
  }
22489
- const manifestTools = await import(pathToFileURL(path30.join(templateDir, "tools", "emit-manifest.mjs")).href);
22681
+ const manifestTools = await import(pathToFileURL(path31.join(templateDir, "tools", "emit-manifest.mjs")).href);
22490
22682
  const { SHARED_FILES, PARITY_FILENAME, hashSharedFiles, pascalCase } = manifestTools;
22491
- const lockPath = path30.join(templateDir, "shared-files.sha256.json");
22683
+ const lockPath = path31.join(templateDir, "shared-files.sha256.json");
22492
22684
  const lock = JSON.parse(await fsp.readFile(lockPath, "utf8"));
22493
22685
  const actual = hashSharedFiles(templateDir);
22494
22686
  const drifted = SHARED_FILES.filter((rel) => lock.files[rel] !== actual[rel]);
@@ -22501,8 +22693,8 @@ async function runAssetNew(options) {
22501
22693
  const triBand = parseBand(options.triBand ?? "500-8000");
22502
22694
  const holder = options.holder?.trim() || HOLDER_PLACEHOLDER;
22503
22695
  const year = (/* @__PURE__ */ new Date()).getUTCFullYear();
22504
- const outDir = path30.resolve(cwd, options.out ?? slug);
22505
- if (fs30.existsSync(outDir) && fs30.readdirSync(outDir).length > 0 && !options.force) {
22696
+ const outDir = path31.resolve(cwd, options.out ?? slug);
22697
+ if (fs31.existsSync(outDir) && fs31.readdirSync(outDir).length > 0 && !options.force) {
22506
22698
  log.error(`${outDir} already holds files. Nothing was touched \u2014 pass --force to write into it anyway.`);
22507
22699
  return 1;
22508
22700
  }
@@ -22530,24 +22722,24 @@ async function runAssetNew(options) {
22530
22722
  };
22531
22723
  await fsp.mkdir(outDir, { recursive: true });
22532
22724
  for (const rel of [...SHARED_FILES, ...EXTRA_FILES]) {
22533
- const to = path30.join(outDir, rel);
22534
- await fsp.mkdir(path30.dirname(to), { recursive: true });
22535
- await fsp.copyFile(path30.join(templateDir, rel), to);
22725
+ const to = path31.join(outDir, rel);
22726
+ await fsp.mkdir(path31.dirname(to), { recursive: true });
22727
+ await fsp.copyFile(path31.join(templateDir, rel), to);
22536
22728
  }
22537
- const pkg = fillTemplate(await fsp.readFile(path30.join(templateDir, "package.json"), "utf8"), {
22729
+ const pkg = fillTemplate(await fsp.readFile(path31.join(templateDir, "package.json"), "utf8"), {
22538
22730
  slug,
22539
22731
  name,
22540
22732
  version
22541
22733
  });
22542
- await fsp.writeFile(path30.join(outDir, "package.json"), pkg, "utf8");
22543
- await fsp.writeFile(path30.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
22544
- await fsp.writeFile(path30.join(outDir, ".gitignore"), GITIGNORE, "utf8");
22734
+ await fsp.writeFile(path31.join(outDir, "package.json"), pkg, "utf8");
22735
+ await fsp.writeFile(path31.join(outDir, "asset.config.json"), JSON.stringify(config, null, 2) + "\n", "utf8");
22736
+ await fsp.writeFile(path31.join(outDir, ".gitignore"), GITIGNORE, "utf8");
22545
22737
  await fsp.writeFile(
22546
- path30.join(outDir, "DESIGN.md"),
22738
+ path31.join(outDir, "DESIGN.md"),
22547
22739
  designDoc({ name, slug, sizeMeters, triBand, holder }),
22548
22740
  "utf8"
22549
22741
  );
22550
- const placeholder = await fsp.readFile(path30.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
22742
+ const placeholder = await fsp.readFile(path31.join(templateDir, "src", "asset", "PLACEHOLDER.ts"), "utf8");
22551
22743
  const seeded = seedAssetSource(placeholder, {
22552
22744
  slug,
22553
22745
  name,
@@ -22558,8 +22750,8 @@ async function runAssetNew(options) {
22558
22750
  pascalCase
22559
22751
  });
22560
22752
  const assetFilename = `${slug.replace(/-([a-z0-9])/g, (_, ch) => ch.toUpperCase())}.ts`;
22561
- await fsp.mkdir(path30.join(outDir, "src", "asset"), { recursive: true });
22562
- await fsp.writeFile(path30.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
22753
+ await fsp.mkdir(path31.join(outDir, "src", "asset"), { recursive: true });
22754
+ await fsp.writeFile(path31.join(outDir, "src", "asset", assetFilename), seeded, "utf8");
22563
22755
  const copied = hashSharedFiles(outDir);
22564
22756
  const mismatched = SHARED_FILES.filter((rel) => copied[rel] !== lock.files[rel]);
22565
22757
  if (mismatched.length) {
@@ -22567,7 +22759,7 @@ async function runAssetNew(options) {
22567
22759
  return 1;
22568
22760
  }
22569
22761
  await fsp.writeFile(
22570
- path30.join(outDir, PARITY_FILENAME),
22762
+ path31.join(outDir, PARITY_FILENAME),
22571
22763
  JSON.stringify(
22572
22764
  {
22573
22765
  note: "Recorded by `genex asset new`. Gate G13 recomputes these; do not hand-edit the viewer.",
@@ -22611,10 +22803,10 @@ async function runAssetNew(options) {
22611
22803
  }
22612
22804
 
22613
22805
  // src/commands/tools.ts
22614
- import path32 from "path";
22806
+ import path33 from "path";
22615
22807
 
22616
22808
  // src/commands/doctor.ts
22617
- import path31 from "path";
22809
+ import path32 from "path";
22618
22810
  var LANE_ORDER = [
22619
22811
  "model",
22620
22812
  "image",
@@ -22895,7 +23087,7 @@ async function fetchLegalStatus(apiUrl, token) {
22895
23087
  }
22896
23088
  async function firstSkillsMarker() {
22897
23089
  for (const target of resolveAgentTargets()) {
22898
- const marker = await readSkillsMarker(path31.join(target.baseDir, "skills"));
23090
+ const marker = await readSkillsMarker(path32.join(target.baseDir, "skills"));
22899
23091
  if (marker) return marker;
22900
23092
  }
22901
23093
  return null;
@@ -22946,8 +23138,8 @@ async function runTools(opts) {
22946
23138
  let totalNew = 0;
22947
23139
  let totalUpdated = 0;
22948
23140
  for (const t of targets) {
22949
- const dest = path32.join(t.baseDir, "skills");
22950
- const { copied, updated } = await copyTemplates(path32.join(templatesDir, "skills"), dest, {
23141
+ const dest = path33.join(t.baseDir, "skills");
23142
+ const { copied, updated } = await copyTemplates(path33.join(templatesDir, "skills"), dest, {
22951
23143
  filter: (rel) => skillFamilyFilter("tools")(`skills/${rel}`)
22952
23144
  });
22953
23145
  await pruneRemovedSkills(dest, log);
@@ -23005,6 +23197,164 @@ async function runTools(opts) {
23005
23197
  log.dim(" Assets land in ./assets/ as files you own \u2014 wire the local path into your game.");
23006
23198
  }
23007
23199
 
23200
+ // src/commands/budget.ts
23201
+ import fs32 from "fs/promises";
23202
+ import path34 from "path";
23203
+ var BUDGET_SOURCE_LINE = "Live from your account; prices can change without a deploy.";
23204
+ var BUDGET_APPROVAL_REQUIRED = "STOP: raising the asset allowance requires the player's explicit approval. Re-run with --user-approved only after they agreed to the number.";
23205
+ function internalKind(kind) {
23206
+ return kind === "agent_session" || kind === "blender_seat" || kind === "avatar_look" || kind.startsWith("outfit") || kind.startsWith("cover_");
23207
+ }
23208
+ async function isGenexProject(cwd) {
23209
+ try {
23210
+ await fs32.access(path34.join(cwd, ".genex"));
23211
+ return true;
23212
+ } catch {
23213
+ return false;
23214
+ }
23215
+ }
23216
+ function runnableKinds(prices) {
23217
+ return Object.entries(prices).filter(([kind, credits]) => !internalKind(kind) && typeof credits === "number" && Number.isFinite(credits)).sort((a, b) => a[1] - b[1] || a[0].localeCompare(b[0]));
23218
+ }
23219
+ function describeDefault(config) {
23220
+ const share = config.share === 0.5 ? "half" : `${Math.round(config.share * 100)}%`;
23221
+ return `default: ${share} of what was spendable at the first paid command, capped at ${config.cap}`;
23222
+ }
23223
+ function isoDay(iso) {
23224
+ const d = new Date(iso);
23225
+ return Number.isNaN(d.getTime()) ? iso : d.toISOString().slice(0, 10);
23226
+ }
23227
+ async function runBudget(opts = {}) {
23228
+ const log = createLogger({ quiet: opts.quiet || opts.json });
23229
+ const cwd = opts.cwd ?? process.cwd();
23230
+ const config = budgetConfig();
23231
+ const fail3 = (message) => {
23232
+ if (opts.json) writeJson({ command: "budget", status: "failed", error: message });
23233
+ else log.error(message);
23234
+ process.exitCode = 1;
23235
+ };
23236
+ const setting = opts.assets !== void 0;
23237
+ if (setting && opts.userApproved !== true) {
23238
+ fail3(BUDGET_APPROVAL_REQUIRED);
23239
+ return;
23240
+ }
23241
+ if (setting && (!Number.isInteger(opts.assets) || opts.assets < 0)) {
23242
+ fail3(`--assets takes a whole number of credits, 0 or more (got ${String(opts.assets)}).`);
23243
+ return;
23244
+ }
23245
+ const token = opts.token ?? await readUserToken(opts.envPath);
23246
+ if (!token) {
23247
+ fail3("Not authorized. Run `genex init` first to sign in.");
23248
+ return;
23249
+ }
23250
+ const meta = await readProject(cwd);
23251
+ const apiUrl = getApiUrl(opts.apiUrl ?? meta?.apiUrl);
23252
+ const snapshot = await fetchCreditsSnapshot(apiUrl, token);
23253
+ let store;
23254
+ let persisted = true;
23255
+ if (setting) {
23256
+ const previous = await readBudgetStore(cwd);
23257
+ store = {
23258
+ v: 1,
23259
+ allowanceCredits: opts.assets,
23260
+ setBy: "user",
23261
+ setAt: (/* @__PURE__ */ new Date()).toISOString(),
23262
+ spendableAtStart: previous?.spendableAtStart ?? snapshot?.spendable ?? 0,
23263
+ balanceAtStart: previous?.balanceAtStart ?? snapshot?.balance ?? 0
23264
+ };
23265
+ persisted = await writeBudgetStore(cwd, store);
23266
+ if (!persisted) {
23267
+ fail3("This folder is not a Genex project (no .genex/), so the allowance has nowhere to live. Run this inside the game folder.");
23268
+ return;
23269
+ }
23270
+ } else if (snapshot && !snapshot.unlimited && !budgetDisabled(config)) {
23271
+ store = await ensureBudgetStore(cwd, {
23272
+ balance: snapshot.balance,
23273
+ spendable: snapshot.spendable,
23274
+ config
23275
+ });
23276
+ persisted = await readBudgetStore(cwd) !== null;
23277
+ } else {
23278
+ store = await readBudgetStore(cwd);
23279
+ persisted = await isGenexProject(cwd);
23280
+ }
23281
+ const spent = spentCredits(await readLedgerRows(cwd));
23282
+ const disabled = budgetDisabled(config);
23283
+ const enforced = !disabled && !(snapshot?.unlimited ?? false);
23284
+ const allowance = store?.allowanceCredits ?? null;
23285
+ const remaining = allowance === null ? null : Math.max(0, allowance - spent);
23286
+ if (opts.json) {
23287
+ writeJson({
23288
+ command: "budget",
23289
+ ...snapshot ? {
23290
+ balance: snapshot.balance,
23291
+ reserved: snapshot.reserved,
23292
+ spendable: snapshot.spendable,
23293
+ unlimited: snapshot.unlimited
23294
+ } : { balance: null, reserved: null, spendable: null, unlimited: null },
23295
+ allowance: {
23296
+ credits: allowance,
23297
+ setBy: store?.setBy ?? null,
23298
+ setAt: store?.setAt ?? null,
23299
+ spendableAtStart: store?.spendableAtStart ?? null,
23300
+ balanceAtStart: store?.balanceAtStart ?? null,
23301
+ enforced,
23302
+ disabled,
23303
+ persisted
23304
+ },
23305
+ spent,
23306
+ remaining,
23307
+ prices: snapshot ? Object.fromEntries(runnableKinds(snapshot.prices)) : null,
23308
+ source: BUDGET_SOURCE_LINE,
23309
+ ...snapshot ? {} : { warning: `Balance and prices couldn't be read from ${apiUrl}.` }
23310
+ });
23311
+ if (!snapshot && !setting) process.exitCode = 1;
23312
+ return;
23313
+ }
23314
+ log.plain(c.bold("genex budget"));
23315
+ if (setting && store) {
23316
+ log.success(`Asset allowance set to ${store.allowanceCredits} credits (approved by the player).`);
23317
+ }
23318
+ if (snapshot) {
23319
+ log.plain(` Balance ${snapshot.balance} credits \xB7 ${snapshot.reserved} reserved \xB7 ${snapshot.spendable} spendable`);
23320
+ } else {
23321
+ log.warn(` Balance couldn't be read from ${apiUrl} \u2014 run ${c.cyan("npx genex doctor")}.`);
23322
+ }
23323
+ if (disabled) {
23324
+ log.plain(` Allowance off (GENEX_ASSET_BUDGET_CAP=0)`);
23325
+ } else if (snapshot?.unlimited) {
23326
+ log.plain(` Allowance not enforced (unlimited operator account)`);
23327
+ } else if (store) {
23328
+ const origin = store.setBy === "user" ? `set by you on ${isoDay(store.setAt)}` : describeDefault(config);
23329
+ log.plain(` Allowance ${store.allowanceCredits} credits (${origin})`);
23330
+ } else {
23331
+ log.plain(` Allowance not derived yet \u2014 it is sized from the wallet at the first paid command`);
23332
+ }
23333
+ log.plain(` Spent ${spent} credits this build (quoted; failed generations are refunded and excluded)`);
23334
+ if (remaining !== null && enforced) {
23335
+ log.plain(` Remaining ${remaining} credits`);
23336
+ }
23337
+ if (!persisted) {
23338
+ log.dim(" (not a Genex project folder \u2014 the allowance is not saved here)");
23339
+ }
23340
+ if (snapshot) {
23341
+ const kinds = runnableKinds(snapshot.prices);
23342
+ if (kinds.length > 0) {
23343
+ log.plain("");
23344
+ log.plain(c.bold(" Prices (credits)"));
23345
+ for (const [kind, credits] of kinds) {
23346
+ log.plain(` ${kind.padEnd(20)} ${String(credits).padStart(5)}`);
23347
+ }
23348
+ }
23349
+ }
23350
+ log.plain("");
23351
+ log.dim(` ${BUDGET_SOURCE_LINE}`);
23352
+ if (enforced && !setting) {
23353
+ log.dim(` More for this game? Ask the player, then: ${c.cyan("npx genex budget --assets <credits> --user-approved")}`);
23354
+ }
23355
+ if (!snapshot && !setting) process.exitCode = 1;
23356
+ }
23357
+
23008
23358
  // src/lib/costs.ts
23009
23359
  var TYPICAL_CREDITS = {
23010
23360
  model: 35,
@@ -23059,6 +23409,11 @@ ${c.bold("Usage")}
23059
23409
  generation lanes are live. Exit 0 all clear,
23060
23410
  1 if something needs fixing \u2014 run it first when
23061
23411
  a command fails.
23412
+ genex budget [--json] This build's asset allowance: balance, allowance,
23413
+ spent so far, what's left, and live prices \u2014 plan
23414
+ the asset table against it. --assets <credits>
23415
+ --user-approved raises it, only once the player
23416
+ has agreed to the number.
23062
23417
  genex auth [options] Connect this machine to your Genex account, or
23063
23418
  finish a sign-in another command started \u2014 it
23064
23419
  resumes the same code, so an interrupted
@@ -23551,6 +23906,9 @@ ${c.bold("Characters")}
23551
23906
  ${c.bold("Workflow")}
23552
23907
  genex doctor [--json] Sign-in, credits, Node/CLI version, and which
23553
23908
  lanes are live. Run it when anything fails.
23909
+ genex budget [--json] Balance, this build's asset allowance, spent,
23910
+ remaining, live prices. --assets <credits>
23911
+ --user-approved raises it (player's say-so first).
23554
23912
  genex wait <id> Pick up a generation enqueued with --no-wait.
23555
23913
  Never bills \u2014 re-running a generate command does.
23556
23914
  genex wait --all One status line per generation from this folder.
@@ -23585,6 +23943,32 @@ Hosting, publishing, multiplayer, remixing and custom domains are the Genex
23585
23943
  platform \u2014 build a game there from one prompt: ${c.cyan("https://genex.games")}
23586
23944
  `;
23587
23945
  var COMMAND_HELP = {
23946
+ budget: `${c.bold("genex budget")} \u2014 this build's asset allowance, and the live numbers to plan against.
23947
+
23948
+ ${c.bold("Usage")}
23949
+ genex budget Balance \xB7 reserved \xB7 spendable; the allowance (default,
23950
+ or set by the player); spent this build; remaining;
23951
+ and the live price per kind, cheapest first.
23952
+ genex budget --json The same, as one object.
23953
+ genex budget --assets <credits> --user-approved
23954
+ Set the allowance to <credits> \u2014 a whole number, 0 or
23955
+ more. Refused without --user-approved: raising it needs
23956
+ the player's explicit approval of the number. 0 means
23957
+ no more paid generation in this build.
23958
+
23959
+ ${c.bold("How the allowance works")}
23960
+ A per-build spend limit, checked before every paid generation in both workspace
23961
+ modes \u2014 the spend limit the shipping ratchet (preview between batches) is not.
23962
+ The default is derived ONCE, at the first paid command: ${DEFAULT_ASSET_BUDGET_SHARE === 0.5 ? "half" : `${Math.round(DEFAULT_ASSET_BUDGET_SHARE * 100)}%`} of the
23963
+ spendable balance, capped at ${DEFAULT_ASSET_BUDGET_CAP} credits. The wallet moving later does not move
23964
+ it \u2014 only --assets does. A refused generation names the numbers and the fix: ask
23965
+ the player, then re-run this with the number they agreed to; or build the thing in
23966
+ code and say why. Failed generations are refunded and never count.
23967
+ Stored in .genex/asset-budget.json. Operator knobs: GENEX_ASSET_BUDGET_SHARE (0-1)
23968
+ and GENEX_ASSET_BUDGET_CAP (credits; 0 turns the budget off).
23969
+
23970
+ Every number printed is live from your account; prices can change without a deploy.
23971
+ `,
23588
23972
  remix: `genex remix <slug-or-Genex-page-URL> [directory] [--preview | --source-only] [--json]
23589
23973
 
23590
23974
  Prepare source and assets at one pinned snapshot. The default destination is a fresh
@@ -23726,6 +24110,8 @@ function parseArgs(argv) {
23726
24110
  "--approve-remesh",
23727
24111
  "--category",
23728
24112
  "--limit",
24113
+ // `genex budget --assets <credits>` — the allowance the player approved.
24114
+ "--assets",
23729
24115
  // `genex motion` value flags.
23730
24116
  "--takes",
23731
24117
  "--seeds",
@@ -24158,6 +24544,14 @@ function applyValueFlag(options, flag, value) {
24158
24544
  options.limit = n;
24159
24545
  break;
24160
24546
  }
24547
+ case "--assets": {
24548
+ const n = Number(value);
24549
+ if (!Number.isInteger(n) || n < 0) {
24550
+ throw new Error(`Invalid --assets value: ${value} (whole credits, 0 or more)`);
24551
+ }
24552
+ options.assets = n;
24553
+ break;
24554
+ }
24161
24555
  case "--timeout": {
24162
24556
  const n = Number(value);
24163
24557
  if (!Number.isFinite(n) || n <= 0) {
@@ -24436,6 +24830,12 @@ async function main() {
24436
24830
  case "doctor":
24437
24831
  await runDoctor(parsed.options);
24438
24832
  break;
24833
+ // The asset allowance in the open (`lib/asset-budget.ts`): the numbers
24834
+ // an agent plans the asset table against, and the one door — with the
24835
+ // player's approval — through which the allowance moves.
24836
+ case "budget":
24837
+ await runBudget(parsed.options);
24838
+ break;
24439
24839
  case "auth":
24440
24840
  await runAuth(parsed.options);
24441
24841
  break;