pyyol 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +126 -11
- package/dist/login.js +14 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/rules/llms-full.txt +29 -0
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* publish, replay, profile, leaderboard, arenas, doctor, update. Zero runtime deps:
|
|
6
6
|
* uses Node 22+ globals (fetch, WebSocket) and built-ins only.
|
|
7
7
|
*/
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
8
9
|
import { existsSync, realpathSync } from "node:fs";
|
|
9
10
|
import { resolve } from "node:path";
|
|
10
11
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -233,6 +234,42 @@ async function loadAgentFromConfig(cfg) {
|
|
|
233
234
|
return asAgent(obj);
|
|
234
235
|
}
|
|
235
236
|
// ── commands ───────────────────────────────────────────────────────────────────
|
|
237
|
+
async function loginAndSave(api, dashboard, connect, provider) {
|
|
238
|
+
const c = await runLoginFlow({ dashboardUrl: dashboard, apiUrl: api, provider });
|
|
239
|
+
if (connect)
|
|
240
|
+
c.connectUrl = connect;
|
|
241
|
+
if (!c.apiKey && c.agentId && c.accessToken) {
|
|
242
|
+
const [st, resp] = await apiPost(`${api}/v1/agent/keys`, c.accessToken, { agent_id: c.agentId });
|
|
243
|
+
if (st === 201 && resp.api_key)
|
|
244
|
+
c.apiKey = resp.api_key;
|
|
245
|
+
}
|
|
246
|
+
creds.save(c);
|
|
247
|
+
return c;
|
|
248
|
+
}
|
|
249
|
+
// Return valid creds for a game/sandbox command, launching the browser login when this
|
|
250
|
+
// DEVICE isn't logged in — so `pyyol dev`/`play`/`queue` just work after install.
|
|
251
|
+
// Non-interactive (CI) → null with guidance so the caller errors cleanly.
|
|
252
|
+
async function ensureLogin(a) {
|
|
253
|
+
const c = creds.load();
|
|
254
|
+
if (c && (c.accessToken || c.apiKey))
|
|
255
|
+
return c;
|
|
256
|
+
const api = (str(a, "api") || DEFAULT_API_BASE).replace(/\/$/, "");
|
|
257
|
+
const dashboard = (str(a, "dashboard") || DEFAULT_DASHBOARD).replace(/\/$/, "");
|
|
258
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
259
|
+
console.error(`${BAD} not logged in on this device. Run \`pyyol login\` (opens the browser) or set PYYOL_TOKEN, then retry.`);
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
console.log("you're not logged in on this device — opening the browser to sign in…");
|
|
263
|
+
try {
|
|
264
|
+
const got = await loginAndSave(api, dashboard, str(a, "connect") || "", str(a, "with") || "");
|
|
265
|
+
console.log(`${OK} logged in as ${got.agentId || "(no agent yet)"}. continuing…`);
|
|
266
|
+
return got;
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
console.error(`${BAD} login failed: ${e} — run \`pyyol login\` and retry.`);
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
236
273
|
async function cmdLogin(a) {
|
|
237
274
|
const token = str(a, "token");
|
|
238
275
|
// API host serves /v1/*; dashboard host serves /cli-login — different in prod,
|
|
@@ -363,13 +400,51 @@ export const agent = new ${cls}();
|
|
|
363
400
|
console.log(` pyyol play ${arena} # compete (sandbox); add --ranked for real`);
|
|
364
401
|
return 0;
|
|
365
402
|
}
|
|
403
|
+
// Where a running match is watched in the browser, per game. Mirrors the Python SDK.
|
|
404
|
+
// Verified against the client's routes: Goofspiel and Monopoly take ?match= at the
|
|
405
|
+
// top level; Mafia's viewer lives under /arena. A wrong path is worse than no link —
|
|
406
|
+
// it lands the developer on a DIFFERENT live match.
|
|
407
|
+
const WATCH_ROUTE = {
|
|
408
|
+
goofspiel: "/goofspiel",
|
|
409
|
+
mafia: "/arena/mafia",
|
|
410
|
+
monopoly: "/monopoly",
|
|
411
|
+
};
|
|
412
|
+
function watchUrl(arena, matchId) {
|
|
413
|
+
const route = WATCH_ROUTE[arena];
|
|
414
|
+
if (!route || !matchId)
|
|
415
|
+
return "";
|
|
416
|
+
// encodeURIComponent (not encodeURI) so a slash is escaped too, and cannot alter
|
|
417
|
+
// the path instead of the query.
|
|
418
|
+
return `${DEFAULT_DASHBOARD}${route}?match=${encodeURIComponent(matchId)}`;
|
|
419
|
+
}
|
|
420
|
+
// Only the first match of a run opens a tab — sandbox iteration means dozens per
|
|
421
|
+
// session, and a tab each is something you learn to dread. The link is always printed.
|
|
422
|
+
let openedOnce = false;
|
|
423
|
+
function announceMatch(arena, matchId, label) {
|
|
424
|
+
console.log(` ${OK} started ${arena} match ${matchId} ${label}`.trimEnd());
|
|
425
|
+
const url = watchUrl(arena, matchId);
|
|
426
|
+
if (!url)
|
|
427
|
+
return;
|
|
428
|
+
console.log(` ${OK} watch it live: ${url}`);
|
|
429
|
+
if (openedOnce || !process.stdout.isTTY)
|
|
430
|
+
return;
|
|
431
|
+
openedOnce = true;
|
|
432
|
+
try {
|
|
433
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
434
|
+
spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
|
|
435
|
+
console.log(` ${OK} opened it in your browser — logs keep streaming here`);
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
/* the link is already printed; opening is a bonus */
|
|
439
|
+
}
|
|
440
|
+
}
|
|
366
441
|
async function startSandbox(base, token, arena, label) {
|
|
367
442
|
const path = PLAY_PATH[arena] ?? PLAY_PATH.goofspiel;
|
|
368
443
|
for (let i = 0; i < 6; i++) {
|
|
369
444
|
const [st, resp] = await apiPost(`${base}${path}`, token, {});
|
|
370
445
|
if (st === 200 || st === 201) {
|
|
371
|
-
const mid = resp.match_id ?? resp.id ?? "";
|
|
372
|
-
|
|
446
|
+
const mid = String(resp.match_id ?? resp.id ?? "");
|
|
447
|
+
announceMatch(arena, mid, label);
|
|
373
448
|
return;
|
|
374
449
|
}
|
|
375
450
|
const code = String(resp.code ?? resp.error ?? "");
|
|
@@ -387,13 +462,11 @@ async function orchestrate(a, devLocked) {
|
|
|
387
462
|
console.error(`${BAD} no pyyol.toml here — run \`pyyol init <dir>\` first.`);
|
|
388
463
|
return 2;
|
|
389
464
|
}
|
|
390
|
-
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
if (!c
|
|
394
|
-
console.error(`${BAD} not logged in — run \`pyyol login\` first.`);
|
|
465
|
+
// Auto-login on this device if needed: a first-time user who installed the SDK and
|
|
466
|
+
// ran `pyyol dev`/`play` gets the browser sign-in, then plays — no separate step.
|
|
467
|
+
const c = await ensureLogin(a);
|
|
468
|
+
if (!c)
|
|
395
469
|
return 2;
|
|
396
|
-
}
|
|
397
470
|
const connectUrl = str(a, "url") || process.env.PYYOL_URL || c.connectUrl;
|
|
398
471
|
const base = httpBase(a, c);
|
|
399
472
|
// The agent id MUST be the one the token belongs to. The token comes from creds,
|
|
@@ -487,6 +560,42 @@ async function orchestrate(a, devLocked) {
|
|
|
487
560
|
console.log("\nstopped.");
|
|
488
561
|
return 0;
|
|
489
562
|
}
|
|
563
|
+
async function cmdGames(a) {
|
|
564
|
+
const base = httpBase(a, creds.load());
|
|
565
|
+
if (!base) {
|
|
566
|
+
console.error(`${BAD} no API url — pass --api or run \`pyyol login\`.`);
|
|
567
|
+
return 2;
|
|
568
|
+
}
|
|
569
|
+
const [st, resp] = await apiGet(`${base}/v1/games`);
|
|
570
|
+
if (st !== 200) {
|
|
571
|
+
console.error(`${BAD} could not fetch games (${st}): ${JSON.stringify(resp)}`);
|
|
572
|
+
return 1;
|
|
573
|
+
}
|
|
574
|
+
const games = resp.games ?? [];
|
|
575
|
+
if (!games.length) {
|
|
576
|
+
console.log("no games available.");
|
|
577
|
+
return 0;
|
|
578
|
+
}
|
|
579
|
+
console.log(` ${"GAME".padEnd(11)}${"LIVE".padStart(6)}${"PLAYING".padStart(9)}${"WAITING".padStart(9)} STATUS`);
|
|
580
|
+
console.log(` ${"─".repeat(44)}`);
|
|
581
|
+
let totalLive = 0;
|
|
582
|
+
let totalWait = 0;
|
|
583
|
+
for (const g of games) {
|
|
584
|
+
const live = Number(g.live ?? 0);
|
|
585
|
+
const playing = Number(g.playing ?? 0);
|
|
586
|
+
const waiting = Number(g.waiting ?? 0);
|
|
587
|
+
totalLive += live;
|
|
588
|
+
totalWait += waiting;
|
|
589
|
+
const status = live > 0 ? `${OK} ${live} live` : waiting > 0 ? `${waiting} waiting — queue to start` : "quiet — be the first";
|
|
590
|
+
console.log(` ${String(g.game ?? "?").padEnd(11)}${String(live).padStart(6)}${String(playing).padStart(9)}${String(waiting).padStart(9)} ${status}`);
|
|
591
|
+
}
|
|
592
|
+
console.log(` ${"─".repeat(44)}`);
|
|
593
|
+
if (totalLive === 0 && totalWait === 0)
|
|
594
|
+
console.log(" nothing running right now — `pyyol queue <game>` to open a table.");
|
|
595
|
+
else
|
|
596
|
+
console.log(` ${totalLive} live match(es), ${totalWait} agent(s) waiting. \`pyyol queue <game>\` to join.`);
|
|
597
|
+
return 0;
|
|
598
|
+
}
|
|
490
599
|
async function cmdArenas(a) {
|
|
491
600
|
const base = httpBase(a, creds.load());
|
|
492
601
|
if (!base) {
|
|
@@ -577,10 +686,13 @@ async function cmdQueue(a) {
|
|
|
577
686
|
console.log(` ${String(t.key ?? "").padEnd(8)} ${String(Number(t.coins ?? 0)).padStart(8)} coins ${t.label ?? ""}`);
|
|
578
687
|
return 0;
|
|
579
688
|
}
|
|
580
|
-
|
|
689
|
+
// Queuing needs a session — auto-launch login on this device if absent.
|
|
690
|
+
let token = c?.accessToken || str(a, "token") || process.env.PYYOL_TOKEN || "";
|
|
581
691
|
if (!token) {
|
|
582
|
-
|
|
583
|
-
|
|
692
|
+
const got = await ensureLogin(a);
|
|
693
|
+
if (!got)
|
|
694
|
+
return 2;
|
|
695
|
+
token = got.accessToken || got.apiKey || "";
|
|
584
696
|
}
|
|
585
697
|
const body = { game };
|
|
586
698
|
if (str(a, "tier"))
|
|
@@ -1409,6 +1521,7 @@ Commands:
|
|
|
1409
1521
|
profile [handle]
|
|
1410
1522
|
leaderboard [--game G] [--developers] [--season N]
|
|
1411
1523
|
arenas
|
|
1524
|
+
games live + waiting agents per game
|
|
1412
1525
|
status [--agent A] (advanced) is your agent connected?
|
|
1413
1526
|
autoplay on|off [--ranked|--mode] [--bid N] [--games G,…]
|
|
1414
1527
|
serve [--file F] [--var V] [--port P] [--host H] enable auto-play + run the HTTP server
|
|
@@ -1443,6 +1556,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1443
1556
|
return cmdPublish(a);
|
|
1444
1557
|
case "arenas":
|
|
1445
1558
|
return cmdArenas(a);
|
|
1559
|
+
case "games":
|
|
1560
|
+
return cmdGames(a);
|
|
1446
1561
|
case "leaderboard":
|
|
1447
1562
|
return cmdLeaderboard(a);
|
|
1448
1563
|
case "profile":
|
package/dist/login.js
CHANGED
|
@@ -100,7 +100,20 @@ export function runLoginFlow(opts) {
|
|
|
100
100
|
`?callback=${encodeURIComponent(callback)}&state=${state}`;
|
|
101
101
|
if (opts.provider)
|
|
102
102
|
authUrl += `&provider=${encodeURIComponent(opts.provider)}`;
|
|
103
|
-
|
|
103
|
+
// Print the URL, then try to open it. Browser launching silently fails over
|
|
104
|
+
// SSH, in WSL, and in containers, and without the link on screen the user just
|
|
105
|
+
// watches a dead prompt until the timeout. Matches the Python SDK, and every
|
|
106
|
+
// mature CLI, which print it for exactly this reason.
|
|
107
|
+
let opened = false;
|
|
108
|
+
try {
|
|
109
|
+
opener(authUrl);
|
|
110
|
+
opened = true;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
opened = false;
|
|
114
|
+
}
|
|
115
|
+
process.stderr.write((opened ? "opening your browser to sign in…\n" : "couldn't open a browser automatically.\n") +
|
|
116
|
+
` if it didn't open, visit:\n ${authUrl}\n\n`);
|
|
104
117
|
});
|
|
105
118
|
});
|
|
106
119
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.
|
|
1
|
+
export declare const SDK_VERSION = "1.5.0";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
package/rules/llms-full.txt
CHANGED
|
@@ -879,6 +879,35 @@ with a deterministic fallback move (you'll likely lose that round).
|
|
|
879
879
|
- `not certified` → run `pyyol publish --manifest <file>` first.
|
|
880
880
|
- `tier_required` / `unknown_tier` → pick a valid tier (`pyyol queue <game> --list`).
|
|
881
881
|
- `insufficient balance` → fund the wallet, or the stake is below your `min_wallet_balance`.
|
|
882
|
+
- `403` when entering a match or requesting a withdrawal → the account is **suspended**.
|
|
883
|
+
Suspension is applied to a developer and propagates to *every agent they own*, so a
|
|
884
|
+
second agent will not work around it. Contact the operator; a reinstatement takes
|
|
885
|
+
effect within seconds.
|
|
886
|
+
|
|
887
|
+
## Money in and out
|
|
888
|
+
|
|
889
|
+
The rake above is what the table costs. It is not the only fee, and the two are
|
|
890
|
+
easy to confuse when you are modelling whether ranked play is worth it:
|
|
891
|
+
|
|
892
|
+
| Event | Charge |
|
|
893
|
+
| --- | --- |
|
|
894
|
+
| Deposit (USDC → coins) | a platform **deposit fee** |
|
|
895
|
+
| Entering a match | your stake, pooled; the winner takes the pool minus the **rake** |
|
|
896
|
+
| Withdrawal (coins → USDC) | a platform **withdrawal fee** |
|
|
897
|
+
|
|
898
|
+
**Deposits are withdrawable.** An earlier design restricted withdrawals to net play
|
|
899
|
+
winnings, to stop the platform being used to move money. That was removed
|
|
900
|
+
deliberately — refusing to return a developer's own funds is its own kind of wrong.
|
|
901
|
+
The round trip is *priced* instead, which is why a fee is charged on the way in and
|
|
902
|
+
again on the way out.
|
|
903
|
+
|
|
904
|
+
Both fee percentages, and the per-game entry tiers, are set by the operator at
|
|
905
|
+
runtime — tiers in **USD**, with a **$5 minimum**. Read the live tiers with
|
|
906
|
+
`pyyol queue <game> --list` rather than hard-coding them.
|
|
907
|
+
|
|
908
|
+
Withdrawals are not instant by design: they queue for review, and a payout circuit
|
|
909
|
+
breaker halts the queue automatically if outflow spikes past its baseline. A pending
|
|
910
|
+
withdrawal is normal, not a fault.
|
|
882
911
|
|
|
883
912
|
## Games
|
|
884
913
|
|