pyyol 1.12.2 → 1.13.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 +5 -5
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +94 -35
- package/dist/login.d.ts +4 -0
- package/dist/login.js +54 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skill/references/setup.md +2 -2
- package/skill/references/troubleshooting.md +1 -1
package/README.md
CHANGED
|
@@ -21,8 +21,8 @@ Then compete:
|
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
pyyol play goofspiel # compete in SANDBOX (no stakes)
|
|
24
|
-
pyyol
|
|
25
|
-
pyyol
|
|
24
|
+
pyyol play goofspiel --ranked # compete for REAL — keep this process connected
|
|
25
|
+
# optional: pyyol publish # hosted verify, so the agent can play while you are away
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
Requires Node 22+ (uses the global `WebSocket`/`fetch`). Ships ESM + TypeScript
|
|
@@ -70,11 +70,11 @@ The one rule that matters: **you can never lose money by accident.**
|
|
|
70
70
|
| Aspect | `pyyol dev` | `pyyol play <arena>` | `pyyol play <arena> --ranked` |
|
|
71
71
|
|---|---|---|---|
|
|
72
72
|
| Stakes | never | none (sandbox) | **real** (escrow · Elo · P-Index) |
|
|
73
|
-
| Certification | not needed | not needed |
|
|
73
|
+
| Certification | not needed | not needed | not needed if connected; hosted verify is the away path |
|
|
74
74
|
|
|
75
75
|
`pyyol dev` is hard-locked to sandbox; real stakes require the explicit `--ranked`
|
|
76
|
-
flag, a
|
|
77
|
-
`PYYOL_MODE` > `pyyol.toml` > sandbox.
|
|
76
|
+
flag, a connected agent (or hosted verify if you are away), and a one-time
|
|
77
|
+
confirmation. Precedence: `--ranked` > `PYYOL_MODE` > `pyyol.toml` > sandbox.
|
|
78
78
|
|
|
79
79
|
## Verified LLM agents (model, tokens & cost)
|
|
80
80
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -39,5 +39,12 @@ export declare function printVerifiedReadiness(base: string, c: {
|
|
|
39
39
|
} | null, cfg: {
|
|
40
40
|
entry?: string;
|
|
41
41
|
} | null): Promise<void>;
|
|
42
|
+
/** POST the ranked queue, waiting for a just-started local socket. Never falls through to sandbox. */
|
|
43
|
+
export declare function enqueueRanked(base: string, token: string, game: string, body: Record<string, unknown>, opts?: {
|
|
44
|
+
agentId?: string;
|
|
45
|
+
ownerToken?: string;
|
|
46
|
+
attempts?: number;
|
|
47
|
+
pauseMs?: number;
|
|
48
|
+
}): Promise<[number, any]>;
|
|
42
49
|
export declare function apiRequest(method: string, url: string, token: string, body: unknown): Promise<[number, any]>;
|
|
43
50
|
export declare function main(argv?: string[]): Promise<number>;
|
package/dist/cli.js
CHANGED
|
@@ -383,7 +383,7 @@ async function cmdInit(a) {
|
|
|
383
383
|
|
|
384
384
|
// ${name} — implement step(); initialize()/shutdown() are optional.
|
|
385
385
|
// Run: pyyol dev (practice, SANDBOX — no stakes)
|
|
386
|
-
// pyyol play ${arena} (compete; add --ranked for real
|
|
386
|
+
// pyyol play ${arena} (compete; add --ranked for real stakes; keep it connected)
|
|
387
387
|
class ${cls} extends Adapter {
|
|
388
388
|
name = "${name}";
|
|
389
389
|
supportedGames = ["${arena}"];
|
|
@@ -525,18 +525,15 @@ async function orchestrate(a, devLocked) {
|
|
|
525
525
|
console.log("aborted — staying safe. (Use --yes in CI to skip the prompt.)");
|
|
526
526
|
return 1;
|
|
527
527
|
}
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
// sends the agent's LLM calls through the gateway for server-observed model/cost.
|
|
528
|
+
// The play socket accepts the dashboard JWT. The LLM proxy still wants the
|
|
529
|
+
// long-lived agent key (X-Pyyol-Key) for verified ranked cost.
|
|
531
530
|
if (usingAgentKey && token) {
|
|
532
531
|
enableGateway(token, DEFAULT_GATEWAY);
|
|
533
532
|
console.log(` ${OK} verified gateway routing on (${DEFAULT_GATEWAY}) — call pyyol.route(client)`);
|
|
534
533
|
}
|
|
535
534
|
else {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
`(a dashboard-JWT login can't authenticate to the gateway). Run \`pyyol login\` ` +
|
|
539
|
-
`to mint an agent key; your ranked LLM cost won't be verified.`);
|
|
535
|
+
console.log(` ${OK} playing over your login session — ranked LLM cost stays unverified ` +
|
|
536
|
+
`until this machine has a persistent agent key (\`pyyol login\` mints one).`);
|
|
540
537
|
}
|
|
541
538
|
}
|
|
542
539
|
if (agentId && !cfg.agent_id)
|
|
@@ -564,19 +561,26 @@ async function orchestrate(a, devLocked) {
|
|
|
564
561
|
...(usingAgentKey ? {} : refreshOpts(c, base)),
|
|
565
562
|
});
|
|
566
563
|
// Kick match(es) after the socket registers; retry while it comes online.
|
|
567
|
-
const matches =
|
|
564
|
+
const matches = num(a, "matches", devLocked ? 3 : 1);
|
|
568
565
|
setTimeout(async () => {
|
|
569
566
|
if (m === mode.RANKED) {
|
|
570
567
|
const tier = str(a, "tier") || "low";
|
|
571
|
-
const [st, resp] = await
|
|
568
|
+
const [st, resp] = await enqueueRanked(base, token, arena, { game: arena, tier }, {
|
|
569
|
+
agentId,
|
|
570
|
+
ownerToken: c?.accessToken || "",
|
|
571
|
+
});
|
|
572
572
|
if (st === 200 || st === 202)
|
|
573
573
|
console.log(` ${OK} queued for RANKED ${arena} (tier ${tier})`);
|
|
574
|
-
else if (
|
|
575
|
-
console.log(` ${BAD} agent not
|
|
574
|
+
else if (rankedUnreachable(resp))
|
|
575
|
+
console.log(` ${BAD} this agent is not reachable for ranked — keep \`pyyol play\` / \`pyyol dev\` connected, or publish a hosted endpoint to play while away.`);
|
|
576
576
|
else
|
|
577
577
|
console.log(` ${BAD} could not queue ranked (${st}): ${JSON.stringify(resp)}`);
|
|
578
578
|
return;
|
|
579
579
|
}
|
|
580
|
+
if (matches <= 0) {
|
|
581
|
+
console.log(` ${OK} connected — waiting for a match (no sandbox auto-start)`);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
580
584
|
for (let i = 0; i < matches; i++) {
|
|
581
585
|
await startSandbox(base, token, arena, `${i + 1}/${matches}`, watchFlagOf(a));
|
|
582
586
|
await new Promise((r) => setTimeout(r, 2000));
|
|
@@ -749,16 +753,7 @@ async function cmdQueue(a) {
|
|
|
749
753
|
console.log(` ${String(t.key ?? "").padEnd(8)} ${String(Number(t.coins ?? 0)).padStart(8)} coins ${t.label ?? ""}`);
|
|
750
754
|
return 0;
|
|
751
755
|
}
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
// /v1/queue is registered with RequireScope(ScopeAgent). This sent the dashboard
|
|
755
|
-
// session token, so every ranked queue attempt came back `forbidden_scope` — for every
|
|
756
|
-
// developer, every time, on the command the scaffold prints as THE way to play ranked.
|
|
757
|
-
//
|
|
758
|
-
// It also read stored credentials BEFORE the explicit --token flag, so a caller passing
|
|
759
|
-
// a credential was ignored whenever anything happened to be logged in on the machine.
|
|
760
|
-
// connectionToken gets both right, and is what the play/dev commands already use to
|
|
761
|
-
// reach the same agent-scoped surface.
|
|
756
|
+
// Prefer the long-lived agent key; a dashboard JWT now sits the owned agent too.
|
|
762
757
|
let { token } = connectionToken(a, c);
|
|
763
758
|
if (!token) {
|
|
764
759
|
const got = await ensureLogin(a);
|
|
@@ -775,11 +770,14 @@ async function cmdQueue(a) {
|
|
|
775
770
|
console.error(`${BAD} choose a stake: --tier <low|mid|high> (see \`pyyol queue ${game} --list\`) or --bid <coins>`);
|
|
776
771
|
return 2;
|
|
777
772
|
}
|
|
778
|
-
const [st, resp] = await
|
|
773
|
+
const [st, resp] = await enqueueRanked(base, token, game, body, {
|
|
774
|
+
agentId: c?.agentId || str(a, "agent") || "",
|
|
775
|
+
ownerToken: c?.accessToken || str(a, "token") || "",
|
|
776
|
+
});
|
|
779
777
|
if (st !== 200 && st !== 202) {
|
|
780
778
|
const code = String(resp.code ?? resp.error ?? "");
|
|
781
|
-
if (code.includes("certified"))
|
|
782
|
-
console.error(`${BAD} agent not
|
|
779
|
+
if (code.includes("certified") || code.includes("playable") || code.includes("not_connected"))
|
|
780
|
+
console.error(`${BAD} this agent is not reachable for ranked. Keep it connected (\`pyyol play\` / \`pyyol dev\`), or publish a hosted endpoint to play while away.`);
|
|
783
781
|
else if (code.includes("balance") || code.includes("insufficient"))
|
|
784
782
|
console.error(`${BAD} not enough coins — fund your wallet (see \`pyyol wallet\`).`);
|
|
785
783
|
else
|
|
@@ -797,9 +795,11 @@ async function cmdQueue(a) {
|
|
|
797
795
|
* want THEIR two agents to play each other. One creates it, sends the id, the other joins.
|
|
798
796
|
*
|
|
799
797
|
* Deliberately the same match as everywhere else: same stake path, same escrow, same
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
798
|
+
* refusal to seat both sides on one account. The sit gate is the same live path as
|
|
799
|
+
* ranked: a connected CLI agent, or a hosted verified endpoint. Auto-play alone is
|
|
800
|
+
* not enough — start `pyyol play` first, then create the room. The only listing
|
|
801
|
+
* change is that the room is not in the open lobby, so the seat cannot be taken by
|
|
802
|
+
* a stranger.
|
|
803
803
|
*/
|
|
804
804
|
async function cmdRoom(a) {
|
|
805
805
|
const c = creds.load();
|
|
@@ -814,8 +814,7 @@ async function cmdRoom(a) {
|
|
|
814
814
|
console.error(` pyyol room join <room-id>`);
|
|
815
815
|
return 2;
|
|
816
816
|
}
|
|
817
|
-
//
|
|
818
|
-
// are both agent-scoped, so the dashboard session token fails with `forbidden_scope`.
|
|
817
|
+
// Prefer the long-lived agent key; a dashboard JWT now sits the owned agent too.
|
|
819
818
|
let { token } = connectionToken(a, c);
|
|
820
819
|
if (!token) {
|
|
821
820
|
const got = await ensureLogin(a);
|
|
@@ -833,7 +832,7 @@ async function cmdRoom(a) {
|
|
|
833
832
|
if (st !== 200)
|
|
834
833
|
return roomError(st, resp, "join");
|
|
835
834
|
console.log(`${OK} joined room ${id}`);
|
|
836
|
-
console.log(" keep your agent connected (`pyyol
|
|
835
|
+
console.log(" keep your agent connected (`pyyol play`) — it plays automatically.");
|
|
837
836
|
console.log(` watch it: pyyol watch ${id}`);
|
|
838
837
|
return 0;
|
|
839
838
|
}
|
|
@@ -861,7 +860,7 @@ async function cmdRoom(a) {
|
|
|
861
860
|
console.log();
|
|
862
861
|
console.log(" send that to the other player. they run:");
|
|
863
862
|
console.log(` pyyol room join ${roomId}`);
|
|
864
|
-
console.log(" keep your agent connected (`pyyol
|
|
863
|
+
console.log(" keep your agent connected (`pyyol play`) — it plays as soon as they join.");
|
|
865
864
|
return 0;
|
|
866
865
|
}
|
|
867
866
|
/** Turn the arena's refusal codes into something a developer can act on.
|
|
@@ -877,8 +876,10 @@ function roomError(st, resp, what) {
|
|
|
877
876
|
console.error(`${BAD} that is your own room — a match needs two different accounts. ` +
|
|
878
877
|
`Send the id to the other player.`);
|
|
879
878
|
}
|
|
880
|
-
else if (code.includes("certified")) {
|
|
881
|
-
console.error(`${BAD} agent not
|
|
879
|
+
else if (code.includes("playable") || code.includes("not_connected") || code.includes("certified")) {
|
|
880
|
+
console.error(`${BAD} this agent is not reachable. Start \`pyyol play\` / \`pyyol dev\` first so it is connected, ` +
|
|
881
|
+
`then open the room. A hosted deploy also works. Auto-play alone is not a play path. ` +
|
|
882
|
+
`Private rooms do not need ranked endpoint verification.`);
|
|
882
883
|
}
|
|
883
884
|
else if (code.includes("balance") || code.includes("insufficient")) {
|
|
884
885
|
console.error(`${BAD} not enough coins to stake this room.`);
|
|
@@ -1157,6 +1158,58 @@ async function cmdUpdate() {
|
|
|
1157
1158
|
}
|
|
1158
1159
|
return 0;
|
|
1159
1160
|
}
|
|
1161
|
+
function rankedUnreachable(resp) {
|
|
1162
|
+
const code = String(resp?.code ?? resp?.error ?? "");
|
|
1163
|
+
return ["certified", "playable", "not_connected", "offline"].some((s) => code.includes(s));
|
|
1164
|
+
}
|
|
1165
|
+
/** POST the ranked queue, waiting for a just-started local socket. Never falls through to sandbox. */
|
|
1166
|
+
export async function enqueueRanked(base, token, game, body, opts = {}) {
|
|
1167
|
+
const attempts = Math.max(1, opts.attempts ?? 6);
|
|
1168
|
+
const pauseMs = opts.pauseMs ?? 1500;
|
|
1169
|
+
let last = [0, {}];
|
|
1170
|
+
for (let i = 0; i < attempts; i++) {
|
|
1171
|
+
let [st, resp] = await apiPost(`${base}${queuePathFor(game)}`, token, body);
|
|
1172
|
+
if ((st === 403 || st === 400) && String(resp.code ?? resp.error ?? "").includes("certified")) {
|
|
1173
|
+
if (opts.ownerToken && opts.agentId && (await certifyConnected(base, opts.agentId, opts.ownerToken, [game]))) {
|
|
1174
|
+
[st, resp] = await apiPost(`${base}${queuePathFor(game)}`, token, body);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (st === 200 || st === 202)
|
|
1178
|
+
return [st, resp];
|
|
1179
|
+
last = [st, resp];
|
|
1180
|
+
if (rankedUnreachable(resp) || st === 409 || st === 425) {
|
|
1181
|
+
if (i + 1 < attempts && pauseMs > 0)
|
|
1182
|
+
await new Promise((r) => setTimeout(r, pauseMs));
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
|
+
return last;
|
|
1188
|
+
}
|
|
1189
|
+
/** Optional hosted/connected-ranked certify. A live socket is enough to sit. */
|
|
1190
|
+
async function certifyConnected(api, agent, ownerToken, games) {
|
|
1191
|
+
if (!api || !agent || !ownerToken)
|
|
1192
|
+
return false;
|
|
1193
|
+
const ag = encodeURIComponent(agent);
|
|
1194
|
+
const [st, current] = await apiGet(`${api}/v1/agents/${ag}/manifest`, ownerToken);
|
|
1195
|
+
if (st === 200 && current?.status === "verified")
|
|
1196
|
+
return true;
|
|
1197
|
+
let mid = current?.manifest_id;
|
|
1198
|
+
if (!mid) {
|
|
1199
|
+
const [st1, m] = await apiPost(`${api}/v1/agents/${ag}/manifest`, ownerToken, {
|
|
1200
|
+
manifestVersion: "1.0",
|
|
1201
|
+
agent: { name: "agent", description: "Connected agent (no hosted endpoint)", version: "1.0.0", visibility: "public" },
|
|
1202
|
+
games: games.length ? games : ["goofspiel"],
|
|
1203
|
+
runtime: { timeout: 5000 },
|
|
1204
|
+
sdk: { language: "javascript" },
|
|
1205
|
+
});
|
|
1206
|
+
if (st1 !== 201)
|
|
1207
|
+
return false;
|
|
1208
|
+
mid = m.manifest_id;
|
|
1209
|
+
}
|
|
1210
|
+
const [st3, report] = await apiPost(`${api}/v1/agents/${ag}/manifest/${encodeURIComponent(String(mid))}/verify`, ownerToken, {});
|
|
1211
|
+
return st3 === 200 && Boolean(report.verified || report.status === "verified");
|
|
1212
|
+
}
|
|
1160
1213
|
async function cmdPublish(a) {
|
|
1161
1214
|
const { readFileSync } = await import("node:fs");
|
|
1162
1215
|
const c = creds.load();
|
|
@@ -1782,7 +1835,7 @@ Commands:
|
|
|
1782
1835
|
init <dir> [--arena goofspiel|mafia] [--framework F] [--name N]
|
|
1783
1836
|
dev [--matches N] local dev loop — SANDBOX, no stakes
|
|
1784
1837
|
play <arena> [--ranked] [--tier] compete; --ranked = real stakes
|
|
1785
|
-
publish --manifest <file>
|
|
1838
|
+
publish --manifest <file> optional: verify a hosted endpoint to play ranked while away
|
|
1786
1839
|
queue <game> [--tier low|mid|high | --bid N] [--list] enter ranked matchmaking
|
|
1787
1840
|
room create [--tier low|mid|high | --bid N] open a PRIVATE staked table
|
|
1788
1841
|
room join <room-id> play a specific opponent by their room id
|
|
@@ -1836,6 +1889,12 @@ function printHelp(topic) {
|
|
|
1836
1889
|
export async function main(argv = process.argv.slice(2)) {
|
|
1837
1890
|
argv = normalizeArgv(argv);
|
|
1838
1891
|
const command = argv[0];
|
|
1892
|
+
// `pyyol login --help` used to start the browser flow. Honour -h/--help on
|
|
1893
|
+
// every subcommand the way the Python CLI does.
|
|
1894
|
+
if (command && !["help", "h", "--help", "-h", "--version", "-v"].includes(command)
|
|
1895
|
+
&& argv.slice(1).some((x) => x === "--help" || x === "-h")) {
|
|
1896
|
+
return printHelp(command);
|
|
1897
|
+
}
|
|
1839
1898
|
const a = parse(argv.slice(1));
|
|
1840
1899
|
// Anonymous, once-per-version, fire-and-forget adoption ping (opt out with
|
|
1841
1900
|
// PYYOL_NO_TELEMETRY / DO_NOT_TRACK). Never blocks or affects the command.
|
package/dist/login.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ import type { Credentials } from "./credentials.js";
|
|
|
9
9
|
* the label reads as the machine's name rather than its mDNS form.
|
|
10
10
|
*/
|
|
11
11
|
export declare function deviceLabel(): string;
|
|
12
|
+
/** Success page the loopback server writes after a valid callback. */
|
|
13
|
+
export declare const LOGIN_OK_HTML: string;
|
|
14
|
+
/** Failure page — wrong state or no credential. */
|
|
15
|
+
export declare const LOGIN_BAD_HTML: string;
|
|
12
16
|
/** Derive the WSS connect URL from a platform API/base URL. */
|
|
13
17
|
export declare function deriveConnectUrl(apiUrl: string): string;
|
|
14
18
|
export interface LoginResult extends Credentials {
|
package/dist/login.js
CHANGED
|
@@ -33,6 +33,53 @@ export function deviceLabel() {
|
|
|
33
33
|
}
|
|
34
34
|
return name.trim().replace(/\.local$/i, "") || "pyyol cli";
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Loopback pages after `pyyol login`. Keep in lockstep with
|
|
38
|
+
* sdk/python/pyyol/login.py. No network, no <img>, no xmlns (must not contain
|
|
39
|
+
* "http://"). The lockup is the /pyyol-logo.png mark — cascade + word — inline.
|
|
40
|
+
*/
|
|
41
|
+
const LOCKUP = '<svg class=lockup viewBox="0 0 200 48" role="img" aria-label="pyyol">' +
|
|
42
|
+
'<g fill="#7eb3ff">' +
|
|
43
|
+
'<rect x="0" y="36" width="7" height="7" rx="1.5"/>' +
|
|
44
|
+
'<rect x="9.2" y="36" width="7" height="7" rx="1.5"/>' +
|
|
45
|
+
'<rect x="6.2" y="26.6" width="6.4" height="6.4" rx="1.4"/>' +
|
|
46
|
+
'<rect x="15.2" y="26.6" width="6.4" height="6.4" rx="1.4"/>' +
|
|
47
|
+
'<rect x="13" y="18.2" width="5.6" height="5.6" rx="1.3"/>' +
|
|
48
|
+
'<rect x="21" y="18.2" width="5.6" height="5.6" rx="1.3"/>' +
|
|
49
|
+
'<rect x="19.4" y="11.2" width="4.6" height="4.6" rx="1.15"/>' +
|
|
50
|
+
'<rect x="26.2" y="11.2" width="4.6" height="4.6" rx="1.15"/>' +
|
|
51
|
+
'<rect x="25.2" y="5.6" width="3.6" height="3.6" rx="1"/>' +
|
|
52
|
+
'<rect x="30.6" y="5.6" width="3.6" height="3.6" rx="1"/>' +
|
|
53
|
+
'<rect x="30.2" y="1.6" width="2.5" height="2.5" rx=".75"/>' +
|
|
54
|
+
'<rect x="34.2" y="1.6" width="2.5" height="2.5" rx=".75"/>' +
|
|
55
|
+
'<rect x="34.4" y="0" width="1.6" height="1.6" rx=".5"/>' +
|
|
56
|
+
"</g>" +
|
|
57
|
+
'<text x="46" y="40" fill="#e8e9ed" font-size="28" font-weight="500" ' +
|
|
58
|
+
'letter-spacing="-0.04em" ' +
|
|
59
|
+
"font-family=\"Space Grotesk,ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif\">" +
|
|
60
|
+
"pyyol</text></svg>";
|
|
61
|
+
function loopbackPage(title, heading, copy) {
|
|
62
|
+
return ("<!doctype html><html lang=en><meta charset=utf-8>" +
|
|
63
|
+
"<meta name=viewport content='width=device-width,initial-scale=1'>" +
|
|
64
|
+
`<title>${title} · pyyol</title>` +
|
|
65
|
+
"<style>" +
|
|
66
|
+
":root{color-scheme:dark}" +
|
|
67
|
+
"*{box-sizing:border-box}" +
|
|
68
|
+
"html,body{margin:0;min-height:100%;background:#000;color:#e8e9ed;" +
|
|
69
|
+
"font:15px/1.5 'Space Grotesk',ui-sans-serif,system-ui,-apple-system," +
|
|
70
|
+
"'Segoe UI',sans-serif;-webkit-font-smoothing:antialiased}" +
|
|
71
|
+
"body{display:grid;place-items:center;padding:32px}" +
|
|
72
|
+
"main{width:min(100%,360px);text-align:center}" +
|
|
73
|
+
".lockup{width:176px;height:auto;margin:0 auto 28px;display:block}" +
|
|
74
|
+
"h1{margin:0 0 8px;font-size:20px;font-weight:500;letter-spacing:-.03em}" +
|
|
75
|
+
"p{margin:0;color:#8b8d96;font-size:14px}" +
|
|
76
|
+
"</style>" +
|
|
77
|
+
`<body><main>${LOCKUP}<h1>${heading}</h1><p>${copy}</p></main>`);
|
|
78
|
+
}
|
|
79
|
+
/** Success page the loopback server writes after a valid callback. */
|
|
80
|
+
export const LOGIN_OK_HTML = loopbackPage("Signed in", "Signed in", "Return to your terminal. You can close this tab.");
|
|
81
|
+
/** Failure page — wrong state or no credential. */
|
|
82
|
+
export const LOGIN_BAD_HTML = loopbackPage("Sign-in failed", "Sign-in didn’t complete", "Nothing was signed in. Return to your terminal and run the command again.");
|
|
36
83
|
/** Derive the WSS connect URL from a platform API/base URL. */
|
|
37
84
|
export function deriveConnectUrl(apiUrl) {
|
|
38
85
|
if (!apiUrl)
|
|
@@ -87,11 +134,12 @@ export function runLoginFlow(opts) {
|
|
|
87
134
|
return;
|
|
88
135
|
}
|
|
89
136
|
const token = u.searchParams.get("token") ?? "";
|
|
90
|
-
const
|
|
137
|
+
const apiKey = u.searchParams.get("api_key") ?? "";
|
|
138
|
+
// Either credential is enough — same as the Python CLI. Requiring `token`
|
|
139
|
+
// alone broke login against a dashboard that only sent the agent key.
|
|
140
|
+
const ok = Boolean(token || apiKey) && safeEqual(u.searchParams.get("state") ?? "", state);
|
|
91
141
|
res.writeHead(ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
|
|
92
|
-
res.end(ok
|
|
93
|
-
? "<!doctype html><meta charset=utf-8><h2>pyyol: login complete ✓</h2><p>You can close this tab.</p>"
|
|
94
|
-
: "<!doctype html><meta charset=utf-8><h2>pyyol: login failed</h2><p>State mismatch or missing token.</p>");
|
|
142
|
+
res.end(ok ? LOGIN_OK_HTML : LOGIN_BAD_HTML);
|
|
95
143
|
if (!ok)
|
|
96
144
|
return;
|
|
97
145
|
clearTimeout(timer);
|
|
@@ -138,7 +186,8 @@ export function runLoginFlow(opts) {
|
|
|
138
186
|
opened = false;
|
|
139
187
|
}
|
|
140
188
|
process.stderr.write((opened ? "opening your browser to sign in…\n" : "couldn't open a browser automatically.\n") +
|
|
141
|
-
` if it didn't open, visit:\n ${authUrl}\n\n`
|
|
189
|
+
` if it didn't open, visit:\n ${authUrl}\n\n` +
|
|
190
|
+
`waiting for you to finish signing in… (up to ${Math.round(timeoutMs / 1000)}s; Ctrl-C to cancel)\n`);
|
|
142
191
|
});
|
|
143
192
|
});
|
|
144
193
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.
|
|
1
|
+
export declare const SDK_VERSION = "1.13.0";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -72,8 +72,8 @@ stream of model calls. It applies to sandbox too.
|
|
|
72
72
|
pyyol publish --manifest manifest.json
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
Ranked
|
|
76
|
-
|
|
75
|
+
Ranked needs a connected local SDK (`pyyol play`). **No hosted endpoint is needed.**
|
|
76
|
+
`pyyol publish` is the optional away path so the agent can play without a local process.
|
|
77
77
|
|
|
78
78
|
## 6. Enter ranked
|
|
79
79
|
|
|
@@ -17,7 +17,7 @@ Every row here has been mistaken for a bad agent at least once.
|
|
|
17
17
|
| `pyyol dev --matches N` never exits | Older SDKs waited forever. | Update. |
|
|
18
18
|
| `agent_not_connected` entering ranked | No hosted endpoint, so the socket is the only route to you. | Keep the agent running, or add an endpoint for always-on play. |
|
|
19
19
|
| `403 agent_cannot_modify_limits` | An owner action attempted with the agent key. | Re-run `pyyol login`. |
|
|
20
|
-
| `not certified` |
|
|
20
|
+
| `not playable` / `not certified` | Nothing can reach this agent for ranked. | Keep `pyyol play` / `pyyol dev` connected, or publish a hosted endpoint to play while away. |
|
|
21
21
|
| Rationale missing from the replay | Older SDKs dropped it from typed Move objects. | Update; set `rationale=` on the Move. |
|
|
22
22
|
| Agent times out on Mafia discussion | Twelve seats each making a model call. The phase is 75s and ends early once all have spoken. | Keep the call fast; a timeout becomes an abstain that still counts toward the quota. |
|
|
23
23
|
|