pyyol 1.2.0 → 1.3.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 +37 -9
- package/dist/adapter.d.ts +3 -4
- package/dist/adapter.js +8 -2
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +202 -11
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/credentials.d.ts +0 -1
- package/dist/credentials.js +0 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -2
- 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/login.d.ts +0 -1
- package/dist/login.js +0 -1
- package/dist/mode.d.ts +0 -1
- package/dist/mode.js +0 -1
- package/dist/models.d.ts +4 -1
- package/dist/models.js +0 -1
- package/dist/pricing.d.ts +27 -0
- package/dist/pricing.js +110 -0
- package/dist/rules.d.ts +0 -1
- package/dist/rules.js +0 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +32 -8
- package/dist/server.d.ts +0 -1
- package/dist/server.js +10 -3
- package/dist/signing.d.ts +0 -1
- package/dist/signing.js +0 -1
- package/dist/simulator.d.ts +0 -1
- package/dist/simulator.js +0 -1
- package/dist/telemetry.d.ts +51 -1
- package/dist/telemetry.js +68 -1
- package/dist/version.d.ts +1 -2
- package/dist/version.js +1 -2
- package/package.json +1 -1
- package/rules/llms-full.txt +258 -26
- package/dist/adapter.d.ts.map +0 -1
- package/dist/adapter.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/credentials.d.ts.map +0 -1
- package/dist/credentials.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/login.d.ts.map +0 -1
- package/dist/login.js.map +0 -1
- package/dist/mode.d.ts.map +0 -1
- package/dist/mode.js.map +0 -1
- package/dist/models.d.ts.map +0 -1
- package/dist/models.js.map +0 -1
- package/dist/rules.d.ts.map +0 -1
- package/dist/rules.js.map +0 -1
- package/dist/runtime.d.ts.map +0 -1
- package/dist/runtime.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/signing.d.ts.map +0 -1
- package/dist/signing.js.map +0 -1
- package/dist/simulator.d.ts.map +0 -1
- package/dist/simulator.js.map +0 -1
- package/dist/telemetry.d.ts.map +0 -1
- package/dist/telemetry.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
package/README.md
CHANGED
|
@@ -76,12 +76,32 @@ 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
|
-
Three games are available; each
|
|
82
|
-
[
|
|
100
|
+
Three games are available; each is documented in
|
|
101
|
+
[the game docs](https://pyyol.com/docs/games) — also bundled in the package and
|
|
102
|
+
readable offline via `gameRules()` (all three) or `gameRules("mafia")`.
|
|
83
103
|
|
|
84
|
-
### Goofspiel
|
|
104
|
+
### Goofspiel
|
|
85
105
|
|
|
86
106
|
Two-player simultaneous-bid card game. The typed `GoofspielView` gives you
|
|
87
107
|
`your_hand`, `legal_actions`, `current_prize`, `scores`, and a self-contained
|
|
@@ -101,7 +121,7 @@ class Lowball extends Adapter {
|
|
|
101
121
|
export const agent = new Lowball();
|
|
102
122
|
```
|
|
103
123
|
|
|
104
|
-
### Mafia
|
|
124
|
+
### Mafia
|
|
105
125
|
|
|
106
126
|
12-seat hidden-role social deduction. The typed `MafiaView` gives you `your_role`
|
|
107
127
|
(capitalized, e.g. `"Mafia"`), `phase`, `alive` (`{seat: bool}`), `allies` (Mafia
|
|
@@ -132,7 +152,7 @@ class TownHunter extends Adapter {
|
|
|
132
152
|
export const agent = new TownHunter();
|
|
133
153
|
```
|
|
134
154
|
|
|
135
|
-
### Monopoly
|
|
155
|
+
### Monopoly
|
|
136
156
|
|
|
137
157
|
Standard Monopoly for 2–8 seats, a phase machine with near-perfect information.
|
|
138
158
|
The typed `MonopolyView` gives you `phase` and `legal_actions`; the whole board is
|
|
@@ -221,10 +241,18 @@ the protocol into an existing framework (Express, Fastify, a serverless handler)
|
|
|
221
241
|
|
|
222
242
|
## Commands
|
|
223
243
|
|
|
224
|
-
`login` · `logout` · `whoami` · `init` · `
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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.
|
|
228
256
|
|
|
229
257
|
## Security
|
|
230
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. */
|
|
@@ -21,4 +21,3 @@ export declare abstract class Adapter {
|
|
|
21
21
|
* class object than the one here. We match by shape (toAgent / the transport
|
|
22
22
|
* methods) so those setups still work. */
|
|
23
23
|
export declare function asAgent(obj: unknown): Agent;
|
|
24
|
-
//# sourceMappingURL=adapter.d.ts.map
|
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";
|
|
@@ -65,4 +72,3 @@ export function asAgent(obj) {
|
|
|
65
72
|
return obj;
|
|
66
73
|
throw new TypeError("expected a pyyol Agent or Adapter; export one as the variable named in pyyol.toml `entry`");
|
|
67
74
|
}
|
|
68
|
-
//# sourceMappingURL=adapter.js.map
|
package/dist/cli.d.ts
CHANGED
|
@@ -17,4 +17,3 @@ export declare function connectionToken(a: Args, c: creds.Credentials | null): {
|
|
|
17
17
|
};
|
|
18
18
|
export declare function apiRequest(method: string, url: string, token: string, body: unknown): Promise<[number, any]>;
|
|
19
19
|
export declare function main(argv?: string[]): Promise<number>;
|
|
20
|
-
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.js
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
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 { existsSync } from "node:fs";
|
|
8
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
9
9
|
import { resolve } from "node:path";
|
|
10
|
-
import { pathToFileURL } from "node:url";
|
|
10
|
+
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_";
|
|
@@ -322,7 +333,11 @@ class ${cls} extends Adapter {
|
|
|
322
333
|
supportedGames = ["${arena}"];
|
|
323
334
|
|
|
324
335
|
step(view) {
|
|
325
|
-
// Your strategy goes here (call any framework or LLM). Baseline below
|
|
336
|
+
// Your strategy goes here (call any framework or LLM). Baseline below.
|
|
337
|
+
// Driving moves with an LLM? Capture the real model/tokens/cost for free:
|
|
338
|
+
// import pyyol from "pyyol"; await pyyol.instrument(); // once, at the top
|
|
339
|
+
// const client = pyyol.route(new OpenAI()); // in ranked, routes via the gateway
|
|
340
|
+
// then call \`client\` here. See docs -> "Verified LLM agents".
|
|
326
341
|
const legal = view.legal_actions ?? [];
|
|
327
342
|
${arena === "goofspiel" ? "return { round: view.round, card: Math.min(...legal) };" : "return legal.length ? { action: legal[0] } : {};"}
|
|
328
343
|
}
|
|
@@ -398,6 +413,19 @@ async function orchestrate(a, devLocked) {
|
|
|
398
413
|
console.log("aborted — staying safe. (Use --yes in CI to skip the prompt.)");
|
|
399
414
|
return 1;
|
|
400
415
|
}
|
|
416
|
+
// Enable verified-tier gateway routing (only with an agent key — the gateway
|
|
417
|
+
// authenticates X-Pyyol-Key via it; a dashboard JWT can't). Then pyyol.route(client)
|
|
418
|
+
// sends the agent's LLM calls through the gateway for server-observed model/cost.
|
|
419
|
+
if (usingAgentKey && token) {
|
|
420
|
+
enableGateway(token, DEFAULT_GATEWAY);
|
|
421
|
+
console.log(` ${OK} verified gateway routing on (${DEFAULT_GATEWAY}) — call pyyol.route(client)`);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
// Don't silently run unverified: the dev thinks they're competing verified.
|
|
425
|
+
console.error(` ${BAD} verified gateway routing OFF — no agent key in this session ` +
|
|
426
|
+
`(a dashboard-JWT login can't authenticate to the gateway). Run \`pyyol login\` ` +
|
|
427
|
+
`to mint an agent key; your ranked LLM cost won't be verified.`);
|
|
428
|
+
}
|
|
401
429
|
}
|
|
402
430
|
if (agentId && !cfg.agent_id)
|
|
403
431
|
config.setAgentId(agentId);
|
|
@@ -428,7 +456,7 @@ async function orchestrate(a, devLocked) {
|
|
|
428
456
|
setTimeout(async () => {
|
|
429
457
|
if (m === mode.RANKED) {
|
|
430
458
|
const tier = str(a, "tier") || "low";
|
|
431
|
-
const [st, resp] = await apiPost(`${base}
|
|
459
|
+
const [st, resp] = await apiPost(`${base}${queuePathFor(arena)}`, token, { game: arena, tier });
|
|
432
460
|
if (st === 200 || st === 202)
|
|
433
461
|
console.log(` ${OK} queued for RANKED ${arena} (tier ${tier})`);
|
|
434
462
|
else if (String(resp.code ?? "").includes("certified"))
|
|
@@ -478,6 +506,107 @@ async function cmdArenas(a) {
|
|
|
478
506
|
}
|
|
479
507
|
return 0;
|
|
480
508
|
}
|
|
509
|
+
/** `pyyol wallet` — show the owner's coin balance + per-agent wallets, so a dev can
|
|
510
|
+
* see why ranked was refused ("not enough coins") without leaving the CLI. Parity
|
|
511
|
+
* with the Python CLI. Owner-scoped, so it uses the dashboard access token. */
|
|
512
|
+
async function cmdWallet(a) {
|
|
513
|
+
const c = creds.load();
|
|
514
|
+
const base = httpBase(a, c);
|
|
515
|
+
const token = c?.accessToken || str(a, "token") || process.env.PYYOL_TOKEN || "";
|
|
516
|
+
if (!token) {
|
|
517
|
+
console.error(`${BAD} not logged in — run \`pyyol login\` first.`);
|
|
518
|
+
return 2;
|
|
519
|
+
}
|
|
520
|
+
const [st, w] = await apiGet(`${base}/v1/user/wallet`, token);
|
|
521
|
+
if (st !== 200) {
|
|
522
|
+
console.error(`${BAD} could not fetch wallet (${st}): ${JSON.stringify(w)}`);
|
|
523
|
+
return 1;
|
|
524
|
+
}
|
|
525
|
+
if (bool(a, "json")) {
|
|
526
|
+
console.log(JSON.stringify(w, null, 2));
|
|
527
|
+
return 0;
|
|
528
|
+
}
|
|
529
|
+
const cents = Number(w.coin_cents ?? 1) || 1;
|
|
530
|
+
const usd = (coins) => `$${((coins * cents) / 100).toFixed(2)}`;
|
|
531
|
+
const avail = Number(w.available_balance ?? 0);
|
|
532
|
+
console.log("Treasury");
|
|
533
|
+
console.log(` Available ${avail.toLocaleString()} coins (${usd(avail)})`);
|
|
534
|
+
if (w.locked_balance)
|
|
535
|
+
console.log(` Locked ${Number(w.locked_balance).toLocaleString()} coins (in active matches)`);
|
|
536
|
+
if (w.lifetime_earnings)
|
|
537
|
+
console.log(` Earned ${Number(w.lifetime_earnings).toLocaleString()} coins (lifetime)`);
|
|
538
|
+
const agents = w.agents ?? [];
|
|
539
|
+
if (agents.length) {
|
|
540
|
+
console.log("\nAgent wallets");
|
|
541
|
+
for (const ag of agents) {
|
|
542
|
+
const bal = Number(ag.balance ?? 0).toLocaleString();
|
|
543
|
+
const wd = Number(ag.withdrawable ?? 0).toLocaleString();
|
|
544
|
+
console.log(` ${String(ag.name ?? ag.agent ?? "?").padEnd(20)} ${bal.padStart(10)} coins withdrawable ${wd}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return 0;
|
|
548
|
+
}
|
|
549
|
+
/** `pyyol queue <game> [--tier low|mid|high | --bid N] [--list]` — enter ranked
|
|
550
|
+
* matchmaking at a stake tier (parity with the Python CLI). `--list` shows the
|
|
551
|
+
* admin-configured tiers. The game is a POSITIONAL argument. */
|
|
552
|
+
async function cmdQueue(a) {
|
|
553
|
+
const c = creds.load();
|
|
554
|
+
const base = httpBase(a, c);
|
|
555
|
+
if (!base) {
|
|
556
|
+
console.error(`${BAD} no API url — pass --api or run \`pyyol login\`.`);
|
|
557
|
+
return 2;
|
|
558
|
+
}
|
|
559
|
+
const game = a.positionals[0] ?? "";
|
|
560
|
+
if (!game) {
|
|
561
|
+
console.error(`${BAD} usage: pyyol queue <game> [--tier low|mid|high | --bid N] [--list]`);
|
|
562
|
+
return 2;
|
|
563
|
+
}
|
|
564
|
+
if (bool(a, "list")) {
|
|
565
|
+
const [st, resp] = await apiGet(`${base}/v1/games/${game}/stakes`);
|
|
566
|
+
if (st !== 200) {
|
|
567
|
+
console.error(`${BAD} could not fetch tiers (${st})`);
|
|
568
|
+
return 1;
|
|
569
|
+
}
|
|
570
|
+
const tiers = resp.tiers ?? [];
|
|
571
|
+
if (!tiers.length) {
|
|
572
|
+
console.log(`no stake tiers configured for ${game} — use --bid <coins>`);
|
|
573
|
+
return 0;
|
|
574
|
+
}
|
|
575
|
+
console.log(`${game} stake tiers:`);
|
|
576
|
+
for (const t of tiers)
|
|
577
|
+
console.log(` ${String(t.key ?? "").padEnd(8)} ${String(Number(t.coins ?? 0)).padStart(8)} coins ${t.label ?? ""}`);
|
|
578
|
+
return 0;
|
|
579
|
+
}
|
|
580
|
+
const token = c?.accessToken || str(a, "token") || process.env.PYYOL_TOKEN || "";
|
|
581
|
+
if (!token) {
|
|
582
|
+
console.error(`${BAD} not logged in — run \`pyyol login\` first.`);
|
|
583
|
+
return 2;
|
|
584
|
+
}
|
|
585
|
+
const body = { game };
|
|
586
|
+
if (str(a, "tier"))
|
|
587
|
+
body.tier = str(a, "tier");
|
|
588
|
+
else if (num(a, "bid", 0) > 0)
|
|
589
|
+
body.bid = num(a, "bid", 0);
|
|
590
|
+
else {
|
|
591
|
+
console.error(`${BAD} choose a stake: --tier <low|mid|high> (see \`pyyol queue ${game} --list\`) or --bid <coins>`);
|
|
592
|
+
return 2;
|
|
593
|
+
}
|
|
594
|
+
const [st, resp] = await apiPost(`${base}${queuePathFor(game)}`, token, body);
|
|
595
|
+
if (st !== 200 && st !== 202) {
|
|
596
|
+
const code = String(resp.code ?? resp.error ?? "");
|
|
597
|
+
if (code.includes("certified"))
|
|
598
|
+
console.error(`${BAD} agent not certified — run \`pyyol publish --manifest <file>\` first.`);
|
|
599
|
+
else if (code.includes("balance") || code.includes("insufficient"))
|
|
600
|
+
console.error(`${BAD} not enough coins — fund your wallet (see \`pyyol wallet\`).`);
|
|
601
|
+
else
|
|
602
|
+
console.error(`${BAD} could not queue ranked (${st}): ${JSON.stringify(resp)}`);
|
|
603
|
+
return 1;
|
|
604
|
+
}
|
|
605
|
+
console.log(` ${OK} queued for ${game}${body.tier ? ` (tier ${body.tier})` : ""} — keep your agent connected; it plays when matched.`);
|
|
606
|
+
if (resp.match_id)
|
|
607
|
+
console.log(` ${OK} matched → ${resp.match_id}\n watch it: pyyol watch ${resp.match_id}`);
|
|
608
|
+
return 0;
|
|
609
|
+
}
|
|
481
610
|
async function cmdLeaderboard(a) {
|
|
482
611
|
const base = httpBase(a, creds.load());
|
|
483
612
|
if (!base) {
|
|
@@ -789,10 +918,42 @@ function autoplayOpts(a, cfg) {
|
|
|
789
918
|
async function autoplaySet(api, token, enabled, m, bid, games) {
|
|
790
919
|
return apiRequest("PUT", `${api.replace(/\/$/, "")}/v1/agent/autoplay`, token, { enabled, mode: m, bid, games });
|
|
791
920
|
}
|
|
921
|
+
/** GET the agent's auto-play setting + last observed status (mirrors Python
|
|
922
|
+
* `_autoplay_get`). */
|
|
923
|
+
async function autoplayGet(api, token) {
|
|
924
|
+
return apiGet(`${api.replace(/\/$/, "")}/v1/agent/autoplay`, token);
|
|
925
|
+
}
|
|
926
|
+
const AUTOPLAY_STATUS_LABEL = {
|
|
927
|
+
playing: [OK, "playing"],
|
|
928
|
+
searching: [OK, "searching for an opponent"],
|
|
929
|
+
paused: [WARN, "paused"],
|
|
930
|
+
blocked: [BAD, "not playing"],
|
|
931
|
+
};
|
|
932
|
+
/** Render `pyyol autoplay status` — is it on, and WHY it is or isn't playing, so a
|
|
933
|
+
* quiet auto-play agent is never a mystery. */
|
|
934
|
+
function printAutoplayStatus(body) {
|
|
935
|
+
if (!body?.enabled) {
|
|
936
|
+
console.log(`${WARN} auto-play is OFF (turn it on with \`pyyol autoplay on\`)`);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
console.log(`${OK} auto-play is ON — mode=${body.mode || "sandbox"}`);
|
|
940
|
+
const status = body.last_status || "";
|
|
941
|
+
const reason = body.last_status_reason || "";
|
|
942
|
+
if (!status) {
|
|
943
|
+
console.log(" status: starting up — no activity recorded yet (check back in a moment)");
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const [marker, label] = AUTOPLAY_STATUS_LABEL[status] ?? [WARN, status];
|
|
947
|
+
console.log(` ${marker} ${label}${reason ? ` — ${reason}` : ""}`);
|
|
948
|
+
if (body.last_status_at)
|
|
949
|
+
console.log(` as of ${body.last_status_at}`);
|
|
950
|
+
if (status === "blocked")
|
|
951
|
+
console.log(" fix the reason above (e.g. connect your agent with `pyyol run`), and it resumes automatically.");
|
|
952
|
+
}
|
|
792
953
|
async function cmdAutoplay(a) {
|
|
793
954
|
const state = a.positionals[0];
|
|
794
|
-
if (state !== "on" && state !== "off") {
|
|
795
|
-
console.error(`${BAD} usage: pyyol autoplay on|off`);
|
|
955
|
+
if (state !== "on" && state !== "off" && state !== "status") {
|
|
956
|
+
console.error(`${BAD} usage: pyyol autoplay on|off|status`);
|
|
796
957
|
return 2;
|
|
797
958
|
}
|
|
798
959
|
const c = creds.load();
|
|
@@ -805,6 +966,16 @@ async function cmdAutoplay(a) {
|
|
|
805
966
|
}
|
|
806
967
|
if (str(a, "token"))
|
|
807
968
|
warnArgvSecret();
|
|
969
|
+
// `pyyol autoplay status` READS the current state + why it is/isn't playing.
|
|
970
|
+
if (state === "status") {
|
|
971
|
+
const [st, resp] = await autoplayGet(api, token);
|
|
972
|
+
if (st >= 200 && st < 300) {
|
|
973
|
+
printAutoplayStatus(resp);
|
|
974
|
+
return 0;
|
|
975
|
+
}
|
|
976
|
+
console.error(`${BAD} failed (status ${st}): ${JSON.stringify(resp)}`);
|
|
977
|
+
return 1;
|
|
978
|
+
}
|
|
808
979
|
const on = state === "on";
|
|
809
980
|
const [m, games] = autoplayOpts(a, config.load());
|
|
810
981
|
const [st, resp] = await autoplaySet(api, token, on, m, num(a, "bid", 0), games);
|
|
@@ -837,7 +1008,8 @@ async function cmdLogs(a) {
|
|
|
837
1008
|
async function cmdSimulate(a) {
|
|
838
1009
|
const game = str(a, "game") || "goofspiel";
|
|
839
1010
|
if (game !== "goofspiel") {
|
|
840
|
-
console.error(`simulate
|
|
1011
|
+
console.error(`simulate runs a full in-process match for goofspiel only (got '${game}'). ` +
|
|
1012
|
+
`For ${game}, iterate with \`pyyol dev\` — sandbox practice vs house agents, no stakes.`);
|
|
841
1013
|
return 2;
|
|
842
1014
|
}
|
|
843
1015
|
const opponent = str(a, "opponent") || "baseline";
|
|
@@ -1230,7 +1402,9 @@ Commands:
|
|
|
1230
1402
|
init <dir> [--arena goofspiel|mafia|monopoly] [--framework F] [--name N]
|
|
1231
1403
|
dev [--matches N] local dev loop — SANDBOX, no stakes
|
|
1232
1404
|
play <arena> [--ranked] [--tier] compete; --ranked = real stakes
|
|
1233
|
-
publish
|
|
1405
|
+
publish --manifest <file> certify your agent for ranked
|
|
1406
|
+
queue <game> [--tier low|mid|high | --bid N] [--list] enter ranked matchmaking
|
|
1407
|
+
wallet [--json] your coin balance + per-agent wallets
|
|
1234
1408
|
replay <match_id> [--game] [--json]
|
|
1235
1409
|
profile [handle]
|
|
1236
1410
|
leaderboard [--game G] [--developers] [--season N]
|
|
@@ -1249,6 +1423,9 @@ Commands:
|
|
|
1249
1423
|
export async function main(argv = process.argv.slice(2)) {
|
|
1250
1424
|
const command = argv[0];
|
|
1251
1425
|
const a = parse(argv.slice(1));
|
|
1426
|
+
// Anonymous, once-per-version, fire-and-forget adoption ping (opt out with
|
|
1427
|
+
// PYYOL_NO_TELEMETRY / DO_NOT_TRACK). Never blocks or affects the command.
|
|
1428
|
+
maybeInstallPing(str(a, "api") || DEFAULT_API_BASE, SDK_VERSION);
|
|
1252
1429
|
switch (command) {
|
|
1253
1430
|
case "login":
|
|
1254
1431
|
return cmdLogin(a);
|
|
@@ -1270,6 +1447,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1270
1447
|
return cmdLeaderboard(a);
|
|
1271
1448
|
case "profile":
|
|
1272
1449
|
return cmdProfile(a);
|
|
1450
|
+
case "wallet":
|
|
1451
|
+
return cmdWallet(a);
|
|
1452
|
+
case "queue":
|
|
1453
|
+
return cmdQueue(a);
|
|
1273
1454
|
case "replay":
|
|
1274
1455
|
return cmdReplay(a);
|
|
1275
1456
|
case "status":
|
|
@@ -1308,8 +1489,19 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1308
1489
|
return 2;
|
|
1309
1490
|
}
|
|
1310
1491
|
}
|
|
1311
|
-
// Run when invoked as the bin (not when imported by tests).
|
|
1312
|
-
|
|
1492
|
+
// Run when invoked as the bin (not when imported by tests). We must resolve
|
|
1493
|
+
// symlinks: npm installs the bin as node_modules/.bin/pyyol → ../pyyol/dist/cli.js,
|
|
1494
|
+
// so process.argv[1] is the SYMLINK path while import.meta.url is the real module
|
|
1495
|
+
// path. Comparing them raw (the old check) never matched under a real install, so
|
|
1496
|
+
// the installed `pyyol` command silently did nothing. realpathSync resolves the
|
|
1497
|
+
// shim to the real file so both sides match.
|
|
1498
|
+
let invoked = false;
|
|
1499
|
+
try {
|
|
1500
|
+
invoked = !!process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
1501
|
+
}
|
|
1502
|
+
catch {
|
|
1503
|
+
invoked = false;
|
|
1504
|
+
}
|
|
1313
1505
|
if (invoked) {
|
|
1314
1506
|
// Set exitCode and let the event loop drain — process.exit() can truncate a
|
|
1315
1507
|
// large piped stdout (e.g. `pyyol replay … --json | jq`) mid-write.
|
|
@@ -1322,4 +1514,3 @@ if (invoked) {
|
|
|
1322
1514
|
process.exitCode = 1;
|
|
1323
1515
|
});
|
|
1324
1516
|
}
|
|
1325
|
-
//# sourceMappingURL=cli.js.map
|
package/dist/config.d.ts
CHANGED
|
@@ -25,4 +25,3 @@ export declare function save(cfg: Config, directory?: string): string;
|
|
|
25
25
|
export declare function setAgentId(agentId: string, path?: string): boolean;
|
|
26
26
|
/** Return a list of human-readable problems (empty = valid). */
|
|
27
27
|
export declare function validate(cfg: Config): string[];
|
|
28
|
-
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
CHANGED
package/dist/credentials.d.ts
CHANGED
package/dist/credentials.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +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";
|
|
26
|
-
|
|
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,5 +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";
|
|
21
|
-
|
|
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 {};
|