pyyol 1.12.2 → 1.12.3

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 CHANGED
@@ -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
- // Enable verified-tier gateway routing (only with an agent key the gateway
529
- // authenticates X-Pyyol-Key via it; a dashboard JWT can't). Then pyyol.route(client)
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
- // Don't silently run unverified: the dev thinks they're competing verified.
537
- console.error(` ${BAD} verified gateway routing OFF no agent key in this session ` +
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)
@@ -568,7 +565,13 @@ async function orchestrate(a, devLocked) {
568
565
  setTimeout(async () => {
569
566
  if (m === mode.RANKED) {
570
567
  const tier = str(a, "tier") || "low";
571
- const [st, resp] = await apiPost(`${base}${queuePathFor(arena)}`, token, { game: arena, tier });
568
+ let [st, resp] = await apiPost(`${base}${queuePathFor(arena)}`, token, { game: arena, tier });
569
+ if ((st === 403 || st === 400) && String(resp.code ?? "").includes("certified")) {
570
+ const owner = c?.accessToken || "";
571
+ if (owner && agentId && (await certifyConnected(base, agentId, owner, [arena]))) {
572
+ [st, resp] = await apiPost(`${base}${queuePathFor(arena)}`, token, { game: arena, tier });
573
+ }
574
+ }
572
575
  if (st === 200 || st === 202)
573
576
  console.log(` ${OK} queued for RANKED ${arena} (tier ${tier})`);
574
577
  else if (String(resp.code ?? "").includes("certified"))
@@ -749,16 +752,7 @@ async function cmdQueue(a) {
749
752
  console.log(` ${String(t.key ?? "").padEnd(8)} ${String(Number(t.coins ?? 0)).padStart(8)} coins ${t.label ?? ""}`);
750
753
  return 0;
751
754
  }
752
- // Queuing is an AGENT action, so it needs the AGENT key.
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.
755
+ // Prefer the long-lived agent key; a dashboard JWT now sits the owned agent too.
762
756
  let { token } = connectionToken(a, c);
763
757
  if (!token) {
764
758
  const got = await ensureLogin(a);
@@ -775,7 +769,14 @@ async function cmdQueue(a) {
775
769
  console.error(`${BAD} choose a stake: --tier <low|mid|high> (see \`pyyol queue ${game} --list\`) or --bid <coins>`);
776
770
  return 2;
777
771
  }
778
- const [st, resp] = await apiPost(`${base}${queuePathFor(game)}`, token, body);
772
+ let [st, resp] = await apiPost(`${base}${queuePathFor(game)}`, token, body);
773
+ if ((st === 403 || st === 400) && String(resp.code ?? resp.error ?? "").includes("certified")) {
774
+ const owner = c?.accessToken || str(a, "token") || "";
775
+ const agent = c?.agentId || str(a, "agent") || "";
776
+ if (owner && agent && (await certifyConnected(base, agent, owner, [game]))) {
777
+ [st, resp] = await apiPost(`${base}${queuePathFor(game)}`, token, body);
778
+ }
779
+ }
779
780
  if (st !== 200 && st !== 202) {
780
781
  const code = String(resp.code ?? resp.error ?? "");
781
782
  if (code.includes("certified"))
@@ -814,8 +815,7 @@ async function cmdRoom(a) {
814
815
  console.error(` pyyol room join <room-id>`);
815
816
  return 2;
816
817
  }
817
- // A room is an AGENT action exactly like `queue`: /v1/room/create and /v1/lobby/join
818
- // are both agent-scoped, so the dashboard session token fails with `forbidden_scope`.
818
+ // Prefer the long-lived agent key; a dashboard JWT now sits the owned agent too.
819
819
  let { token } = connectionToken(a, c);
820
820
  if (!token) {
821
821
  const got = await ensureLogin(a);
@@ -1157,6 +1157,30 @@ async function cmdUpdate() {
1157
1157
  }
1158
1158
  return 0;
1159
1159
  }
1160
+ /** Certify a connected-ranked manifest (no hosted URL) so queue/play can sit. */
1161
+ async function certifyConnected(api, agent, ownerToken, games) {
1162
+ if (!api || !agent || !ownerToken)
1163
+ return false;
1164
+ const ag = encodeURIComponent(agent);
1165
+ const [st, current] = await apiGet(`${api}/v1/agents/${ag}/manifest`, ownerToken);
1166
+ if (st === 200 && current?.status === "verified")
1167
+ return true;
1168
+ let mid = current?.manifest_id;
1169
+ if (!mid) {
1170
+ const [st1, m] = await apiPost(`${api}/v1/agents/${ag}/manifest`, ownerToken, {
1171
+ manifestVersion: "1.0",
1172
+ agent: { name: "agent", description: "Connected agent (no hosted endpoint)", version: "1.0.0", visibility: "public" },
1173
+ games: games.length ? games : ["goofspiel"],
1174
+ runtime: { timeout: 5000 },
1175
+ sdk: { language: "javascript" },
1176
+ });
1177
+ if (st1 !== 201)
1178
+ return false;
1179
+ mid = m.manifest_id;
1180
+ }
1181
+ const [st3, report] = await apiPost(`${api}/v1/agents/${ag}/manifest/${encodeURIComponent(String(mid))}/verify`, ownerToken, {});
1182
+ return st3 === 200 && Boolean(report.verified || report.status === "verified");
1183
+ }
1160
1184
  async function cmdPublish(a) {
1161
1185
  const { readFileSync } = await import("node:fs");
1162
1186
  const c = creds.load();
@@ -1836,6 +1860,12 @@ function printHelp(topic) {
1836
1860
  export async function main(argv = process.argv.slice(2)) {
1837
1861
  argv = normalizeArgv(argv);
1838
1862
  const command = argv[0];
1863
+ // `pyyol login --help` used to start the browser flow. Honour -h/--help on
1864
+ // every subcommand the way the Python CLI does.
1865
+ if (command && !["help", "h", "--help", "-h", "--version", "-v"].includes(command)
1866
+ && argv.slice(1).some((x) => x === "--help" || x === "-h")) {
1867
+ return printHelp(command);
1868
+ }
1839
1869
  const a = parse(argv.slice(1));
1840
1870
  // Anonymous, once-per-version, fire-and-forget adoption ping (opt out with
1841
1871
  // 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&rsquo;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 ok = Boolean(token) && safeEqual(u.searchParams.get("state") ?? "", state);
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.12.2";
1
+ export declare const SDK_VERSION = "1.12.3";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // GENERATED by scripts/genversion.mjs — do not edit by hand.
2
2
  // Source of truth is the "version" field in package.json.
3
- export const SDK_VERSION = "1.12.2"; // x-release-please-version
3
+ export const SDK_VERSION = "1.12.3"; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyyol",
3
- "version": "1.12.2",
3
+ "version": "1.12.3",
4
4
  "description": "Official JS/TS SDK for pyyol — run AI game-playing agents locally over a WebSocket (Beta)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",