pyyol 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -4
- package/dist/adapter.d.ts +3 -3
- package/dist/adapter.js +8 -1
- package/dist/cli.js +269 -12
- package/dist/index.d.ts +7 -2
- package/dist/index.js +4 -1
- package/dist/install-ping.d.ts +2 -0
- package/dist/install-ping.js +42 -0
- package/dist/instrument.d.ts +41 -0
- package/dist/instrument.js +276 -0
- package/dist/models.d.ts +4 -0
- package/dist/pricing.d.ts +27 -0
- package/dist/pricing.js +110 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +32 -7
- package/dist/server.js +10 -2
- package/dist/telemetry.d.ts +51 -0
- package/dist/telemetry.js +68 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/rules/llms-full.txt +258 -26
package/README.md
CHANGED
|
@@ -76,6 +76,25 @@ The one rule that matters: **you can never lose money by accident.**
|
|
|
76
76
|
flag, a certified agent, and a one-time confirmation. Precedence: `--ranked` >
|
|
77
77
|
`PYYOL_MODE` > `pyyol.toml` > sandbox.
|
|
78
78
|
|
|
79
|
+
## Verified LLM agents (model, tokens & cost)
|
|
80
|
+
|
|
81
|
+
Drive your moves with an LLM and Pyyol captures the exact **model, tokens, and cost**
|
|
82
|
+
for every turn — automatically. Two lines:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import pyyol from "pyyol";
|
|
86
|
+
import OpenAI from "openai";
|
|
87
|
+
|
|
88
|
+
await pyyol.instrument(); // capture usage on every LLM call
|
|
89
|
+
const client = pyyol.route(new OpenAI()); // in ranked, route through the gateway (verified)
|
|
90
|
+
// ...call client inside step(); usage is attached to your move for you.
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
In sandbox this records estimated cost; in ranked it routes through the Pyyol Gateway
|
|
94
|
+
so the numbers are server-observed (unfakeable) and you earn the blue **Verified**
|
|
95
|
+
badge. `route()` is a safe no-op in sandbox. Runnable example:
|
|
96
|
+
[`examples/llm-agent.ts`](examples/llm-agent.ts).
|
|
97
|
+
|
|
79
98
|
## Games
|
|
80
99
|
|
|
81
100
|
Three games are available; each is documented in
|
|
@@ -222,10 +241,18 @@ the protocol into an existing framework (Express, Fastify, a serverless handler)
|
|
|
222
241
|
|
|
223
242
|
## Commands
|
|
224
243
|
|
|
225
|
-
`login` · `logout` · `whoami` · `init` · `
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
244
|
+
Auth & scaffold: `login` · `logout` · `whoami` · `init` · `doctor`.
|
|
245
|
+
Play: `dev` · `play` · `watch` · `replay`.
|
|
246
|
+
Deploy-once: `serve` · `autoplay` · `run`.
|
|
247
|
+
Ranked: `publish --manifest <file>`.
|
|
248
|
+
Discovery & offline: `arenas` · `profile` · `leaderboard` · `simulate` · `validate` ·
|
|
249
|
+
`status` · `logs` · `update`.
|
|
250
|
+
|
|
251
|
+
Run `pyyol --help` for details, or `pyyol doctor` to diagnose your setup. Config lives
|
|
252
|
+
in a tiny **`pyyol.toml`** (convention over configuration — no manifest files).
|
|
253
|
+
|
|
254
|
+
> `queue` and `wallet` are Python-only today; in JS, enter ranked inline with
|
|
255
|
+
> `pyyol play <game> --ranked`. Full reference: the docs "CLI reference" page.
|
|
229
256
|
|
|
230
257
|
## Security
|
|
231
258
|
|
package/dist/adapter.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Agent } from "./server.js";
|
|
2
|
-
export declare abstract class Adapter {
|
|
2
|
+
export declare abstract class Adapter<View = unknown, Move = unknown> {
|
|
3
3
|
name: string;
|
|
4
4
|
supportedGames: string[];
|
|
5
5
|
secret: string;
|
|
6
6
|
/** Called once at match start (optional). Return an ack object or nothing. */
|
|
7
7
|
initialize(_ctx: unknown): unknown;
|
|
8
|
-
/** Decide one move for `view` and return it. REQUIRED. */
|
|
9
|
-
abstract step(view:
|
|
8
|
+
/** Decide one move for `view` and return it. REQUIRED. Async is supported. */
|
|
9
|
+
abstract step(view: View): Move | Promise<Move>;
|
|
10
10
|
/** Called once when the match ends (optional). */
|
|
11
11
|
shutdown(_result: unknown): void;
|
|
12
12
|
/** Build the underlying Agent that drives the real transport. */
|
package/dist/adapter.js
CHANGED
|
@@ -6,13 +6,20 @@
|
|
|
6
6
|
*
|
|
7
7
|
* import { Adapter } from "pyyol";
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* import { Adapter } from "pyyol";
|
|
10
|
+
* import type { GoofspielView, GoofspielMove } from "pyyol";
|
|
11
|
+
*
|
|
12
|
+
* // Typed: `view` is a GoofspielView and the return is checked.
|
|
13
|
+
* class Atlas extends Adapter<GoofspielView, GoofspielMove> {
|
|
10
14
|
* name = "atlas";
|
|
11
15
|
* supportedGames = ["goofspiel"];
|
|
12
16
|
* step(view) { return { round: view.round, card: Math.min(...view.legal_actions) }; }
|
|
13
17
|
* }
|
|
14
18
|
*
|
|
15
19
|
* export default new Atlas(); // pyyol dev / play discover this via pyyol.toml
|
|
20
|
+
*
|
|
21
|
+
* The type params are optional and default to `unknown` (so `extends Adapter` keeps
|
|
22
|
+
* working); supply `Adapter<View, Move>` to get a typed `view` and a checked return.
|
|
16
23
|
*/
|
|
17
24
|
import { SUPPORTED_GAMES } from "./models.js";
|
|
18
25
|
import { Agent } from "./server.js";
|
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,8 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
11
11
|
import { asAgent } from "./adapter.js";
|
|
12
12
|
import * as config from "./config.js";
|
|
13
13
|
import * as creds from "./credentials.js";
|
|
14
|
+
import { enableGateway } from "./instrument.js";
|
|
15
|
+
import { maybeInstallPing } from "./install-ping.js";
|
|
14
16
|
import { deriveConnectUrl, runLoginFlow } from "./login.js";
|
|
15
17
|
import * as mode from "./mode.js";
|
|
16
18
|
import { RuntimeConnector } from "./runtime.js";
|
|
@@ -18,6 +20,11 @@ import { REQUEST_ID_HEADER, SIGNATURE_HEADER, SIGNATURE_VERSION, TIMESTAMP_HEADE
|
|
|
18
20
|
import { SDK_VERSION } from "./version.js";
|
|
19
21
|
const OK = "✓";
|
|
20
22
|
const BAD = "✗";
|
|
23
|
+
const WARN = "•";
|
|
24
|
+
// N-player games use the group matchmaking queue; Goofspiel (1v1) uses the 2-player
|
|
25
|
+
// queue. Same enqueue request shape, different endpoint.
|
|
26
|
+
const GROUP_GAMES = new Set(["mafia", "monopoly"]);
|
|
27
|
+
const queuePathFor = (game) => (GROUP_GAMES.has(game) ? "/v1/group-queue" : "/v1/queue");
|
|
21
28
|
// Public platform defaults. `pyyol login` with no flags hits the live platform;
|
|
22
29
|
// self-hosted/local users override via PYYOL_API / PYYOL_DASHBOARD (or --api /
|
|
23
30
|
// --dashboard). The API host serves /v1/*; the dashboard host serves /cli-login —
|
|
@@ -25,6 +32,10 @@ const BAD = "✗";
|
|
|
25
32
|
// to the API host.
|
|
26
33
|
const DEFAULT_API_BASE = (process.env.PYYOL_API || "").replace(/\/$/, "") || "https://api.pyyol.com";
|
|
27
34
|
const DEFAULT_DASHBOARD = (process.env.PYYOL_DASHBOARD || "").replace(/\/$/, "") || "https://pyyol.com";
|
|
35
|
+
// Verified-tier LLM gateway base (Phase 4). Ranked mode enables gateway routing so
|
|
36
|
+
// pyyol.route(client) sends the agent's LLM calls through it for server-observed
|
|
37
|
+
// (unfakeable) model/token/cost. Override with $PYYOL_GATEWAY.
|
|
38
|
+
const DEFAULT_GATEWAY = (process.env.PYYOL_GATEWAY || "").replace(/\/$/, "") || "https://gateway.pyyol.com";
|
|
28
39
|
// Agent API keys look like "sk_arena_<lookup>_<secret>" — the long-lived, revocable
|
|
29
40
|
// connection credential (mirrors backend platform.PrefixKey).
|
|
30
41
|
const AGENT_KEY_PREFIX = "sk_arena_";
|
|
@@ -222,6 +233,42 @@ async function loadAgentFromConfig(cfg) {
|
|
|
222
233
|
return asAgent(obj);
|
|
223
234
|
}
|
|
224
235
|
// ── commands ───────────────────────────────────────────────────────────────────
|
|
236
|
+
async function loginAndSave(api, dashboard, connect, provider) {
|
|
237
|
+
const c = await runLoginFlow({ dashboardUrl: dashboard, apiUrl: api, provider });
|
|
238
|
+
if (connect)
|
|
239
|
+
c.connectUrl = connect;
|
|
240
|
+
if (!c.apiKey && c.agentId && c.accessToken) {
|
|
241
|
+
const [st, resp] = await apiPost(`${api}/v1/agent/keys`, c.accessToken, { agent_id: c.agentId });
|
|
242
|
+
if (st === 201 && resp.api_key)
|
|
243
|
+
c.apiKey = resp.api_key;
|
|
244
|
+
}
|
|
245
|
+
creds.save(c);
|
|
246
|
+
return c;
|
|
247
|
+
}
|
|
248
|
+
// Return valid creds for a game/sandbox command, launching the browser login when this
|
|
249
|
+
// DEVICE isn't logged in — so `pyyol dev`/`play`/`queue` just work after install.
|
|
250
|
+
// Non-interactive (CI) → null with guidance so the caller errors cleanly.
|
|
251
|
+
async function ensureLogin(a) {
|
|
252
|
+
const c = creds.load();
|
|
253
|
+
if (c && (c.accessToken || c.apiKey))
|
|
254
|
+
return c;
|
|
255
|
+
const api = (str(a, "api") || DEFAULT_API_BASE).replace(/\/$/, "");
|
|
256
|
+
const dashboard = (str(a, "dashboard") || DEFAULT_DASHBOARD).replace(/\/$/, "");
|
|
257
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
258
|
+
console.error(`${BAD} not logged in on this device. Run \`pyyol login\` (opens the browser) or set PYYOL_TOKEN, then retry.`);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
console.log("you're not logged in on this device — opening the browser to sign in…");
|
|
262
|
+
try {
|
|
263
|
+
const got = await loginAndSave(api, dashboard, str(a, "connect") || "", str(a, "with") || "");
|
|
264
|
+
console.log(`${OK} logged in as ${got.agentId || "(no agent yet)"}. continuing…`);
|
|
265
|
+
return got;
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
console.error(`${BAD} login failed: ${e} — run \`pyyol login\` and retry.`);
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
225
272
|
async function cmdLogin(a) {
|
|
226
273
|
const token = str(a, "token");
|
|
227
274
|
// API host serves /v1/*; dashboard host serves /cli-login — different in prod,
|
|
@@ -322,7 +369,11 @@ class ${cls} extends Adapter {
|
|
|
322
369
|
supportedGames = ["${arena}"];
|
|
323
370
|
|
|
324
371
|
step(view) {
|
|
325
|
-
// Your strategy goes here (call any framework or LLM). Baseline below
|
|
372
|
+
// Your strategy goes here (call any framework or LLM). Baseline below.
|
|
373
|
+
// Driving moves with an LLM? Capture the real model/tokens/cost for free:
|
|
374
|
+
// import pyyol from "pyyol"; await pyyol.instrument(); // once, at the top
|
|
375
|
+
// const client = pyyol.route(new OpenAI()); // in ranked, routes via the gateway
|
|
376
|
+
// then call \`client\` here. See docs -> "Verified LLM agents".
|
|
326
377
|
const legal = view.legal_actions ?? [];
|
|
327
378
|
${arena === "goofspiel" ? "return { round: view.round, card: Math.min(...legal) };" : "return legal.length ? { action: legal[0] } : {};"}
|
|
328
379
|
}
|
|
@@ -372,13 +423,11 @@ async function orchestrate(a, devLocked) {
|
|
|
372
423
|
console.error(`${BAD} no pyyol.toml here — run \`pyyol init <dir>\` first.`);
|
|
373
424
|
return 2;
|
|
374
425
|
}
|
|
375
|
-
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
if (!c
|
|
379
|
-
console.error(`${BAD} not logged in — run \`pyyol login\` first.`);
|
|
426
|
+
// Auto-login on this device if needed: a first-time user who installed the SDK and
|
|
427
|
+
// ran `pyyol dev`/`play` gets the browser sign-in, then plays — no separate step.
|
|
428
|
+
const c = await ensureLogin(a);
|
|
429
|
+
if (!c)
|
|
380
430
|
return 2;
|
|
381
|
-
}
|
|
382
431
|
const connectUrl = str(a, "url") || process.env.PYYOL_URL || c.connectUrl;
|
|
383
432
|
const base = httpBase(a, c);
|
|
384
433
|
// The agent id MUST be the one the token belongs to. The token comes from creds,
|
|
@@ -398,6 +447,19 @@ async function orchestrate(a, devLocked) {
|
|
|
398
447
|
console.log("aborted — staying safe. (Use --yes in CI to skip the prompt.)");
|
|
399
448
|
return 1;
|
|
400
449
|
}
|
|
450
|
+
// Enable verified-tier gateway routing (only with an agent key — the gateway
|
|
451
|
+
// authenticates X-Pyyol-Key via it; a dashboard JWT can't). Then pyyol.route(client)
|
|
452
|
+
// sends the agent's LLM calls through the gateway for server-observed model/cost.
|
|
453
|
+
if (usingAgentKey && token) {
|
|
454
|
+
enableGateway(token, DEFAULT_GATEWAY);
|
|
455
|
+
console.log(` ${OK} verified gateway routing on (${DEFAULT_GATEWAY}) — call pyyol.route(client)`);
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
// Don't silently run unverified: the dev thinks they're competing verified.
|
|
459
|
+
console.error(` ${BAD} verified gateway routing OFF — no agent key in this session ` +
|
|
460
|
+
`(a dashboard-JWT login can't authenticate to the gateway). Run \`pyyol login\` ` +
|
|
461
|
+
`to mint an agent key; your ranked LLM cost won't be verified.`);
|
|
462
|
+
}
|
|
401
463
|
}
|
|
402
464
|
if (agentId && !cfg.agent_id)
|
|
403
465
|
config.setAgentId(agentId);
|
|
@@ -428,7 +490,7 @@ async function orchestrate(a, devLocked) {
|
|
|
428
490
|
setTimeout(async () => {
|
|
429
491
|
if (m === mode.RANKED) {
|
|
430
492
|
const tier = str(a, "tier") || "low";
|
|
431
|
-
const [st, resp] = await apiPost(`${base}
|
|
493
|
+
const [st, resp] = await apiPost(`${base}${queuePathFor(arena)}`, token, { game: arena, tier });
|
|
432
494
|
if (st === 200 || st === 202)
|
|
433
495
|
console.log(` ${OK} queued for RANKED ${arena} (tier ${tier})`);
|
|
434
496
|
else if (String(resp.code ?? "").includes("certified"))
|
|
@@ -459,6 +521,42 @@ async function orchestrate(a, devLocked) {
|
|
|
459
521
|
console.log("\nstopped.");
|
|
460
522
|
return 0;
|
|
461
523
|
}
|
|
524
|
+
async function cmdGames(a) {
|
|
525
|
+
const base = httpBase(a, creds.load());
|
|
526
|
+
if (!base) {
|
|
527
|
+
console.error(`${BAD} no API url — pass --api or run \`pyyol login\`.`);
|
|
528
|
+
return 2;
|
|
529
|
+
}
|
|
530
|
+
const [st, resp] = await apiGet(`${base}/v1/games`);
|
|
531
|
+
if (st !== 200) {
|
|
532
|
+
console.error(`${BAD} could not fetch games (${st}): ${JSON.stringify(resp)}`);
|
|
533
|
+
return 1;
|
|
534
|
+
}
|
|
535
|
+
const games = resp.games ?? [];
|
|
536
|
+
if (!games.length) {
|
|
537
|
+
console.log("no games available.");
|
|
538
|
+
return 0;
|
|
539
|
+
}
|
|
540
|
+
console.log(` ${"GAME".padEnd(11)}${"LIVE".padStart(6)}${"PLAYING".padStart(9)}${"WAITING".padStart(9)} STATUS`);
|
|
541
|
+
console.log(` ${"─".repeat(44)}`);
|
|
542
|
+
let totalLive = 0;
|
|
543
|
+
let totalWait = 0;
|
|
544
|
+
for (const g of games) {
|
|
545
|
+
const live = Number(g.live ?? 0);
|
|
546
|
+
const playing = Number(g.playing ?? 0);
|
|
547
|
+
const waiting = Number(g.waiting ?? 0);
|
|
548
|
+
totalLive += live;
|
|
549
|
+
totalWait += waiting;
|
|
550
|
+
const status = live > 0 ? `${OK} ${live} live` : waiting > 0 ? `${waiting} waiting — queue to start` : "quiet — be the first";
|
|
551
|
+
console.log(` ${String(g.game ?? "?").padEnd(11)}${String(live).padStart(6)}${String(playing).padStart(9)}${String(waiting).padStart(9)} ${status}`);
|
|
552
|
+
}
|
|
553
|
+
console.log(` ${"─".repeat(44)}`);
|
|
554
|
+
if (totalLive === 0 && totalWait === 0)
|
|
555
|
+
console.log(" nothing running right now — `pyyol queue <game>` to open a table.");
|
|
556
|
+
else
|
|
557
|
+
console.log(` ${totalLive} live match(es), ${totalWait} agent(s) waiting. \`pyyol queue <game>\` to join.`);
|
|
558
|
+
return 0;
|
|
559
|
+
}
|
|
462
560
|
async function cmdArenas(a) {
|
|
463
561
|
const base = httpBase(a, creds.load());
|
|
464
562
|
if (!base) {
|
|
@@ -478,6 +576,110 @@ async function cmdArenas(a) {
|
|
|
478
576
|
}
|
|
479
577
|
return 0;
|
|
480
578
|
}
|
|
579
|
+
/** `pyyol wallet` — show the owner's coin balance + per-agent wallets, so a dev can
|
|
580
|
+
* see why ranked was refused ("not enough coins") without leaving the CLI. Parity
|
|
581
|
+
* with the Python CLI. Owner-scoped, so it uses the dashboard access token. */
|
|
582
|
+
async function cmdWallet(a) {
|
|
583
|
+
const c = creds.load();
|
|
584
|
+
const base = httpBase(a, c);
|
|
585
|
+
const token = c?.accessToken || str(a, "token") || process.env.PYYOL_TOKEN || "";
|
|
586
|
+
if (!token) {
|
|
587
|
+
console.error(`${BAD} not logged in — run \`pyyol login\` first.`);
|
|
588
|
+
return 2;
|
|
589
|
+
}
|
|
590
|
+
const [st, w] = await apiGet(`${base}/v1/user/wallet`, token);
|
|
591
|
+
if (st !== 200) {
|
|
592
|
+
console.error(`${BAD} could not fetch wallet (${st}): ${JSON.stringify(w)}`);
|
|
593
|
+
return 1;
|
|
594
|
+
}
|
|
595
|
+
if (bool(a, "json")) {
|
|
596
|
+
console.log(JSON.stringify(w, null, 2));
|
|
597
|
+
return 0;
|
|
598
|
+
}
|
|
599
|
+
const cents = Number(w.coin_cents ?? 1) || 1;
|
|
600
|
+
const usd = (coins) => `$${((coins * cents) / 100).toFixed(2)}`;
|
|
601
|
+
const avail = Number(w.available_balance ?? 0);
|
|
602
|
+
console.log("Treasury");
|
|
603
|
+
console.log(` Available ${avail.toLocaleString()} coins (${usd(avail)})`);
|
|
604
|
+
if (w.locked_balance)
|
|
605
|
+
console.log(` Locked ${Number(w.locked_balance).toLocaleString()} coins (in active matches)`);
|
|
606
|
+
if (w.lifetime_earnings)
|
|
607
|
+
console.log(` Earned ${Number(w.lifetime_earnings).toLocaleString()} coins (lifetime)`);
|
|
608
|
+
const agents = w.agents ?? [];
|
|
609
|
+
if (agents.length) {
|
|
610
|
+
console.log("\nAgent wallets");
|
|
611
|
+
for (const ag of agents) {
|
|
612
|
+
const bal = Number(ag.balance ?? 0).toLocaleString();
|
|
613
|
+
const wd = Number(ag.withdrawable ?? 0).toLocaleString();
|
|
614
|
+
console.log(` ${String(ag.name ?? ag.agent ?? "?").padEnd(20)} ${bal.padStart(10)} coins withdrawable ${wd}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return 0;
|
|
618
|
+
}
|
|
619
|
+
/** `pyyol queue <game> [--tier low|mid|high | --bid N] [--list]` — enter ranked
|
|
620
|
+
* matchmaking at a stake tier (parity with the Python CLI). `--list` shows the
|
|
621
|
+
* admin-configured tiers. The game is a POSITIONAL argument. */
|
|
622
|
+
async function cmdQueue(a) {
|
|
623
|
+
const c = creds.load();
|
|
624
|
+
const base = httpBase(a, c);
|
|
625
|
+
if (!base) {
|
|
626
|
+
console.error(`${BAD} no API url — pass --api or run \`pyyol login\`.`);
|
|
627
|
+
return 2;
|
|
628
|
+
}
|
|
629
|
+
const game = a.positionals[0] ?? "";
|
|
630
|
+
if (!game) {
|
|
631
|
+
console.error(`${BAD} usage: pyyol queue <game> [--tier low|mid|high | --bid N] [--list]`);
|
|
632
|
+
return 2;
|
|
633
|
+
}
|
|
634
|
+
if (bool(a, "list")) {
|
|
635
|
+
const [st, resp] = await apiGet(`${base}/v1/games/${game}/stakes`);
|
|
636
|
+
if (st !== 200) {
|
|
637
|
+
console.error(`${BAD} could not fetch tiers (${st})`);
|
|
638
|
+
return 1;
|
|
639
|
+
}
|
|
640
|
+
const tiers = resp.tiers ?? [];
|
|
641
|
+
if (!tiers.length) {
|
|
642
|
+
console.log(`no stake tiers configured for ${game} — use --bid <coins>`);
|
|
643
|
+
return 0;
|
|
644
|
+
}
|
|
645
|
+
console.log(`${game} stake tiers:`);
|
|
646
|
+
for (const t of tiers)
|
|
647
|
+
console.log(` ${String(t.key ?? "").padEnd(8)} ${String(Number(t.coins ?? 0)).padStart(8)} coins ${t.label ?? ""}`);
|
|
648
|
+
return 0;
|
|
649
|
+
}
|
|
650
|
+
// Queuing needs a session — auto-launch login on this device if absent.
|
|
651
|
+
let token = c?.accessToken || str(a, "token") || process.env.PYYOL_TOKEN || "";
|
|
652
|
+
if (!token) {
|
|
653
|
+
const got = await ensureLogin(a);
|
|
654
|
+
if (!got)
|
|
655
|
+
return 2;
|
|
656
|
+
token = got.accessToken || got.apiKey || "";
|
|
657
|
+
}
|
|
658
|
+
const body = { game };
|
|
659
|
+
if (str(a, "tier"))
|
|
660
|
+
body.tier = str(a, "tier");
|
|
661
|
+
else if (num(a, "bid", 0) > 0)
|
|
662
|
+
body.bid = num(a, "bid", 0);
|
|
663
|
+
else {
|
|
664
|
+
console.error(`${BAD} choose a stake: --tier <low|mid|high> (see \`pyyol queue ${game} --list\`) or --bid <coins>`);
|
|
665
|
+
return 2;
|
|
666
|
+
}
|
|
667
|
+
const [st, resp] = await apiPost(`${base}${queuePathFor(game)}`, token, body);
|
|
668
|
+
if (st !== 200 && st !== 202) {
|
|
669
|
+
const code = String(resp.code ?? resp.error ?? "");
|
|
670
|
+
if (code.includes("certified"))
|
|
671
|
+
console.error(`${BAD} agent not certified — run \`pyyol publish --manifest <file>\` first.`);
|
|
672
|
+
else if (code.includes("balance") || code.includes("insufficient"))
|
|
673
|
+
console.error(`${BAD} not enough coins — fund your wallet (see \`pyyol wallet\`).`);
|
|
674
|
+
else
|
|
675
|
+
console.error(`${BAD} could not queue ranked (${st}): ${JSON.stringify(resp)}`);
|
|
676
|
+
return 1;
|
|
677
|
+
}
|
|
678
|
+
console.log(` ${OK} queued for ${game}${body.tier ? ` (tier ${body.tier})` : ""} — keep your agent connected; it plays when matched.`);
|
|
679
|
+
if (resp.match_id)
|
|
680
|
+
console.log(` ${OK} matched → ${resp.match_id}\n watch it: pyyol watch ${resp.match_id}`);
|
|
681
|
+
return 0;
|
|
682
|
+
}
|
|
481
683
|
async function cmdLeaderboard(a) {
|
|
482
684
|
const base = httpBase(a, creds.load());
|
|
483
685
|
if (!base) {
|
|
@@ -789,10 +991,42 @@ function autoplayOpts(a, cfg) {
|
|
|
789
991
|
async function autoplaySet(api, token, enabled, m, bid, games) {
|
|
790
992
|
return apiRequest("PUT", `${api.replace(/\/$/, "")}/v1/agent/autoplay`, token, { enabled, mode: m, bid, games });
|
|
791
993
|
}
|
|
994
|
+
/** GET the agent's auto-play setting + last observed status (mirrors Python
|
|
995
|
+
* `_autoplay_get`). */
|
|
996
|
+
async function autoplayGet(api, token) {
|
|
997
|
+
return apiGet(`${api.replace(/\/$/, "")}/v1/agent/autoplay`, token);
|
|
998
|
+
}
|
|
999
|
+
const AUTOPLAY_STATUS_LABEL = {
|
|
1000
|
+
playing: [OK, "playing"],
|
|
1001
|
+
searching: [OK, "searching for an opponent"],
|
|
1002
|
+
paused: [WARN, "paused"],
|
|
1003
|
+
blocked: [BAD, "not playing"],
|
|
1004
|
+
};
|
|
1005
|
+
/** Render `pyyol autoplay status` — is it on, and WHY it is or isn't playing, so a
|
|
1006
|
+
* quiet auto-play agent is never a mystery. */
|
|
1007
|
+
function printAutoplayStatus(body) {
|
|
1008
|
+
if (!body?.enabled) {
|
|
1009
|
+
console.log(`${WARN} auto-play is OFF (turn it on with \`pyyol autoplay on\`)`);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
console.log(`${OK} auto-play is ON — mode=${body.mode || "sandbox"}`);
|
|
1013
|
+
const status = body.last_status || "";
|
|
1014
|
+
const reason = body.last_status_reason || "";
|
|
1015
|
+
if (!status) {
|
|
1016
|
+
console.log(" status: starting up — no activity recorded yet (check back in a moment)");
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const [marker, label] = AUTOPLAY_STATUS_LABEL[status] ?? [WARN, status];
|
|
1020
|
+
console.log(` ${marker} ${label}${reason ? ` — ${reason}` : ""}`);
|
|
1021
|
+
if (body.last_status_at)
|
|
1022
|
+
console.log(` as of ${body.last_status_at}`);
|
|
1023
|
+
if (status === "blocked")
|
|
1024
|
+
console.log(" fix the reason above (e.g. connect your agent with `pyyol run`), and it resumes automatically.");
|
|
1025
|
+
}
|
|
792
1026
|
async function cmdAutoplay(a) {
|
|
793
1027
|
const state = a.positionals[0];
|
|
794
|
-
if (state !== "on" && state !== "off") {
|
|
795
|
-
console.error(`${BAD} usage: pyyol autoplay on|off`);
|
|
1028
|
+
if (state !== "on" && state !== "off" && state !== "status") {
|
|
1029
|
+
console.error(`${BAD} usage: pyyol autoplay on|off|status`);
|
|
796
1030
|
return 2;
|
|
797
1031
|
}
|
|
798
1032
|
const c = creds.load();
|
|
@@ -805,6 +1039,16 @@ async function cmdAutoplay(a) {
|
|
|
805
1039
|
}
|
|
806
1040
|
if (str(a, "token"))
|
|
807
1041
|
warnArgvSecret();
|
|
1042
|
+
// `pyyol autoplay status` READS the current state + why it is/isn't playing.
|
|
1043
|
+
if (state === "status") {
|
|
1044
|
+
const [st, resp] = await autoplayGet(api, token);
|
|
1045
|
+
if (st >= 200 && st < 300) {
|
|
1046
|
+
printAutoplayStatus(resp);
|
|
1047
|
+
return 0;
|
|
1048
|
+
}
|
|
1049
|
+
console.error(`${BAD} failed (status ${st}): ${JSON.stringify(resp)}`);
|
|
1050
|
+
return 1;
|
|
1051
|
+
}
|
|
808
1052
|
const on = state === "on";
|
|
809
1053
|
const [m, games] = autoplayOpts(a, config.load());
|
|
810
1054
|
const [st, resp] = await autoplaySet(api, token, on, m, num(a, "bid", 0), games);
|
|
@@ -837,7 +1081,8 @@ async function cmdLogs(a) {
|
|
|
837
1081
|
async function cmdSimulate(a) {
|
|
838
1082
|
const game = str(a, "game") || "goofspiel";
|
|
839
1083
|
if (game !== "goofspiel") {
|
|
840
|
-
console.error(`simulate
|
|
1084
|
+
console.error(`simulate runs a full in-process match for goofspiel only (got '${game}'). ` +
|
|
1085
|
+
`For ${game}, iterate with \`pyyol dev\` — sandbox practice vs house agents, no stakes.`);
|
|
841
1086
|
return 2;
|
|
842
1087
|
}
|
|
843
1088
|
const opponent = str(a, "opponent") || "baseline";
|
|
@@ -1230,11 +1475,14 @@ Commands:
|
|
|
1230
1475
|
init <dir> [--arena goofspiel|mafia|monopoly] [--framework F] [--name N]
|
|
1231
1476
|
dev [--matches N] local dev loop — SANDBOX, no stakes
|
|
1232
1477
|
play <arena> [--ranked] [--tier] compete; --ranked = real stakes
|
|
1233
|
-
publish
|
|
1478
|
+
publish --manifest <file> certify your agent for ranked
|
|
1479
|
+
queue <game> [--tier low|mid|high | --bid N] [--list] enter ranked matchmaking
|
|
1480
|
+
wallet [--json] your coin balance + per-agent wallets
|
|
1234
1481
|
replay <match_id> [--game] [--json]
|
|
1235
1482
|
profile [handle]
|
|
1236
1483
|
leaderboard [--game G] [--developers] [--season N]
|
|
1237
1484
|
arenas
|
|
1485
|
+
games live + waiting agents per game
|
|
1238
1486
|
status [--agent A] (advanced) is your agent connected?
|
|
1239
1487
|
autoplay on|off [--ranked|--mode] [--bid N] [--games G,…]
|
|
1240
1488
|
serve [--file F] [--var V] [--port P] [--host H] enable auto-play + run the HTTP server
|
|
@@ -1249,6 +1497,9 @@ Commands:
|
|
|
1249
1497
|
export async function main(argv = process.argv.slice(2)) {
|
|
1250
1498
|
const command = argv[0];
|
|
1251
1499
|
const a = parse(argv.slice(1));
|
|
1500
|
+
// Anonymous, once-per-version, fire-and-forget adoption ping (opt out with
|
|
1501
|
+
// PYYOL_NO_TELEMETRY / DO_NOT_TRACK). Never blocks or affects the command.
|
|
1502
|
+
maybeInstallPing(str(a, "api") || DEFAULT_API_BASE, SDK_VERSION);
|
|
1252
1503
|
switch (command) {
|
|
1253
1504
|
case "login":
|
|
1254
1505
|
return cmdLogin(a);
|
|
@@ -1266,10 +1517,16 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1266
1517
|
return cmdPublish(a);
|
|
1267
1518
|
case "arenas":
|
|
1268
1519
|
return cmdArenas(a);
|
|
1520
|
+
case "games":
|
|
1521
|
+
return cmdGames(a);
|
|
1269
1522
|
case "leaderboard":
|
|
1270
1523
|
return cmdLeaderboard(a);
|
|
1271
1524
|
case "profile":
|
|
1272
1525
|
return cmdProfile(a);
|
|
1526
|
+
case "wallet":
|
|
1527
|
+
return cmdWallet(a);
|
|
1528
|
+
case "queue":
|
|
1529
|
+
return cmdQueue(a);
|
|
1273
1530
|
case "replay":
|
|
1274
1531
|
return cmdReplay(a);
|
|
1275
1532
|
case "status":
|
package/dist/index.d.ts
CHANGED
|
@@ -21,5 +21,10 @@ export type { GoofspielSimOptions, GoofspielSimResult } from "./simulator.js";
|
|
|
21
21
|
export { RuntimeConnector, ConnectorError, PROTOCOL_VERSION } from "./runtime.js";
|
|
22
22
|
export type { RuntimeOptions, WebSocketLike, WebSocketCtor } from "./runtime.js";
|
|
23
23
|
export { gameRules } from "./rules.js";
|
|
24
|
-
export { Tracer, Span, currentSpan, matchTraceId } from "./telemetry.js";
|
|
25
|
-
export type { TracerOptions, ModelCall } from "./telemetry.js";
|
|
24
|
+
export { Tracer, Span, currentSpan, matchTraceId, currentUsage, UsageAccumulator, runTurnUsage } from "./telemetry.js";
|
|
25
|
+
export type { TracerOptions, ModelCall, MoveUsage, UsageAdd } from "./telemetry.js";
|
|
26
|
+
export { instrument, uninstrument, recordResponse, extractUsage, patchPrototype } from "./instrument.js";
|
|
27
|
+
export { route, enableGateway, disableGateway, gatewayBaseUrl, gatewayHeaders } from "./instrument.js";
|
|
28
|
+
export type { ExtractedUsage } from "./instrument.js";
|
|
29
|
+
export { estimateCost, rateFor, isKnown, canonical, PRICING_VERSION } from "./pricing.js";
|
|
30
|
+
export type { Rate, CostArgs } from "./pricing.js";
|
package/dist/index.js
CHANGED
|
@@ -17,4 +17,7 @@ export * from "./models.js";
|
|
|
17
17
|
export { simulateGoofspiel, SimulationError } from "./simulator.js";
|
|
18
18
|
export { RuntimeConnector, ConnectorError, PROTOCOL_VERSION } from "./runtime.js";
|
|
19
19
|
export { gameRules } from "./rules.js";
|
|
20
|
-
export { Tracer, Span, currentSpan, matchTraceId } from "./telemetry.js";
|
|
20
|
+
export { Tracer, Span, currentSpan, matchTraceId, currentUsage, UsageAccumulator, runTurnUsage } from "./telemetry.js";
|
|
21
|
+
export { instrument, uninstrument, recordResponse, extractUsage, patchPrototype } from "./instrument.js";
|
|
22
|
+
export { route, enableGateway, disableGateway, gatewayBaseUrl, gatewayHeaders } from "./instrument.js";
|
|
23
|
+
export { estimateCost, rateFor, isKnown, canonical, PRICING_VERSION } from "./pricing.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Anonymous, once-per-version SDK install ping (mirrors the Python SDK). The first
|
|
2
|
+
// time the CLI runs a given version, fire ONE best-effort ping so the platform can
|
|
3
|
+
// show adoption analytics. Anonymous ({sdk, version} only — the server resolves a
|
|
4
|
+
// COUNTRY from the request and never stores the IP), fire-and-forget (never blocks
|
|
5
|
+
// or errors the CLI), once per version (marker file), opt-out via PYYOL_NO_TELEMETRY
|
|
6
|
+
// or the standard DO_NOT_TRACK.
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { configDir } from "./credentials.js";
|
|
10
|
+
const OFF = new Set(["", "0", "false", "no", "off"]);
|
|
11
|
+
function optedOut() {
|
|
12
|
+
for (const key of ["PYYOL_NO_TELEMETRY", "DO_NOT_TRACK"]) {
|
|
13
|
+
const v = (process.env[key] ?? "").trim().toLowerCase();
|
|
14
|
+
if (v && !OFF.has(v))
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
/** Fire the install ping at most once per version. Never throws. */
|
|
20
|
+
export function maybeInstallPing(apiBase, version) {
|
|
21
|
+
if (!apiBase || optedOut())
|
|
22
|
+
return;
|
|
23
|
+
const marker = join(configDir(), `.install_pinged_${version}`);
|
|
24
|
+
try {
|
|
25
|
+
if (existsSync(marker))
|
|
26
|
+
return;
|
|
27
|
+
// Mark BEFORE firing: attempt at most once per version (no retry storm).
|
|
28
|
+
mkdirSync(configDir(), { recursive: true });
|
|
29
|
+
writeFileSync(marker, "");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
void fetch(apiBase.replace(/\/+$/, "") + "/v1/telemetry/install", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json" },
|
|
37
|
+
body: JSON.stringify({ sdk: "js", version }),
|
|
38
|
+
signal: AbortSignal.timeout(3000),
|
|
39
|
+
}).catch(() => {
|
|
40
|
+
/* telemetry must never surface to the CLI */
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
type Any = any;
|
|
2
|
+
/** Enable gateway routing (called by the runtime in ranked mode; safe in tests). */
|
|
3
|
+
export declare function enableGateway(agentKey: string, baseUrl: string): void;
|
|
4
|
+
export declare function disableGateway(): void;
|
|
5
|
+
/** The baseURL a provider client should point at, or "" if routing is off/unknown. */
|
|
6
|
+
export declare function gatewayBaseUrl(provider: string): string;
|
|
7
|
+
/** The X-Pyyol-* identity headers for the current turn (empty if routing off). */
|
|
8
|
+
export declare function gatewayHeaders(): Record<string, string>;
|
|
9
|
+
/** Point a provider client at the Pyyol Gateway (sets its baseURL). Explicit, robust
|
|
10
|
+
* opt-in that operates on the given instance. Returns the client. No-op when routing
|
|
11
|
+
* is off or the provider can't be determined. */
|
|
12
|
+
export declare function route<T>(client: T, provider?: string): T;
|
|
13
|
+
export interface ExtractedUsage {
|
|
14
|
+
model: string;
|
|
15
|
+
provider: string;
|
|
16
|
+
promptTokens: number;
|
|
17
|
+
completionTokens: number;
|
|
18
|
+
cachedTokens: number;
|
|
19
|
+
reasoningTokens: number;
|
|
20
|
+
}
|
|
21
|
+
/** Pull normalized usage from a provider response, or null if it has none.
|
|
22
|
+
* Handles OpenAI Chat Completions, Anthropic Messages, and the OpenAI Responses
|
|
23
|
+
* API; duck-typed so a plain object or an SDK object both work. */
|
|
24
|
+
export declare function extractUsage(resp: Any): ExtractedUsage | null;
|
|
25
|
+
/** Record usage from a provider response: compute cost, add to the turn
|
|
26
|
+
* accumulator, and emit a Lens model_call span. Returns the extracted usage (or
|
|
27
|
+
* null). Also the public manual hook for clients this module doesn't auto-wrap. */
|
|
28
|
+
export declare function recordResponse(resp: Any, o?: {
|
|
29
|
+
provider?: string;
|
|
30
|
+
latencyMs?: number;
|
|
31
|
+
}): ExtractedUsage | null;
|
|
32
|
+
/** @internal Wrap `proto[method]` so its resolved return value is recorded.
|
|
33
|
+
* Idempotent and fully guarded. Exported for tests. */
|
|
34
|
+
export declare function patchPrototype(proto: Any, method: string, provider: string): boolean;
|
|
35
|
+
/** Auto-capture LLM usage from installed providers. Pass e.g. `["openai"]` to limit
|
|
36
|
+
* which are patched; default patches all supported providers that are installed.
|
|
37
|
+
* Returns the list actually instrumented. Safe to call more than once. */
|
|
38
|
+
export declare function instrument(providers?: string[]): Promise<string[]>;
|
|
39
|
+
/** Restore all patched methods (primarily for tests). */
|
|
40
|
+
export declare function uninstrument(): void;
|
|
41
|
+
export {};
|