@genex-ai/cli-demo 1.34.9-dev.735 → 1.35.0-dev.736
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
|
@@ -418,7 +418,7 @@ async function authorizeLoopback(authBaseUrl, options) {
|
|
|
418
418
|
cleanup();
|
|
419
419
|
resolve(token);
|
|
420
420
|
};
|
|
421
|
-
const
|
|
421
|
+
const fail4 = (err) => {
|
|
422
422
|
if (settled) return;
|
|
423
423
|
settled = true;
|
|
424
424
|
cleanup();
|
|
@@ -435,7 +435,7 @@ async function authorizeLoopback(authBaseUrl, options) {
|
|
|
435
435
|
}
|
|
436
436
|
});
|
|
437
437
|
});
|
|
438
|
-
server.on("error", (err) =>
|
|
438
|
+
server.on("error", (err) => fail4(err));
|
|
439
439
|
server.listen(0, "127.0.0.1", () => {
|
|
440
440
|
const { port } = server.address();
|
|
441
441
|
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
@@ -466,7 +466,7 @@ async function authorizeLoopback(authBaseUrl, options) {
|
|
|
466
466
|
);
|
|
467
467
|
}
|
|
468
468
|
timer = setTimeout(() => {
|
|
469
|
-
|
|
469
|
+
fail4(
|
|
470
470
|
new Error(
|
|
471
471
|
"Timed out waiting for authorization. Re-run the command to try again."
|
|
472
472
|
)
|
|
@@ -569,6 +569,7 @@ function openBrowser(url, onError = () => {
|
|
|
569
569
|
}) {
|
|
570
570
|
let command;
|
|
571
571
|
let args;
|
|
572
|
+
if (process.env.GENEX_NO_BROWSER === "1") return false;
|
|
572
573
|
const custom = (process.env.GENEX_BROWSER || process.env.BROWSER)?.trim();
|
|
573
574
|
const customParts = custom ? tokenizeCommand(custom) : [];
|
|
574
575
|
if (customParts.length > 0) {
|
|
@@ -901,6 +902,7 @@ Important note: put soul into your creations, with many details and love. Aim to
|
|
|
901
902
|
21. A turn ends in exactly one of two ways: on a question the player must answer before the next step can be chosen, or on a handoff \u2014 the draft link, what changed, what to try, and what is still open. Never close a turn on a promise. "I'll keep building", "while I keep working", "adding that now" are things you say and then DO before you stop; if you are stopping, say that you have stopped and what remains. While the Build plan's \`Now:\` line has open work and no answer is needed from the player, the turn is not over: preview, hand off, and start the next milestone in the same turn, until the requested outcome is reached or the platform's own budget ends the session. If a stop was forced on you anyway, the first line of your next turn is the promise you left and where it stands.
|
|
902
903
|
22. **What the player sees must read as the thing it is**, in this game's own style: a building as that building, a person as a person, a prop as that prop, a surface as its material \u2014 and a coloured box, a capsule, or a flat grey block standing in for one of them is never its finished version. Generation is the default route to that bar wherever code will not honestly reach it: the player's character, whatever the request names, whatever the player walks up to, enters, or interacts with, and the music and sound underneath \u2014 reach for the belt on your own judgment, without stopping to ask; a status line saying what you queued is enough, and \`--no-wait\` keeps you building while it lands. Procedural code is a first-class engine for what is structural, repeated, distant, or parametric \u2014 terrain, sky, fences, paving, modular kits, filler \u2014 and for anything else only when the result meets the same bar and you have looked at it in a capture before its row says \`landed\`. Start with one reusable implementation per distinct required gameplay or visual role. Background populations default to reusable procedural bodies and motion or compatible existing rigged assets. Catalog clips need a compatible skeleton and may be paid. Reserve paid custom bodies for the player and characters examined or interacted with closely, while honoring explicit user requests. Add variants only for an unmet requirement; unspent allowance is not unfinished work. Allowance questions use timeoutPolicy no-consent: silence does not raise the allowance. Record role, reuse and completion criteria in the existing Assets table. Decide per object, not per category, and write the route on each Assets row in \`DESIGN.md\` \u2014 the paid flow for generated pieces, the procedural flow with its local path for code-built ones. An Assets table with no generation in it is a decision, not a default: record it as \`Generation: none \u2014 <why code alone reaches the bar here>\` or generate.
|
|
903
904
|
23. **The screenshots you take to look at your own work go in \`.genex/scratch/\`.** Captures, render comparisons, before/after strips, traces and metrics dumps are how you SEE what you built \u2014 they are not part of what you built, and \`genex preview\` pushes the whole folder to the game's source, every time, forever. \`.genex/scratch/\` already exists for this and is already ignored, so there is nothing to set up: write the capture as \`.genex/scratch/arena-before.png\` and read it back from there. Never put them in a folder of your own at the top level: \`progress/\`, \`reports/\`, \`shots/\` and their kind are pushed like source and become permanent weight in the game and in every remix of it \u2014 one real game reached 11 GB and 10,332 committed screenshots exactly this way. If you find such a folder already there, add it to \`.gitignore\`; never delete the player's files (law 18).
|
|
905
|
+
24. **Benchmark before declaring an in-game LLM price.** If the game calls a language model while the player plays \u2014 an NPC answering in its own words, a quest written for this save, a judge reading what the player typed \u2014 the PLAYER pays for it, and \`estimateCoins\` is the FIXED price of a started attempt: charged whether it succeeds, fails, is canceled, or stops at its own budget. Never pick that number by judgement or from a vendor's rate card. Run the real prompt on your own coins \u2014 \`npx genex llm bench "<the prompt>" --schema <file> --samples 3 --max-coins <n> --user-approved\` \u2014 and declare the figure it recommends, recorded in \`DESIGN.md\` with its model and date; re-benchmark whenever the prompt, the schema or the model changes, because a prompt edit is a price change. Check the lane first with \`npx genex llm models\`: where it is off the routes 404, so build the feature behind a graceful unavailable state and say so in the handoff. A repeated call is a standing budget the player approves once (\`requestSpendGrant()\`, then \`generate({ grantId })\`), never a popup per turn, and its disclosed call rate is computed from the game's own loop. Model output is data the game validates against its own expectation \u2014 never executed, and never authority over coin, items, entitlements or rewards. Load \`$genex-llm-in-games\` before writing any of it.
|
|
904
906
|
${CONTRACT_END}
|
|
905
907
|
`;
|
|
906
908
|
var TOOLS_CONTRACT_HEAD = `# Genex Tools (always in effect in this folder)
|
|
@@ -1001,7 +1003,7 @@ async function writeToolsContract(projectDir, opts = {}) {
|
|
|
1001
1003
|
}
|
|
1002
1004
|
async function healAncestorContracts(projectDir, stopDir = os3.homedir()) {
|
|
1003
1005
|
const findings = [];
|
|
1004
|
-
const
|
|
1006
|
+
const stop2 = path5.resolve(stopDir);
|
|
1005
1007
|
let dir = path5.dirname(path5.resolve(projectDir));
|
|
1006
1008
|
for (let depth = 0; depth < 20; depth++) {
|
|
1007
1009
|
try {
|
|
@@ -1043,7 +1045,7 @@ async function healAncestorContracts(projectDir, stopDir = os3.homedir()) {
|
|
|
1043
1045
|
}
|
|
1044
1046
|
} catch {
|
|
1045
1047
|
}
|
|
1046
|
-
if (dir ===
|
|
1048
|
+
if (dir === stop2) break;
|
|
1047
1049
|
const parent = path5.dirname(dir);
|
|
1048
1050
|
if (parent === dir) break;
|
|
1049
1051
|
dir = parent;
|
|
@@ -1485,8 +1487,8 @@ async function installedPackageVersion(cwd, name) {
|
|
|
1485
1487
|
return null;
|
|
1486
1488
|
}
|
|
1487
1489
|
}
|
|
1488
|
-
function formatNudge(label, latest, installed,
|
|
1489
|
-
return `\u2B06 Genex ${label} ${latest} available (installed ${installed}) \u2014 run: ${
|
|
1490
|
+
function formatNudge(label, latest, installed, install2) {
|
|
1491
|
+
return `\u2B06 Genex ${label} ${latest} available (installed ${installed}) \u2014 run: ${install2}`;
|
|
1490
1492
|
}
|
|
1491
1493
|
async function reportUpdateNudges(check, log, cwd = process.cwd(), cachePath = getUpdateCachePath()) {
|
|
1492
1494
|
try {
|
|
@@ -1704,8 +1706,8 @@ async function ensureLocalCli(log, cwd = process.cwd(), opts = {}) {
|
|
|
1704
1706
|
const spec = `${CLI_PACKAGE}@${CLI_CHANNEL}`;
|
|
1705
1707
|
const { command, args } = installArgs(pm, spec);
|
|
1706
1708
|
log.step(`Adding ${c.cyan(CLI_PACKAGE)} as a devDependency (${pm}) so npx genex resolves here\u2026`);
|
|
1707
|
-
const
|
|
1708
|
-
const result = await
|
|
1709
|
+
const run4 = opts.runner ?? spawnInstall;
|
|
1710
|
+
const result = await run4(command, args, cwd);
|
|
1709
1711
|
if (result.ok) {
|
|
1710
1712
|
log.success(`${c.cyan("npx genex")} now resolves in this folder.`);
|
|
1711
1713
|
} else {
|
|
@@ -1746,10 +1748,10 @@ function sourceCrc32(bytes, previous = 0) {
|
|
|
1746
1748
|
var RemixError = class extends Error {
|
|
1747
1749
|
code;
|
|
1748
1750
|
status;
|
|
1749
|
-
constructor(code, message,
|
|
1751
|
+
constructor(code, message, status2) {
|
|
1750
1752
|
super(message);
|
|
1751
1753
|
this.code = code;
|
|
1752
|
-
this.status =
|
|
1754
|
+
this.status = status2;
|
|
1753
1755
|
}
|
|
1754
1756
|
};
|
|
1755
1757
|
var badArchive = (message) => {
|
|
@@ -1926,8 +1928,8 @@ function draftPageUrl(dashboard, slug) {
|
|
|
1926
1928
|
function publicPageUrl(dashboard, slug) {
|
|
1927
1929
|
return `${dashboard.replace(/\/+$/, "")}/${slug}`;
|
|
1928
1930
|
}
|
|
1929
|
-
function gamePageUrl(dashboard, slug,
|
|
1930
|
-
return
|
|
1931
|
+
function gamePageUrl(dashboard, slug, status2) {
|
|
1932
|
+
return status2 === "published" ? publicPageUrl(dashboard, slug) : draftPageUrl(dashboard, slug);
|
|
1931
1933
|
}
|
|
1932
1934
|
|
|
1933
1935
|
// src/lib/deploy.ts
|
|
@@ -2005,10 +2007,10 @@ async function readLedger(cwd = process.cwd()) {
|
|
|
2005
2007
|
}
|
|
2006
2008
|
return [...entries.values()];
|
|
2007
2009
|
}
|
|
2008
|
-
async function recordTerminal(id,
|
|
2010
|
+
async function recordTerminal(id, status2, urls, cwd = process.cwd()) {
|
|
2009
2011
|
const existing = (await readLedger(cwd)).find((e) => e.id === id);
|
|
2010
|
-
if (existing && existing.status ===
|
|
2011
|
-
await append(cwd, { t: "done", id, status, ...urls?.length ? { urls } : {} });
|
|
2012
|
+
if (existing && existing.status === status2) return;
|
|
2013
|
+
await append(cwd, { t: "done", id, status: status2, ...urls?.length ? { urls } : {} });
|
|
2012
2014
|
}
|
|
2013
2015
|
async function recordShipped(commit, cwd = process.cwd()) {
|
|
2014
2016
|
await append(cwd, { t: "ship", at: (/* @__PURE__ */ new Date()).toISOString(), ...commit ? { commit } : {} });
|
|
@@ -2035,13 +2037,13 @@ async function countFailed(kind, cwd = process.cwd()) {
|
|
|
2035
2037
|
}
|
|
2036
2038
|
async function trailingFailures(kind, cwd = process.cwd()) {
|
|
2037
2039
|
const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
|
|
2038
|
-
let
|
|
2040
|
+
let run4 = 0;
|
|
2039
2041
|
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
2040
|
-
const
|
|
2041
|
-
if (
|
|
2042
|
-
if (
|
|
2042
|
+
const status2 = rows[i].status;
|
|
2043
|
+
if (status2 === "completed") break;
|
|
2044
|
+
if (status2 === "failed") run4 += 1;
|
|
2043
2045
|
}
|
|
2044
|
-
return
|
|
2046
|
+
return run4;
|
|
2045
2047
|
}
|
|
2046
2048
|
async function countOutcomes(kind, cwd = process.cwd()) {
|
|
2047
2049
|
const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
|
|
@@ -2578,25 +2580,25 @@ function embedBootNudges(log, d) {
|
|
|
2578
2580
|
}
|
|
2579
2581
|
}
|
|
2580
2582
|
function generationNudges(log, g) {
|
|
2581
|
-
const
|
|
2583
|
+
const describe2 = (refs) => refs.map((r) => `${r.kind} ${r.id} ("${r.prompt.slice(0, 48)}")`).join("; ");
|
|
2582
2584
|
if (g.unpicked.length) {
|
|
2583
2585
|
log.warn(
|
|
2584
|
-
`${g.unpicked.length} generation(s) enqueued 15+ minutes ago were never picked up: ${
|
|
2586
|
+
`${g.unpicked.length} generation(s) enqueued 15+ minutes ago were never picked up: ${describe2(g.unpicked)}. Run npx genex wait <id> on each (npx genex wait --all shows every status at once) \u2014 landed assets get wired in before the last push of a session, never parked for "later".`
|
|
2585
2587
|
);
|
|
2586
2588
|
}
|
|
2587
2589
|
if (g.unresolved.length) {
|
|
2588
2590
|
log.warn(
|
|
2589
|
-
`${g.unresolved.length} generation(s) are wired into the game but were never picked up, so their real status is unknown: ${
|
|
2591
|
+
`${g.unresolved.length} generation(s) are wired into the game but were never picked up, so their real status is unknown: ${describe2(g.unresolved)}. A generation that failed server-side (credits, moderation) leaves a dead URL \u2014 the asset just silently never appears. One call checks them all: npx genex wait --all.`
|
|
2590
2592
|
);
|
|
2591
2593
|
}
|
|
2592
2594
|
if (g.deadWired.length) {
|
|
2593
2595
|
log.warn(
|
|
2594
|
-
`${g.deadWired.length} FAILED generation(s) are still wired into the game: ${
|
|
2596
|
+
`${g.deadWired.length} FAILED generation(s) are still wired into the game: ${describe2(g.deadWired)}. Those URLs will never exist \u2014 each asset silently never appears. Replace the wiring or re-generate (that bills a new one), and tell the user what changed.`
|
|
2595
2597
|
);
|
|
2596
2598
|
}
|
|
2597
2599
|
if (g.unwired.length) {
|
|
2598
2600
|
log.warn(
|
|
2599
|
-
`${g.unwired.length} finished generation(s) are wired nowhere in the game: ${
|
|
2601
|
+
`${g.unwired.length} finished generation(s) are wired nowhere in the game: ${describe2(g.unwired)}. Each was paid for \u2014 wire it in (its URL embeds the id) or tell the user in one plain line why it stays unused.`
|
|
2600
2602
|
);
|
|
2601
2603
|
}
|
|
2602
2604
|
if (g.musicNoSlider) {
|
|
@@ -2760,14 +2762,14 @@ function classify(cells) {
|
|
|
2760
2762
|
const name = cells[0] ?? "";
|
|
2761
2763
|
if (/^<.*>$/.test(name)) return null;
|
|
2762
2764
|
const rest = cells.slice(1).join(" ");
|
|
2763
|
-
const
|
|
2764
|
-
if (PROCEDURAL_ROW.test(rest)) return { name, detail: rest, lane: null, status, procedural: true };
|
|
2765
|
+
const status2 = (STATUS.exec(rest) ?? [""])[0].toLowerCase();
|
|
2766
|
+
if (PROCEDURAL_ROW.test(rest)) return { name, detail: rest, lane: null, status: status2, procedural: true };
|
|
2765
2767
|
const token = new RegExp(LANE_TOKEN.source).exec(rest)?.[1];
|
|
2766
2768
|
const fromSkill = token ? LANE_OF_SKILL[token] : void 0;
|
|
2767
|
-
if (fromSkill) return { name, detail: rest, lane: fromSkill, status, procedural: false };
|
|
2769
|
+
if (fromSkill) return { name, detail: rest, lane: fromSkill, status: status2, procedural: false };
|
|
2768
2770
|
if (!PAID_ROW.test(rest) && !/https?:\/\//.test(rest) && !/generating\s*\(/i.test(rest)) return null;
|
|
2769
2771
|
const lane = LANE_WORDS.find(([re]) => re.test(rest))?.[1] ?? null;
|
|
2770
|
-
return { name, detail: rest, lane, status, procedural: false };
|
|
2772
|
+
return { name, detail: rest, lane, status: status2, procedural: false };
|
|
2771
2773
|
}
|
|
2772
2774
|
function auditGenerationPlan(input) {
|
|
2773
2775
|
const { design, ledger } = input;
|
|
@@ -2836,8 +2838,8 @@ function auditGenerationPlan(input) {
|
|
|
2836
2838
|
if (lanesRun.has(lane) || lanesPending.has(lane) || owed.has(lane) || lane === "skybox") continue;
|
|
2837
2839
|
if (!rows.some((r) => r.lane === lane)) owed.set(lane, ["declared as this subsystem's lane"]);
|
|
2838
2840
|
}
|
|
2839
|
-
for (const [lane,
|
|
2840
|
-
const shown =
|
|
2841
|
+
for (const [lane, names2] of owed) {
|
|
2842
|
+
const shown = names2.slice(0, 3).join(", ") + (names2.length > 3 ? `, +${names2.length - 3} more` : "");
|
|
2841
2843
|
warnings.push(
|
|
2842
2844
|
`DESIGN.md plans generated ${lane} work (${shown}) and this project has no ${lane} generation that landed \u2014 the plan says so and the generation ledger has nothing completed. Generate it (${LANE_COMMAND[lane] ?? `npx genex ${lane}`} "<prompt>" --no-wait), or change the row to say what happens instead and why.`
|
|
2843
2845
|
);
|
|
@@ -3487,8 +3489,8 @@ function requireOpt(value, flag, sub) {
|
|
|
3487
3489
|
async function uiExtract(opts, log) {
|
|
3488
3490
|
const input = requireOpt(opts.input, "--in", "extract");
|
|
3489
3491
|
const outDir = requireOpt(opts.outDir, "--out-dir", "extract");
|
|
3490
|
-
const
|
|
3491
|
-
if (
|
|
3492
|
+
const names2 = (opts.names ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
3493
|
+
if (names2.length === 0) fail("Missing --names (comma-separated element names, reading order).");
|
|
3492
3494
|
const minPixels = opts.minPixels ?? 2e3;
|
|
3493
3495
|
const padding = opts.padding ?? 8;
|
|
3494
3496
|
const chroma = opts.chroma ?? 240;
|
|
@@ -3497,7 +3499,7 @@ async function uiExtract(opts, log) {
|
|
|
3497
3499
|
const { width: W, height: H, data } = sheet;
|
|
3498
3500
|
await fs16.mkdir(outDir, { recursive: true });
|
|
3499
3501
|
log.plain(c.bold("genex ui extract"));
|
|
3500
|
-
log.dim(` ${input} (${W}x${H}) \u2192 ${outDir}, ${
|
|
3502
|
+
log.dim(` ${input} (${W}x${H}) \u2192 ${outDir}, ${names2.length} names`);
|
|
3501
3503
|
let hasTransparency = false;
|
|
3502
3504
|
for (let i = 0; i < W * H; i++) {
|
|
3503
3505
|
if (data[i * 4 + 3] < 250) {
|
|
@@ -3533,32 +3535,32 @@ async function uiExtract(opts, log) {
|
|
|
3533
3535
|
if (dilatePx > 0) {
|
|
3534
3536
|
const tmp = new Uint8Array(W * H);
|
|
3535
3537
|
for (let y = 0; y < H; y++) {
|
|
3536
|
-
let
|
|
3538
|
+
let run4 = 0;
|
|
3537
3539
|
for (let x = 0; x < W; x++) {
|
|
3538
|
-
if (subject[y * W + x])
|
|
3539
|
-
else if (
|
|
3540
|
-
tmp[y * W + x] =
|
|
3540
|
+
if (subject[y * W + x]) run4 = dilatePx;
|
|
3541
|
+
else if (run4 > 0) run4--;
|
|
3542
|
+
tmp[y * W + x] = run4 > 0 || subject[y * W + x] ? 1 : 0;
|
|
3541
3543
|
}
|
|
3542
|
-
|
|
3544
|
+
run4 = 0;
|
|
3543
3545
|
for (let x = W - 1; x >= 0; x--) {
|
|
3544
|
-
if (subject[y * W + x])
|
|
3545
|
-
else if (
|
|
3546
|
-
if (
|
|
3546
|
+
if (subject[y * W + x]) run4 = dilatePx;
|
|
3547
|
+
else if (run4 > 0) run4--;
|
|
3548
|
+
if (run4 > 0) tmp[y * W + x] = 1;
|
|
3547
3549
|
}
|
|
3548
3550
|
}
|
|
3549
3551
|
const tmp2 = new Uint8Array(W * H);
|
|
3550
3552
|
for (let x = 0; x < W; x++) {
|
|
3551
|
-
let
|
|
3553
|
+
let run4 = 0;
|
|
3552
3554
|
for (let y = 0; y < H; y++) {
|
|
3553
|
-
if (tmp[y * W + x])
|
|
3554
|
-
else if (
|
|
3555
|
-
tmp2[y * W + x] =
|
|
3555
|
+
if (tmp[y * W + x]) run4 = dilatePx;
|
|
3556
|
+
else if (run4 > 0) run4--;
|
|
3557
|
+
tmp2[y * W + x] = run4 > 0 || tmp[y * W + x] ? 1 : 0;
|
|
3556
3558
|
}
|
|
3557
|
-
|
|
3559
|
+
run4 = 0;
|
|
3558
3560
|
for (let y = H - 1; y >= 0; y--) {
|
|
3559
|
-
if (tmp[y * W + x])
|
|
3560
|
-
else if (
|
|
3561
|
-
if (
|
|
3561
|
+
if (tmp[y * W + x]) run4 = dilatePx;
|
|
3562
|
+
else if (run4 > 0) run4--;
|
|
3563
|
+
if (run4 > 0) tmp2[y * W + x] = 1;
|
|
3562
3564
|
}
|
|
3563
3565
|
}
|
|
3564
3566
|
connect = tmp2;
|
|
@@ -3604,9 +3606,9 @@ async function uiExtract(opts, log) {
|
|
|
3604
3606
|
}
|
|
3605
3607
|
);
|
|
3606
3608
|
}
|
|
3607
|
-
if (big.length <
|
|
3609
|
+
if (big.length < names2.length) {
|
|
3608
3610
|
log.warn(
|
|
3609
|
-
`Only ${big.length} components \u2265 ${minPixels}px; expected ${
|
|
3611
|
+
`Only ${big.length} components \u2265 ${minPixels}px; expected ${names2.length}. Lower --min-pixels or check the sheet.`
|
|
3610
3612
|
);
|
|
3611
3613
|
}
|
|
3612
3614
|
for (const comp of big) {
|
|
@@ -3648,7 +3650,7 @@ async function uiExtract(opts, log) {
|
|
|
3648
3650
|
band.items.sort((a, b) => a.cx - b.cx);
|
|
3649
3651
|
sorted.push(...band.items);
|
|
3650
3652
|
}
|
|
3651
|
-
const nameOf = (i) => i <
|
|
3653
|
+
const nameOf = (i) => i < names2.length ? names2[i] : `extra-${i - names2.length}`;
|
|
3652
3654
|
for (let i = 0; i < sorted.length; i++) {
|
|
3653
3655
|
const comp = sorted[i];
|
|
3654
3656
|
const x0 = Math.max(0, comp.minX - padding);
|
|
@@ -3678,7 +3680,7 @@ async function uiExtract(opts, log) {
|
|
|
3678
3680
|
const extractions = [];
|
|
3679
3681
|
for (let i = 0; i < sorted.length; i++) {
|
|
3680
3682
|
const comp = sorted[i];
|
|
3681
|
-
const name = i <
|
|
3683
|
+
const name = i < names2.length ? names2[i] : `extra-${i - names2.length}`;
|
|
3682
3684
|
const padX0 = Math.max(0, comp.minX - padding);
|
|
3683
3685
|
const padY0 = Math.max(0, comp.minY - padding);
|
|
3684
3686
|
const padX1 = Math.min(W, comp.maxX + padding + 1);
|
|
@@ -3775,7 +3777,7 @@ async function uiExtract(opts, log) {
|
|
|
3775
3777
|
2
|
|
3776
3778
|
)
|
|
3777
3779
|
);
|
|
3778
|
-
const missing =
|
|
3780
|
+
const missing = names2.filter((n) => !extractions.some((e) => e.name === n));
|
|
3779
3781
|
log.plain("");
|
|
3780
3782
|
log.success(`Extracted ${extractions.length} elements \u2192 ${outDir} (+ .bbox.json sidecars).`);
|
|
3781
3783
|
if (missing.length > 0) {
|
|
@@ -4105,13 +4107,13 @@ function autoDetectPairs(image, opts, log) {
|
|
|
4105
4107
|
lastCy = cell.cy;
|
|
4106
4108
|
}
|
|
4107
4109
|
annotatedCells.sort((a, b) => bandOf.get(a) - bandOf.get(b) || a.cx - b.cx);
|
|
4108
|
-
const
|
|
4109
|
-
if (
|
|
4110
|
+
const names2 = (opts.names ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
4111
|
+
if (names2.length > 0 && names2.length !== annotatedCells.length) {
|
|
4110
4112
|
fail(
|
|
4111
|
-
`--names has ${
|
|
4113
|
+
`--names has ${names2.length} name(s) but --auto detected ${annotatedCells.length} annotated cell(s). Match the counts (reading order) or drop --names for meter-1..n.`
|
|
4112
4114
|
);
|
|
4113
4115
|
}
|
|
4114
|
-
for (const name of
|
|
4116
|
+
for (const name of names2) {
|
|
4115
4117
|
if (!/^[a-z0-9][a-z0-9-]*$/i.test(name)) fail("Invalid pair name.", { name });
|
|
4116
4118
|
}
|
|
4117
4119
|
const scoreTwin = (a, c2) => {
|
|
@@ -4187,7 +4189,7 @@ function autoDetectPairs(image, opts, log) {
|
|
|
4187
4189
|
h: boxH
|
|
4188
4190
|
});
|
|
4189
4191
|
out.push({
|
|
4190
|
-
name:
|
|
4192
|
+
name: names2[i] ?? `meter-${i + 1}`,
|
|
4191
4193
|
cleanBox: place(best.cell.minX, best.cell.minY, cw, ch),
|
|
4192
4194
|
annotatedBox: place(ann.minX, ann.minY, aw, ah),
|
|
4193
4195
|
// D6: --auto used to leave the component count ungated (null). --expect N
|
|
@@ -5688,9 +5690,9 @@ import crypto3 from "crypto";
|
|
|
5688
5690
|
async function createDraftProject(opts) {
|
|
5689
5691
|
const { apiUrl, token, dashboardUrl, log } = opts;
|
|
5690
5692
|
log.step("Creating your project\u2026");
|
|
5691
|
-
const
|
|
5692
|
-
for (let i = 0; i <
|
|
5693
|
-
const name =
|
|
5693
|
+
const names2 = [opts.name, `${opts.name}-${randomSuffix()}`];
|
|
5694
|
+
for (let i = 0; i < names2.length; i++) {
|
|
5695
|
+
const name = names2[i];
|
|
5694
5696
|
let res;
|
|
5695
5697
|
try {
|
|
5696
5698
|
res = await apiFetch(`${apiUrl}/api/projects`, {
|
|
@@ -7287,9 +7289,9 @@ async function runDomain(opts) {
|
|
|
7287
7289
|
return;
|
|
7288
7290
|
}
|
|
7289
7291
|
if (sub === "verify") {
|
|
7290
|
-
const
|
|
7291
|
-
if (
|
|
7292
|
-
else if (
|
|
7292
|
+
const status2 = String(data.status ?? "");
|
|
7293
|
+
if (status2 === "active") log.success(`${hostname} is live.`);
|
|
7294
|
+
else if (status2 === "failed") log.error(`${hostname} is stopped \u2014 its verification record is missing.`);
|
|
7293
7295
|
else log.plain(`${hostname} isn't verified yet \u2014 DNS changes can take a few minutes to spread.`);
|
|
7294
7296
|
return;
|
|
7295
7297
|
}
|
|
@@ -7352,7 +7354,7 @@ async function runShop(opts) {
|
|
|
7352
7354
|
const auth = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
7353
7355
|
const call = (url, init2) => apiFetch(url, { ...init2, headers: { ...auth, ...init2?.headers ?? {} } });
|
|
7354
7356
|
const projectSkus = `${apiUrl}/api/coin/projects/${encodeURIComponent(meta.id)}/skus`;
|
|
7355
|
-
async function
|
|
7357
|
+
async function fail4(res2, what) {
|
|
7356
7358
|
if (printedStructuredError(res2)) {
|
|
7357
7359
|
process.exitCode = 1;
|
|
7358
7360
|
return;
|
|
@@ -7373,7 +7375,7 @@ async function runShop(opts) {
|
|
|
7373
7375
|
}
|
|
7374
7376
|
if (sub === "list") {
|
|
7375
7377
|
const res2 = await call(projectSkus);
|
|
7376
|
-
if (!res2.ok) return
|
|
7378
|
+
if (!res2.ok) return fail4(res2, "read your shop");
|
|
7377
7379
|
const body = await res2.json();
|
|
7378
7380
|
if (opts.json) {
|
|
7379
7381
|
log.plain(JSON.stringify(body, null, 2));
|
|
@@ -7418,7 +7420,7 @@ async function runShop(opts) {
|
|
|
7418
7420
|
...opts.resellable !== void 0 ? { resellable: opts.resellable } : {}
|
|
7419
7421
|
})
|
|
7420
7422
|
});
|
|
7421
|
-
if (!res2.ok) return
|
|
7423
|
+
if (!res2.ok) return fail4(res2, "add that item");
|
|
7422
7424
|
const sku = await res2.json();
|
|
7423
7425
|
if (opts.json) {
|
|
7424
7426
|
log.plain(JSON.stringify(sku, null, 2));
|
|
@@ -7439,7 +7441,7 @@ async function runShop(opts) {
|
|
|
7439
7441
|
const skuUrl = `${apiUrl}/api/coin/skus/${encodeURIComponent(skuId)}`;
|
|
7440
7442
|
if (sub === "test") {
|
|
7441
7443
|
const res2 = await call(`${skuUrl}/test-grant`, { method: "POST" });
|
|
7442
|
-
if (!res2.ok) return
|
|
7444
|
+
if (!res2.ok) return fail4(res2, "grant yourself a test copy");
|
|
7443
7445
|
const got = await res2.json();
|
|
7444
7446
|
log.success(`You now own ${c.cyan(got.name)} in this game.`);
|
|
7445
7447
|
log.dim(" It cost nothing and no sale was recorded \u2014 it is a test copy.");
|
|
@@ -7462,14 +7464,14 @@ async function runShop(opts) {
|
|
|
7462
7464
|
return;
|
|
7463
7465
|
}
|
|
7464
7466
|
const res2 = await call(skuUrl, { method: "PATCH", body: JSON.stringify(patch) });
|
|
7465
|
-
if (!res2.ok) return
|
|
7467
|
+
if (!res2.ok) return fail4(res2, "update that item");
|
|
7466
7468
|
const sku = await res2.json();
|
|
7467
7469
|
log.success(`Updated ${c.cyan(sku.name)} \u2014 ${sku.priceCoins} coin.`);
|
|
7468
7470
|
if (opts.resellable === false) log.dim(" Listings already on the market stay up.");
|
|
7469
7471
|
return;
|
|
7470
7472
|
}
|
|
7471
7473
|
const res = await call(skuUrl, { method: "DELETE" });
|
|
7472
|
-
if (!res.ok) return
|
|
7474
|
+
if (!res.ok) return fail4(res, "remove that item");
|
|
7473
7475
|
log.success("Removed from the shop.");
|
|
7474
7476
|
log.dim(" Players who already bought it keep it.");
|
|
7475
7477
|
}
|
|
@@ -8475,6 +8477,7 @@ function tokenize(cmd) {
|
|
|
8475
8477
|
function openUrl(url) {
|
|
8476
8478
|
let command;
|
|
8477
8479
|
let args;
|
|
8480
|
+
if (process.env.GENEX_NO_BROWSER === "1") return;
|
|
8478
8481
|
const custom = (process.env.GENEX_BROWSER || process.env.BROWSER)?.trim();
|
|
8479
8482
|
const customParts = custom ? tokenize(custom) : [];
|
|
8480
8483
|
if (customParts.length > 0) {
|
|
@@ -9297,10 +9300,10 @@ function reportLocalFiles(result, log, json) {
|
|
|
9297
9300
|
}
|
|
9298
9301
|
}
|
|
9299
9302
|
function laneFailureAdvice(kind, outcomes) {
|
|
9300
|
-
const
|
|
9301
|
-
if (
|
|
9303
|
+
const run4 = outcomes.trailingFailed ?? (outcomes.completed > 0 ? 0 : outcomes.failed);
|
|
9304
|
+
if (run4 < 2) return null;
|
|
9302
9305
|
const everSucceeded = outcomes.completed > 0;
|
|
9303
|
-
const opening = everSucceeded ? `\u270B the last ${
|
|
9306
|
+
const opening = everSucceeded ? `\u270B the last ${run4} ${kind} generations in this project all failed.` : `\u270B ${run4} ${kind} generations attempted in this project, ${run4} failed, none succeeded.`;
|
|
9304
9307
|
return ` ${c.bold(opening)} That pattern is the LANE, not your prompt \u2014 a fourth attempt bills the same and returns the same. Build this one in code, use a different lane, or tell the user in one plain line what is unavailable and carry on with the rest of the game.`;
|
|
9305
9308
|
}
|
|
9306
9309
|
function videoFailureAdvice(failures) {
|
|
@@ -9610,7 +9613,7 @@ function reportCharacterPreviewTerminal(view, log, opts = {}) {
|
|
|
9610
9613
|
const intent = requestedBuildOf(view);
|
|
9611
9614
|
const nextCommand = characterFinalizeCommand(view.id, intent);
|
|
9612
9615
|
const sourceUrl = previewSource(view);
|
|
9613
|
-
const
|
|
9616
|
+
const stop2 = `STOP \u2014 show the front, back, left, and right views to the user. The high-detail source has ${sourceFaceCount} faces and remains preserved in R2. Ask them to approve a separate 10,000-face triangle rigging copy.`;
|
|
9614
9617
|
if (opts.open) openUrl(views.front);
|
|
9615
9618
|
if (opts.json) {
|
|
9616
9619
|
writeJson({
|
|
@@ -9639,7 +9642,7 @@ function reportCharacterPreviewTerminal(view, log, opts = {}) {
|
|
|
9639
9642
|
if (sourceUrl) log.plain(` High-detail source: ${sourceUrl}`);
|
|
9640
9643
|
log.plain("");
|
|
9641
9644
|
log.plain(` ${c.bold("\u{1F441} Show these to the user")} \u2014 include all four views.`);
|
|
9642
|
-
log.plain(` ${c.bold(
|
|
9645
|
+
log.plain(` ${c.bold(stop2)}`);
|
|
9643
9646
|
log.plain(` After approval: ${c.cyan(nextCommand)}`);
|
|
9644
9647
|
log.plain("");
|
|
9645
9648
|
}
|
|
@@ -19416,8 +19419,8 @@ async function runAnims(opts) {
|
|
|
19416
19419
|
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path28.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
19417
19420
|
);
|
|
19418
19421
|
for (const [selector, entries] of resolved) {
|
|
19419
|
-
const
|
|
19420
|
-
if (
|
|
19422
|
+
const names2 = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
19423
|
+
if (names2.length > 0) log.dim(` ${selector}: ${names2.join(", ")}`);
|
|
19421
19424
|
}
|
|
19422
19425
|
log.info(
|
|
19423
19426
|
`Wiring: load the ${c.cyan("genex-threejs-character-controller")} skill \u2192 "Animation packs" (loadCharacterClips picks these up automatically).`
|
|
@@ -20829,22 +20832,22 @@ async function context2(opts) {
|
|
|
20829
20832
|
if (!token) return null;
|
|
20830
20833
|
return { token, apiUrl };
|
|
20831
20834
|
}
|
|
20832
|
-
async function readVideo(
|
|
20835
|
+
async function readVideo(path44, log) {
|
|
20833
20836
|
let bytes;
|
|
20834
20837
|
try {
|
|
20835
|
-
bytes = await readFile(
|
|
20838
|
+
bytes = await readFile(path44);
|
|
20836
20839
|
} catch {
|
|
20837
|
-
log.error(`Can't read ${
|
|
20840
|
+
log.error(`Can't read ${path44}.`);
|
|
20838
20841
|
return null;
|
|
20839
20842
|
}
|
|
20840
20843
|
if (bytes.byteLength > MAX_VIDEO_BYTES) {
|
|
20841
|
-
log.error(`${basename(
|
|
20844
|
+
log.error(`${basename(path44)} is ${(bytes.byteLength / 1e6).toFixed(0)} MB \u2014 the limit is ${MAX_VIDEO_BYTES / 1e6} MB.`);
|
|
20842
20845
|
return null;
|
|
20843
20846
|
}
|
|
20844
20847
|
return bytes;
|
|
20845
20848
|
}
|
|
20846
|
-
async function uploadVideo(apiUrl, token, characterId,
|
|
20847
|
-
const contentType = /\.mov$/i.test(
|
|
20849
|
+
async function uploadVideo(apiUrl, token, characterId, path44, bytes, log) {
|
|
20850
|
+
const contentType = /\.mov$/i.test(path44) ? "video/quicktime" : "video/mp4";
|
|
20848
20851
|
const minted = await apiFetch(
|
|
20849
20852
|
`${apiUrl}/api/characters/${encodeURIComponent(characterId)}/motions/video-url`,
|
|
20850
20853
|
{
|
|
@@ -20859,7 +20862,7 @@ async function uploadVideo(apiUrl, token, characterId, path37, bytes, log) {
|
|
|
20859
20862
|
return null;
|
|
20860
20863
|
}
|
|
20861
20864
|
const { uploadUrl, videoUrl } = await minted.json();
|
|
20862
|
-
log.dim(` uploading ${basename(
|
|
20865
|
+
log.dim(` uploading ${basename(path44)} (${(bytes.byteLength / 1e6).toFixed(1)} MB)\u2026`);
|
|
20863
20866
|
const put = await fetch(uploadUrl, {
|
|
20864
20867
|
method: "PUT",
|
|
20865
20868
|
headers: { "Content-Type": contentType, "Content-Length": String(bytes.byteLength) },
|
|
@@ -21859,11 +21862,11 @@ function mAimhold(C, w) {
|
|
|
21859
21862
|
const r = yawMed * Math.PI / 180;
|
|
21860
21863
|
const axis = [Math.sin(r), 0, Math.cos(r)];
|
|
21861
21864
|
const fwdF = fwdSrc.map(({ v, L }) => (v[0] * axis[0] + v[1] * axis[1] + v[2] * axis[2]) / (L + 1e-9));
|
|
21862
|
-
let
|
|
21865
|
+
let run4 = 0, best = 0;
|
|
21863
21866
|
for (let i = 0; i < fwdF.length; i++) {
|
|
21864
21867
|
const ok = dev[i] < 15 && dyF[i] < -0.22 && gapF[i] > 0.18 && gapF[i] < 0.46 && fwdF[i] > 0.75 && hunchF[i] < 16;
|
|
21865
|
-
|
|
21866
|
-
best = Math.max(best,
|
|
21868
|
+
run4 = ok ? run4 + 1 : 0;
|
|
21869
|
+
best = Math.max(best, run4);
|
|
21867
21870
|
}
|
|
21868
21871
|
let holdBest = 0, holdBestScore = -Infinity;
|
|
21869
21872
|
for (let i = 0; i < fwdF.length; i++) {
|
|
@@ -22217,15 +22220,15 @@ function cutOneshot(t, cfg) {
|
|
|
22217
22220
|
const cc = folded(t);
|
|
22218
22221
|
const air = cc.map((c2) => c2[0] === 0 && c2[1] === 0);
|
|
22219
22222
|
let best = null;
|
|
22220
|
-
let
|
|
22223
|
+
let run4 = null;
|
|
22221
22224
|
for (let f = 0; f < air.length; f++) {
|
|
22222
|
-
if (air[f])
|
|
22225
|
+
if (air[f]) run4 = run4 ? [run4[0], f] : [f, f];
|
|
22223
22226
|
else {
|
|
22224
|
-
if (
|
|
22225
|
-
|
|
22227
|
+
if (run4 && (!best || run4[1] - run4[0] > best[1] - best[0])) best = run4;
|
|
22228
|
+
run4 = null;
|
|
22226
22229
|
}
|
|
22227
22230
|
}
|
|
22228
|
-
if (
|
|
22231
|
+
if (run4 && (!best || run4[1] - run4[0] > best[1] - best[0])) best = run4;
|
|
22229
22232
|
let s, e;
|
|
22230
22233
|
if (best) {
|
|
22231
22234
|
s = best[0];
|
|
@@ -22469,8 +22472,8 @@ async function expandTakes(selectors) {
|
|
|
22469
22472
|
for (const sel of selectors) {
|
|
22470
22473
|
const st = await fs30.stat(sel).catch(() => null);
|
|
22471
22474
|
if (st?.isDirectory()) {
|
|
22472
|
-
const
|
|
22473
|
-
for (const n of
|
|
22475
|
+
const names2 = await fs30.readdir(sel);
|
|
22476
|
+
for (const n of names2.sort()) if (n.endsWith(".npz")) out.push(path31.join(sel, n));
|
|
22474
22477
|
} else if (st?.isFile()) {
|
|
22475
22478
|
out.push(sel);
|
|
22476
22479
|
} else {
|
|
@@ -23265,10 +23268,592 @@ async function runAssetNew(options) {
|
|
|
23265
23268
|
}
|
|
23266
23269
|
|
|
23267
23270
|
// src/commands/tools.ts
|
|
23268
|
-
import
|
|
23271
|
+
import path36 from "path";
|
|
23269
23272
|
|
|
23270
23273
|
// src/commands/doctor.ts
|
|
23274
|
+
import path35 from "path";
|
|
23275
|
+
|
|
23276
|
+
// src/commands/llm.ts
|
|
23277
|
+
import fs33 from "fs/promises";
|
|
23271
23278
|
import path34 from "path";
|
|
23279
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
23280
|
+
var SUBS3 = ["models", "bench", "price"];
|
|
23281
|
+
var LLM_BENCH_APPROVAL_REQUIRED = "STOP: `genex llm bench` runs the real model and spends the player's COIN \u2014 a wallet this build's asset allowance cannot see. Re-run with --max-coins <n> --user-approved only after they agreed to the number.";
|
|
23282
|
+
var LLM_LANE_OFF_LINE = "The runtime LLM lane is off on this stand \u2014 in-game generate() answers 404 here, and nothing can be benchmarked.";
|
|
23283
|
+
var LLM_BENCH_FILE = ".genex/llm-bench.json";
|
|
23284
|
+
var DEFAULT_BENCH_SAMPLES = 3;
|
|
23285
|
+
var DEFAULT_SAMPLE_TIMEOUT_SEC = 180;
|
|
23286
|
+
async function readRuntimeLane(apiUrl, token) {
|
|
23287
|
+
const empty = (state) => ({
|
|
23288
|
+
state,
|
|
23289
|
+
models: null,
|
|
23290
|
+
recommendedDeclaredHeadroomBps: null,
|
|
23291
|
+
externalProviders: null
|
|
23292
|
+
});
|
|
23293
|
+
try {
|
|
23294
|
+
const res = await apiFetch(`${apiUrl}/api/runtime/models`, {
|
|
23295
|
+
...token ? { headers: { Authorization: `Bearer ${token}` } } : {},
|
|
23296
|
+
signal: AbortSignal.timeout(6e3)
|
|
23297
|
+
});
|
|
23298
|
+
if (res.status === 404) return empty("off");
|
|
23299
|
+
if (res.status === 503) return empty("unavailable");
|
|
23300
|
+
if (!res.ok) return empty("unknown");
|
|
23301
|
+
const body = await res.json().catch(() => null);
|
|
23302
|
+
if (!body || !Array.isArray(body.models)) return empty("unknown");
|
|
23303
|
+
return {
|
|
23304
|
+
state: "live",
|
|
23305
|
+
models: body.models,
|
|
23306
|
+
recommendedDeclaredHeadroomBps: typeof body.recommendedDeclaredHeadroomBps === "number" && Number.isFinite(body.recommendedDeclaredHeadroomBps) ? body.recommendedDeclaredHeadroomBps : null,
|
|
23307
|
+
externalProviders: Array.isArray(body.externalProviders) ? body.externalProviders : null
|
|
23308
|
+
};
|
|
23309
|
+
} catch {
|
|
23310
|
+
return empty("unknown");
|
|
23311
|
+
}
|
|
23312
|
+
}
|
|
23313
|
+
function percentile2(values, p) {
|
|
23314
|
+
if (values.length === 0) return null;
|
|
23315
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
23316
|
+
const rank2 = Math.ceil(p / 100 * sorted.length);
|
|
23317
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, rank2 - 1))];
|
|
23318
|
+
}
|
|
23319
|
+
function recommendEstimateCoins(p95, headroomBps) {
|
|
23320
|
+
return Math.max(1, Math.ceil(p95 * (1e4 + headroomBps) / 1e4));
|
|
23321
|
+
}
|
|
23322
|
+
function recommendPerCallMaxCoins(p95, max, headroomBps) {
|
|
23323
|
+
return Math.max(recommendEstimateCoins(p95, headroomBps), recommendEstimateCoins(max, headroomBps));
|
|
23324
|
+
}
|
|
23325
|
+
function usdPerMillion(value) {
|
|
23326
|
+
return `$${value.toFixed(value < 1 ? 4 : 2)}`;
|
|
23327
|
+
}
|
|
23328
|
+
function usd(value) {
|
|
23329
|
+
return value === null ? "unknown" : `$${value.toFixed(6)}`;
|
|
23330
|
+
}
|
|
23331
|
+
async function runLlm(opts = {}) {
|
|
23332
|
+
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
23333
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
23334
|
+
const sub = opts.name?.trim() || "models";
|
|
23335
|
+
const fail4 = (command, message, extra = {}) => {
|
|
23336
|
+
if (opts.json) writeJsonLine({ command, status: "failed", error: message, ...extra });
|
|
23337
|
+
else log.error(message);
|
|
23338
|
+
process.exitCode = 1;
|
|
23339
|
+
};
|
|
23340
|
+
if (!SUBS3.includes(sub)) {
|
|
23341
|
+
fail4(`llm ${sub}`, `Unknown subcommand ${sub}. Use: ${SUBS3.join(", ")}.`);
|
|
23342
|
+
return;
|
|
23343
|
+
}
|
|
23344
|
+
if (sub === "price") {
|
|
23345
|
+
await reportSavedPrice(cwd, opts, log);
|
|
23346
|
+
return;
|
|
23347
|
+
}
|
|
23348
|
+
if (sub === "bench") {
|
|
23349
|
+
if (opts.maxCoins === void 0 || opts.userApproved !== true) {
|
|
23350
|
+
fail4("llm bench", LLM_BENCH_APPROVAL_REQUIRED);
|
|
23351
|
+
return;
|
|
23352
|
+
}
|
|
23353
|
+
if (!Number.isInteger(opts.maxCoins) || opts.maxCoins < 1) {
|
|
23354
|
+
fail4("llm bench", `--max-coins takes a whole number of coin, 1 or more (got ${String(opts.maxCoins)}).`);
|
|
23355
|
+
return;
|
|
23356
|
+
}
|
|
23357
|
+
if (!opts.benchPrompt?.trim()) {
|
|
23358
|
+
fail4("llm bench", '`genex llm bench` needs the prompt your game would send, e.g. genex llm bench "<prompt>" --max-coins <n> --user-approved.');
|
|
23359
|
+
return;
|
|
23360
|
+
}
|
|
23361
|
+
if (opts.jsonOutput && opts.textOutput) {
|
|
23362
|
+
fail4("llm bench", "Pass --json-output or --text, not both.");
|
|
23363
|
+
return;
|
|
23364
|
+
}
|
|
23365
|
+
const ws = await resolveWorkspace(cwd);
|
|
23366
|
+
if (ws.mode === "tools" && !ws.hosted) {
|
|
23367
|
+
if (opts.json) writeJsonLine({ command: "llm bench", status: "failed", error: "`genex llm bench` is not part of Genex Tools." });
|
|
23368
|
+
else reportPlatformRefused(log, "llm bench");
|
|
23369
|
+
process.exitCode = 1;
|
|
23370
|
+
return;
|
|
23371
|
+
}
|
|
23372
|
+
}
|
|
23373
|
+
const meta = await readProject(cwd);
|
|
23374
|
+
if (sub === "bench" && !meta?.id) {
|
|
23375
|
+
fail4(
|
|
23376
|
+
"llm bench",
|
|
23377
|
+
"This folder isn't linked to a game \u2014 a benchmark is billed to a project you own."
|
|
23378
|
+
);
|
|
23379
|
+
if (!opts.json) {
|
|
23380
|
+
log.dim(` Run ${c.cyan("genex link")} (or ${c.cyan("genex list")} to find the slug) first.`);
|
|
23381
|
+
}
|
|
23382
|
+
return;
|
|
23383
|
+
}
|
|
23384
|
+
const apiUrl = getApiUrl(opts.apiUrl ?? meta?.apiUrl);
|
|
23385
|
+
let token = opts.token ?? await readUserToken(opts.envPath, apiUrl);
|
|
23386
|
+
if (!token) {
|
|
23387
|
+
if (opts.noAuth) {
|
|
23388
|
+
fail4(`llm ${sub}`, "Not signed in. Re-run without --no-auth to connect.");
|
|
23389
|
+
return;
|
|
23390
|
+
}
|
|
23391
|
+
log.plain("Not signed in \u2014 connecting\u2026");
|
|
23392
|
+
try {
|
|
23393
|
+
assertAuthorizationOrigin(apiUrl, getApiUrl(opts.apiUrl));
|
|
23394
|
+
token = await authorize(apiUrl, getAuthUrl(opts.authUrl), {
|
|
23395
|
+
envPath: opts.envPath,
|
|
23396
|
+
log,
|
|
23397
|
+
inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
|
|
23398
|
+
});
|
|
23399
|
+
} catch (err) {
|
|
23400
|
+
if (err instanceof AuthPendingError) {
|
|
23401
|
+
printAuthHandoff(log, err);
|
|
23402
|
+
log.dim(`Then re-run ${c.cyan(`genex llm ${sub}`)}.`);
|
|
23403
|
+
return;
|
|
23404
|
+
}
|
|
23405
|
+
fail4(`llm ${sub}`, `Sign-in didn't complete: ${err instanceof Error ? err.message : String(err)}`);
|
|
23406
|
+
return;
|
|
23407
|
+
}
|
|
23408
|
+
await writeUserToken(token, opts.envPath, apiUrl);
|
|
23409
|
+
}
|
|
23410
|
+
if (sub === "models") {
|
|
23411
|
+
await reportModels(apiUrl, token, opts, log);
|
|
23412
|
+
return;
|
|
23413
|
+
}
|
|
23414
|
+
await runBench({ apiUrl, token, projectId: meta.id, cwd, opts, log });
|
|
23415
|
+
}
|
|
23416
|
+
async function reportModels(apiUrl, token, opts, log) {
|
|
23417
|
+
const probe = await apiFetch(`${apiUrl}/api/runtime/models`, {
|
|
23418
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
23419
|
+
}).catch(() => null);
|
|
23420
|
+
if (probe?.status === 403) {
|
|
23421
|
+
const body = await probe.json().catch(() => ({}));
|
|
23422
|
+
if (!printedStructuredError(probe)) {
|
|
23423
|
+
log.error(
|
|
23424
|
+
body.error === "credential_scope" ? "This credential can't reach the runtime lane." : "The server refused this credential for the runtime lane."
|
|
23425
|
+
);
|
|
23426
|
+
log.dim(" An API key or an MCP connector token is scoped to the asset routes and can never");
|
|
23427
|
+
log.dim(" benchmark. Sign this machine in with the CLI's own device-code flow instead:");
|
|
23428
|
+
log.dim(` ${c.cyan("genex auth")} \u2014 it prints a short link and a code; approve it on any device.`);
|
|
23429
|
+
}
|
|
23430
|
+
if (opts.json) {
|
|
23431
|
+
writeJsonLine({
|
|
23432
|
+
command: "llm models",
|
|
23433
|
+
status: "failed",
|
|
23434
|
+
error: body.error ?? "forbidden",
|
|
23435
|
+
enabled: null,
|
|
23436
|
+
models: null,
|
|
23437
|
+
externalProviders: null,
|
|
23438
|
+
recommendedDeclaredHeadroomBps: null
|
|
23439
|
+
});
|
|
23440
|
+
}
|
|
23441
|
+
process.exitCode = 1;
|
|
23442
|
+
return;
|
|
23443
|
+
}
|
|
23444
|
+
const lane = await readRuntimeLane(apiUrl, token);
|
|
23445
|
+
if (opts.json) {
|
|
23446
|
+
writeJsonLine({
|
|
23447
|
+
command: "llm models",
|
|
23448
|
+
status: lane.state,
|
|
23449
|
+
error: null,
|
|
23450
|
+
enabled: lane.state === "live",
|
|
23451
|
+
models: lane.models,
|
|
23452
|
+
externalProviders: lane.externalProviders,
|
|
23453
|
+
recommendedDeclaredHeadroomBps: lane.recommendedDeclaredHeadroomBps
|
|
23454
|
+
});
|
|
23455
|
+
if (lane.state === "unknown") process.exitCode = 1;
|
|
23456
|
+
return;
|
|
23457
|
+
}
|
|
23458
|
+
log.plain(c.bold("genex llm models"));
|
|
23459
|
+
log.dim(` ${apiUrl}`);
|
|
23460
|
+
log.plain("");
|
|
23461
|
+
if (lane.state === "off") {
|
|
23462
|
+
log.plain(` ${LLM_LANE_OFF_LINE}`);
|
|
23463
|
+
log.dim(" Build the feature without an in-game model, or ask the operator to turn the lane on.");
|
|
23464
|
+
return;
|
|
23465
|
+
}
|
|
23466
|
+
if (lane.state === "unavailable") {
|
|
23467
|
+
log.warn(" This stand has the lane on but cannot serve its model list.");
|
|
23468
|
+
log.dim(" That is a server configuration problem, not yours \u2014 nothing here can be benchmarked yet.");
|
|
23469
|
+
return;
|
|
23470
|
+
}
|
|
23471
|
+
if (lane.state === "unknown" || !lane.models) {
|
|
23472
|
+
log.error(` Couldn't read the runtime lane from ${apiUrl}.`);
|
|
23473
|
+
log.dim(` Check the network, then ${c.cyan("genex doctor")}.`);
|
|
23474
|
+
process.exitCode = 1;
|
|
23475
|
+
return;
|
|
23476
|
+
}
|
|
23477
|
+
log.plain(` ${lane.models.length} model${lane.models.length === 1 ? "" : "s"} live:`);
|
|
23478
|
+
for (const m of lane.models) {
|
|
23479
|
+
log.plain(
|
|
23480
|
+
` ${c.cyan(m.id)} ${m.label} \u2014 ${usdPerMillion(m.inputUsdPerMillion)} in / ${usdPerMillion(m.outputUsdPerMillion)} out per million tokens`
|
|
23481
|
+
);
|
|
23482
|
+
}
|
|
23483
|
+
if (lane.externalProviders && lane.externalProviders.length > 0) {
|
|
23484
|
+
log.plain("");
|
|
23485
|
+
log.plain(` Personal plans offered here: ${lane.externalProviders.map((p) => p.label).join(", ")}`);
|
|
23486
|
+
}
|
|
23487
|
+
log.plain("");
|
|
23488
|
+
if (lane.recommendedDeclaredHeadroomBps === null) {
|
|
23489
|
+
log.dim(" This stand serves no recommended headroom, so no price can be recommended from a bench.");
|
|
23490
|
+
} else {
|
|
23491
|
+
log.dim(` Recommended headroom over benchmarked coins on this stand: ${lane.recommendedDeclaredHeadroomBps} bps.`);
|
|
23492
|
+
}
|
|
23493
|
+
log.dim(" Those per-million rates are the PROVIDER's; what your game is charged is the platform");
|
|
23494
|
+
log.dim(` tariff on top, which only a real call reveals: ${c.cyan('genex llm bench "<prompt>" --max-coins <n> --user-approved')}`);
|
|
23495
|
+
}
|
|
23496
|
+
async function runBench(args) {
|
|
23497
|
+
const { apiUrl, token, projectId, cwd, opts, log } = args;
|
|
23498
|
+
const maxCoins = opts.maxCoins;
|
|
23499
|
+
const samples = opts.samples ?? DEFAULT_BENCH_SAMPLES;
|
|
23500
|
+
const prompt = opts.benchPrompt.trim();
|
|
23501
|
+
const auth = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
23502
|
+
const call = (url, init2) => apiFetch(url, { ...init2, headers: { ...auth, ...init2?.headers ?? {} } });
|
|
23503
|
+
let schema;
|
|
23504
|
+
if (opts.schemaPath) {
|
|
23505
|
+
try {
|
|
23506
|
+
schema = JSON.parse(await fs33.readFile(path34.resolve(cwd, opts.schemaPath), "utf8"));
|
|
23507
|
+
} catch (err) {
|
|
23508
|
+
benchFailed(opts, log, `Couldn't read --schema ${opts.schemaPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
23509
|
+
return;
|
|
23510
|
+
}
|
|
23511
|
+
}
|
|
23512
|
+
const outputFormat = schema !== void 0 || opts.jsonOutput ? "json" : "text";
|
|
23513
|
+
if (schema !== void 0 && opts.textOutput) {
|
|
23514
|
+
benchFailed(opts, log, "--schema asks for a structured answer, so --text cannot apply. Drop one of them.");
|
|
23515
|
+
return;
|
|
23516
|
+
}
|
|
23517
|
+
const lane = await readRuntimeLane(apiUrl, token);
|
|
23518
|
+
if (lane.state !== "live" || !lane.models || lane.models.length === 0) {
|
|
23519
|
+
if (lane.state === "off") {
|
|
23520
|
+
if (opts.json) writeJsonLine({ ...emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCoins, opts), status: "off", error: null });
|
|
23521
|
+
else {
|
|
23522
|
+
log.plain(LLM_LANE_OFF_LINE);
|
|
23523
|
+
log.dim(" Nothing was spent.");
|
|
23524
|
+
}
|
|
23525
|
+
return;
|
|
23526
|
+
}
|
|
23527
|
+
benchFailed(
|
|
23528
|
+
opts,
|
|
23529
|
+
log,
|
|
23530
|
+
lane.state === "unavailable" ? "This stand has the lane on but cannot serve its model list \u2014 nothing can be benchmarked yet." : `Couldn't read the runtime lane from ${apiUrl}.`
|
|
23531
|
+
);
|
|
23532
|
+
return;
|
|
23533
|
+
}
|
|
23534
|
+
const modelId = opts.modelId ?? lane.models[0].id;
|
|
23535
|
+
if (!lane.models.some((m) => m.id === modelId)) {
|
|
23536
|
+
benchFailed(opts, log, `This stand does not serve ${modelId}. Run \`genex llm models\` for the ids it does.`);
|
|
23537
|
+
return;
|
|
23538
|
+
}
|
|
23539
|
+
const balanceBefore = await readCoinBalance(apiUrl, token);
|
|
23540
|
+
if (!opts.json) {
|
|
23541
|
+
log.plain(c.bold("genex llm bench"));
|
|
23542
|
+
log.dim(` ${apiUrl}`);
|
|
23543
|
+
log.plain("");
|
|
23544
|
+
log.plain(` Model ${c.cyan(modelId)}`);
|
|
23545
|
+
log.plain(` Samples ${samples} real attempt${samples === 1 ? "" : "s"}, ${outputFormat} output`);
|
|
23546
|
+
log.plain(` Approved ${maxCoins} coin per attempt \u2014 at worst ${maxCoins * samples} coin for this run`);
|
|
23547
|
+
log.plain(
|
|
23548
|
+
` Balance ${balanceBefore === null ? "couldn't be read" : `${balanceBefore} coin spendable`}`
|
|
23549
|
+
);
|
|
23550
|
+
log.plain("");
|
|
23551
|
+
}
|
|
23552
|
+
const base = `${apiUrl}/api/runtime/development/projects/${encodeURIComponent(projectId)}/generations`;
|
|
23553
|
+
const results = [];
|
|
23554
|
+
let inflight = null;
|
|
23555
|
+
const onSigint = () => {
|
|
23556
|
+
const target = inflight;
|
|
23557
|
+
inflight = null;
|
|
23558
|
+
if (!opts.json) log.warn("Stopping \u2014 cancelling the attempt in flight.");
|
|
23559
|
+
void (async () => {
|
|
23560
|
+
if (target) {
|
|
23561
|
+
await call(`${base}/${encodeURIComponent(target)}/cancel`, { method: "POST" }).catch(() => {
|
|
23562
|
+
});
|
|
23563
|
+
}
|
|
23564
|
+
process.exit(130);
|
|
23565
|
+
})();
|
|
23566
|
+
};
|
|
23567
|
+
process.once("SIGINT", onSigint);
|
|
23568
|
+
try {
|
|
23569
|
+
for (let i = 0; i < samples; i++) {
|
|
23570
|
+
const res = await call(base, {
|
|
23571
|
+
method: "POST",
|
|
23572
|
+
body: JSON.stringify({
|
|
23573
|
+
maxCoins,
|
|
23574
|
+
request: {
|
|
23575
|
+
idempotencyKey: `bench-${randomUUID2()}`,
|
|
23576
|
+
modelId,
|
|
23577
|
+
prompt,
|
|
23578
|
+
outputFormat,
|
|
23579
|
+
...schema !== void 0 ? { schema } : {}
|
|
23580
|
+
}
|
|
23581
|
+
})
|
|
23582
|
+
});
|
|
23583
|
+
if (!res.ok) {
|
|
23584
|
+
const stop2 = await explainBenchRefusal(res, log, opts, i, results.length);
|
|
23585
|
+
if (stop2) break;
|
|
23586
|
+
continue;
|
|
23587
|
+
}
|
|
23588
|
+
const started = await res.json().catch(() => ({}));
|
|
23589
|
+
if (!started.id) {
|
|
23590
|
+
results.push({ id: null, status: null, billingStatus: null, chargedCoins: null, costUsd: null, error: "no_generation_id" });
|
|
23591
|
+
continue;
|
|
23592
|
+
}
|
|
23593
|
+
inflight = started.id;
|
|
23594
|
+
const settled = await pollSettled(call, `${base}/${encodeURIComponent(started.id)}`, opts.timeoutSec ?? DEFAULT_SAMPLE_TIMEOUT_SEC);
|
|
23595
|
+
inflight = null;
|
|
23596
|
+
const row = {
|
|
23597
|
+
id: started.id,
|
|
23598
|
+
status: settled?.status ?? null,
|
|
23599
|
+
billingStatus: settled?.billingStatus ?? null,
|
|
23600
|
+
chargedCoins: typeof settled?.chargedCoins === "number" ? settled.chargedCoins : null,
|
|
23601
|
+
costUsd: typeof settled?.usage?.costUsd === "number" ? settled.usage.costUsd : null,
|
|
23602
|
+
error: settled?.error ?? (settled ? null : "timed_out")
|
|
23603
|
+
};
|
|
23604
|
+
results.push(row);
|
|
23605
|
+
if (!opts.json) {
|
|
23606
|
+
log.plain(
|
|
23607
|
+
` ${row.status === "succeeded" ? c.green("\u2713") : c.yellow("!")} sample ${i + 1} ${String(row.chargedCoins ?? "\u2014").padStart(4)} coin charged \xB7 provider ${usd(row.costUsd)}${row.error ? ` \xB7 ${row.error}` : ""}`
|
|
23608
|
+
);
|
|
23609
|
+
}
|
|
23610
|
+
}
|
|
23611
|
+
} finally {
|
|
23612
|
+
process.removeListener("SIGINT", onSigint);
|
|
23613
|
+
}
|
|
23614
|
+
const charged = results.filter((r) => r.billingStatus === "final" && typeof r.chargedCoins === "number").map((r) => r.chargedCoins);
|
|
23615
|
+
const settledCount = charged.length;
|
|
23616
|
+
const p50 = percentile2(charged, 50);
|
|
23617
|
+
const p95 = percentile2(charged, 95);
|
|
23618
|
+
const max = charged.length ? Math.max(...charged) : null;
|
|
23619
|
+
const headroomBps = lane.recommendedDeclaredHeadroomBps;
|
|
23620
|
+
const recommended = p95 !== null && headroomBps !== null ? recommendEstimateCoins(p95, headroomBps) : null;
|
|
23621
|
+
const ceiling = p95 !== null && max !== null && headroomBps !== null ? recommendPerCallMaxCoins(p95, max, headroomBps) : null;
|
|
23622
|
+
const balanceAfter = await readCoinBalance(apiUrl, token);
|
|
23623
|
+
const record = {
|
|
23624
|
+
v: 1,
|
|
23625
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23626
|
+
apiUrl,
|
|
23627
|
+
projectId,
|
|
23628
|
+
modelId,
|
|
23629
|
+
prompt,
|
|
23630
|
+
outputFormat,
|
|
23631
|
+
samples,
|
|
23632
|
+
completed: settledCount,
|
|
23633
|
+
chargedCoins: charged,
|
|
23634
|
+
p50,
|
|
23635
|
+
p95,
|
|
23636
|
+
max,
|
|
23637
|
+
recommendedDeclaredHeadroomBps: headroomBps,
|
|
23638
|
+
recommendedEstimateCoins: recommended,
|
|
23639
|
+
recommendedPerCallMaxCoins: ceiling
|
|
23640
|
+
};
|
|
23641
|
+
const savedTo = await saveBench(cwd, record);
|
|
23642
|
+
if (opts.json) {
|
|
23643
|
+
writeJsonLine({
|
|
23644
|
+
command: "llm bench",
|
|
23645
|
+
status: charged.length > 0 ? "ok" : "failed",
|
|
23646
|
+
error: charged.length > 0 ? null : "no_sample_settled",
|
|
23647
|
+
apiUrl,
|
|
23648
|
+
projectId,
|
|
23649
|
+
modelId,
|
|
23650
|
+
prompt,
|
|
23651
|
+
outputFormat,
|
|
23652
|
+
schemaPath: opts.schemaPath ?? null,
|
|
23653
|
+
samples,
|
|
23654
|
+
completed: settledCount,
|
|
23655
|
+
maxCoinsPerSample: maxCoins,
|
|
23656
|
+
balanceBefore,
|
|
23657
|
+
balanceAfter,
|
|
23658
|
+
results,
|
|
23659
|
+
chargedCoins: { p50, p95, max },
|
|
23660
|
+
recommendedDeclaredHeadroomBps: headroomBps,
|
|
23661
|
+
recommendedEstimateCoins: recommended,
|
|
23662
|
+
recommendedPerCallMaxCoins: ceiling,
|
|
23663
|
+
savedTo
|
|
23664
|
+
});
|
|
23665
|
+
if (charged.length === 0) process.exitCode = 1;
|
|
23666
|
+
return;
|
|
23667
|
+
}
|
|
23668
|
+
log.plain("");
|
|
23669
|
+
if (charged.length === 0) {
|
|
23670
|
+
log.error(" No sample settled, so there is nothing to price from.");
|
|
23671
|
+
process.exitCode = 1;
|
|
23672
|
+
return;
|
|
23673
|
+
}
|
|
23674
|
+
log.plain(c.bold(" Charged coins"));
|
|
23675
|
+
log.plain(` p50 ${p50} p95 ${p95} max ${max} (${charged.length} of ${samples} settled)`);
|
|
23676
|
+
if (balanceBefore !== null && balanceAfter !== null) {
|
|
23677
|
+
log.plain(` Balance ${balanceBefore} \u2192 ${balanceAfter} coin spendable`);
|
|
23678
|
+
}
|
|
23679
|
+
log.plain("");
|
|
23680
|
+
printRecommendation(log, record);
|
|
23681
|
+
if (savedTo) log.dim(` Saved to ${savedTo} \u2014 re-read it any time with ${c.cyan("genex llm price")}.`);
|
|
23682
|
+
}
|
|
23683
|
+
function printRecommendation(log, record) {
|
|
23684
|
+
if (record.recommendedEstimateCoins === null || record.p95 === null) {
|
|
23685
|
+
log.warn(" No price recommended.");
|
|
23686
|
+
log.dim(
|
|
23687
|
+
record.recommendedDeclaredHeadroomBps === null ? " This stand served no recommended headroom, and the multiplier is the server's to set \u2014" : " No sample settled, so there is no p95 to build a price on \u2014"
|
|
23688
|
+
);
|
|
23689
|
+
log.dim(" declaring a number from this run would be a guess dressed as a measurement.");
|
|
23690
|
+
return;
|
|
23691
|
+
}
|
|
23692
|
+
log.plain(c.bold(` Declare estimateCoins: ${record.recommendedEstimateCoins}`));
|
|
23693
|
+
log.dim(
|
|
23694
|
+
` = p95 of charged coins (${record.p95}) plus this stand's recommended headroom (${record.recommendedDeclaredHeadroomBps} bps),`
|
|
23695
|
+
);
|
|
23696
|
+
log.dim(` measured over ${record.completed} settled attempt${record.completed === 1 ? "" : "s"} on ${record.modelId}.`);
|
|
23697
|
+
log.dim(" That number is a PRICE: a started attempt is charged in full, including one that fails.");
|
|
23698
|
+
if (record.recommendedPerCallMaxCoins != null) {
|
|
23699
|
+
log.plain(c.bold(` Grant perCallMaxCoins: ${record.recommendedPerCallMaxCoins}`));
|
|
23700
|
+
log.dim(` = the same headroom over the worst sample (${record.max}), and never below the price above it.`);
|
|
23701
|
+
log.dim(" The ceiling is the price's room to be wrong; declaring one under the price is refused.");
|
|
23702
|
+
}
|
|
23703
|
+
}
|
|
23704
|
+
async function explainBenchRefusal(res, log, opts, index, settledSoFar) {
|
|
23705
|
+
if (printedStructuredError(res)) return true;
|
|
23706
|
+
const body = await res.json().catch(() => ({}));
|
|
23707
|
+
if (opts.json) return res.status !== 429;
|
|
23708
|
+
if (res.status === 404 && body.error === "not_found") {
|
|
23709
|
+
log.error(` ${LLM_LANE_OFF_LINE}`);
|
|
23710
|
+
return true;
|
|
23711
|
+
}
|
|
23712
|
+
if (res.status === 403 && body.error === "credential_scope") {
|
|
23713
|
+
log.error(" This credential can't reach the runtime lane \u2014 an API key or MCP token is scoped out.");
|
|
23714
|
+
log.dim(` Sign in with ${c.cyan("genex auth")} and re-run.`);
|
|
23715
|
+
return true;
|
|
23716
|
+
}
|
|
23717
|
+
if (res.status === 403 && body.error === "project_owner_required") {
|
|
23718
|
+
log.error(" A benchmark is billed to a project YOU own, and this one isn't yours.");
|
|
23719
|
+
return true;
|
|
23720
|
+
}
|
|
23721
|
+
if (res.status === 402) {
|
|
23722
|
+
log.error(" Not enough coin to start the attempt.");
|
|
23723
|
+
return true;
|
|
23724
|
+
}
|
|
23725
|
+
log.error(
|
|
23726
|
+
` Sample ${index + 1} was refused: ${body.message ?? body.error ?? `HTTP ${res.status}`}${settledSoFar > 0 ? " (earlier samples still count)" : ""}`
|
|
23727
|
+
);
|
|
23728
|
+
return res.status >= 500 || res.status === 401 || res.status === 403;
|
|
23729
|
+
}
|
|
23730
|
+
async function pollSettled(call, url, timeoutSec) {
|
|
23731
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
23732
|
+
let last = null;
|
|
23733
|
+
while (Date.now() < deadline) {
|
|
23734
|
+
const res = await call(url).catch(() => null);
|
|
23735
|
+
if (res?.ok) {
|
|
23736
|
+
last = await res.json().catch(() => null);
|
|
23737
|
+
if (last?.billingStatus === "final") return last;
|
|
23738
|
+
}
|
|
23739
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
23740
|
+
}
|
|
23741
|
+
return last;
|
|
23742
|
+
}
|
|
23743
|
+
async function readCoinBalance(apiUrl, token) {
|
|
23744
|
+
try {
|
|
23745
|
+
const res = await apiFetch(`${apiUrl}/api/coin/balance`, {
|
|
23746
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
23747
|
+
signal: AbortSignal.timeout(6e3)
|
|
23748
|
+
});
|
|
23749
|
+
if (!res.ok) return null;
|
|
23750
|
+
const body = await res.json().catch(() => null);
|
|
23751
|
+
return typeof body?.spendable === "number" ? body.spendable : null;
|
|
23752
|
+
} catch {
|
|
23753
|
+
return null;
|
|
23754
|
+
}
|
|
23755
|
+
}
|
|
23756
|
+
async function saveBench(cwd, record) {
|
|
23757
|
+
try {
|
|
23758
|
+
const file = path34.join(cwd, LLM_BENCH_FILE);
|
|
23759
|
+
await fs33.mkdir(path34.dirname(file), { recursive: true });
|
|
23760
|
+
await fs33.writeFile(file, JSON.stringify(record, null, 2) + "\n");
|
|
23761
|
+
return LLM_BENCH_FILE;
|
|
23762
|
+
} catch {
|
|
23763
|
+
return null;
|
|
23764
|
+
}
|
|
23765
|
+
}
|
|
23766
|
+
function benchFailed(opts, log, message) {
|
|
23767
|
+
if (opts.json) writeJsonLine({ command: "llm bench", status: "failed", error: message });
|
|
23768
|
+
else log.error(message);
|
|
23769
|
+
process.exitCode = 1;
|
|
23770
|
+
}
|
|
23771
|
+
function emptyBenchJson(apiUrl, projectId, prompt, outputFormat, samples, maxCoins, opts) {
|
|
23772
|
+
return {
|
|
23773
|
+
command: "llm bench",
|
|
23774
|
+
apiUrl,
|
|
23775
|
+
projectId,
|
|
23776
|
+
modelId: opts.modelId ?? null,
|
|
23777
|
+
prompt,
|
|
23778
|
+
outputFormat,
|
|
23779
|
+
schemaPath: opts.schemaPath ?? null,
|
|
23780
|
+
samples,
|
|
23781
|
+
completed: 0,
|
|
23782
|
+
maxCoinsPerSample: maxCoins,
|
|
23783
|
+
balanceBefore: null,
|
|
23784
|
+
balanceAfter: null,
|
|
23785
|
+
results: [],
|
|
23786
|
+
chargedCoins: { p50: null, p95: null, max: null },
|
|
23787
|
+
recommendedDeclaredHeadroomBps: null,
|
|
23788
|
+
recommendedEstimateCoins: null,
|
|
23789
|
+
recommendedPerCallMaxCoins: null,
|
|
23790
|
+
savedTo: null
|
|
23791
|
+
};
|
|
23792
|
+
}
|
|
23793
|
+
async function reportSavedPrice(cwd, opts, log) {
|
|
23794
|
+
let record = null;
|
|
23795
|
+
try {
|
|
23796
|
+
record = JSON.parse(await fs33.readFile(path34.join(cwd, LLM_BENCH_FILE), "utf8"));
|
|
23797
|
+
} catch {
|
|
23798
|
+
record = null;
|
|
23799
|
+
}
|
|
23800
|
+
if (!record) {
|
|
23801
|
+
if (opts.json) {
|
|
23802
|
+
writeJsonLine({
|
|
23803
|
+
command: "llm price",
|
|
23804
|
+
status: "failed",
|
|
23805
|
+
error: "no_bench",
|
|
23806
|
+
ranAt: null,
|
|
23807
|
+
modelId: null,
|
|
23808
|
+
samples: null,
|
|
23809
|
+
completed: null,
|
|
23810
|
+
p50: null,
|
|
23811
|
+
p95: null,
|
|
23812
|
+
max: null,
|
|
23813
|
+
recommendedDeclaredHeadroomBps: null,
|
|
23814
|
+
recommendedEstimateCoins: null,
|
|
23815
|
+
recommendedPerCallMaxCoins: null,
|
|
23816
|
+
savedTo: null
|
|
23817
|
+
});
|
|
23818
|
+
} else {
|
|
23819
|
+
log.error("Nothing has been benchmarked in this folder yet.");
|
|
23820
|
+
log.dim(` Run ${c.cyan('genex llm bench "<prompt>" --max-coins <n> --user-approved')} first.`);
|
|
23821
|
+
}
|
|
23822
|
+
process.exitCode = 1;
|
|
23823
|
+
return;
|
|
23824
|
+
}
|
|
23825
|
+
if (opts.json) {
|
|
23826
|
+
writeJsonLine({
|
|
23827
|
+
command: "llm price",
|
|
23828
|
+
status: record.recommendedEstimateCoins === null ? "failed" : "ok",
|
|
23829
|
+
error: record.recommendedEstimateCoins === null ? "no_recommendation" : null,
|
|
23830
|
+
ranAt: record.ranAt ?? null,
|
|
23831
|
+
modelId: record.modelId ?? null,
|
|
23832
|
+
samples: record.samples ?? null,
|
|
23833
|
+
completed: record.completed ?? null,
|
|
23834
|
+
p50: record.p50 ?? null,
|
|
23835
|
+
p95: record.p95 ?? null,
|
|
23836
|
+
max: record.max ?? null,
|
|
23837
|
+
recommendedDeclaredHeadroomBps: record.recommendedDeclaredHeadroomBps ?? null,
|
|
23838
|
+
recommendedEstimateCoins: record.recommendedEstimateCoins ?? null,
|
|
23839
|
+
recommendedPerCallMaxCoins: record.recommendedPerCallMaxCoins ?? null,
|
|
23840
|
+
savedTo: LLM_BENCH_FILE
|
|
23841
|
+
});
|
|
23842
|
+
if (record.recommendedEstimateCoins === null) process.exitCode = 1;
|
|
23843
|
+
return;
|
|
23844
|
+
}
|
|
23845
|
+
log.plain(c.bold("genex llm price"));
|
|
23846
|
+
log.dim(` from ${LLM_BENCH_FILE}, benchmarked ${record.ranAt}`);
|
|
23847
|
+
log.plain("");
|
|
23848
|
+
printRecommendation(log, record);
|
|
23849
|
+
if (record.recommendedEstimateCoins === null) process.exitCode = 1;
|
|
23850
|
+
}
|
|
23851
|
+
function writeJsonLine(value) {
|
|
23852
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
23853
|
+
`);
|
|
23854
|
+
}
|
|
23855
|
+
|
|
23856
|
+
// src/commands/doctor.ts
|
|
23272
23857
|
var LANE_ORDER = [
|
|
23273
23858
|
"model",
|
|
23274
23859
|
"image",
|
|
@@ -23339,15 +23924,18 @@ async function runDoctor(opts = {}) {
|
|
|
23339
23924
|
let credits = null;
|
|
23340
23925
|
let lanes = null;
|
|
23341
23926
|
let legal = null;
|
|
23927
|
+
let runtime = null;
|
|
23342
23928
|
if (authState === "ok" && token) {
|
|
23343
|
-
[credits, lanes, legal] = await Promise.all([
|
|
23929
|
+
[credits, lanes, legal, runtime] = await Promise.all([
|
|
23344
23930
|
fetchCredits(apiUrl, token),
|
|
23345
23931
|
fetchLanes(apiUrl, token),
|
|
23346
|
-
fetchLegalStatus(apiUrl, token)
|
|
23932
|
+
fetchLegalStatus(apiUrl, token),
|
|
23933
|
+
readRuntimeLane(apiUrl, token)
|
|
23347
23934
|
]);
|
|
23348
23935
|
}
|
|
23349
23936
|
rows.push(termsRow(authState, legal));
|
|
23350
23937
|
rows.push(creditsRow(authState, credits));
|
|
23938
|
+
rows.push(runtimeRow(authState, runtime));
|
|
23351
23939
|
if (opts.json) {
|
|
23352
23940
|
process.stdout.write(
|
|
23353
23941
|
JSON.stringify(
|
|
@@ -23366,6 +23954,10 @@ async function runDoctor(opts = {}) {
|
|
|
23366
23954
|
terms: legal,
|
|
23367
23955
|
credits,
|
|
23368
23956
|
lanes: lanes ? { paused: lanes.paused, lanes: lanes.lanes } : null,
|
|
23957
|
+
// Same always-present rule as `assetsMode`: a consumer must be able
|
|
23958
|
+
// to tell a CLI that KNOWS the runtime lane (key present, object or
|
|
23959
|
+
// null) from one that predates it (no key at all).
|
|
23960
|
+
runtime,
|
|
23369
23961
|
ok: !rows.some((r) => r.bad)
|
|
23370
23962
|
},
|
|
23371
23963
|
null,
|
|
@@ -23504,6 +24096,29 @@ function creditsRow(state, info) {
|
|
|
23504
24096
|
fix: info.balance <= 0 ? "Out of credits \u2014 generation is refused until the refill or a top-up." : void 0
|
|
23505
24097
|
};
|
|
23506
24098
|
}
|
|
24099
|
+
function runtimeRow(state, lane) {
|
|
24100
|
+
if (state !== "ok") return { label: "Runtime", value: "\u2014 (sign in first)" };
|
|
24101
|
+
switch (lane?.state) {
|
|
24102
|
+
case "live": {
|
|
24103
|
+
const n = lane.models?.length ?? 0;
|
|
24104
|
+
return {
|
|
24105
|
+
label: "Runtime",
|
|
24106
|
+
value: `in-game LLM lane live \xB7 ${n} model${n === 1 ? "" : "s"}`,
|
|
24107
|
+
fix: 'Price a call before a game declares one: `npx genex llm bench "<prompt>" --max-coins <n> --user-approved`.'
|
|
24108
|
+
};
|
|
24109
|
+
}
|
|
24110
|
+
case "off":
|
|
24111
|
+
return {
|
|
24112
|
+
label: "Runtime",
|
|
24113
|
+
value: "in-game LLM lane off on this stand",
|
|
24114
|
+
fix: "generate() in a game answers 404 here \u2014 build the feature without a model, or ask the operator."
|
|
24115
|
+
};
|
|
24116
|
+
case "unavailable":
|
|
24117
|
+
return { label: "Runtime", value: "in-game LLM lane on, but its model list can't be served" };
|
|
24118
|
+
default:
|
|
24119
|
+
return { label: "Runtime", value: "couldn't be read from this server" };
|
|
24120
|
+
}
|
|
24121
|
+
}
|
|
23507
24122
|
function shortDate(iso) {
|
|
23508
24123
|
const d = new Date(iso);
|
|
23509
24124
|
return Number.isNaN(d.getTime()) ? iso : d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
@@ -23549,7 +24164,7 @@ async function fetchLegalStatus(apiUrl, token) {
|
|
|
23549
24164
|
}
|
|
23550
24165
|
async function firstSkillsMarker() {
|
|
23551
24166
|
for (const target of resolveAgentTargets()) {
|
|
23552
|
-
const marker = await readSkillsMarker(
|
|
24167
|
+
const marker = await readSkillsMarker(path35.join(target.baseDir, "skills"));
|
|
23553
24168
|
if (marker) return marker;
|
|
23554
24169
|
}
|
|
23555
24170
|
return null;
|
|
@@ -23600,8 +24215,8 @@ async function runTools(opts) {
|
|
|
23600
24215
|
let totalNew = 0;
|
|
23601
24216
|
let totalUpdated = 0;
|
|
23602
24217
|
for (const t of targets) {
|
|
23603
|
-
const dest =
|
|
23604
|
-
const { copied, updated } = await copyTemplates(
|
|
24218
|
+
const dest = path36.join(t.baseDir, "skills");
|
|
24219
|
+
const { copied, updated } = await copyTemplates(path36.join(templatesDir, "skills"), dest, {
|
|
23605
24220
|
filter: (rel) => skillFamilyFilter("tools")(`skills/${rel}`)
|
|
23606
24221
|
});
|
|
23607
24222
|
await pruneRemovedSkills(dest, log);
|
|
@@ -23611,211 +24226,1755 @@ async function runTools(opts) {
|
|
|
23611
24226
|
totalUpdated += updated.length;
|
|
23612
24227
|
log.dim(` ${t.label}: ${added} added, ${updated.length} refreshed \u2192 ${dest}`);
|
|
23613
24228
|
}
|
|
23614
|
-
log.success(
|
|
23615
|
-
`Tool skills ready (${totalNew} added, ${totalUpdated} refreshed across ${targets.length} agent${targets.length === 1 ? "" : "s"}).`
|
|
23616
|
-
);
|
|
23617
|
-
const apiUrl = getApiUrl(opts.apiUrl);
|
|
23618
|
-
const { path: markerPath } = await writeWorkspace({
|
|
23619
|
-
mode: "tools",
|
|
24229
|
+
log.success(
|
|
24230
|
+
`Tool skills ready (${totalNew} added, ${totalUpdated} refreshed across ${targets.length} agent${targets.length === 1 ? "" : "s"}).`
|
|
24231
|
+
);
|
|
24232
|
+
const apiUrl = getApiUrl(opts.apiUrl);
|
|
24233
|
+
const { path: markerPath } = await writeWorkspace({
|
|
24234
|
+
mode: "tools",
|
|
24235
|
+
apiUrl,
|
|
24236
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
24237
|
+
});
|
|
24238
|
+
log.dim(` saved ${c.cyan(markerPath)}`);
|
|
24239
|
+
await writeGitignore(process.cwd(), log);
|
|
24240
|
+
if (await writeToolsContract(process.cwd())) {
|
|
24241
|
+
log.dim(" wrote the Genex Tools rules into AGENTS.md (managed block) + CLAUDE.md stub");
|
|
24242
|
+
}
|
|
24243
|
+
await healAncestorContractsAndReport(log, process.cwd());
|
|
24244
|
+
await ensureLocalCli(log, process.cwd(), { skip: opts.noInstall });
|
|
24245
|
+
log.plain("");
|
|
24246
|
+
const authBaseUrl = getAuthUrl(opts.authUrl);
|
|
24247
|
+
let token = await readUserToken(opts.envPath, apiUrl);
|
|
24248
|
+
if (!token) {
|
|
24249
|
+
try {
|
|
24250
|
+
token = await authorize(apiUrl, authBaseUrl, {
|
|
24251
|
+
envPath: opts.envPath,
|
|
24252
|
+
log,
|
|
24253
|
+
inlineWaitMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
|
|
24254
|
+
});
|
|
24255
|
+
} catch (err) {
|
|
24256
|
+
if (err instanceof AuthPendingError) {
|
|
24257
|
+
printAuthHandoff(log, err);
|
|
24258
|
+
log.dim(
|
|
24259
|
+
"Everything else is set up \u2014 `genex auth` finishes the sign-in, then you can generate."
|
|
24260
|
+
);
|
|
24261
|
+
return;
|
|
24262
|
+
}
|
|
24263
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
24264
|
+
log.dim("The workspace is set up. Re-run `genex tools` to finish authorizing.");
|
|
24265
|
+
throw markPrinted(err);
|
|
24266
|
+
}
|
|
24267
|
+
const { path: tokenPath } = await writeUserToken(token, opts.envPath, apiUrl);
|
|
24268
|
+
log.success(`Authorized. Saved your token to ${c.cyan(tokenPath)}.`);
|
|
24269
|
+
log.plain("");
|
|
24270
|
+
}
|
|
24271
|
+
await reportToolsInit(apiUrl, token);
|
|
24272
|
+
await runDoctor({ apiUrl: opts.apiUrl, envPath: opts.envPath, quiet: opts.quiet });
|
|
24273
|
+
log.plain("");
|
|
24274
|
+
log.success("Genex Tools is ready. \u{1F9F0}");
|
|
24275
|
+
log.dim(" Assets land in ./assets/ as files you own \u2014 wire the local path into your game.");
|
|
24276
|
+
}
|
|
24277
|
+
|
|
24278
|
+
// src/commands/budget.ts
|
|
24279
|
+
import fs34 from "fs/promises";
|
|
24280
|
+
import path37 from "path";
|
|
24281
|
+
var BUDGET_SOURCE_LINE = "Live from your account; prices can change without a deploy.";
|
|
24282
|
+
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.";
|
|
24283
|
+
function internalKind(kind) {
|
|
24284
|
+
return kind === "agent_session" || kind === "blender_seat" || kind.startsWith("cover_");
|
|
24285
|
+
}
|
|
24286
|
+
async function isGenexProject(cwd) {
|
|
24287
|
+
try {
|
|
24288
|
+
await fs34.access(path37.join(cwd, ".genex"));
|
|
24289
|
+
return true;
|
|
24290
|
+
} catch {
|
|
24291
|
+
return false;
|
|
24292
|
+
}
|
|
24293
|
+
}
|
|
24294
|
+
function runnableKinds(prices) {
|
|
24295
|
+
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]));
|
|
24296
|
+
}
|
|
24297
|
+
function describeDefault(config) {
|
|
24298
|
+
const share = config.share === 0.5 ? "half" : `${Math.round(config.share * 100)}%`;
|
|
24299
|
+
return `default: ${share} of what was spendable at the first paid command, capped at ${config.cap}`;
|
|
24300
|
+
}
|
|
24301
|
+
function isoDay(iso) {
|
|
24302
|
+
const d = new Date(iso);
|
|
24303
|
+
return Number.isNaN(d.getTime()) ? iso : d.toISOString().slice(0, 10);
|
|
24304
|
+
}
|
|
24305
|
+
async function runBudget(opts = {}) {
|
|
24306
|
+
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
24307
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
24308
|
+
const config = budgetConfig();
|
|
24309
|
+
const fail4 = (message) => {
|
|
24310
|
+
if (opts.json) writeJson({ command: "budget", status: "failed", error: message });
|
|
24311
|
+
else log.error(message);
|
|
24312
|
+
process.exitCode = 1;
|
|
24313
|
+
};
|
|
24314
|
+
const setting = opts.assets !== void 0;
|
|
24315
|
+
if (setting && opts.userApproved !== true) {
|
|
24316
|
+
fail4(BUDGET_APPROVAL_REQUIRED);
|
|
24317
|
+
return;
|
|
24318
|
+
}
|
|
24319
|
+
if (setting && (!Number.isInteger(opts.assets) || opts.assets < 0)) {
|
|
24320
|
+
fail4(`--assets takes a whole number of credits, 0 or more (got ${String(opts.assets)}).`);
|
|
24321
|
+
return;
|
|
24322
|
+
}
|
|
24323
|
+
const meta = await readProject(cwd);
|
|
24324
|
+
const apiUrl = getApiUrl(opts.apiUrl ?? meta?.apiUrl);
|
|
24325
|
+
const token = opts.token ?? await readUserToken(opts.envPath, apiUrl);
|
|
24326
|
+
if (!token) {
|
|
24327
|
+
fail4("Not authorized. Run `genex init` first to sign in.");
|
|
24328
|
+
return;
|
|
24329
|
+
}
|
|
24330
|
+
const snapshot = await fetchCreditsSnapshot(apiUrl, token);
|
|
24331
|
+
let store;
|
|
24332
|
+
let persisted = true;
|
|
24333
|
+
if (setting) {
|
|
24334
|
+
const previous = await readBudgetStore(cwd);
|
|
24335
|
+
store = {
|
|
24336
|
+
v: 1,
|
|
24337
|
+
allowanceCredits: opts.assets,
|
|
24338
|
+
setBy: "user",
|
|
24339
|
+
setAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
24340
|
+
spendableAtStart: previous?.spendableAtStart ?? snapshot?.spendable ?? 0,
|
|
24341
|
+
balanceAtStart: previous?.balanceAtStart ?? snapshot?.balance ?? 0
|
|
24342
|
+
};
|
|
24343
|
+
persisted = await writeBudgetStore(cwd, store);
|
|
24344
|
+
if (!persisted) {
|
|
24345
|
+
fail4("This folder is not a Genex project (no .genex/), so the allowance has nowhere to live. Run this inside the game folder.");
|
|
24346
|
+
return;
|
|
24347
|
+
}
|
|
24348
|
+
} else if (snapshot && !snapshot.unlimited && !budgetDisabled(config)) {
|
|
24349
|
+
store = await ensureBudgetStore(cwd, {
|
|
24350
|
+
balance: snapshot.balance,
|
|
24351
|
+
spendable: snapshot.spendable,
|
|
24352
|
+
config
|
|
24353
|
+
});
|
|
24354
|
+
persisted = await readBudgetStore(cwd) !== null;
|
|
24355
|
+
} else {
|
|
24356
|
+
store = await readBudgetStore(cwd);
|
|
24357
|
+
persisted = await isGenexProject(cwd);
|
|
24358
|
+
}
|
|
24359
|
+
const spent = spentCredits(await readLedgerRows(cwd));
|
|
24360
|
+
const disabled = budgetDisabled(config);
|
|
24361
|
+
const enforced = !disabled && !(snapshot?.unlimited ?? false);
|
|
24362
|
+
const allowance = store?.allowanceCredits ?? null;
|
|
24363
|
+
const remaining = allowance === null ? null : Math.max(0, allowance - spent);
|
|
24364
|
+
if (opts.json) {
|
|
24365
|
+
writeJson({
|
|
24366
|
+
command: "budget",
|
|
24367
|
+
...snapshot ? {
|
|
24368
|
+
balance: snapshot.balance,
|
|
24369
|
+
reserved: snapshot.reserved,
|
|
24370
|
+
spendable: snapshot.spendable,
|
|
24371
|
+
unlimited: snapshot.unlimited
|
|
24372
|
+
} : { balance: null, reserved: null, spendable: null, unlimited: null },
|
|
24373
|
+
allowance: {
|
|
24374
|
+
credits: allowance,
|
|
24375
|
+
setBy: store?.setBy ?? null,
|
|
24376
|
+
setAt: store?.setAt ?? null,
|
|
24377
|
+
spendableAtStart: store?.spendableAtStart ?? null,
|
|
24378
|
+
balanceAtStart: store?.balanceAtStart ?? null,
|
|
24379
|
+
enforced,
|
|
24380
|
+
disabled,
|
|
24381
|
+
persisted
|
|
24382
|
+
},
|
|
24383
|
+
spent,
|
|
24384
|
+
remaining,
|
|
24385
|
+
prices: snapshot ? Object.fromEntries(runnableKinds(snapshot.prices)) : null,
|
|
24386
|
+
source: BUDGET_SOURCE_LINE,
|
|
24387
|
+
...snapshot ? {} : { warning: `Balance and prices couldn't be read from ${apiUrl}.` }
|
|
24388
|
+
});
|
|
24389
|
+
if (!snapshot && !setting) process.exitCode = 1;
|
|
24390
|
+
return;
|
|
24391
|
+
}
|
|
24392
|
+
log.plain(c.bold("genex budget"));
|
|
24393
|
+
if (setting && store) {
|
|
24394
|
+
log.success(`Asset allowance set to ${store.allowanceCredits} credits (approved by the player).`);
|
|
24395
|
+
}
|
|
24396
|
+
if (snapshot) {
|
|
24397
|
+
log.plain(` Balance ${snapshot.balance} credits \xB7 ${snapshot.reserved} reserved \xB7 ${snapshot.spendable} spendable`);
|
|
24398
|
+
} else {
|
|
24399
|
+
log.warn(` Balance couldn't be read from ${apiUrl} \u2014 run ${c.cyan("npx genex doctor")}.`);
|
|
24400
|
+
}
|
|
24401
|
+
if (disabled) {
|
|
24402
|
+
log.plain(` Allowance off (GENEX_ASSET_BUDGET_CAP=0)`);
|
|
24403
|
+
} else if (snapshot?.unlimited) {
|
|
24404
|
+
log.plain(` Allowance not enforced (unlimited operator account)`);
|
|
24405
|
+
} else if (store) {
|
|
24406
|
+
const origin = store.setBy === "user" ? `set by you on ${isoDay(store.setAt)}` : describeDefault(config);
|
|
24407
|
+
log.plain(` Allowance ${store.allowanceCredits} credits (${origin})`);
|
|
24408
|
+
} else {
|
|
24409
|
+
log.plain(` Allowance not derived yet \u2014 it is sized from the wallet at the first paid command`);
|
|
24410
|
+
}
|
|
24411
|
+
log.plain(` Spent ${spent} credits this build (quoted; failed generations are refunded and excluded)`);
|
|
24412
|
+
if (remaining !== null && enforced) {
|
|
24413
|
+
log.plain(` Remaining ${remaining} credits`);
|
|
24414
|
+
}
|
|
24415
|
+
if (!persisted) {
|
|
24416
|
+
log.dim(" (not a Genex project folder \u2014 the allowance is not saved here)");
|
|
24417
|
+
}
|
|
24418
|
+
if (snapshot) {
|
|
24419
|
+
const kinds = runnableKinds(snapshot.prices);
|
|
24420
|
+
if (kinds.length > 0) {
|
|
24421
|
+
log.plain("");
|
|
24422
|
+
log.plain(c.bold(" Prices (credits)"));
|
|
24423
|
+
for (const [kind, credits] of kinds) {
|
|
24424
|
+
log.plain(` ${kind.padEnd(20)} ${String(credits).padStart(5)}`);
|
|
24425
|
+
}
|
|
24426
|
+
}
|
|
24427
|
+
}
|
|
24428
|
+
log.plain("");
|
|
24429
|
+
log.dim(` ${BUDGET_SOURCE_LINE}`);
|
|
24430
|
+
if (enforced && !setting) {
|
|
24431
|
+
log.dim(` More for this game? Ask the player, then: ${c.cyan("npx genex budget --assets <credits> --user-approved")}`);
|
|
24432
|
+
}
|
|
24433
|
+
if (!snapshot && !setting) process.exitCode = 1;
|
|
24434
|
+
}
|
|
24435
|
+
|
|
24436
|
+
// src/commands/player.ts
|
|
24437
|
+
import fs39 from "fs";
|
|
24438
|
+
import fsp2 from "fs/promises";
|
|
24439
|
+
import os12 from "os";
|
|
24440
|
+
import path43 from "path";
|
|
24441
|
+
import { spawn as spawn8 } from "child_process";
|
|
24442
|
+
|
|
24443
|
+
// src/lib/player/paths.ts
|
|
24444
|
+
import os8 from "os";
|
|
24445
|
+
import path38 from "path";
|
|
24446
|
+
var PLAYER_DRY_RUN_ENV = "GENEX_PLAYER_DRY_RUN";
|
|
24447
|
+
function playerDryRun() {
|
|
24448
|
+
const raw = process.env[PLAYER_DRY_RUN_ENV];
|
|
24449
|
+
return raw === "1" || raw === "true";
|
|
24450
|
+
}
|
|
24451
|
+
function playerPaths(home = os8.homedir()) {
|
|
24452
|
+
const dir = home === os8.homedir() ? getGenexDir() : path38.join(home, ".genex");
|
|
24453
|
+
const logDir = path38.join(dir, "logs");
|
|
24454
|
+
return {
|
|
24455
|
+
credential: path38.join(dir, "player.json"),
|
|
24456
|
+
state: path38.join(dir, "player-state.json"),
|
|
24457
|
+
relaunch: path38.join(dir, "player-relaunch.json"),
|
|
24458
|
+
logDir,
|
|
24459
|
+
log: path38.join(logDir, "player.log"),
|
|
24460
|
+
errorLog: path38.join(logDir, "player.error.log")
|
|
24461
|
+
};
|
|
24462
|
+
}
|
|
24463
|
+
|
|
24464
|
+
// src/lib/player/store.ts
|
|
24465
|
+
import fs35 from "fs/promises";
|
|
24466
|
+
import path39 from "path";
|
|
24467
|
+
async function readPlayerInstall(home) {
|
|
24468
|
+
try {
|
|
24469
|
+
const raw = await fs35.readFile(playerPaths(home).credential, "utf8");
|
|
24470
|
+
const parsed = JSON.parse(raw);
|
|
24471
|
+
if (!parsed || typeof parsed.token !== "string" || typeof parsed.apiUrl !== "string") return null;
|
|
24472
|
+
return parsed;
|
|
24473
|
+
} catch {
|
|
24474
|
+
return null;
|
|
24475
|
+
}
|
|
24476
|
+
}
|
|
24477
|
+
async function writePlayerInstall(install2, home) {
|
|
24478
|
+
const file = playerPaths(home).credential;
|
|
24479
|
+
const record = { ...install2, apiUrl: normalizeApiOrigin(install2.apiUrl) };
|
|
24480
|
+
await writeSecretFile(file, JSON.stringify(record, null, 2) + "\n");
|
|
24481
|
+
return file;
|
|
24482
|
+
}
|
|
24483
|
+
function installExpired(install2, now = Date.now()) {
|
|
24484
|
+
const at = Date.parse(install2.expiresAt);
|
|
24485
|
+
return Number.isFinite(at) && at <= now;
|
|
24486
|
+
}
|
|
24487
|
+
function describeInstall(install2) {
|
|
24488
|
+
return {
|
|
24489
|
+
apiUrl: install2.apiUrl,
|
|
24490
|
+
watcherId: install2.watcherId ?? null,
|
|
24491
|
+
label: install2.label ?? null,
|
|
24492
|
+
installedAt: install2.installedAt,
|
|
24493
|
+
version: install2.version,
|
|
24494
|
+
expiresAt: install2.expiresAt
|
|
24495
|
+
};
|
|
24496
|
+
}
|
|
24497
|
+
async function readPlayerState(home) {
|
|
24498
|
+
try {
|
|
24499
|
+
return JSON.parse(await fs35.readFile(playerPaths(home).state, "utf8"));
|
|
24500
|
+
} catch {
|
|
24501
|
+
return null;
|
|
24502
|
+
}
|
|
24503
|
+
}
|
|
24504
|
+
async function writePlayerState(state, home) {
|
|
24505
|
+
const file = playerPaths(home).state;
|
|
24506
|
+
try {
|
|
24507
|
+
await fs35.mkdir(path39.dirname(file), { recursive: true, mode: 448 });
|
|
24508
|
+
await fs35.writeFile(file, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
|
|
24509
|
+
} catch {
|
|
24510
|
+
}
|
|
24511
|
+
}
|
|
24512
|
+
async function removePlayerFiles(home) {
|
|
24513
|
+
const paths = playerPaths(home);
|
|
24514
|
+
const removed = [];
|
|
24515
|
+
for (const file of [paths.credential, paths.state, paths.relaunch]) {
|
|
24516
|
+
const existed = await fs35.stat(file).then(() => true, () => false);
|
|
24517
|
+
await fs35.rm(file, { force: true }).catch(() => {
|
|
24518
|
+
});
|
|
24519
|
+
if (existed) removed.push(file);
|
|
24520
|
+
}
|
|
24521
|
+
return removed;
|
|
24522
|
+
}
|
|
24523
|
+
|
|
24524
|
+
// src/lib/player/auth.ts
|
|
24525
|
+
var MIN_POLL_INTERVAL_MS2 = 1e3;
|
|
24526
|
+
var DEFAULT_PLAYER_WAIT_MS = 10 * 60 * 1e3;
|
|
24527
|
+
var REASSURE_EVERY_MS2 = 30 * 1e3;
|
|
24528
|
+
var PLAYER_NOT_APPROVED = "Nobody approved this machine, so nothing was installed. Re-run `genex player install` when you can open the link.";
|
|
24529
|
+
var sleep4 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
24530
|
+
async function authorizePlayer(apiBaseUrl, options) {
|
|
24531
|
+
const apiUrl = normalizeApiOrigin(apiBaseUrl);
|
|
24532
|
+
const { log, waitMs = DEFAULT_PLAYER_WAIT_MS, open = openBrowser } = options;
|
|
24533
|
+
const started = await startPlayerDeviceAuth(apiUrl, options.label);
|
|
24534
|
+
log.plain("");
|
|
24535
|
+
log.step("Approve this machine for in-game model requests.");
|
|
24536
|
+
log.plain([
|
|
24537
|
+
"",
|
|
24538
|
+
` ${c.cyan("\u2192")} Open: ${c.cyan(started.verifyUrl)}`,
|
|
24539
|
+
` ${c.cyan("\u2192")} Code: ${c.bold(formatUserCode(started.userCode))}`,
|
|
24540
|
+
"",
|
|
24541
|
+
" The card names the Genex watcher, not a full account connection: it can",
|
|
24542
|
+
" answer the in-game requests you approve and spend nothing.",
|
|
24543
|
+
""
|
|
24544
|
+
].join("\n"));
|
|
24545
|
+
let warned = false;
|
|
24546
|
+
const warnManual = () => {
|
|
24547
|
+
if (warned) return;
|
|
24548
|
+
warned = true;
|
|
24549
|
+
log.dim(" (couldn't open a browser here \u2014 open the link above yourself)");
|
|
24550
|
+
};
|
|
24551
|
+
if (!open(started.verifyUrl, warnManual)) warnManual();
|
|
24552
|
+
const deadline = Date.now() + waitMs;
|
|
24553
|
+
let intervalMs = Math.max((started.interval || 2) * 1e3, MIN_POLL_INTERVAL_MS2);
|
|
24554
|
+
let nextReassureAt = Date.now() + REASSURE_EVERY_MS2;
|
|
24555
|
+
for (; ; ) {
|
|
24556
|
+
let res;
|
|
24557
|
+
try {
|
|
24558
|
+
res = await apiFetch(`${apiUrl}/api/cli/device/poll`, {
|
|
24559
|
+
method: "POST",
|
|
24560
|
+
headers: { "Content-Type": "application/json" },
|
|
24561
|
+
body: JSON.stringify({ deviceCode: started.deviceCode })
|
|
24562
|
+
}, { noTermsWait: true });
|
|
24563
|
+
} catch {
|
|
24564
|
+
res = new Response(null, { status: 599 });
|
|
24565
|
+
}
|
|
24566
|
+
if (res.status === 410) throw new Error("That code expired. Re-run `genex player install` for a fresh one.");
|
|
24567
|
+
if (res.ok) {
|
|
24568
|
+
const body = await res.json().catch(() => null);
|
|
24569
|
+
if (body?.status === "approved" && body.token) {
|
|
24570
|
+
if (body.scope !== void 0 && body.scope !== "player") {
|
|
24571
|
+
throw new Error("This Genex server minted a full account credential instead of a player one \u2014 it is too old for the watcher. Update the stand, or run `genex player install` against one that is current.");
|
|
24572
|
+
}
|
|
24573
|
+
return {
|
|
24574
|
+
token: bindToken(body.token, apiUrl),
|
|
24575
|
+
expiresAt: body.expiresAt ?? new Date(Date.now() + 30 * 24 * 36e5).toISOString()
|
|
24576
|
+
};
|
|
24577
|
+
}
|
|
24578
|
+
if (body?.status === "denied") throw new Error("That request was declined in the browser. Nothing was installed.");
|
|
24579
|
+
if (body?.interval) intervalMs = Math.max(body.interval * 1e3, MIN_POLL_INTERVAL_MS2);
|
|
24580
|
+
}
|
|
24581
|
+
if (Date.now() + intervalMs > deadline) throw new Error(PLAYER_NOT_APPROVED);
|
|
24582
|
+
if (Date.now() >= nextReassureAt) {
|
|
24583
|
+
log.dim(" still waiting for you to approve\u2026");
|
|
24584
|
+
nextReassureAt = Date.now() + REASSURE_EVERY_MS2;
|
|
24585
|
+
}
|
|
24586
|
+
await sleep4(intervalMs);
|
|
24587
|
+
}
|
|
24588
|
+
}
|
|
24589
|
+
async function startPlayerDeviceAuth(apiUrl, label) {
|
|
24590
|
+
const body = JSON.stringify({ ...label ? { label } : {}, scope: "player" });
|
|
24591
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
24592
|
+
const res = await apiFetch(`${apiUrl}/api/cli/device/start`, {
|
|
24593
|
+
method: "POST",
|
|
24594
|
+
headers: { "Content-Type": "application/json" },
|
|
24595
|
+
body
|
|
24596
|
+
}, { noTermsWait: true });
|
|
24597
|
+
if (res.status === 409) continue;
|
|
24598
|
+
if (res.status === 404 || res.status === 501) {
|
|
24599
|
+
throw new Error("This Genex server has no player sign-in \u2014 the watcher needs a current stand.");
|
|
24600
|
+
}
|
|
24601
|
+
if (!res.ok) throw new Error(`Couldn't start sign-in (HTTP ${res.status}). Check your connection and re-run this command.`);
|
|
24602
|
+
return await res.json();
|
|
24603
|
+
}
|
|
24604
|
+
throw new Error("Couldn't start sign-in \u2014 please re-run this command.");
|
|
24605
|
+
}
|
|
24606
|
+
|
|
24607
|
+
// src/lib/player/api.ts
|
|
24608
|
+
var PLAYER_VERSION_HEADER = "x-genex-player-version";
|
|
24609
|
+
var EXTERNAL_PROVIDER_HEADER = "X-Genex-External-Provider";
|
|
24610
|
+
var PlayerUpdateRequiredError = class extends Error {
|
|
24611
|
+
minVersion;
|
|
24612
|
+
latestVersion;
|
|
24613
|
+
constructor(minVersion, latestVersion) {
|
|
24614
|
+
super(`This watcher is below the server's floor (${minVersion}); the current version is ${latestVersion}.`);
|
|
24615
|
+
this.name = "PlayerUpdateRequiredError";
|
|
24616
|
+
this.minVersion = minVersion;
|
|
24617
|
+
this.latestVersion = latestVersion;
|
|
24618
|
+
}
|
|
24619
|
+
};
|
|
24620
|
+
var PlayerAuthError = class extends Error {
|
|
24621
|
+
status;
|
|
24622
|
+
code;
|
|
24623
|
+
constructor(status2, code) {
|
|
24624
|
+
super(`Genex refused this machine's credential (HTTP ${status2}${code ? `, ${code}` : ""}).`);
|
|
24625
|
+
this.name = "PlayerAuthError";
|
|
24626
|
+
this.status = status2;
|
|
24627
|
+
this.code = code;
|
|
24628
|
+
}
|
|
24629
|
+
};
|
|
24630
|
+
var PlayerApiError = class extends Error {
|
|
24631
|
+
status;
|
|
24632
|
+
code;
|
|
24633
|
+
constructor(status2, code) {
|
|
24634
|
+
super(`HTTP ${status2}${code ? ` ${code}` : ""}`);
|
|
24635
|
+
this.name = "PlayerApiError";
|
|
24636
|
+
this.status = status2;
|
|
24637
|
+
this.code = code;
|
|
24638
|
+
}
|
|
24639
|
+
};
|
|
24640
|
+
var PlayerClient = class {
|
|
24641
|
+
apiUrl;
|
|
24642
|
+
version;
|
|
24643
|
+
token;
|
|
24644
|
+
timeoutMs;
|
|
24645
|
+
constructor(options) {
|
|
24646
|
+
this.apiUrl = options.apiUrl;
|
|
24647
|
+
this.token = options.token;
|
|
24648
|
+
this.version = options.version ?? getCliVersion();
|
|
24649
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
24650
|
+
}
|
|
24651
|
+
async call(path44, init2 = {}, provider) {
|
|
24652
|
+
const headers = {
|
|
24653
|
+
Authorization: `Bearer ${this.token}`,
|
|
24654
|
+
[PLAYER_VERSION_HEADER]: this.version,
|
|
24655
|
+
...provider ? { [EXTERNAL_PROVIDER_HEADER]: provider } : {},
|
|
24656
|
+
...init2.body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
24657
|
+
};
|
|
24658
|
+
const res = await apiFetch(`${this.apiUrl}${path44}`, {
|
|
24659
|
+
...init2,
|
|
24660
|
+
headers,
|
|
24661
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
24662
|
+
}, { noTermsWait: true });
|
|
24663
|
+
const body = await res.json().catch(() => null);
|
|
24664
|
+
if (res.ok) return body ?? {};
|
|
24665
|
+
const code = typeof body?.error === "string" ? body.error : "";
|
|
24666
|
+
if (res.status === 426) {
|
|
24667
|
+
throw new PlayerUpdateRequiredError(
|
|
24668
|
+
typeof body?.minVersion === "string" ? body.minVersion : "unknown",
|
|
24669
|
+
typeof body?.latestVersion === "string" ? body.latestVersion : "latest"
|
|
24670
|
+
);
|
|
24671
|
+
}
|
|
24672
|
+
if (res.status === 401 || res.status === 403) throw new PlayerAuthError(res.status, code);
|
|
24673
|
+
throw new PlayerApiError(res.status, code);
|
|
24674
|
+
}
|
|
24675
|
+
/** The versioned runner policy — read on start and refreshed periodically. */
|
|
24676
|
+
async policy() {
|
|
24677
|
+
return await this.call("/api/runtime/player/policy");
|
|
24678
|
+
}
|
|
24679
|
+
/** "I am here, this is what I can run." Also how a watcher learns its id. */
|
|
24680
|
+
async heartbeat(input) {
|
|
24681
|
+
return await this.call("/api/runtime/player/watchers/heartbeat", {
|
|
24682
|
+
method: "POST",
|
|
24683
|
+
body: JSON.stringify({
|
|
24684
|
+
...input.watcherId ? { watcherId: input.watcherId } : {},
|
|
24685
|
+
version: this.version,
|
|
24686
|
+
platform: input.platform,
|
|
24687
|
+
runners: input.runners,
|
|
24688
|
+
...input.label ? { label: input.label } : {}
|
|
24689
|
+
})
|
|
24690
|
+
});
|
|
24691
|
+
}
|
|
24692
|
+
/** Everything this player has waiting on ONE funding lane. */
|
|
24693
|
+
async pending(provider) {
|
|
24694
|
+
const body = await this.call("/api/runtime/external/generations", {}, provider);
|
|
24695
|
+
return Array.isArray(body.generations) ? body.generations : [];
|
|
24696
|
+
}
|
|
24697
|
+
/** Take a row for a bounded lease. Idempotent for the holder; 409 otherwise.
|
|
24698
|
+
* Answers with the job, so a runner needs no second read. */
|
|
24699
|
+
async claim(id, provider, watcherId) {
|
|
24700
|
+
return await this.call(`/api/runtime/external/generations/${encodeURIComponent(id)}/claim`, {
|
|
24701
|
+
method: "POST",
|
|
24702
|
+
body: JSON.stringify({ watcherId })
|
|
24703
|
+
}, provider);
|
|
24704
|
+
}
|
|
24705
|
+
/** Hand a row back unfinished. `rate_limited` parks the budget behind it. */
|
|
24706
|
+
async release(id, provider, input) {
|
|
24707
|
+
return await this.call(`/api/runtime/external/generations/${encodeURIComponent(id)}/release`, {
|
|
24708
|
+
method: "POST",
|
|
24709
|
+
body: JSON.stringify({
|
|
24710
|
+
watcherId: input.watcherId,
|
|
24711
|
+
reason: input.reason,
|
|
24712
|
+
...input.retryAfterSeconds ? { retryAfterSeconds: Math.min(86400, Math.max(1, Math.round(input.retryAfterSeconds))) } : {}
|
|
24713
|
+
})
|
|
24714
|
+
}, provider);
|
|
24715
|
+
}
|
|
24716
|
+
/** The answer. `watcherId` names the lease holder — only it may submit. */
|
|
24717
|
+
async submit(id, provider, watcherId, output) {
|
|
24718
|
+
return await this.call(`/api/runtime/external/generations/${encodeURIComponent(id)}/result`, {
|
|
24719
|
+
method: "POST",
|
|
24720
|
+
body: JSON.stringify({ output, watcherId })
|
|
24721
|
+
}, provider);
|
|
24722
|
+
}
|
|
24723
|
+
};
|
|
24724
|
+
|
|
24725
|
+
// src/lib/player/runners.ts
|
|
24726
|
+
import { spawn as spawn6, spawnSync } from "child_process";
|
|
24727
|
+
import fs36 from "fs/promises";
|
|
24728
|
+
import fsSync from "fs";
|
|
24729
|
+
import os9 from "os";
|
|
24730
|
+
import path40 from "path";
|
|
24731
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
24732
|
+
var WATCHER_RUNNERS = ["claude", "codex"];
|
|
24733
|
+
var RUNNER_FOR_FAMILY = { claude: "claude", chatgpt: "codex" };
|
|
24734
|
+
var PROVIDER_FOR_RUNNER = { claude: "claude", codex: "chatgpt" };
|
|
24735
|
+
var FALLBACK_POLICY = {
|
|
24736
|
+
version: "fallback",
|
|
24737
|
+
minVersion: "0.0.0",
|
|
24738
|
+
latestVersion: "0.0.0",
|
|
24739
|
+
pollIntervalSeconds: 5,
|
|
24740
|
+
backoff: { initialSeconds: 5, maxSeconds: 300, factor: 2, jitterRatio: 0.2 },
|
|
24741
|
+
claude: { args: ["-p"], modelFlag: "--model" },
|
|
24742
|
+
codex: { args: ["exec"], modelFlag: "--model" }
|
|
24743
|
+
};
|
|
24744
|
+
var CLAUDE_DENIED_TOOLS = [
|
|
24745
|
+
"Bash",
|
|
24746
|
+
"Edit",
|
|
24747
|
+
"Write",
|
|
24748
|
+
"Read",
|
|
24749
|
+
"Glob",
|
|
24750
|
+
"Grep",
|
|
24751
|
+
"WebFetch",
|
|
24752
|
+
"WebSearch",
|
|
24753
|
+
"Task",
|
|
24754
|
+
"NotebookEdit",
|
|
24755
|
+
"TodoWrite",
|
|
24756
|
+
"SlashCommand",
|
|
24757
|
+
"KillShell",
|
|
24758
|
+
"BashOutput"
|
|
24759
|
+
];
|
|
24760
|
+
var BYTES_PER_TOKEN = 4;
|
|
24761
|
+
var HARD_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
24762
|
+
var DEFAULT_TIMEOUT_MS = 10 * 6e4;
|
|
24763
|
+
function runnerBinaryEnv(runner) {
|
|
24764
|
+
return `GENEX_PLAYER_${runner.toUpperCase()}_BIN`;
|
|
24765
|
+
}
|
|
24766
|
+
function runnerBinary(runner, env = process.env, platform = process.platform) {
|
|
24767
|
+
const override = env[runnerBinaryEnv(runner)]?.trim();
|
|
24768
|
+
if (override) {
|
|
24769
|
+
try {
|
|
24770
|
+
return fsSync.statSync(override).isFile() ? override : null;
|
|
24771
|
+
} catch {
|
|
24772
|
+
return null;
|
|
24773
|
+
}
|
|
24774
|
+
}
|
|
24775
|
+
return resolveOnPath(runner, env, platform);
|
|
24776
|
+
}
|
|
24777
|
+
function detectRunners(env = process.env, platform = process.platform) {
|
|
24778
|
+
const found = [];
|
|
24779
|
+
for (const name of WATCHER_RUNNERS) if (runnerBinary(name, env, platform)) found.push(name);
|
|
24780
|
+
return found;
|
|
24781
|
+
}
|
|
24782
|
+
function resolveOnPath(name, env = process.env, platform = process.platform) {
|
|
24783
|
+
const win = platform === "win32";
|
|
24784
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(win ? ";" : ":").filter(Boolean);
|
|
24785
|
+
const candidates = win ? [`${name}.cmd`, `${name}.exe`, `${name}.bat`, name] : [name];
|
|
24786
|
+
for (const dir of dirs) {
|
|
24787
|
+
for (const candidate of candidates) {
|
|
24788
|
+
const file = path40.join(dir, candidate);
|
|
24789
|
+
try {
|
|
24790
|
+
const stat = fsSync.statSync(file);
|
|
24791
|
+
if (!stat.isFile()) continue;
|
|
24792
|
+
if (!win) fsSync.accessSync(file, fsSync.constants.X_OK);
|
|
24793
|
+
return file;
|
|
24794
|
+
} catch {
|
|
24795
|
+
}
|
|
24796
|
+
}
|
|
24797
|
+
}
|
|
24798
|
+
return null;
|
|
24799
|
+
}
|
|
24800
|
+
function names(args, ...flags) {
|
|
24801
|
+
return args.some((a) => flags.includes(a) || flags.some((f) => a.startsWith(`${f}=`)));
|
|
24802
|
+
}
|
|
24803
|
+
function claudeArgv(job, policy = {}) {
|
|
24804
|
+
const base = [...policy.args ?? ["-p"]];
|
|
24805
|
+
const argv = [...base];
|
|
24806
|
+
const add = (flag, value, ...aliases) => {
|
|
24807
|
+
if (names(base, flag, ...aliases)) return;
|
|
24808
|
+
argv.push(flag);
|
|
24809
|
+
if (value !== void 0) argv.push(value);
|
|
24810
|
+
};
|
|
24811
|
+
if (!names(base, "-p", "--print")) argv.unshift("-p");
|
|
24812
|
+
if (policy.modelFlag && !names(base, policy.modelFlag)) argv.push(policy.modelFlag, job.model.cliModel);
|
|
24813
|
+
add("--output-format", "json");
|
|
24814
|
+
const structured = job.outputFormat === "json" && job.schema !== void 0;
|
|
24815
|
+
add("--max-turns", structured ? "2" : "1");
|
|
24816
|
+
add("--no-session-persistence");
|
|
24817
|
+
add("--permission-prompts", "none");
|
|
24818
|
+
add("--strict-mcp-config");
|
|
24819
|
+
argv.push("--append-system-prompt", job.system);
|
|
24820
|
+
if (structured && !names(base, "--json-schema")) {
|
|
24821
|
+
argv.push("--json-schema", JSON.stringify(job.schema));
|
|
24822
|
+
}
|
|
24823
|
+
if (!names(base, "--disallowedTools", "--disallowed-tools")) {
|
|
24824
|
+
argv.push("--disallowedTools", ...CLAUDE_DENIED_TOOLS);
|
|
24825
|
+
}
|
|
24826
|
+
return argv;
|
|
24827
|
+
}
|
|
24828
|
+
function codexArgv(job, policy = {}, files) {
|
|
24829
|
+
const base = [...policy.args ?? ["exec"]];
|
|
24830
|
+
const argv = [...base];
|
|
24831
|
+
const add = (flag, value) => {
|
|
24832
|
+
if (names(base, flag)) return;
|
|
24833
|
+
argv.push(flag);
|
|
24834
|
+
if (value !== void 0) argv.push(value);
|
|
24835
|
+
};
|
|
24836
|
+
if (!names(base, "exec") && base[0] !== "exec") argv.unshift("exec");
|
|
24837
|
+
add("--ephemeral");
|
|
24838
|
+
add("--ignore-user-config");
|
|
24839
|
+
add("--skip-git-repo-check");
|
|
24840
|
+
add("--sandbox", "read-only");
|
|
24841
|
+
add("-C", files.cwd);
|
|
24842
|
+
if (files.schemaFile && !names(base, "--output-schema")) argv.push("--output-schema", files.schemaFile);
|
|
24843
|
+
add("-o", files.outFile);
|
|
24844
|
+
add("--color", "never");
|
|
24845
|
+
if (policy.modelFlag && !names(base, policy.modelFlag)) argv.push(policy.modelFlag, job.model.cliModel);
|
|
24846
|
+
argv.push("-");
|
|
24847
|
+
return argv;
|
|
24848
|
+
}
|
|
24849
|
+
function classifyFailure(text) {
|
|
24850
|
+
const t = text.toLowerCase();
|
|
24851
|
+
if (/rate[ -]?limit|usage limit|quota|too many requests|429|\bresets?\b|limit reached/.test(t)) {
|
|
24852
|
+
const retry = parseRetrySeconds(t);
|
|
24853
|
+
return retry === void 0 ? { release: "rate_limited" } : { release: "rate_limited", retryAfterSeconds: retry };
|
|
24854
|
+
}
|
|
24855
|
+
if (/not logged in|please log ?in|\blogin\b|log in to|unauthorized|401|invalid api key|authentication (failed|required)|credentials/.test(t)) {
|
|
24856
|
+
return { release: "auth_expired" };
|
|
24857
|
+
}
|
|
24858
|
+
if (/enoent|command not found|no such file or directory|is not recognized/.test(t)) {
|
|
24859
|
+
return { release: "runner_unavailable" };
|
|
24860
|
+
}
|
|
24861
|
+
return { release: "failed" };
|
|
24862
|
+
}
|
|
24863
|
+
function parseRetrySeconds(text) {
|
|
24864
|
+
const header = /retry[ -]after:?\s*(\d+)/.exec(text);
|
|
24865
|
+
if (header?.[1]) return clampSeconds(Number(header[1]));
|
|
24866
|
+
const phrase = /(?:try again|retry|resets?|wait|available again)\D{0,20}?(\d+)\s*(second|sec|minute|min|hour|hr)s?/.exec(text);
|
|
24867
|
+
if (!phrase?.[1] || !phrase[2]) return void 0;
|
|
24868
|
+
const n = Number(phrase[1]);
|
|
24869
|
+
const unit = phrase[2];
|
|
24870
|
+
const seconds = unit.startsWith("h") ? n * 3600 : unit.startsWith("m") ? n * 60 : n;
|
|
24871
|
+
return clampSeconds(seconds);
|
|
24872
|
+
}
|
|
24873
|
+
function clampSeconds(value) {
|
|
24874
|
+
if (!Number.isFinite(value) || value <= 0) return void 0;
|
|
24875
|
+
return Math.min(86400, Math.max(1, Math.round(value)));
|
|
24876
|
+
}
|
|
24877
|
+
function outputByteCeiling(maxOutputTokens) {
|
|
24878
|
+
const fromTokens = Math.max(1024, Math.round(maxOutputTokens * BYTES_PER_TOKEN));
|
|
24879
|
+
return Math.min(HARD_OUTPUT_BYTES, fromTokens);
|
|
24880
|
+
}
|
|
24881
|
+
function parseClaudeEnvelope(stdout) {
|
|
24882
|
+
const trimmed = stdout.trim();
|
|
24883
|
+
if (!trimmed) return { ok: false, detail: "no_output" };
|
|
24884
|
+
const start = trimmed.lastIndexOf("\n{");
|
|
24885
|
+
const candidate = start >= 0 ? trimmed.slice(start + 1) : trimmed;
|
|
24886
|
+
let body;
|
|
24887
|
+
try {
|
|
24888
|
+
body = JSON.parse(candidate);
|
|
24889
|
+
} catch {
|
|
24890
|
+
return { ok: false, detail: "unparseable_envelope", text: trimmed };
|
|
24891
|
+
}
|
|
24892
|
+
const text = typeof body.result === "string" ? body.result : void 0;
|
|
24893
|
+
if (body.is_error === true || typeof body.subtype === "string" && body.subtype !== "success") {
|
|
24894
|
+
return { ok: false, detail: typeof body.subtype === "string" ? body.subtype : "is_error", text };
|
|
24895
|
+
}
|
|
24896
|
+
const structured = body.structured_output;
|
|
24897
|
+
return { ok: true, ...structured !== void 0 && structured !== null ? { result: structured } : {}, ...text !== void 0 ? { text } : {} };
|
|
24898
|
+
}
|
|
24899
|
+
function readOutput(value, job) {
|
|
24900
|
+
if (value.result !== void 0) {
|
|
24901
|
+
const size = Buffer.byteLength(JSON.stringify(value.result) ?? "");
|
|
24902
|
+
if (size > outputByteCeiling(job.maxOutputTokens)) return { ok: false, detail: "output_too_large" };
|
|
24903
|
+
return { ok: true, output: value.result };
|
|
24904
|
+
}
|
|
24905
|
+
const text = (value.text ?? "").trim();
|
|
24906
|
+
if (!text) return { ok: false, detail: "empty_output" };
|
|
24907
|
+
if (Buffer.byteLength(text) > outputByteCeiling(job.maxOutputTokens)) return { ok: false, detail: "output_too_large" };
|
|
24908
|
+
if (job.outputFormat !== "json") return { ok: true, output: text };
|
|
24909
|
+
const fenced = /^```(?:json)?\s*\n([\s\S]*?)\n```$/.exec(text);
|
|
24910
|
+
try {
|
|
24911
|
+
return { ok: true, output: JSON.parse(fenced?.[1] ?? text) };
|
|
24912
|
+
} catch {
|
|
24913
|
+
return { ok: false, detail: "output_not_json" };
|
|
24914
|
+
}
|
|
24915
|
+
}
|
|
24916
|
+
function killRunner(child, platform = process.platform, killTreeFn) {
|
|
24917
|
+
const pid = child.pid;
|
|
24918
|
+
if (typeof pid !== "number") return;
|
|
24919
|
+
if (platform === "win32") {
|
|
24920
|
+
try {
|
|
24921
|
+
if (killTreeFn) killTreeFn(pid);
|
|
24922
|
+
else {
|
|
24923
|
+
const done = spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true });
|
|
24924
|
+
if (done.error || done.status !== 0) child.kill("SIGKILL");
|
|
24925
|
+
}
|
|
24926
|
+
} catch {
|
|
24927
|
+
try {
|
|
24928
|
+
child.kill("SIGKILL");
|
|
24929
|
+
} catch {
|
|
24930
|
+
}
|
|
24931
|
+
}
|
|
24932
|
+
return;
|
|
24933
|
+
}
|
|
24934
|
+
try {
|
|
24935
|
+
process.kill(-pid, "SIGKILL");
|
|
24936
|
+
} catch {
|
|
24937
|
+
}
|
|
24938
|
+
}
|
|
24939
|
+
async function runJob(job, options = {}) {
|
|
24940
|
+
const started = Date.now();
|
|
24941
|
+
const policy = options.policy ?? FALLBACK_POLICY;
|
|
24942
|
+
const runner = RUNNER_FOR_FAMILY[job.model.family];
|
|
24943
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
24944
|
+
const log = options.log ?? (() => {
|
|
24945
|
+
});
|
|
24946
|
+
const spawnFn = options.spawnFn ?? spawn6;
|
|
24947
|
+
const platform = options.platform ?? process.platform;
|
|
24948
|
+
const resolved = options.binary ?? runnerBinary(runner);
|
|
24949
|
+
if (!resolved) {
|
|
24950
|
+
return { ok: false, release: "runner_unavailable", detail: `missing:${runner}`, durationMs: Date.now() - started };
|
|
24951
|
+
}
|
|
24952
|
+
const cwd = path40.join(os9.tmpdir(), `genex-player-${randomUUID3()}`);
|
|
24953
|
+
await fs36.mkdir(cwd, { recursive: true, mode: 448 });
|
|
24954
|
+
const outFile = path40.join(cwd, "result.out");
|
|
24955
|
+
let argv;
|
|
24956
|
+
if (runner === "claude") {
|
|
24957
|
+
argv = claudeArgv(job, policy.claude ?? FALLBACK_POLICY.claude ?? {});
|
|
24958
|
+
} else {
|
|
24959
|
+
let schemaFile;
|
|
24960
|
+
if (job.outputFormat === "json" && job.schema !== void 0) {
|
|
24961
|
+
schemaFile = path40.join(cwd, "schema.json");
|
|
24962
|
+
await fs36.writeFile(schemaFile, JSON.stringify(job.schema), { mode: 384 });
|
|
24963
|
+
}
|
|
24964
|
+
argv = codexArgv(job, policy.codex ?? FALLBACK_POLICY.codex ?? {}, { cwd, outFile, ...schemaFile ? { schemaFile } : {} });
|
|
24965
|
+
}
|
|
24966
|
+
log(` ${runner} ${job.model.cliModel} (no tools)`);
|
|
24967
|
+
try {
|
|
24968
|
+
const child = spawnFn(resolved, argv, {
|
|
24969
|
+
cwd,
|
|
24970
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
24971
|
+
// The player's own environment, minus anything that would redirect the
|
|
24972
|
+
// binary off their subscription and onto a billed API account.
|
|
24973
|
+
env: {
|
|
24974
|
+
...process.env,
|
|
24975
|
+
ANTHROPIC_API_KEY: void 0,
|
|
24976
|
+
ANTHROPIC_AUTH_TOKEN: void 0,
|
|
24977
|
+
ANTHROPIC_BASE_URL: void 0,
|
|
24978
|
+
OPENAI_API_KEY: void 0,
|
|
24979
|
+
OPENAI_BASE_URL: void 0
|
|
24980
|
+
},
|
|
24981
|
+
// A killable process GROUP on POSIX. On Windows it buys nothing — there
|
|
24982
|
+
// are no groups — and `killRunner` uses taskkill there instead.
|
|
24983
|
+
detached: platform !== "win32"
|
|
24984
|
+
});
|
|
24985
|
+
child.stdin?.end(job.user);
|
|
24986
|
+
const out = [];
|
|
24987
|
+
const err = [];
|
|
24988
|
+
child.stdout?.on("data", (d) => out.push(d.toString()));
|
|
24989
|
+
child.stderr?.on("data", (d) => err.push(d.toString()));
|
|
24990
|
+
const kill = () => killRunner(child, platform, options.killTreeFn);
|
|
24991
|
+
const code = await new Promise((resolve) => {
|
|
24992
|
+
const timer = setTimeout(() => {
|
|
24993
|
+
kill();
|
|
24994
|
+
resolve(null);
|
|
24995
|
+
}, timeoutMs);
|
|
24996
|
+
const onAbort = () => {
|
|
24997
|
+
kill();
|
|
24998
|
+
resolve("aborted");
|
|
24999
|
+
};
|
|
25000
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
25001
|
+
const done = (value) => {
|
|
25002
|
+
clearTimeout(timer);
|
|
25003
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
25004
|
+
resolve(value);
|
|
25005
|
+
};
|
|
25006
|
+
child.once("error", () => done("spawn-failed"));
|
|
25007
|
+
child.once("close", (c2) => done(c2));
|
|
25008
|
+
});
|
|
25009
|
+
const durationMs = Date.now() - started;
|
|
25010
|
+
if (code === "spawn-failed") return { ok: false, release: "runner_unavailable", detail: `missing:${runner}`, durationMs };
|
|
25011
|
+
if (code === "aborted") return { ok: false, release: "runner_unavailable", detail: "shutdown", exitCode: null, durationMs };
|
|
25012
|
+
if (code === null) return { ok: false, release: "failed", detail: "timeout", exitCode: null, durationMs };
|
|
25013
|
+
const stderrText = err.join("");
|
|
25014
|
+
if (runner === "claude") {
|
|
25015
|
+
const envelope = parseClaudeEnvelope(out.join(""));
|
|
25016
|
+
if (!envelope.ok) {
|
|
25017
|
+
const classified = classifyFailure(`${envelope.detail ?? ""} ${envelope.text ?? ""} ${stderrText}`);
|
|
25018
|
+
return { ok: false, ...classified, detail: envelope.detail ?? "failed", exitCode: code, durationMs };
|
|
25019
|
+
}
|
|
25020
|
+
const read2 = readOutput(envelope, job);
|
|
25021
|
+
if (!read2.ok) return { ok: false, release: "failed", detail: read2.detail, exitCode: code, durationMs };
|
|
25022
|
+
return { ok: true, output: read2.output, exitCode: code, durationMs };
|
|
25023
|
+
}
|
|
25024
|
+
const text = await fs36.readFile(outFile, "utf8").catch(() => "");
|
|
25025
|
+
if (code !== 0 && !text.trim()) {
|
|
25026
|
+
const classified = classifyFailure(`${stderrText} exit ${code}`);
|
|
25027
|
+
return { ok: false, ...classified, detail: `exit_${code}`, exitCode: code, durationMs };
|
|
25028
|
+
}
|
|
25029
|
+
const read = readOutput({ text }, job);
|
|
25030
|
+
if (!read.ok) return { ok: false, release: "failed", detail: read.detail, exitCode: code, durationMs };
|
|
25031
|
+
return { ok: true, output: read.output, exitCode: code, durationMs };
|
|
25032
|
+
} finally {
|
|
25033
|
+
await fs36.rm(cwd, { recursive: true, force: true }).catch(() => {
|
|
25034
|
+
});
|
|
25035
|
+
}
|
|
25036
|
+
}
|
|
25037
|
+
|
|
25038
|
+
// src/lib/player/service.ts
|
|
25039
|
+
import fs37 from "fs/promises";
|
|
25040
|
+
import os10 from "os";
|
|
25041
|
+
import path41 from "path";
|
|
25042
|
+
import { execFile } from "child_process";
|
|
25043
|
+
import { promisify } from "util";
|
|
25044
|
+
var run2 = promisify(execFile);
|
|
25045
|
+
var SERVICE_LABEL = "games.genex.player";
|
|
25046
|
+
var SYSTEMD_UNIT = "genex-player.service";
|
|
25047
|
+
var PLAYER_AUTOSTART_SPEC = "@genex-ai/cli-demo@latest";
|
|
25048
|
+
var SUPERVISED_ENV = "GENEX_PLAYER_SUPERVISED";
|
|
25049
|
+
function supervisedWatcher(env = process.env) {
|
|
25050
|
+
return env[SUPERVISED_ENV] === "1";
|
|
25051
|
+
}
|
|
25052
|
+
function servicePlatform(platform = process.platform) {
|
|
25053
|
+
if (platform === "darwin" || platform === "win32") return platform;
|
|
25054
|
+
return "linux";
|
|
25055
|
+
}
|
|
25056
|
+
function servicePaths(platform, home = os10.homedir()) {
|
|
25057
|
+
const { log, errorLog } = playerPaths(home);
|
|
25058
|
+
if (platform === "darwin") return { log, errorLog, unit: path41.join(home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`) };
|
|
25059
|
+
if (platform === "linux") return { log, errorLog, unit: path41.join(home, ".config", "systemd", "user", SYSTEMD_UNIT) };
|
|
25060
|
+
return { log, errorLog, unit: path41.join(home, "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "genex-player.cmd") };
|
|
25061
|
+
}
|
|
25062
|
+
function xmlEscape(value) {
|
|
25063
|
+
return value.replace(/[<>&'"]/g, (ch) => ({ "<": "<", ">": ">", "&": "&", "'": "'", '"': """ })[ch]);
|
|
25064
|
+
}
|
|
25065
|
+
function unitFile(platform, input, paths) {
|
|
25066
|
+
if (platform === "darwin") {
|
|
25067
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
25068
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
25069
|
+
<plist version="1.0">
|
|
25070
|
+
<dict>
|
|
25071
|
+
<key>Label</key><string>${SERVICE_LABEL}</string>
|
|
25072
|
+
<key>ProgramArguments</key>
|
|
25073
|
+
<array>
|
|
25074
|
+
${[input.launcher, ...input.args].map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n")}
|
|
25075
|
+
</array>
|
|
25076
|
+
<key>EnvironmentVariables</key>
|
|
25077
|
+
<dict>
|
|
25078
|
+
<key>PATH</key><string>${xmlEscape(input.pathEnv)}</string>
|
|
25079
|
+
<key>HOME</key><string>${xmlEscape(input.home)}</string>
|
|
25080
|
+
<!-- Tells the watcher a supervisor started it, so an update is taken by
|
|
25081
|
+
stopping (exit 1, restarted below, resolving @latest again) rather
|
|
25082
|
+
than by spawning a detached grandchild: the parent's exit IS this
|
|
25083
|
+
job's exit, and that grandchild would have run unsupervised. -->
|
|
25084
|
+
<key>${SUPERVISED_ENV}</key><string>1</string>
|
|
25085
|
+
</dict>
|
|
25086
|
+
<key>RunAtLoad</key><true/>
|
|
25087
|
+
<!-- KeepAlive, but NOT unconditionally: exit 0 means the watcher decided to
|
|
25088
|
+
stop (a signal, or a credential only a person with a browser can
|
|
25089
|
+
replace), and restarting that every 30 seconds is an invisible loop on
|
|
25090
|
+
somebody else's machine. A crash comes back \u2014 and so does an update,
|
|
25091
|
+
which exits non-zero on purpose and is bounded by the relaunch cooldown. -->
|
|
25092
|
+
<key>KeepAlive</key>
|
|
25093
|
+
<dict><key>SuccessfulExit</key><false/></dict>
|
|
25094
|
+
<key>ThrottleInterval</key><integer>30</integer>
|
|
25095
|
+
<key>StandardOutPath</key><string>${xmlEscape(paths.log)}</string>
|
|
25096
|
+
<key>StandardErrorPath</key><string>${xmlEscape(paths.errorLog)}</string>
|
|
25097
|
+
</dict>
|
|
25098
|
+
</plist>
|
|
25099
|
+
`;
|
|
25100
|
+
}
|
|
25101
|
+
if (platform === "linux") {
|
|
25102
|
+
return `[Unit]
|
|
25103
|
+
Description=Genex player watcher \u2014 answer the in-game requests you approved, on your own subscription
|
|
25104
|
+
After=network-online.target
|
|
25105
|
+
|
|
25106
|
+
[Service]
|
|
25107
|
+
Type=simple
|
|
25108
|
+
ExecStart=${[input.launcher, ...input.args].map((a) => JSON.stringify(a)).join(" ")}
|
|
25109
|
+
Environment=PATH=${input.pathEnv}
|
|
25110
|
+
Environment=HOME=${input.home}
|
|
25111
|
+
# The supervisor marker: an update is taken by stopping and letting this unit
|
|
25112
|
+
# start the current version, never by spawning a child systemd is not watching.
|
|
25113
|
+
Environment=${SUPERVISED_ENV}=1
|
|
25114
|
+
# on-failure rather than always, for the reason spelled out in the plist above:
|
|
25115
|
+
# a clean exit is a decision (signal, dead credential) and must stay stopped; a
|
|
25116
|
+
# crash comes back, and so does an update, which exits non-zero on purpose.
|
|
25117
|
+
Restart=on-failure
|
|
25118
|
+
RestartSec=30
|
|
25119
|
+
StandardOutput=append:${paths.log}
|
|
25120
|
+
StandardError=append:${paths.errorLog}
|
|
25121
|
+
|
|
25122
|
+
[Install]
|
|
25123
|
+
WantedBy=default.target
|
|
25124
|
+
`;
|
|
25125
|
+
}
|
|
25126
|
+
return `@echo off\r
|
|
25127
|
+
set "PATH=${input.pathEnv}"\r
|
|
25128
|
+
start "" /b "${input.launcher}" ${input.args.map((a) => `"${a}"`).join(" ")} >> "${paths.log}" 2>> "${paths.errorLog}"\r
|
|
25129
|
+
`;
|
|
25130
|
+
}
|
|
25131
|
+
function autostartCommand(opts) {
|
|
25132
|
+
const npx = resolveOnPath("npx");
|
|
25133
|
+
const tail = ["player", "run", ...opts.apiUrl ? ["--api-url", opts.apiUrl] : []];
|
|
25134
|
+
if (npx) return { launcher: npx, args: ["-y", PLAYER_AUTOSTART_SPEC, ...tail], fallback: false };
|
|
25135
|
+
return { launcher: process.execPath, args: [process.argv[1] ?? "", ...tail], fallback: true };
|
|
25136
|
+
}
|
|
25137
|
+
async function installService(input, platform = servicePlatform()) {
|
|
25138
|
+
const paths = servicePaths(platform, input.home);
|
|
25139
|
+
await fs37.mkdir(path41.dirname(paths.unit), { recursive: true });
|
|
25140
|
+
await fs37.mkdir(path41.dirname(paths.log), { recursive: true, mode: 448 });
|
|
25141
|
+
await fs37.writeFile(paths.unit, unitFile(platform, input, paths), { mode: platform === "win32" ? 420 : 384 });
|
|
25142
|
+
if (playerDryRun()) return { unit: paths.unit, loaded: false, detail: "dry run \u2014 the unit was written, the system was not asked" };
|
|
25143
|
+
if (platform === "darwin") {
|
|
25144
|
+
const domain = `gui/${process.getuid?.() ?? 501}`;
|
|
25145
|
+
await run2("launchctl", ["bootout", domain, paths.unit]).catch(() => {
|
|
25146
|
+
});
|
|
25147
|
+
try {
|
|
25148
|
+
await run2("launchctl", ["bootstrap", domain, paths.unit]);
|
|
25149
|
+
} catch (error) {
|
|
25150
|
+
return { unit: paths.unit, loaded: false, detail: `launchctl refused it: ${String(error.message).split("\n")[0]}` };
|
|
25151
|
+
}
|
|
25152
|
+
const printed2 = await run2("launchctl", ["print", `${domain}/${SERVICE_LABEL}`]).then((r) => r.stdout).catch(() => "");
|
|
25153
|
+
return { unit: paths.unit, loaded: printed2.includes(SERVICE_LABEL), detail: printed2.includes("state = running") ? "running" : "registered" };
|
|
25154
|
+
}
|
|
25155
|
+
if (platform === "linux") {
|
|
25156
|
+
await run2("systemctl", ["--user", "daemon-reload"]).catch(() => {
|
|
25157
|
+
});
|
|
25158
|
+
try {
|
|
25159
|
+
await run2("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT]);
|
|
25160
|
+
} catch (error) {
|
|
25161
|
+
return { unit: paths.unit, loaded: false, detail: `systemctl refused it: ${String(error.message).split("\n")[0]}` };
|
|
25162
|
+
}
|
|
25163
|
+
const active = await run2("systemctl", ["--user", "is-active", SYSTEMD_UNIT]).then((r) => r.stdout.trim()).catch(() => "unknown");
|
|
25164
|
+
return { unit: paths.unit, loaded: active === "active", detail: active };
|
|
25165
|
+
}
|
|
25166
|
+
return { unit: paths.unit, loaded: true, detail: "starts at your next sign-in" };
|
|
25167
|
+
}
|
|
25168
|
+
async function serviceInstalled(home = os10.homedir(), platform = servicePlatform()) {
|
|
25169
|
+
const paths = servicePaths(platform, home);
|
|
25170
|
+
return await fs37.stat(paths.unit).then(() => paths.unit, () => null);
|
|
25171
|
+
}
|
|
25172
|
+
async function stopService(home = os10.homedir(), platform = servicePlatform()) {
|
|
25173
|
+
const paths = servicePaths(platform, home);
|
|
25174
|
+
const present = await serviceInstalled(home, platform);
|
|
25175
|
+
if (playerDryRun()) return { unit: paths.unit, loaded: false, detail: present ? "dry run \u2014 the system was not asked" : "nothing installed" };
|
|
25176
|
+
if (platform === "darwin") {
|
|
25177
|
+
const out2 = await run2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, paths.unit]).then(() => "stopped").catch(() => "was not running");
|
|
25178
|
+
return { unit: paths.unit, loaded: false, detail: out2 };
|
|
25179
|
+
}
|
|
25180
|
+
if (platform === "linux") {
|
|
25181
|
+
const out2 = await run2("systemctl", ["--user", "stop", SYSTEMD_UNIT]).then(() => "stopped").catch(() => "was not running");
|
|
25182
|
+
return { unit: paths.unit, loaded: false, detail: out2 };
|
|
25183
|
+
}
|
|
25184
|
+
const out = await run2("taskkill", ["/IM", "genex-player.cmd", "/F"]).then(() => "stopped").catch(() => "was not running");
|
|
25185
|
+
return { unit: paths.unit, loaded: false, detail: out };
|
|
25186
|
+
}
|
|
25187
|
+
async function uninstallService(home = os10.homedir(), platform = servicePlatform()) {
|
|
25188
|
+
const paths = servicePaths(platform, home);
|
|
25189
|
+
const present = await serviceInstalled(home, platform);
|
|
25190
|
+
if (!playerDryRun()) {
|
|
25191
|
+
if (platform === "darwin") await run2("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}`, paths.unit]).catch(() => {
|
|
25192
|
+
});
|
|
25193
|
+
if (platform === "linux") {
|
|
25194
|
+
await run2("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT]).catch(() => {
|
|
25195
|
+
});
|
|
25196
|
+
await run2("systemctl", ["--user", "daemon-reload"]).catch(() => {
|
|
25197
|
+
});
|
|
25198
|
+
}
|
|
25199
|
+
}
|
|
25200
|
+
await fs37.rm(paths.unit, { force: true }).catch(() => {
|
|
25201
|
+
});
|
|
25202
|
+
return { unit: paths.unit, loaded: false, detail: present ? "removed" : "nothing installed" };
|
|
25203
|
+
}
|
|
25204
|
+
|
|
25205
|
+
// src/lib/player/relaunch.ts
|
|
25206
|
+
import fs38 from "fs/promises";
|
|
25207
|
+
import fsSync2 from "fs";
|
|
25208
|
+
import path42 from "path";
|
|
25209
|
+
import { spawn as spawn7 } from "child_process";
|
|
25210
|
+
var RELAUNCH_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
25211
|
+
function relaunchPlan(input) {
|
|
25212
|
+
const base = autostartCommand({ ...input.apiUrl ? { apiUrl: input.apiUrl } : {} });
|
|
25213
|
+
if (base.fallback) {
|
|
25214
|
+
return { launcher: base.launcher, args: base.args };
|
|
25215
|
+
}
|
|
25216
|
+
const spec = input.version && input.version !== "latest" ? `@genex-ai/cli-demo@${input.version}` : PLAYER_AUTOSTART_SPEC;
|
|
25217
|
+
const sub = input.sub ?? "run";
|
|
25218
|
+
return {
|
|
25219
|
+
launcher: base.launcher,
|
|
25220
|
+
args: [
|
|
25221
|
+
"-y",
|
|
25222
|
+
spec,
|
|
25223
|
+
"player",
|
|
25224
|
+
sub,
|
|
25225
|
+
"--relaunched",
|
|
25226
|
+
...input.extraArgs ?? [],
|
|
25227
|
+
...input.apiUrl ? ["--api-url", input.apiUrl] : []
|
|
25228
|
+
]
|
|
25229
|
+
};
|
|
25230
|
+
}
|
|
25231
|
+
function relaunchPossible() {
|
|
25232
|
+
return !autostartCommand({}).fallback;
|
|
25233
|
+
}
|
|
25234
|
+
async function relaunchOnCooldown(home, now = Date.now()) {
|
|
25235
|
+
try {
|
|
25236
|
+
const raw = await fs38.readFile(playerPaths(home).relaunch, "utf8");
|
|
25237
|
+
const at = Date.parse(JSON.parse(raw).at ?? "");
|
|
25238
|
+
return Number.isFinite(at) && now - at < RELAUNCH_COOLDOWN_MS;
|
|
25239
|
+
} catch {
|
|
25240
|
+
return false;
|
|
25241
|
+
}
|
|
25242
|
+
}
|
|
25243
|
+
async function markRelaunch(version, home) {
|
|
25244
|
+
const file = playerPaths(home).relaunch;
|
|
25245
|
+
try {
|
|
25246
|
+
await fs38.mkdir(path42.dirname(file), { recursive: true, mode: 448 });
|
|
25247
|
+
await fs38.writeFile(file, JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), version }) + "\n", { mode: 384 });
|
|
25248
|
+
} catch {
|
|
25249
|
+
}
|
|
25250
|
+
}
|
|
25251
|
+
async function relaunchWatcher(input) {
|
|
25252
|
+
const plan = relaunchPlan(input);
|
|
25253
|
+
const log = input.log ?? (() => {
|
|
25254
|
+
});
|
|
25255
|
+
if (input.relaunched) return { launched: false, plan, reason: "relaunched_child" };
|
|
25256
|
+
if (!relaunchPossible()) return { launched: false, plan, reason: "no_npx" };
|
|
25257
|
+
if (await relaunchOnCooldown(input.home)) return { launched: false, plan, reason: "cooldown" };
|
|
25258
|
+
await markRelaunch(input.version, input.home);
|
|
25259
|
+
if (supervisedWatcher()) {
|
|
25260
|
+
log(`player: stopping so the login item restarts as ${input.version} (it resolves ${PLAYER_AUTOSTART_SPEC} each start)`);
|
|
25261
|
+
return { launched: false, plan, reason: "supervisor_restarts" };
|
|
25262
|
+
}
|
|
25263
|
+
if (playerDryRun()) {
|
|
25264
|
+
log(`player: would relaunch: ${JSON.stringify([plan.launcher, ...plan.args])}`);
|
|
25265
|
+
return { launched: true, plan, reason: "dry_run" };
|
|
25266
|
+
}
|
|
25267
|
+
let out;
|
|
25268
|
+
let err;
|
|
25269
|
+
const paths = playerPaths(input.home);
|
|
25270
|
+
try {
|
|
25271
|
+
await fs38.mkdir(paths.logDir, { recursive: true, mode: 448 });
|
|
25272
|
+
out = fsSync2.openSync(paths.log, "a", 384);
|
|
25273
|
+
err = fsSync2.openSync(paths.errorLog, "a", 384);
|
|
25274
|
+
} catch {
|
|
25275
|
+
}
|
|
25276
|
+
try {
|
|
25277
|
+
const child = spawn7(plan.launcher, plan.args, {
|
|
25278
|
+
detached: true,
|
|
25279
|
+
stdio: ["ignore", out ?? "ignore", err ?? "ignore"]
|
|
25280
|
+
});
|
|
25281
|
+
child.on("error", (reason) => log(`player: relaunch could not start (${reason.message})`));
|
|
25282
|
+
child.unref();
|
|
25283
|
+
} finally {
|
|
25284
|
+
for (const fd of [out, err]) if (fd !== void 0) try {
|
|
25285
|
+
fsSync2.closeSync(fd);
|
|
25286
|
+
} catch {
|
|
25287
|
+
}
|
|
25288
|
+
}
|
|
25289
|
+
log(`player: relaunching as ${input.version}`);
|
|
25290
|
+
return { launched: true, plan };
|
|
25291
|
+
}
|
|
25292
|
+
|
|
25293
|
+
// src/lib/player/loop.ts
|
|
25294
|
+
import os11 from "os";
|
|
25295
|
+
var POLICY_REFRESH_MS = 5 * 6e4;
|
|
25296
|
+
var MAX_HEARTBEAT_MS = 6e4;
|
|
25297
|
+
var LEASE_RENEW_MS = 45e3;
|
|
25298
|
+
var JOB_TIMEOUT_MS = 10 * 6e4;
|
|
25299
|
+
var SHUTDOWN_RETRY_SECONDS = 5;
|
|
25300
|
+
var MAX_PROVIDER_COOLDOWN_MS = 60 * 6e4;
|
|
25301
|
+
var sleep5 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
25302
|
+
function platformTag(platform = process.platform) {
|
|
25303
|
+
return platform.toLowerCase().replace(/[^a-z0-9_-]/g, "").slice(0, 40) || "unknown";
|
|
25304
|
+
}
|
|
25305
|
+
function backoffMs(policy, attempt, random = Math.random) {
|
|
25306
|
+
const b = policy.backoff ?? FALLBACK_POLICY.backoff;
|
|
25307
|
+
const raw = Math.min(b.maxSeconds, b.initialSeconds * Math.pow(b.factor, Math.max(0, attempt - 1)));
|
|
25308
|
+
const jitter = 1 + (random() * 2 - 1) * b.jitterRatio;
|
|
25309
|
+
return Math.max(1e3, Math.round(raw * jitter * 1e3));
|
|
25310
|
+
}
|
|
25311
|
+
function isNewer(a, b) {
|
|
25312
|
+
const pa = /^(\d+)\.(\d+)\.(\d+)/.exec(a);
|
|
25313
|
+
const pb = /^(\d+)\.(\d+)\.(\d+)/.exec(b);
|
|
25314
|
+
if (!pa || !pb) return false;
|
|
25315
|
+
for (let i = 1; i <= 3; i++) {
|
|
25316
|
+
const x = Number(pa[i]);
|
|
25317
|
+
const y = Number(pb[i]);
|
|
25318
|
+
if (x !== y) return x > y;
|
|
25319
|
+
}
|
|
25320
|
+
return false;
|
|
25321
|
+
}
|
|
25322
|
+
function workable(row, watcherId, now = Date.now()) {
|
|
25323
|
+
if (!row.runner) return false;
|
|
25324
|
+
if (!row.claimedBy || row.claimedBy === watcherId) return true;
|
|
25325
|
+
const until = Date.parse(row.leaseExpiresAt ?? "");
|
|
25326
|
+
return Number.isFinite(until) && until <= now;
|
|
25327
|
+
}
|
|
25328
|
+
async function runWatcherLoop(options) {
|
|
25329
|
+
const { client, install: install2, log } = options;
|
|
25330
|
+
const runJobFn = options.runJobFn ?? runJob;
|
|
25331
|
+
const keepGoing = options.keepGoing ?? (() => true);
|
|
25332
|
+
const relaunchReady = options.relaunchReady ?? (() => true);
|
|
25333
|
+
const label = install2.label;
|
|
25334
|
+
let policy = FALLBACK_POLICY;
|
|
25335
|
+
let runners = detectRunners();
|
|
25336
|
+
let watcherId = install2.watcherId ?? "";
|
|
25337
|
+
let policyAt = 0;
|
|
25338
|
+
let heartbeatAt = 0;
|
|
25339
|
+
let failures = 0;
|
|
25340
|
+
let warnedNoRunners = false;
|
|
25341
|
+
let warnedStuckVersion = false;
|
|
25342
|
+
let stop2 = null;
|
|
25343
|
+
const held = { row: null };
|
|
25344
|
+
const cooldown = /* @__PURE__ */ new Map();
|
|
25345
|
+
const state = {
|
|
25346
|
+
v: 1,
|
|
25347
|
+
pid: process.pid,
|
|
25348
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25349
|
+
lastHeartbeatAt: null,
|
|
25350
|
+
lastPollAt: null,
|
|
25351
|
+
lastJobAt: null,
|
|
25352
|
+
watcherId: watcherId || null,
|
|
25353
|
+
policyVersion: null,
|
|
25354
|
+
runners,
|
|
25355
|
+
jobsCompleted: 0,
|
|
25356
|
+
jobsReleased: 0,
|
|
25357
|
+
lastError: null
|
|
25358
|
+
};
|
|
25359
|
+
const shutdown = new AbortController();
|
|
25360
|
+
const onSignal = (signal) => {
|
|
25361
|
+
if (stop2) return;
|
|
25362
|
+
stop2 = { kind: "signal", signal };
|
|
25363
|
+
shutdown.abort();
|
|
25364
|
+
log(`player: ${signal} \u2014 handing back anything in flight`);
|
|
25365
|
+
};
|
|
25366
|
+
const sigint = () => onSignal("SIGINT");
|
|
25367
|
+
const sigterm = () => onSignal("SIGTERM");
|
|
25368
|
+
process.on("SIGINT", sigint);
|
|
25369
|
+
process.on("SIGTERM", sigterm);
|
|
25370
|
+
try {
|
|
25371
|
+
for (; ; ) {
|
|
25372
|
+
if (stop2 || !keepGoing()) break;
|
|
25373
|
+
const now = Date.now();
|
|
25374
|
+
if (now >= policyAt) {
|
|
25375
|
+
try {
|
|
25376
|
+
policy = await client.policy();
|
|
25377
|
+
policyAt = now + POLICY_REFRESH_MS;
|
|
25378
|
+
state.policyVersion = policy.version ?? null;
|
|
25379
|
+
runners = detectRunners();
|
|
25380
|
+
state.runners = runners;
|
|
25381
|
+
warnedNoRunners = false;
|
|
25382
|
+
failures = 0;
|
|
25383
|
+
} catch (error) {
|
|
25384
|
+
const fatal = classifyFatal(error);
|
|
25385
|
+
if (fatal) {
|
|
25386
|
+
stop2 = fatal;
|
|
25387
|
+
break;
|
|
25388
|
+
}
|
|
25389
|
+
failures += 1;
|
|
25390
|
+
policyAt = now + POLICY_REFRESH_MS;
|
|
25391
|
+
log(`player: policy unreadable (${describe(error)}); using the built-in one`);
|
|
25392
|
+
}
|
|
25393
|
+
}
|
|
25394
|
+
if (now >= heartbeatAt) {
|
|
25395
|
+
try {
|
|
25396
|
+
const beat = await client.heartbeat({
|
|
25397
|
+
...watcherId ? { watcherId } : {},
|
|
25398
|
+
platform: platformTag(),
|
|
25399
|
+
runners,
|
|
25400
|
+
...label ? { label } : {}
|
|
25401
|
+
});
|
|
25402
|
+
watcherId = beat.watcherId;
|
|
25403
|
+
state.watcherId = watcherId;
|
|
25404
|
+
state.lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
25405
|
+
state.policyVersion = beat.policyVersion ?? state.policyVersion;
|
|
25406
|
+
heartbeatAt = now + Math.min(MAX_HEARTBEAT_MS, Math.max(5e3, (beat.pollIntervalSeconds || 5) * 1e3 * 6));
|
|
25407
|
+
failures = 0;
|
|
25408
|
+
if (isNewer(beat.latestVersion, client.version)) {
|
|
25409
|
+
if (await relaunchReady(beat.latestVersion)) {
|
|
25410
|
+
stop2 = { kind: "update_available", latestVersion: beat.latestVersion };
|
|
25411
|
+
break;
|
|
25412
|
+
}
|
|
25413
|
+
if (!warnedStuckVersion) {
|
|
25414
|
+
warnedStuckVersion = true;
|
|
25415
|
+
log(`player: ${beat.latestVersion} is current and this is ${client.version}, but nothing here can fetch it \u2014 still answering`);
|
|
25416
|
+
}
|
|
25417
|
+
}
|
|
25418
|
+
} catch (error) {
|
|
25419
|
+
const fatal = classifyFatal(error);
|
|
25420
|
+
if (fatal) {
|
|
25421
|
+
stop2 = fatal;
|
|
25422
|
+
break;
|
|
25423
|
+
}
|
|
25424
|
+
failures += 1;
|
|
25425
|
+
state.lastError = describe(error);
|
|
25426
|
+
heartbeatAt = now + backoffMs(policy, failures);
|
|
25427
|
+
log(`player: heartbeat failed (${describe(error)})`);
|
|
25428
|
+
await sleep5(backoffMs(policy, failures));
|
|
25429
|
+
continue;
|
|
25430
|
+
}
|
|
25431
|
+
}
|
|
25432
|
+
const providers = [...new Set(runners.map((r) => PROVIDER_FOR_RUNNER[r]))];
|
|
25433
|
+
if (providers.length === 0 && !warnedNoRunners) {
|
|
25434
|
+
warnedNoRunners = true;
|
|
25435
|
+
log("player: no claude or codex binary here \u2014 nothing can be answered yet");
|
|
25436
|
+
}
|
|
25437
|
+
let worked = false;
|
|
25438
|
+
for (const provider of providers) {
|
|
25439
|
+
if (stop2) break;
|
|
25440
|
+
const until = cooldown.get(provider) ?? 0;
|
|
25441
|
+
if (until > Date.now()) continue;
|
|
25442
|
+
if (until) cooldown.delete(provider);
|
|
25443
|
+
let pending;
|
|
25444
|
+
try {
|
|
25445
|
+
pending = await client.pending(provider);
|
|
25446
|
+
state.lastPollAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
25447
|
+
failures = 0;
|
|
25448
|
+
} catch (error) {
|
|
25449
|
+
const fatal = classifyFatal(error);
|
|
25450
|
+
if (fatal) {
|
|
25451
|
+
stop2 = fatal;
|
|
25452
|
+
break;
|
|
25453
|
+
}
|
|
25454
|
+
failures += 1;
|
|
25455
|
+
state.lastError = describe(error);
|
|
25456
|
+
log(`player: poll failed (${describe(error)})`);
|
|
25457
|
+
await sleep5(backoffMs(policy, failures));
|
|
25458
|
+
continue;
|
|
25459
|
+
}
|
|
25460
|
+
for (const row of pending) {
|
|
25461
|
+
if (stop2) break;
|
|
25462
|
+
if (!workable(row, watcherId)) continue;
|
|
25463
|
+
if (await handleOne(row, provider)) worked = true;
|
|
25464
|
+
break;
|
|
25465
|
+
}
|
|
25466
|
+
}
|
|
25467
|
+
if (stop2) break;
|
|
25468
|
+
await writePlayerState(state, options.home);
|
|
25469
|
+
if (options.once) {
|
|
25470
|
+
stop2 = { kind: "once" };
|
|
25471
|
+
break;
|
|
25472
|
+
}
|
|
25473
|
+
const interval = options.pollIntervalMsOverride ?? Math.max(1e3, (policy.pollIntervalSeconds || 5) * 1e3);
|
|
25474
|
+
if (!worked) await sleep5(interval);
|
|
25475
|
+
}
|
|
25476
|
+
} finally {
|
|
25477
|
+
process.off("SIGINT", sigint);
|
|
25478
|
+
process.off("SIGTERM", sigterm);
|
|
25479
|
+
const stranded = held.row;
|
|
25480
|
+
if (stranded && watcherId) {
|
|
25481
|
+
await client.release(stranded.id, stranded.provider, {
|
|
25482
|
+
watcherId,
|
|
25483
|
+
reason: "runner_unavailable",
|
|
25484
|
+
retryAfterSeconds: SHUTDOWN_RETRY_SECONDS
|
|
25485
|
+
}).catch(() => {
|
|
25486
|
+
});
|
|
25487
|
+
held.row = null;
|
|
25488
|
+
state.jobsReleased += 1;
|
|
25489
|
+
log(`player: released ${stranded.id} (shutdown)`);
|
|
25490
|
+
}
|
|
25491
|
+
await writePlayerState(state, options.home);
|
|
25492
|
+
}
|
|
25493
|
+
return { stop: stop2 ?? { kind: "once" }, state };
|
|
25494
|
+
async function handleOne(row, provider) {
|
|
25495
|
+
const job = row.runner;
|
|
25496
|
+
let claimed;
|
|
25497
|
+
try {
|
|
25498
|
+
claimed = await client.claim(row.id, provider, watcherId);
|
|
25499
|
+
} catch (error) {
|
|
25500
|
+
if (error instanceof PlayerApiError && (error.code === "already_claimed" || error.code === "external_request_not_pending")) {
|
|
25501
|
+
log(`player: ${row.id} taken by another machine`);
|
|
25502
|
+
return false;
|
|
25503
|
+
}
|
|
25504
|
+
const fatal = classifyFatal(error);
|
|
25505
|
+
if (fatal) {
|
|
25506
|
+
stop2 = fatal;
|
|
25507
|
+
return false;
|
|
25508
|
+
}
|
|
25509
|
+
failures += 1;
|
|
25510
|
+
log(`player: could not claim ${row.id} (${describe(error)})`);
|
|
25511
|
+
await sleep5(backoffMs(policy, failures));
|
|
25512
|
+
return false;
|
|
25513
|
+
}
|
|
25514
|
+
held.row = { id: row.id, provider };
|
|
25515
|
+
const live = claimed.runner ?? job;
|
|
25516
|
+
const renew = setInterval(() => {
|
|
25517
|
+
void client.claim(row.id, provider, watcherId).catch(() => {
|
|
25518
|
+
});
|
|
25519
|
+
}, LEASE_RENEW_MS);
|
|
25520
|
+
renew.unref?.();
|
|
25521
|
+
log(`player: ${row.id} ${live.mode} ${live.model.cliModel} running`);
|
|
25522
|
+
let outcome2;
|
|
25523
|
+
try {
|
|
25524
|
+
outcome2 = await runJobFn(live, {
|
|
25525
|
+
policy,
|
|
25526
|
+
timeoutMs: Math.min(JOB_TIMEOUT_MS, Math.max(3e4, (live.leaseSeconds || 120) * 1e3 * 5)),
|
|
25527
|
+
signal: shutdown.signal,
|
|
25528
|
+
log
|
|
25529
|
+
});
|
|
25530
|
+
} finally {
|
|
25531
|
+
clearInterval(renew);
|
|
25532
|
+
}
|
|
25533
|
+
state.lastJobAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
25534
|
+
const shuttingDown = stop2 !== null;
|
|
25535
|
+
if (!outcome2.ok || shuttingDown) {
|
|
25536
|
+
const reason = shuttingDown ? "runner_unavailable" : outcome2.release ?? "failed";
|
|
25537
|
+
const retryAfterSeconds = shuttingDown ? SHUTDOWN_RETRY_SECONDS : outcome2.retryAfterSeconds ?? (reason === "rate_limited" ? (policy.backoff ?? FALLBACK_POLICY.backoff).maxSeconds : void 0);
|
|
25538
|
+
state.jobsReleased += 1;
|
|
25539
|
+
state.lastError = reason;
|
|
25540
|
+
try {
|
|
25541
|
+
await client.release(row.id, provider, { watcherId, reason, ...retryAfterSeconds ? { retryAfterSeconds } : {} });
|
|
25542
|
+
held.row = null;
|
|
25543
|
+
} catch (error) {
|
|
25544
|
+
const fatal = classifyFatal(error);
|
|
25545
|
+
if (fatal) {
|
|
25546
|
+
stop2 = fatal;
|
|
25547
|
+
held.row = null;
|
|
25548
|
+
return true;
|
|
25549
|
+
}
|
|
25550
|
+
log(`player: release of ${row.id} failed (${describe(error)})`);
|
|
25551
|
+
}
|
|
25552
|
+
log(`player: ${row.id} released ${reason}${outcome2.detail ? ` (${outcome2.detail})` : ""} after ${outcome2.durationMs}ms`);
|
|
25553
|
+
if (reason === "rate_limited" || reason === "auth_expired") {
|
|
25554
|
+
const waitMs = Math.min(MAX_PROVIDER_COOLDOWN_MS, Math.max(
|
|
25555
|
+
1e3,
|
|
25556
|
+
(retryAfterSeconds ?? (policy.backoff ?? FALLBACK_POLICY.backoff).maxSeconds) * 1e3
|
|
25557
|
+
));
|
|
25558
|
+
cooldown.set(provider, Date.now() + waitMs);
|
|
25559
|
+
log(`player: ${provider} waits ${Math.round(waitMs / 1e3)}s before the next request`);
|
|
25560
|
+
}
|
|
25561
|
+
if (reason === "auth_expired") {
|
|
25562
|
+
const binary = live.model.family === "claude" ? "claude" : "codex";
|
|
25563
|
+
log(`player: your ${binary} CLI is not signed in \u2014 run \`${binary} login\` on this machine`);
|
|
25564
|
+
}
|
|
25565
|
+
return true;
|
|
25566
|
+
}
|
|
25567
|
+
try {
|
|
25568
|
+
const receipt = await client.submit(row.id, provider, watcherId, outcome2.output);
|
|
25569
|
+
held.row = null;
|
|
25570
|
+
state.jobsCompleted += 1;
|
|
25571
|
+
log(`player: ${row.id} delivered in ${outcome2.durationMs}ms (${receipt.status})`);
|
|
25572
|
+
} catch (error) {
|
|
25573
|
+
const fatal = classifyFatal(error);
|
|
25574
|
+
if (fatal) {
|
|
25575
|
+
stop2 = fatal;
|
|
25576
|
+
return true;
|
|
25577
|
+
}
|
|
25578
|
+
state.lastError = describe(error);
|
|
25579
|
+
log(`player: ${row.id} could not be submitted (${describe(error)})`);
|
|
25580
|
+
await client.release(row.id, provider, { watcherId, reason: "failed" }).catch(() => {
|
|
25581
|
+
});
|
|
25582
|
+
held.row = null;
|
|
25583
|
+
state.jobsReleased += 1;
|
|
25584
|
+
}
|
|
25585
|
+
return true;
|
|
25586
|
+
}
|
|
25587
|
+
}
|
|
25588
|
+
function classifyFatal(error) {
|
|
25589
|
+
if (error instanceof PlayerUpdateRequiredError) {
|
|
25590
|
+
return { kind: "update_required", minVersion: error.minVersion, latestVersion: error.latestVersion };
|
|
25591
|
+
}
|
|
25592
|
+
if (error instanceof PlayerAuthError) return { kind: "auth", status: error.status, code: error.code };
|
|
25593
|
+
return null;
|
|
25594
|
+
}
|
|
25595
|
+
function describe(error) {
|
|
25596
|
+
if (error instanceof PlayerApiError) return `${error.status}${error.code ? ` ${error.code}` : ""}`;
|
|
25597
|
+
if (error instanceof Error) return error.name === "TimeoutError" ? "timeout" : error.name;
|
|
25598
|
+
return "error";
|
|
25599
|
+
}
|
|
25600
|
+
function defaultLabel2() {
|
|
25601
|
+
try {
|
|
25602
|
+
return os11.hostname().replace(/[^\w .@-]/g, "").trim().slice(0, 64) || "This machine";
|
|
25603
|
+
} catch {
|
|
25604
|
+
return "This machine";
|
|
25605
|
+
}
|
|
25606
|
+
}
|
|
25607
|
+
|
|
25608
|
+
// src/commands/player.ts
|
|
25609
|
+
var SUBS4 = ["install", "run", "status", "stop", "uninstall"];
|
|
25610
|
+
var PLAYER_NOT_INSTALLED = "This machine is not set up to answer in-game model requests \u2014 run `genex player install` first.";
|
|
25611
|
+
var PLAYER_REAUTH_REQUIRED = "Genex no longer accepts this machine's approval \u2014 run `genex player install` again.";
|
|
25612
|
+
var PLAYER_NO_RUNNERS = "Neither `claude` nor `codex` is on this machine's PATH, so nothing can be answered yet. Install the one whose subscription you pay for, sign into it, and this watcher picks it up on its own.";
|
|
25613
|
+
async function runPlayer(opts = {}) {
|
|
25614
|
+
const log = createLogger({ quiet: opts.quiet || opts.json });
|
|
25615
|
+
const sub = opts.name?.trim() || "status";
|
|
25616
|
+
if (!SUBS4.includes(sub)) {
|
|
25617
|
+
fail3(opts, log, `player ${sub}`, `Unknown subcommand ${sub}. Use: ${SUBS4.join(", ")}.`);
|
|
25618
|
+
return;
|
|
25619
|
+
}
|
|
25620
|
+
if (opts.dryRun) process.env[PLAYER_DRY_RUN_ENV] = "1";
|
|
25621
|
+
if (sub === "install") return await install(opts, log);
|
|
25622
|
+
if (sub === "run") return await run3(opts, log);
|
|
25623
|
+
if (sub === "status") return await status(opts, log);
|
|
25624
|
+
if (sub === "stop") return await stop(opts, log);
|
|
25625
|
+
return await uninstall(opts, log);
|
|
25626
|
+
}
|
|
25627
|
+
async function install(opts, log) {
|
|
25628
|
+
const apiUrl = normalizeApiOrigin(getApiUrl(opts.apiUrl));
|
|
25629
|
+
const label = opts.playerLabel?.trim() || defaultLabel2();
|
|
25630
|
+
let approval;
|
|
25631
|
+
try {
|
|
25632
|
+
approval = await authorizePlayer(apiUrl, {
|
|
25633
|
+
log,
|
|
25634
|
+
label,
|
|
25635
|
+
...opts.timeoutSec ? { waitMs: opts.timeoutSec * 1e3 } : {}
|
|
25636
|
+
});
|
|
25637
|
+
} catch (error) {
|
|
25638
|
+
fail3(opts, log, "player install", error instanceof Error ? error.message : String(error));
|
|
25639
|
+
return;
|
|
25640
|
+
}
|
|
25641
|
+
let record = {
|
|
25642
|
+
v: 1,
|
|
23620
25643
|
apiUrl,
|
|
23621
|
-
|
|
23622
|
-
|
|
23623
|
-
|
|
23624
|
-
|
|
23625
|
-
|
|
23626
|
-
|
|
23627
|
-
|
|
23628
|
-
|
|
23629
|
-
|
|
23630
|
-
|
|
23631
|
-
|
|
23632
|
-
|
|
23633
|
-
|
|
23634
|
-
|
|
23635
|
-
|
|
23636
|
-
|
|
23637
|
-
|
|
23638
|
-
|
|
25644
|
+
token: approval.token,
|
|
25645
|
+
expiresAt: approval.expiresAt,
|
|
25646
|
+
label,
|
|
25647
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25648
|
+
version: getCliVersion()
|
|
25649
|
+
};
|
|
25650
|
+
const credentialFile = await writePlayerInstall(record);
|
|
25651
|
+
const runners = detectRunners();
|
|
25652
|
+
const client = new PlayerClient({ apiUrl, token: approval.token });
|
|
25653
|
+
try {
|
|
25654
|
+
const beat = await client.heartbeat({ platform: platformTag(), runners, label });
|
|
25655
|
+
record = { ...record, watcherId: beat.watcherId };
|
|
25656
|
+
await writePlayerInstall(record);
|
|
25657
|
+
} catch (error) {
|
|
25658
|
+
if (error instanceof PlayerUpdateRequiredError) {
|
|
25659
|
+
const outcome2 = await relaunchWatcher({
|
|
25660
|
+
version: error.latestVersion,
|
|
25661
|
+
apiUrl,
|
|
25662
|
+
sub: "install",
|
|
25663
|
+
// The flags the refused install carried, so the newer one finishes
|
|
25664
|
+
// the same install rather than a default one.
|
|
25665
|
+
extraArgs: [
|
|
25666
|
+
...opts.playerLabel ? ["--label", opts.playerLabel] : [],
|
|
25667
|
+
...opts.noAutostart ? ["--no-autostart"] : []
|
|
25668
|
+
],
|
|
25669
|
+
relaunched: Boolean(opts.relaunched),
|
|
25670
|
+
log: (line) => log.plain(line)
|
|
23639
25671
|
});
|
|
23640
|
-
|
|
23641
|
-
if (
|
|
23642
|
-
|
|
23643
|
-
log.
|
|
23644
|
-
"Everything else is set up \u2014 `genex auth` finishes the sign-in, then you can generate."
|
|
23645
|
-
);
|
|
25672
|
+
log.warn(`This Genex needs player watcher ${error.minVersion} or newer; ${error.latestVersion} is current.`);
|
|
25673
|
+
if (!outcome2.launched) {
|
|
25674
|
+
log.plain(` ${c.cyan("\u2192")} Update and re-run: ${c.cyan(`npx -y @genex-ai/cli-demo@${error.latestVersion} player install`)}`);
|
|
25675
|
+
fail3(opts, log, "player install", "player_update_required", { minVersion: error.minVersion, latestVersion: error.latestVersion });
|
|
23646
25676
|
return;
|
|
23647
25677
|
}
|
|
23648
|
-
log.
|
|
23649
|
-
|
|
23650
|
-
throw markPrinted(err);
|
|
25678
|
+
log.plain(` ${c.cyan("\u2192")} Picked up by ${c.cyan(`@genex-ai/cli-demo@${error.latestVersion}`)} \u2014 it finishes the install.`);
|
|
25679
|
+
return;
|
|
23651
25680
|
}
|
|
23652
|
-
|
|
23653
|
-
|
|
23654
|
-
|
|
25681
|
+
fail3(opts, log, "player install", error instanceof PlayerAuthError ? "Genex refused the approval this machine just collected. Re-run `genex player install`." : `Couldn't reach Genex to register this machine (${error instanceof Error ? error.message : String(error)}).`);
|
|
25682
|
+
return;
|
|
25683
|
+
}
|
|
25684
|
+
const platform = servicePlatform();
|
|
25685
|
+
const paths = servicePaths(platform);
|
|
25686
|
+
let unit = null;
|
|
25687
|
+
let unitDetail = "not installed (--no-autostart)";
|
|
25688
|
+
let supervised = false;
|
|
25689
|
+
if (!opts.noAutostart) {
|
|
25690
|
+
const command = autostartCommand({ ...usesNonDefaultStand(opts, apiUrl) ? { apiUrl } : {} });
|
|
25691
|
+
const result = await installService({
|
|
25692
|
+
launcher: command.launcher,
|
|
25693
|
+
args: command.args,
|
|
25694
|
+
// See lib/player/service.ts: a login agent inherits almost no PATH, and
|
|
25695
|
+
// this program exists to run binaries that are rarely on the default one.
|
|
25696
|
+
pathEnv: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
|
|
25697
|
+
home: os12.homedir()
|
|
25698
|
+
}, platform);
|
|
25699
|
+
unit = result.unit;
|
|
25700
|
+
unitDetail = result.detail;
|
|
25701
|
+
supervised = result.loaded;
|
|
25702
|
+
if (command.fallback) {
|
|
25703
|
+
log.warn("No `npx` on this machine's PATH, so the login entry runs THIS copy of the CLI and will not update itself.");
|
|
25704
|
+
}
|
|
25705
|
+
}
|
|
25706
|
+
let started = supervised;
|
|
25707
|
+
if (!started && !playerDryRun()) started = await spawnDetachedRun(apiUrl, opts);
|
|
25708
|
+
if (opts.json) {
|
|
25709
|
+
writeJsonLine2({
|
|
25710
|
+
command: "player install",
|
|
25711
|
+
status: "ok",
|
|
25712
|
+
error: null,
|
|
25713
|
+
...describeInstall(record),
|
|
25714
|
+
runners,
|
|
25715
|
+
credentialFile,
|
|
25716
|
+
autostart: unit,
|
|
25717
|
+
autostartDetail: unitDetail,
|
|
25718
|
+
running: started,
|
|
25719
|
+
log: paths.log
|
|
25720
|
+
});
|
|
25721
|
+
return;
|
|
23655
25722
|
}
|
|
23656
|
-
await reportToolsInit(apiUrl, token);
|
|
23657
|
-
await runDoctor({ apiUrl: opts.apiUrl, envPath: opts.envPath, quiet: opts.quiet });
|
|
23658
25723
|
log.plain("");
|
|
23659
|
-
log.success(
|
|
23660
|
-
log.
|
|
25724
|
+
log.success(`This machine can now answer the in-game model requests you approve, on your own subscription.`);
|
|
25725
|
+
log.plain("");
|
|
25726
|
+
log.plain(oneParagraph({ record, runners, unit, unitDetail, started, logFile: paths.log, credentialFile }));
|
|
25727
|
+
if (runners.length === 0) log.warn(PLAYER_NO_RUNNERS);
|
|
23661
25728
|
}
|
|
23662
|
-
|
|
23663
|
-
|
|
23664
|
-
|
|
23665
|
-
|
|
23666
|
-
|
|
23667
|
-
|
|
23668
|
-
|
|
23669
|
-
|
|
25729
|
+
function oneParagraph(input) {
|
|
25730
|
+
const runnerText = input.runners.length === 0 ? "no runner yet" : input.runners.map((r) => r === "claude" ? "Claude Code" : "Codex").join(" and ");
|
|
25731
|
+
return [
|
|
25732
|
+
` Genex approval stored in ${input.credentialFile} (only this machine can read it). It can`,
|
|
25733
|
+
" answer approved requests and spend nothing \u2014 no coins, no credits, no",
|
|
25734
|
+
" account changes. Your Claude or ChatGPT sign-in is never read, copied or",
|
|
25735
|
+
" sent anywhere; the watcher only starts the official CLI you already use.",
|
|
25736
|
+
` Runners ${runnerText}`,
|
|
25737
|
+
input.unit ? ` Starts at login ${input.unit} (${input.unitDetail})` : " Starts at login no \u2014 run `genex player run` yourself",
|
|
25738
|
+
` Running now ${input.started ? "yes" : "no \u2014 start it with `genex player run`"}`,
|
|
25739
|
+
` Log ${input.logFile} (ids and timings only, never what a game asked for)`,
|
|
25740
|
+
" Stop it genex player stop \xB7 and for good: genex player uninstall",
|
|
25741
|
+
"",
|
|
25742
|
+
" You never need to paste anything again: approve a request in a game and the answer",
|
|
25743
|
+
" comes back on its own. Revoke this machine any time from your Genex dashboard."
|
|
25744
|
+
].join("\n");
|
|
23670
25745
|
}
|
|
23671
|
-
async function
|
|
25746
|
+
async function spawnDetachedRun(apiUrl, opts) {
|
|
25747
|
+
let out;
|
|
25748
|
+
let err;
|
|
23672
25749
|
try {
|
|
23673
|
-
|
|
25750
|
+
const paths = playerPaths();
|
|
25751
|
+
await fsp2.mkdir(paths.logDir, { recursive: true, mode: 448 });
|
|
25752
|
+
out = fs39.openSync(paths.log, "a", 384);
|
|
25753
|
+
err = fs39.openSync(paths.errorLog, "a", 384);
|
|
25754
|
+
const args = [process.argv[1] ?? "", "player", "run", ...opts.apiUrl ? ["--api-url", apiUrl] : []];
|
|
25755
|
+
const child = spawn8(process.execPath, args, { detached: true, stdio: ["ignore", out, err] });
|
|
25756
|
+
child.on("error", () => {
|
|
25757
|
+
});
|
|
25758
|
+
child.unref();
|
|
23674
25759
|
return true;
|
|
23675
25760
|
} catch {
|
|
23676
25761
|
return false;
|
|
25762
|
+
} finally {
|
|
25763
|
+
for (const fd of [out, err]) if (fd !== void 0) try {
|
|
25764
|
+
fs39.closeSync(fd);
|
|
25765
|
+
} catch {
|
|
25766
|
+
}
|
|
23677
25767
|
}
|
|
23678
25768
|
}
|
|
23679
|
-
function
|
|
23680
|
-
return
|
|
23681
|
-
}
|
|
23682
|
-
function describeDefault(config) {
|
|
23683
|
-
const share = config.share === 0.5 ? "half" : `${Math.round(config.share * 100)}%`;
|
|
23684
|
-
return `default: ${share} of what was spendable at the first paid command, capped at ${config.cap}`;
|
|
23685
|
-
}
|
|
23686
|
-
function isoDay(iso) {
|
|
23687
|
-
const d = new Date(iso);
|
|
23688
|
-
return Number.isNaN(d.getTime()) ? iso : d.toISOString().slice(0, 10);
|
|
25769
|
+
function usesNonDefaultStand(opts, apiUrl) {
|
|
25770
|
+
return apiUrl !== normalizeApiOrigin(getApiUrl()) || Boolean(opts.apiUrl);
|
|
23689
25771
|
}
|
|
23690
|
-
async function
|
|
23691
|
-
const
|
|
23692
|
-
|
|
23693
|
-
|
|
23694
|
-
const fail3 = (message) => {
|
|
23695
|
-
if (opts.json) writeJson({ command: "budget", status: "failed", error: message });
|
|
23696
|
-
else log.error(message);
|
|
23697
|
-
process.exitCode = 1;
|
|
23698
|
-
};
|
|
23699
|
-
const setting = opts.assets !== void 0;
|
|
23700
|
-
if (setting && opts.userApproved !== true) {
|
|
23701
|
-
fail3(BUDGET_APPROVAL_REQUIRED);
|
|
25772
|
+
async function run3(opts, log) {
|
|
25773
|
+
const record = await readPlayerInstall();
|
|
25774
|
+
if (!record) {
|
|
25775
|
+
fail3(opts, log, "player run", PLAYER_NOT_INSTALLED);
|
|
23702
25776
|
return;
|
|
23703
25777
|
}
|
|
23704
|
-
if (
|
|
23705
|
-
fail3(
|
|
25778
|
+
if (installExpired(record)) {
|
|
25779
|
+
fail3(opts, log, "player run", `This machine's Genex approval expired on ${record.expiresAt} \u2014 run \`genex player install\` again.`);
|
|
23706
25780
|
return;
|
|
23707
25781
|
}
|
|
23708
|
-
const
|
|
23709
|
-
|
|
23710
|
-
const
|
|
23711
|
-
|
|
23712
|
-
|
|
23713
|
-
|
|
25782
|
+
const apiUrl = opts.apiUrl ? normalizeApiOrigin(getApiUrl(opts.apiUrl)) : record.apiUrl;
|
|
25783
|
+
bindToken(record.token, apiUrl);
|
|
25784
|
+
const client = new PlayerClient({ apiUrl, token: record.token });
|
|
25785
|
+
const line = (text) => {
|
|
25786
|
+
process.stdout.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${text}
|
|
25787
|
+
`);
|
|
25788
|
+
};
|
|
25789
|
+
if (opts.foreground) {
|
|
25790
|
+
log.plain(c.bold("genex player run"));
|
|
25791
|
+
log.dim(` ${apiUrl} \xB7 watcher ${record.watcherId ?? "(unregistered)"} \xB7 v${getCliVersion()}`);
|
|
25792
|
+
log.dim(" Approve a request in a game and it runs here. Ctrl-C to stop.");
|
|
25793
|
+
}
|
|
25794
|
+
const result = await runWatcherLoop({
|
|
25795
|
+
install: record,
|
|
25796
|
+
client,
|
|
25797
|
+
...opts.once ? { once: true } : {},
|
|
25798
|
+
log: line,
|
|
25799
|
+
// Only break off answering requests for a merely AVAILABLE update when a
|
|
25800
|
+
// relaunch will actually follow. A person watching a foreground run is
|
|
25801
|
+
// told about it instead — their terminal is not a thing to restart behind
|
|
25802
|
+
// their back.
|
|
25803
|
+
relaunchReady: async () => !opts.foreground && !opts.relaunched && relaunchPossible() && !await relaunchOnCooldown()
|
|
25804
|
+
});
|
|
25805
|
+
if (result.state.watcherId && result.state.watcherId !== record.watcherId) {
|
|
25806
|
+
await writePlayerInstall({ ...record, watcherId: result.state.watcherId }).catch(() => {
|
|
25807
|
+
});
|
|
23714
25808
|
}
|
|
23715
|
-
const
|
|
23716
|
-
|
|
23717
|
-
|
|
23718
|
-
|
|
23719
|
-
|
|
23720
|
-
store = {
|
|
23721
|
-
v: 1,
|
|
23722
|
-
allowanceCredits: opts.assets,
|
|
23723
|
-
setBy: "user",
|
|
23724
|
-
setAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23725
|
-
spendableAtStart: previous?.spendableAtStart ?? snapshot?.spendable ?? 0,
|
|
23726
|
-
balanceAtStart: previous?.balanceAtStart ?? snapshot?.balance ?? 0
|
|
23727
|
-
};
|
|
23728
|
-
persisted = await writeBudgetStore(cwd, store);
|
|
23729
|
-
if (!persisted) {
|
|
23730
|
-
fail3("This folder is not a Genex project (no .genex/), so the allowance has nowhere to live. Run this inside the game folder.");
|
|
25809
|
+
const stop2 = result.stop;
|
|
25810
|
+
if (stop2.kind === "update_required" || stop2.kind === "update_available") {
|
|
25811
|
+
const version = stop2.kind === "update_required" ? stop2.latestVersion : stop2.latestVersion;
|
|
25812
|
+
if (opts.foreground) {
|
|
25813
|
+
line(`player: version ${version} is current; this is ${getCliVersion()} \u2014 restart with npx -y @genex-ai/cli-demo@latest player run`);
|
|
23731
25814
|
return;
|
|
23732
25815
|
}
|
|
23733
|
-
|
|
23734
|
-
|
|
23735
|
-
|
|
23736
|
-
|
|
23737
|
-
|
|
25816
|
+
const outcome2 = await relaunchWatcher({
|
|
25817
|
+
version,
|
|
25818
|
+
apiUrl,
|
|
25819
|
+
sub: "run",
|
|
25820
|
+
relaunched: Boolean(opts.relaunched),
|
|
25821
|
+
log: line
|
|
23738
25822
|
});
|
|
23739
|
-
|
|
23740
|
-
|
|
23741
|
-
|
|
23742
|
-
|
|
25823
|
+
if (outcome2.reason === "supervisor_restarts") {
|
|
25824
|
+
process.exitCode = 1;
|
|
25825
|
+
return;
|
|
25826
|
+
}
|
|
25827
|
+
if (!outcome2.launched) line(`player: not relaunching (${outcome2.reason ?? "unknown"}) \u2014 still ${getCliVersion()}, current is ${version}`);
|
|
25828
|
+
return;
|
|
25829
|
+
}
|
|
25830
|
+
if (stop2.kind === "auth") {
|
|
25831
|
+
line(`player: ${PLAYER_REAUTH_REQUIRED}`);
|
|
25832
|
+
if (opts.foreground) process.exitCode = 1;
|
|
25833
|
+
return;
|
|
25834
|
+
}
|
|
25835
|
+
if (stop2.kind === "signal") line("player: stopped");
|
|
25836
|
+
}
|
|
25837
|
+
async function status(opts, log) {
|
|
25838
|
+
const record = await readPlayerInstall();
|
|
25839
|
+
const state = await readPlayerState();
|
|
25840
|
+
const runners = detectRunners();
|
|
25841
|
+
const platform = servicePlatform();
|
|
25842
|
+
const unit = await serviceInstalled(os12.homedir(), platform);
|
|
25843
|
+
const paths = playerPaths();
|
|
25844
|
+
const running = state?.pid ? pidAlive(state.pid) : null;
|
|
25845
|
+
const server = { reachable: false, policyVersion: null, minVersion: null, latestVersion: null, pending: null, error: null };
|
|
25846
|
+
if (record && !installExpired(record)) {
|
|
25847
|
+
const apiUrl = opts.apiUrl ? normalizeApiOrigin(getApiUrl(opts.apiUrl)) : record.apiUrl;
|
|
25848
|
+
bindToken(record.token, apiUrl);
|
|
25849
|
+
const client = new PlayerClient({ apiUrl, token: record.token, timeoutMs: 8e3 });
|
|
25850
|
+
try {
|
|
25851
|
+
const policy = await client.policy();
|
|
25852
|
+
server.reachable = true;
|
|
25853
|
+
server.policyVersion = policy.version ?? null;
|
|
25854
|
+
server.minVersion = policy.minVersion ?? null;
|
|
25855
|
+
server.latestVersion = policy.latestVersion ?? null;
|
|
25856
|
+
let pending = 0;
|
|
25857
|
+
for (const provider of new Set(runners.map((r) => PROVIDER_FOR_RUNNER[r]))) {
|
|
25858
|
+
pending += (await client.pending(provider)).length;
|
|
25859
|
+
}
|
|
25860
|
+
server.pending = pending;
|
|
25861
|
+
} catch (error) {
|
|
25862
|
+
server.error = error instanceof PlayerApiError ? `${error.status} ${error.code}`.trim() : error instanceof PlayerAuthError ? "credential_rejected" : error instanceof PlayerUpdateRequiredError ? "player_update_required" : "unreachable";
|
|
25863
|
+
}
|
|
23743
25864
|
}
|
|
23744
|
-
const spent = spentCredits(await readLedgerRows(cwd));
|
|
23745
|
-
const disabled = budgetDisabled(config);
|
|
23746
|
-
const enforced = !disabled && !(snapshot?.unlimited ?? false);
|
|
23747
|
-
const allowance = store?.allowanceCredits ?? null;
|
|
23748
|
-
const remaining = allowance === null ? null : Math.max(0, allowance - spent);
|
|
23749
25865
|
if (opts.json) {
|
|
23750
|
-
|
|
23751
|
-
|
|
23752
|
-
|
|
23753
|
-
|
|
23754
|
-
|
|
23755
|
-
|
|
23756
|
-
|
|
23757
|
-
|
|
23758
|
-
|
|
23759
|
-
|
|
23760
|
-
|
|
23761
|
-
|
|
23762
|
-
|
|
23763
|
-
|
|
23764
|
-
|
|
23765
|
-
|
|
23766
|
-
|
|
23767
|
-
|
|
23768
|
-
|
|
23769
|
-
|
|
23770
|
-
|
|
23771
|
-
|
|
23772
|
-
|
|
25866
|
+
const described2 = record ? describeInstall(record) : null;
|
|
25867
|
+
writeJsonLine2({
|
|
25868
|
+
command: "player status",
|
|
25869
|
+
status: record ? "ok" : "not_installed",
|
|
25870
|
+
error: record ? null : "not_installed",
|
|
25871
|
+
installed: Boolean(record),
|
|
25872
|
+
apiUrl: described2?.apiUrl ?? null,
|
|
25873
|
+
watcherId: described2?.watcherId ?? null,
|
|
25874
|
+
label: described2?.label ?? null,
|
|
25875
|
+
installedAt: described2?.installedAt ?? null,
|
|
25876
|
+
version: described2?.version ?? null,
|
|
25877
|
+
cliVersion: getCliVersion(),
|
|
25878
|
+
credentialExpiresAt: described2?.expiresAt ?? null,
|
|
25879
|
+
credentialExpired: record ? installExpired(record) : null,
|
|
25880
|
+
runners,
|
|
25881
|
+
autostart: unit,
|
|
25882
|
+
running,
|
|
25883
|
+
pid: state?.pid ?? null,
|
|
25884
|
+
startedAt: state?.startedAt ?? null,
|
|
25885
|
+
lastHeartbeatAt: state?.lastHeartbeatAt ?? null,
|
|
25886
|
+
lastPollAt: state?.lastPollAt ?? null,
|
|
25887
|
+
lastJobAt: state?.lastJobAt ?? null,
|
|
25888
|
+
jobsCompleted: state?.jobsCompleted ?? null,
|
|
25889
|
+
jobsReleased: state?.jobsReleased ?? null,
|
|
25890
|
+
lastError: state?.lastError ?? null,
|
|
25891
|
+
logFile: paths.log,
|
|
25892
|
+
server
|
|
23773
25893
|
});
|
|
23774
|
-
if (!
|
|
25894
|
+
if (!record) process.exitCode = 1;
|
|
23775
25895
|
return;
|
|
23776
25896
|
}
|
|
23777
|
-
log.plain(c.bold("genex
|
|
23778
|
-
if (
|
|
23779
|
-
log.
|
|
23780
|
-
}
|
|
23781
|
-
|
|
23782
|
-
|
|
23783
|
-
} else {
|
|
23784
|
-
log.warn(` Balance couldn't be read from ${apiUrl} \u2014 run ${c.cyan("npx genex doctor")}.`);
|
|
25897
|
+
log.plain(c.bold("genex player status"));
|
|
25898
|
+
if (!record) {
|
|
25899
|
+
log.plain("");
|
|
25900
|
+
log.plain(` ${PLAYER_NOT_INSTALLED}`);
|
|
25901
|
+
process.exitCode = 1;
|
|
25902
|
+
return;
|
|
23785
25903
|
}
|
|
23786
|
-
|
|
23787
|
-
|
|
23788
|
-
|
|
23789
|
-
|
|
23790
|
-
}
|
|
23791
|
-
|
|
23792
|
-
|
|
23793
|
-
|
|
23794
|
-
|
|
25904
|
+
const described = describeInstall(record);
|
|
25905
|
+
log.dim(` ${described.apiUrl}`);
|
|
25906
|
+
log.plain("");
|
|
25907
|
+
log.plain(` Watcher ${described.watcherId ?? "not registered yet"}${described.label ? ` \xB7 ${described.label}` : ""}`);
|
|
25908
|
+
log.plain(` Installed ${described.installedAt} by v${described.version} (running v${getCliVersion()})`);
|
|
25909
|
+
log.plain(` Approval ${installExpired(record) ? c.red("expired") : "valid"} until ${described.expiresAt}`);
|
|
25910
|
+
log.plain(` Runners ${runners.length ? runners.join(", ") : "none on PATH"}`);
|
|
25911
|
+
log.plain(` Login entry ${unit ?? "none"}`);
|
|
25912
|
+
log.plain(` Process ${running === null ? "never started here" : running ? `running (pid ${state?.pid})` : "not running"}`);
|
|
25913
|
+
if (state) {
|
|
25914
|
+
log.plain(` Last beat ${state.lastHeartbeatAt ?? "never"} poll ${state.lastPollAt ?? "never"} job ${state.lastJobAt ?? "never"}`);
|
|
25915
|
+
log.plain(` Answered ${state.jobsCompleted} \xB7 handed back ${state.jobsReleased}${state.lastError ? ` \xB7 last issue ${state.lastError}` : ""}`);
|
|
25916
|
+
}
|
|
25917
|
+
log.plain(` Server ${server.reachable ? `reachable \xB7 policy ${server.policyVersion} \xB7 needs \u2265 ${server.minVersion}, current ${server.latestVersion} \xB7 ${server.pending ?? 0} waiting` : `not reachable (${server.error ?? "unknown"})`}`);
|
|
25918
|
+
log.plain(` Log ${paths.log}`);
|
|
25919
|
+
if (installExpired(record) || server.error === "credential_rejected") process.exitCode = 1;
|
|
25920
|
+
}
|
|
25921
|
+
function pidAlive(pid) {
|
|
25922
|
+
try {
|
|
25923
|
+
process.kill(pid, 0);
|
|
25924
|
+
return true;
|
|
25925
|
+
} catch (error) {
|
|
25926
|
+
return error.code === "EPERM";
|
|
23795
25927
|
}
|
|
23796
|
-
|
|
23797
|
-
|
|
23798
|
-
|
|
25928
|
+
}
|
|
25929
|
+
async function stop(opts, log) {
|
|
25930
|
+
const platform = servicePlatform();
|
|
25931
|
+
const result = await stopService(os12.homedir(), platform);
|
|
25932
|
+
const state = await readPlayerState();
|
|
25933
|
+
let signalled = false;
|
|
25934
|
+
if (state?.pid && pidAlive(state.pid) && state.pid !== process.pid && !playerDryRun()) {
|
|
25935
|
+
try {
|
|
25936
|
+
process.kill(state.pid, "SIGTERM");
|
|
25937
|
+
signalled = true;
|
|
25938
|
+
} catch {
|
|
25939
|
+
}
|
|
23799
25940
|
}
|
|
23800
|
-
if (
|
|
23801
|
-
|
|
25941
|
+
if (opts.json) {
|
|
25942
|
+
writeJsonLine2({ command: "player stop", status: "ok", error: null, unit: result.unit, detail: result.detail, signalledPid: signalled ? state?.pid ?? null : null });
|
|
25943
|
+
return;
|
|
23802
25944
|
}
|
|
23803
|
-
|
|
23804
|
-
|
|
23805
|
-
|
|
23806
|
-
|
|
23807
|
-
|
|
23808
|
-
|
|
23809
|
-
|
|
23810
|
-
|
|
25945
|
+
log.success(`Stopped: ${result.detail}${signalled ? ` (and signalled pid ${state?.pid})` : ""}`);
|
|
25946
|
+
log.dim(` The installation is still here \u2014 start it again with ${c.cyan("genex player run")}, or remove it with ${c.cyan("genex player uninstall")}.`);
|
|
25947
|
+
}
|
|
25948
|
+
async function uninstall(opts, log) {
|
|
25949
|
+
const platform = servicePlatform();
|
|
25950
|
+
const state = await readPlayerState();
|
|
25951
|
+
if (state?.pid && pidAlive(state.pid) && state.pid !== process.pid && !playerDryRun()) {
|
|
25952
|
+
try {
|
|
25953
|
+
process.kill(state.pid, "SIGTERM");
|
|
25954
|
+
} catch {
|
|
23811
25955
|
}
|
|
23812
25956
|
}
|
|
23813
|
-
|
|
23814
|
-
|
|
23815
|
-
if (
|
|
23816
|
-
|
|
25957
|
+
const service = await uninstallService(os12.homedir(), platform);
|
|
25958
|
+
const removed = await removePlayerFiles();
|
|
25959
|
+
if (opts.json) {
|
|
25960
|
+
writeJsonLine2({ command: "player uninstall", status: "ok", error: null, unit: service.unit, detail: service.detail, removed });
|
|
25961
|
+
return;
|
|
23817
25962
|
}
|
|
23818
|
-
|
|
25963
|
+
log.success(`Removed: ${service.detail} (${path43.basename(service.unit)})`);
|
|
25964
|
+
for (const file of removed) log.dim(` deleted ${file}`);
|
|
25965
|
+
if (removed.length === 0) log.dim(" no credential was stored here");
|
|
25966
|
+
log.plain("");
|
|
25967
|
+
log.plain(" This machine can no longer answer in-game requests. The approval itself can also be");
|
|
25968
|
+
log.plain(" revoked from your Genex dashboard, which ends it everywhere at once.");
|
|
25969
|
+
}
|
|
25970
|
+
function fail3(opts, log, command, message, extra = {}) {
|
|
25971
|
+
if (opts.json) writeJsonLine2({ command, status: "failed", error: message, ...extra });
|
|
25972
|
+
else log.error(message);
|
|
25973
|
+
process.exitCode = 1;
|
|
25974
|
+
}
|
|
25975
|
+
function writeJsonLine2(value) {
|
|
25976
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
25977
|
+
`);
|
|
23819
25978
|
}
|
|
23820
25979
|
|
|
23821
25980
|
// src/lib/costs.ts
|
|
@@ -23879,6 +26038,27 @@ ${c.bold("Usage")}
|
|
|
23879
26038
|
the asset table against it. --assets <credits>
|
|
23880
26039
|
--user-approved raises it, only once the player
|
|
23881
26040
|
has agreed to the number.
|
|
26041
|
+
genex llm <sub> [options] In-game model calls: models | bench | price.
|
|
26042
|
+
"models" says whether this stand serves the
|
|
26043
|
+
runtime LLM lane at all, and at what provider
|
|
26044
|
+
rates. "bench" runs the real model on YOUR OWN
|
|
26045
|
+
coin and reports what each attempt was charged;
|
|
26046
|
+
it needs --max-coins <n> --user-approved, like
|
|
26047
|
+
any spend the player has to agree to. "price"
|
|
26048
|
+
reprints the last run's recommended
|
|
26049
|
+
estimateCoins. Declare a game's price from a
|
|
26050
|
+
bench, never from a guess: a started attempt is
|
|
26051
|
+
charged in full, including one that fails.
|
|
26052
|
+
genex player <sub> [options] Answer the in-game model requests you approve, on your
|
|
26053
|
+
OWN Claude or ChatGPT subscription: install | run |
|
|
26054
|
+
status | stop | uninstall. "install" signs this machine
|
|
26055
|
+
in (a player-scoped approval that can spend nothing),
|
|
26056
|
+
finds the official claude / codex CLI you already
|
|
26057
|
+
use, and starts a small background program at login.
|
|
26058
|
+
It never reads, stores or forwards your Claude or
|
|
26059
|
+
ChatGPT sign-in \u2014 it only starts the CLI you installed.
|
|
26060
|
+
Approve a request in a game and the answer comes back
|
|
26061
|
+
on its own; nothing to paste, ever again.
|
|
23882
26062
|
genex auth [options] Connect this machine to your Genex account, or
|
|
23883
26063
|
finish a sign-in another command started \u2014 it
|
|
23884
26064
|
resumes the same code, so an interrupted
|
|
@@ -24301,6 +26481,12 @@ ${c.bold("Examples")}
|
|
|
24301
26481
|
genex shop set sku_123 --icon https://assets.genex.technology/generations/abc123/image-main
|
|
24302
26482
|
genex shop test sku_123
|
|
24303
26483
|
genex shop remove sku_123
|
|
26484
|
+
genex llm models
|
|
26485
|
+
genex llm bench "Reply with one short taunt." --samples 5 --max-coins 20 --user-approved
|
|
26486
|
+
genex llm price
|
|
26487
|
+
genex player install
|
|
26488
|
+
genex player status --json
|
|
26489
|
+
genex player uninstall
|
|
24304
26490
|
genex publish --no-push --title "My Game"
|
|
24305
26491
|
genex model "weathered wooden barrel with iron bands"
|
|
24306
26492
|
genex model --image ./concept.png
|
|
@@ -24530,6 +26716,73 @@ ${c.bold("How the allowance works")}
|
|
|
24530
26716
|
and GENEX_ASSET_BUDGET_CAP (credits; 0 turns the budget off).
|
|
24531
26717
|
|
|
24532
26718
|
Every number printed is live from your account; prices can change without a deploy.
|
|
26719
|
+
`,
|
|
26720
|
+
llm: `${c.bold("genex llm")} \u2014 in-game model calls: is the lane live, what does one call cost, what should the game declare?
|
|
26721
|
+
|
|
26722
|
+
${c.bold("Usage")}
|
|
26723
|
+
genex llm models [--json] Which models this stand serves, their provider rates per
|
|
26724
|
+
million tokens, and the headroom it recommends over a
|
|
26725
|
+
benchmark. Off on this stand: it says so and exits 0 \u2014
|
|
26726
|
+
build the feature without a model rather than promising
|
|
26727
|
+
a 404.
|
|
26728
|
+
genex llm bench "<prompt>" --max-coins <n> --user-approved [options]
|
|
26729
|
+
Run the real model <n> times through the development
|
|
26730
|
+
lane and report what each attempt was CHARGED. Refused
|
|
26731
|
+
without both flags, before any network call: this spends
|
|
26732
|
+
COIN, a wallet the build's asset allowance cannot see.
|
|
26733
|
+
Needs a folder linked to a game you own.
|
|
26734
|
+
genex llm price [--json] The recommendation from the last bench in this folder.
|
|
26735
|
+
Reads a file; spends nothing.
|
|
26736
|
+
|
|
26737
|
+
${c.bold("Bench options")}
|
|
26738
|
+
--model <id> One of the ids \`genex llm models\` prints (default: the first one).
|
|
26739
|
+
--samples <n> How many real attempts (default ${DEFAULT_BENCH_SAMPLES}). More samples, better p95.
|
|
26740
|
+
--schema <file> A JSON file holding the output schema; implies JSON output.
|
|
26741
|
+
--json-output Ask for JSON without a schema. --text Ask for text (the default).
|
|
26742
|
+
--max-coins <n> The per-attempt ceiling the player approved. The run's worst case is
|
|
26743
|
+
that number times --samples, and it is printed before anything starts.
|
|
26744
|
+
--timeout <seconds> Bounds the wait for each attempt.
|
|
26745
|
+
--json One machine-readable object; every key is always present, null when
|
|
26746
|
+
the CLI could not learn it.
|
|
26747
|
+
|
|
26748
|
+
${c.bold("Why a benchmark and not an estimate")}
|
|
26749
|
+
\`estimateCoins\` is a PRICE, not a guess: declaring it charges it, in full, even when the
|
|
26750
|
+
attempt fails, is cancelled, or stops at its budget. And three layers sit between the
|
|
26751
|
+
provider's rate card and that number \u2014 the provider's cost, the platform's tariff (already
|
|
26752
|
+
inside every charged coin), and your headroom. Adding a margin to the provider's rate
|
|
26753
|
+
silently drops the tariff and under-prices every call, so the recommendation is built from
|
|
26754
|
+
CHARGED COINS and the headroom comes from the server. A stand that serves no headroom gets
|
|
26755
|
+
no recommendation, because the multiplier is not the CLI's to invent.
|
|
26756
|
+
|
|
26757
|
+
Results land in ${LLM_BENCH_FILE} \u2014 gitignored, machine-local, re-read by \`genex llm price\`.
|
|
26758
|
+
`,
|
|
26759
|
+
player: `${c.bold("genex player")} \u2014 answer the in-game model requests you approve, on your own Claude or ChatGPT subscription.
|
|
26760
|
+
|
|
26761
|
+
${c.bold("Usage")}
|
|
26762
|
+
genex player install [--label <name>] [--no-autostart] [--dry-run] [--json]
|
|
26763
|
+
Sign this machine in, find the official CLI you already
|
|
26764
|
+
use, and start a small background program at login.
|
|
26765
|
+
genex player run [--once] [--foreground]
|
|
26766
|
+
The loop itself: poll, take one request, run it, send the
|
|
26767
|
+
answer back. Started for you by \`install\`; run it yourself
|
|
26768
|
+
with --foreground to watch it work.
|
|
26769
|
+
genex player status [--json] What is installed here, what the server says, and when this
|
|
26770
|
+
machine last spoke to it.
|
|
26771
|
+
genex player stop Stop the background program; keep the installation.
|
|
26772
|
+
genex player uninstall Stop it, remove the login entry, delete the approval.
|
|
26773
|
+
|
|
26774
|
+
${c.bold("What it does and does not touch")}
|
|
26775
|
+
It NEVER reads, stores, forwards or intermediates your Claude or ChatGPT sign-in \u2014 no token,
|
|
26776
|
+
no keychain entry, no session file. It starts the official CLI you installed and signed into,
|
|
26777
|
+
hands it the request on stdin with no tools and an empty scratch directory, and reads what it
|
|
26778
|
+
prints. Genex never sees your vendor account.
|
|
26779
|
+
|
|
26780
|
+
Its GENEX approval is a player-scoped one: it can answer requests you already approved and can
|
|
26781
|
+
spend nothing \u2014 no coins, no credits, no account changes. It lives in ~/.genex/player.json,
|
|
26782
|
+
separate from your creator sign-in, and the dashboard can revoke it at any time.
|
|
26783
|
+
|
|
26784
|
+
The log under ~/.genex/logs/ carries ids, timings and outcomes \u2014 never what a game asked for
|
|
26785
|
+
and never what the model answered.
|
|
24533
26786
|
`,
|
|
24534
26787
|
remix: `genex remix <slug-or-Genex-page-URL> [directory] [--preview | --source-only] [--json]
|
|
24535
26788
|
|
|
@@ -24714,6 +26967,16 @@ function parseArgs(argv) {
|
|
|
24714
26967
|
"--limit",
|
|
24715
26968
|
// `genex budget --assets <credits>` — the allowance the player approved.
|
|
24716
26969
|
"--assets",
|
|
26970
|
+
// `genex llm bench` value flags. `--max-coins` is the COIN half of the
|
|
26971
|
+
// same approval shape as `--assets` (a different economy, its own gate).
|
|
26972
|
+
"--model",
|
|
26973
|
+
"--samples",
|
|
26974
|
+
"--schema",
|
|
26975
|
+
"--max-coins",
|
|
26976
|
+
// `genex player install --label <name>` — what this machine is called in
|
|
26977
|
+
// the dashboard. Its own key rather than the generic 2nd positional,
|
|
26978
|
+
// which `player` spends on the subcommand.
|
|
26979
|
+
"--label",
|
|
24717
26980
|
// `genex motion` value flags.
|
|
24718
26981
|
"--takes",
|
|
24719
26982
|
"--seeds",
|
|
@@ -24791,6 +27054,33 @@ function parseArgs(argv) {
|
|
|
24791
27054
|
case "--user-approved":
|
|
24792
27055
|
parsed.options.userApproved = true;
|
|
24793
27056
|
break;
|
|
27057
|
+
// `genex player` switches.
|
|
27058
|
+
case "--no-autostart":
|
|
27059
|
+
parsed.options.noAutostart = true;
|
|
27060
|
+
break;
|
|
27061
|
+
case "--once":
|
|
27062
|
+
parsed.options.once = true;
|
|
27063
|
+
break;
|
|
27064
|
+
case "--foreground":
|
|
27065
|
+
parsed.options.foreground = true;
|
|
27066
|
+
break;
|
|
27067
|
+
// Set by a watcher that re-executed ITSELF on a newer version, so the
|
|
27068
|
+
// child cannot relaunch again and spin (lib/player/relaunch.ts).
|
|
27069
|
+
case "--relaunched":
|
|
27070
|
+
parsed.options.relaunched = true;
|
|
27071
|
+
break;
|
|
27072
|
+
case "--dry-run":
|
|
27073
|
+
parsed.options.dryRun = true;
|
|
27074
|
+
break;
|
|
27075
|
+
// `genex llm bench` output shape. Named `--json-output` because `--json`
|
|
27076
|
+
// already means "print one machine-readable object" everywhere in this
|
|
27077
|
+
// CLI, and the model's answer format is a different question.
|
|
27078
|
+
case "--json-output":
|
|
27079
|
+
parsed.options.jsonOutput = true;
|
|
27080
|
+
break;
|
|
27081
|
+
case "--text":
|
|
27082
|
+
parsed.options.textOutput = true;
|
|
27083
|
+
break;
|
|
24794
27084
|
case "--direct-text":
|
|
24795
27085
|
parsed.options.directText = true;
|
|
24796
27086
|
break;
|
|
@@ -24948,6 +27238,8 @@ function parseArgs(argv) {
|
|
|
24948
27238
|
} else {
|
|
24949
27239
|
(parsed.options.selectors ??= []).push(arg);
|
|
24950
27240
|
}
|
|
27241
|
+
} else if (parsed.command === "llm") {
|
|
27242
|
+
parsed.options.benchPrompt = parsed.options.benchPrompt ? `${parsed.options.benchPrompt} ${arg}` : arg;
|
|
24951
27243
|
} else if (parsed.command === "shop") {
|
|
24952
27244
|
if (!parsed.options.hostname) parsed.options.hostname = arg;
|
|
24953
27245
|
else {
|
|
@@ -25173,6 +27465,31 @@ function applyValueFlag(options, flag, value) {
|
|
|
25173
27465
|
options.assets = n;
|
|
25174
27466
|
break;
|
|
25175
27467
|
}
|
|
27468
|
+
case "--model":
|
|
27469
|
+
options.modelId = value;
|
|
27470
|
+
break;
|
|
27471
|
+
case "--label":
|
|
27472
|
+
options.playerLabel = value;
|
|
27473
|
+
break;
|
|
27474
|
+
case "--schema":
|
|
27475
|
+
options.schemaPath = value;
|
|
27476
|
+
break;
|
|
27477
|
+
case "--samples": {
|
|
27478
|
+
const n = Number(value);
|
|
27479
|
+
if (!Number.isInteger(n) || n < 1 || n > 25) {
|
|
27480
|
+
throw new Error(`Invalid --samples value: ${value} (expected 1-25)`);
|
|
27481
|
+
}
|
|
27482
|
+
options.samples = n;
|
|
27483
|
+
break;
|
|
27484
|
+
}
|
|
27485
|
+
case "--max-coins": {
|
|
27486
|
+
const n = Number(value);
|
|
27487
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
27488
|
+
throw new Error(`Invalid --max-coins value: ${value} (whole coin, 1 or more)`);
|
|
27489
|
+
}
|
|
27490
|
+
options.maxCoins = n;
|
|
27491
|
+
break;
|
|
27492
|
+
}
|
|
25176
27493
|
case "--timeout": {
|
|
25177
27494
|
const n = Number(value);
|
|
25178
27495
|
if (!Number.isFinite(n) || n <= 0) {
|
|
@@ -25384,9 +27701,11 @@ async function main() {
|
|
|
25384
27701
|
return;
|
|
25385
27702
|
}
|
|
25386
27703
|
const isBlenderMcp = parsed.command === "blender" && parsed.options.name === "mcp";
|
|
25387
|
-
const
|
|
27704
|
+
const isPlayerLoop = parsed.command === "player" && (parsed.options.name ?? "status") === "run";
|
|
27705
|
+
const quietUpdates = Boolean(parsed.options.quiet || parsed.options.json || isBlenderMcp || isPlayerLoop);
|
|
25388
27706
|
const updateLog = createLogger({ quiet: quietUpdates });
|
|
25389
|
-
|
|
27707
|
+
const ownsItsOwnInstall = parsed.command === "init" || parsed.command === "tools" || parsed.command === "remix";
|
|
27708
|
+
if (!ownsItsOwnInstall && parsed.command !== "player") {
|
|
25390
27709
|
await syncSkills(updateLog, workspace);
|
|
25391
27710
|
}
|
|
25392
27711
|
const updateCheck = startUpdateCheck();
|
|
@@ -25459,6 +27778,20 @@ async function main() {
|
|
|
25459
27778
|
case "budget":
|
|
25460
27779
|
await runBudget(parsed.options);
|
|
25461
27780
|
break;
|
|
27781
|
+
// The in-game model lane: is it live here, what does one call really
|
|
27782
|
+
// cost, what should the game declare. `bench` is the only CLI command
|
|
27783
|
+
// that spends COIN, so it carries its own --max-coins/--user-approved
|
|
27784
|
+
// gate — the credits allowance cannot see that wallet.
|
|
27785
|
+
case "llm":
|
|
27786
|
+
await runLlm(parsed.options);
|
|
27787
|
+
break;
|
|
27788
|
+
// The PLAYER's own program, not a builder's: it answers the in-game
|
|
27789
|
+
// model requests they approved by running the official `claude` /
|
|
27790
|
+
// `codex` CLI they signed into themselves, and never touches a vendor
|
|
27791
|
+
// credential. The second positional is the subcommand.
|
|
27792
|
+
case "player":
|
|
27793
|
+
await runPlayer(parsed.options);
|
|
27794
|
+
break;
|
|
25462
27795
|
case "auth":
|
|
25463
27796
|
await runAuth(parsed.options);
|
|
25464
27797
|
break;
|