@chipzen-ai/bot 0.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.
@@ -0,0 +1,746 @@
1
+ /**
2
+ * Core data models — Action, GameState, Card.
3
+ *
4
+ * Field naming uses idiomatic camelCase for the SDK's user-facing
5
+ * surface. The on-the-wire JSON the protocol uses is snake_case
6
+ * (Layer 1 / Layer 2 spec); the parsers in this module translate.
7
+ */
8
+ /**
9
+ * A standard playing card. `rank` is one of `2`-`9`, `T`, `J`, `Q`,
10
+ * `K`, `A`. `suit` is one of `h` (hearts), `d` (diamonds), `c`
11
+ * (clubs), `s` (spades).
12
+ */
13
+ interface Card {
14
+ readonly rank: string;
15
+ readonly suit: string;
16
+ }
17
+ /**
18
+ * Parse a card from its 2-character wire representation, e.g. `"Ah"`.
19
+ *
20
+ * Throws `Error` on malformed input; the wire format is
21
+ * always exactly 2 characters with a valid rank + suit.
22
+ */
23
+ declare function cardFromString(s: string): Card;
24
+ /** Render a card back to its 2-character wire form. */
25
+ declare function cardToString(c: Card): string;
26
+ type ActionKind = "fold" | "check" | "call" | "raise" | "all_in";
27
+ /**
28
+ * The action a bot returns from `decide()`.
29
+ *
30
+ * Construct via the static factories (`Action.fold()` etc.) — they
31
+ * validate the action vs. amount invariants for you.
32
+ */
33
+ declare class Action {
34
+ readonly action: ActionKind;
35
+ readonly amount?: number | undefined;
36
+ private constructor();
37
+ static fold(): Action;
38
+ static check(): Action;
39
+ static call(): Action;
40
+ static raiseTo(amount: number): Action;
41
+ static allIn(): Action;
42
+ /**
43
+ * Serialize to the two-layer `turn_action` payload shape the server expects.
44
+ *
45
+ * Returns `{action, params}` where `params` carries the raise amount
46
+ * for `raise` and is empty for everything else.
47
+ */
48
+ toWire(): {
49
+ action: ActionKind;
50
+ params: Record<string, unknown>;
51
+ };
52
+ }
53
+ /**
54
+ * A single entry from `state.actionHistory`. Synthetic blind/ante
55
+ * entries (`post_small_blind`, `post_big_blind`, `post_ante`) appear
56
+ * here too — the server generates them; bots do not submit them.
57
+ */
58
+ interface ActionHistoryEntry {
59
+ readonly seat: number;
60
+ readonly action: string;
61
+ readonly amount?: number;
62
+ readonly isTimeout?: boolean;
63
+ }
64
+ /**
65
+ * Built from the server's `turn_request` message. The parser in
66
+ * `parseGameState` converts the wire-format snake_case to the
67
+ * camelCase fields below.
68
+ */
69
+ interface GameState {
70
+ readonly handNumber: number;
71
+ readonly phase: "preflop" | "flop" | "turn" | "river";
72
+ readonly holeCards: readonly Card[];
73
+ readonly board: readonly Card[];
74
+ readonly pot: number;
75
+ readonly yourStack: number;
76
+ readonly opponentStacks: readonly number[];
77
+ readonly yourSeat: number;
78
+ readonly dealerSeat: number;
79
+ readonly toCall: number;
80
+ readonly minRaise: number;
81
+ readonly maxRaise: number;
82
+ readonly validActions: readonly string[];
83
+ readonly actionHistory: readonly ActionHistoryEntry[];
84
+ readonly roundId: string;
85
+ readonly requestId: string;
86
+ }
87
+ interface RawTurnRequest {
88
+ request_id?: string;
89
+ round_id?: string;
90
+ valid_actions?: string[];
91
+ state?: Record<string, unknown> | null;
92
+ }
93
+ /**
94
+ * Parse a `turn_request` envelope into a `GameState`.
95
+ *
96
+ * The wire shape is documented in
97
+ * `docs/protocol/POKER-GAME-STATE-PROTOCOL.md`. All fields default
98
+ * to safe values when absent — but a real server always sends them.
99
+ */
100
+ declare function parseGameState(message: RawTurnRequest): GameState;
101
+
102
+ /**
103
+ * `Bot` abstract base class.
104
+ *
105
+ * Subclass and override `decide()`. Lifecycle hooks
106
+ * (`onMatchStart` / `onRoundStart` / `onPhaseChange` / `onTurnResult` /
107
+ * `onRoundResult` / `onMatchEnd`) are optional — defaults are no-ops.
108
+ * Override the ones you need; the SDK's session loop will call them
109
+ * at the right point.
110
+ */
111
+
112
+ declare abstract class Bot {
113
+ /**
114
+ * The only required override. Called every time the server sends a
115
+ * `turn_request` for your seat. Return one of:
116
+ * `Action.fold()`, `Action.check()`, `Action.call()`,
117
+ * `Action.raiseTo(amount)`, `Action.allIn()`.
118
+ *
119
+ * The returned action's wire-form `action` string MUST be in
120
+ * `state.validActions`; raise amounts MUST satisfy
121
+ * `state.minRaise <= amount <= state.maxRaise`. If the bot returns
122
+ * an illegal action, the SDK's safe-fallback substitutes `check`
123
+ * (or `fold` if check is illegal) and the platform sends a
124
+ * `bot_error` event to the human's UI.
125
+ */
126
+ abstract decide(state: GameState): Action;
127
+ /** Called once when the `match_start` message arrives. */
128
+ onMatchStart(_matchInfo: Record<string, unknown>): void;
129
+ /**
130
+ * Called at the start of every hand with the raw `round_start` message.
131
+ *
132
+ * Override if you need the Layer 1 envelope (`round_id`,
133
+ * `round_number`) or the full Layer 2 `state` payload. For most bots,
134
+ * `onHandStart` is the simpler hook.
135
+ */
136
+ onRoundStart(_message: Record<string, unknown>): void;
137
+ /**
138
+ * Called when the flop, turn, or river is dealt. Useful for triggering
139
+ * postflop planning *between* your turns rather than inside `decide`.
140
+ */
141
+ onPhaseChange(_message: Record<string, unknown>): void;
142
+ /**
143
+ * Called after every participant's action is broadcast — yours and
144
+ * every opponent's. Use for opponent modeling, timing analysis, or
145
+ * stack tracking.
146
+ *
147
+ * This hook runs *before* the next `turn_request` is dispatched, and
148
+ * runs serially. Slow work here eats into your decide budget — see
149
+ * the DEV-MANUAL §6 for the "queue drain" failure mode.
150
+ */
151
+ onTurnResult(_message: Record<string, unknown>): void;
152
+ /** Called when a hand ends (`round_result` message). */
153
+ onRoundResult(_message: Record<string, unknown>): void;
154
+ /** Called once when the match ends. */
155
+ onMatchEnd(_results: Record<string, unknown>): void;
156
+ /**
157
+ * Called after each `turn_action` is sent, with the wall-clock time
158
+ * `decide()` took in milliseconds.
159
+ *
160
+ * Default is a no-op. Override to track decision latency — useful for
161
+ * spotting when your bot is drifting toward the platform's turn-timeout
162
+ * budget. See chipzen-ai/chipzen-sdk#46.
163
+ */
164
+ onDecisionLatency(_latencyMs: number): void;
165
+ }
166
+
167
+ /**
168
+ * Retry / backoff policy for the WebSocket client.
169
+ *
170
+ * When the connection to the Chipzen server drops (TCP reset, heartbeat
171
+ * miss, transient network failure, etc.) the SDK reconnects within the
172
+ * server's reconnect grace window. The pacing of those reconnect attempts
173
+ * is configurable via {@link RetryPolicy}, accepted by both `runBot` and
174
+ * `runExternalBot`.
175
+ *
176
+ * The default policy mirrors the Python SDK's spec (External-API Issue 26):
177
+ * 5 attempts, 500ms initial backoff, doubling each attempt, capped at
178
+ * 30 seconds. The defaults are sensible for the typical home-network
179
+ * deployment; devs on noisy connections may want a longer backoff or
180
+ * more attempts.
181
+ *
182
+ * Note: this policy controls **only** how reconnect attempts are paced.
183
+ * The 30-second server-side grace window itself is unchanged; if the
184
+ * reconnects burn through the window the session is considered lost and
185
+ * the server terminates the match-side state.
186
+ */
187
+ /** Backoff knobs accepted by {@link RetryPolicy}. All fields are optional. */
188
+ interface RetryPolicyOptions {
189
+ /**
190
+ * Maximum number of reconnection attempts after a connection drop or
191
+ * heartbeat miss. Must be `>= 0`. `0` disables reconnection entirely
192
+ * (the first connect failure raises). Default `5`.
193
+ */
194
+ maxReconnectAttempts?: number;
195
+ /**
196
+ * Delay before the **first** reconnect attempt, in milliseconds. Must
197
+ * be `>= 0`. Default `500`.
198
+ */
199
+ initialBackoffMs?: number;
200
+ /**
201
+ * Upper bound for any single backoff delay, in milliseconds. Must be
202
+ * `>= initialBackoffMs`. Default `30000` (matches the server-side grace
203
+ * window so a single backoff never exceeds the window itself).
204
+ */
205
+ maxBackoffMs?: number;
206
+ /**
207
+ * Exponential factor applied between attempts. `2.0` doubles the delay
208
+ * each attempt. Must be `>= 1.0`; `1.0` produces constant backoff.
209
+ * Default `2.0`.
210
+ */
211
+ backoffMultiplier?: number;
212
+ }
213
+ /**
214
+ * Backoff knobs applied to reconnect attempts.
215
+ *
216
+ * Backoff progression for attempt `n` (1-indexed) is:
217
+ *
218
+ * min(initialBackoffMs * backoffMultiplier ** (n - 1), maxBackoffMs)
219
+ *
220
+ * Examples (defaults):
221
+ *
222
+ * attempt 1: 500 ms
223
+ * attempt 2: 1000 ms
224
+ * attempt 3: 2000 ms
225
+ * attempt 4: 4000 ms
226
+ * attempt 5: 8000 ms
227
+ * attempt 6: 16000 ms (would be next, but capped by attempts=5)
228
+ */
229
+ declare class RetryPolicy {
230
+ readonly maxReconnectAttempts: number;
231
+ readonly initialBackoffMs: number;
232
+ readonly maxBackoffMs: number;
233
+ readonly backoffMultiplier: number;
234
+ constructor(options?: RetryPolicyOptions);
235
+ /**
236
+ * Return the delay (in ms) to wait **before** the given attempt.
237
+ *
238
+ * @param attempt 1-indexed attempt number. `attempt=1` is the first
239
+ * reconnect after a drop, `attempt=2` the second, etc.
240
+ * @returns The delay in milliseconds, capped at `maxBackoffMs`.
241
+ * @throws Error if `attempt < 1`.
242
+ */
243
+ backoffMs(attempt: number): number;
244
+ }
245
+ /**
246
+ * The default {@link RetryPolicy} used when `runBot` / `runExternalBot`
247
+ * is called without an explicit `retryPolicy` argument.
248
+ */
249
+ declare const DEFAULT_RETRY_POLICY: RetryPolicy;
250
+
251
+ /**
252
+ * WebSocket client for the Chipzen two-layer protocol.
253
+ *
254
+ * The user-facing surface is `runBot(url, bot, options)`. Internals
255
+ * (session loop, mock-friendly helpers) are exported with `_` prefix
256
+ * for the conformance harness and tests; they are not part of the
257
+ * supported API.
258
+ */
259
+
260
+ /**
261
+ * Raised when `bot.decide()` errors and `safeMode` is `false`.
262
+ *
263
+ * Distinguished from transport/connection errors so the caller treats it
264
+ * as terminal (a deterministic bot bug, not a transient disconnect) and
265
+ * does NOT reconnect-retry it. See chipzen-ai/chipzen-sdk#52.
266
+ */
267
+ declare class BotDecisionError extends Error {
268
+ constructor(message: string);
269
+ }
270
+ /**
271
+ * Optional knobs for `runBot`. All fields default to sensible values
272
+ * matching the platform's expectations.
273
+ */
274
+ interface RunBotOptions {
275
+ /** Bot API token. Required for the `/bot` endpoint; empty is fine for local dev. */
276
+ token?: string | null;
277
+ /** Single-use ticket alternative to `token` (competitive endpoints). */
278
+ ticket?: string | null;
279
+ /** Match UUID. Auto-extracted from the URL if not supplied. */
280
+ matchId?: string;
281
+ /** Client software name sent in the `hello` handshake. */
282
+ clientName?: string;
283
+ /** Client software version sent in the `hello` handshake. Defaults to the SDK version. */
284
+ clientVersion?: string;
285
+ /**
286
+ * Reconnect attempt **cap**. When given, overrides the attempt count
287
+ * from `retryPolicy` (the policy's backoff knobs still apply). When
288
+ * omitted, the policy's `maxReconnectAttempts` is used.
289
+ */
290
+ maxRetries?: number;
291
+ /**
292
+ * {@link RetryPolicy} controlling reconnect attempts + exponential
293
+ * backoff. Defaults to {@link DEFAULT_RETRY_POLICY}.
294
+ */
295
+ retryPolicy?: RetryPolicy;
296
+ /**
297
+ * When `true` (default), an exception thrown by `bot.decide()` is
298
+ * folded (a safe fallback action is sent). Set `false` for dev/eval so
299
+ * the first exception propagates as a {@link BotDecisionError} and the
300
+ * process exits non-zero (chipzen-ai/chipzen-sdk#52).
301
+ */
302
+ safeMode?: boolean;
303
+ /**
304
+ * Override the WS `User-Agent` header. Defaults to
305
+ * `chipzen-sdk-js/<version>` (a non-default UA also clears the
306
+ * platform's Cloudflare bot-fight rule; chipzen-ai/chipzen-sdk#46).
307
+ */
308
+ userAgent?: string;
309
+ }
310
+ /** Protocol versions this client claims to support in the handshake. */
311
+ declare const SUPPORTED_PROTOCOL_VERSIONS: readonly ["1.0"];
312
+ /**
313
+ * Connect a bot to the Chipzen server and play until the match ends.
314
+ *
315
+ * Resolves on `match_end` (with the `match_end` payload, ignored by most
316
+ * callers). Rejects if the connection cannot be established (after the
317
+ * reconnect budget is exhausted) or if `safeMode` is `false` and
318
+ * `bot.decide()` throws (a terminal {@link BotDecisionError}).
319
+ */
320
+ declare function runBot(url: string, bot: Bot, options?: RunBotOptions): Promise<Record<string, unknown> | null>;
321
+ interface AsyncMessageReader {
322
+ next(): Promise<string | null>;
323
+ }
324
+
325
+ /**
326
+ * `chipzen.toml` discovery and parsing for the SDK.
327
+ *
328
+ * Devs running an external-API bot should be able to drop their long-lived
329
+ * API token into a config file once and forget about it, instead of
330
+ * hard-coding `token="cz_extbot_..."` into source. This module implements
331
+ * the discovery + parsing half of that convention; `runExternalBot` (and
332
+ * the `chipzen-sdk run-external` CLI) consume the result and prefer
333
+ * explicit kwargs over config-file values.
334
+ *
335
+ * Mirrors the Python SDK's `chipzen.config` (External-API Issue 23,
336
+ * chipzen-ai/chipzen-sdk#42).
337
+ *
338
+ * Discovery
339
+ * ---------
340
+ *
341
+ * Search order, first match wins:
342
+ *
343
+ * 1. `./chipzen.toml` (current working directory)
344
+ * 2. `~/.chipzen/chipzen.toml` (user-home config)
345
+ * 3. `/etc/chipzen/chipzen.toml` (system config, POSIX only — silently
346
+ * skipped on Windows where `/etc` does not exist)
347
+ *
348
+ * If no file is found, `loadChipzenConfig` returns `null` and the caller
349
+ * falls back to whatever explicit arguments were passed. A clear error is
350
+ * only raised when a file IS found but is malformed or missing the
351
+ * expected section.
352
+ *
353
+ * File format
354
+ * -----------
355
+ *
356
+ * [external_api]
357
+ * token = "cz_extbot_<32-char-base62-random>"
358
+ * url = "wss://chipzen.ai/ws/external/bot/<bot_id>" # optional
359
+ * bot_id = "<bot-uuid>" # optional
360
+ *
361
+ * All three fields are optional and must be quoted strings. We parse the
362
+ * single `[external_api]` table with a minimal inline reader rather than
363
+ * pulling in a TOML dependency — keeping the package's single runtime dep
364
+ * (`ws`).
365
+ */
366
+ declare const CONFIG_FILENAME = "chipzen.toml";
367
+ declare const SECTION_NAME = "external_api";
368
+ /**
369
+ * Parsed contents of a `chipzen.toml` file. All `[external_api]` fields
370
+ * are optional; absence yields `null`.
371
+ */
372
+ interface ChipzenConfig {
373
+ /** Filesystem path the config was loaded from (for error messages). */
374
+ readonly path: string;
375
+ /** Value of `[external_api] token` if present, else `null`. */
376
+ readonly token: string | null;
377
+ /** Value of `[external_api] url` if present, else `null`. */
378
+ readonly url: string | null;
379
+ /** Value of `[external_api] bot_id` if present, else `null`. */
380
+ readonly botId: string | null;
381
+ }
382
+ /**
383
+ * Raised when a `chipzen.toml` is found but cannot be used (malformed,
384
+ * missing the `[external_api]` section, or a field is the wrong type).
385
+ *
386
+ * A "found but unusable" file is always a hard error — silent fallback
387
+ * would mask typos that would otherwise be obvious.
388
+ */
389
+ declare class ChipzenConfigError extends Error {
390
+ constructor(message: string);
391
+ }
392
+ /**
393
+ * Discover and parse a `chipzen.toml` from the search path.
394
+ *
395
+ * @param paths Override the default search order. When omitted, uses
396
+ * cwd → `~/.chipzen/` → `/etc/chipzen/` (POSIX only).
397
+ * @returns A {@link ChipzenConfig} if a file was found and parsed; `null`
398
+ * if no file exists on the search path. The "no file" case is NOT an
399
+ * error — the SDK falls back to explicit kwargs in that case.
400
+ * @throws ChipzenConfigError if a file is found but is malformed, lacks
401
+ * the `[external_api]` section, or has a wrong-typed token / url / bot_id.
402
+ */
403
+ declare function loadChipzenConfig(paths?: string[]): ChipzenConfig | null;
404
+ /**
405
+ * Return the token to use, honoring the precedence rules.
406
+ *
407
+ * 1. If `explicitToken` is non-`undefined`/non-`null`, return it. Even an
408
+ * empty string wins — the dev was explicit.
409
+ * 2. If `explicitTicket` is set, return `null` (ticket-auth; no token).
410
+ * 3. Otherwise, if `config` carries a token, return it.
411
+ * 4. Otherwise, return `null`.
412
+ */
413
+ declare function resolveToken(opts: {
414
+ explicitToken?: string | null;
415
+ explicitTicket?: string | null;
416
+ config?: ChipzenConfig | null;
417
+ }): string | null;
418
+ /**
419
+ * Return the URL override to use, honoring the precedence rules.
420
+ *
421
+ * 1. If `explicitUrl` is set, return it.
422
+ * 2. Otherwise, if `config` carries a `url`, return it.
423
+ * 3. Otherwise, return `null`.
424
+ */
425
+ declare function resolveUrl(opts: {
426
+ explicitUrl?: string | null;
427
+ config?: ChipzenConfig | null;
428
+ }): string | null;
429
+
430
+ /**
431
+ * Environment-aware connection helper for the external-API lobby.
432
+ *
433
+ * Devs running an external-API bot against Chipzen shouldn't need to
434
+ * remember the exact lobby WebSocket URL per environment. This module
435
+ * exposes {@link connectToChipzen} — a small one-liner that returns a
436
+ * fully-populated {@link ConnectionConfig} containing the resolved `url`,
437
+ * `token`, and `retryPolicy`, ready to hand off to `runExternalBot`.
438
+ *
439
+ * Mirrors the Python SDK's `chipzen.connect` (External-API Issue 24,
440
+ * chipzen-ai/chipzen-sdk#43).
441
+ *
442
+ * Environment → URL mapping:
443
+ *
444
+ * prod -> wss://chipzen.ai/ws/external/bot/{botId}
445
+ * staging -> wss://staging.chipzen.ai/ws/external/bot/{botId}
446
+ * local -> ws://localhost:8001/ws/external/bot/{botId}
447
+ *
448
+ * Precedence (highest first) for the final WebSocket URL:
449
+ *
450
+ * 1. `[external_api].url` from a discovered `chipzen.toml` — a config-file
451
+ * URL ALWAYS wins (most explicit, user-managed override).
452
+ * 2. An **explicitly passed** `env` argument.
453
+ * 3. The `CHIPZEN_ENV` environment variable, if set to a recognized value.
454
+ * 4. The default of `"prod"`.
455
+ */
456
+
457
+ /** Recognized target environments. */
458
+ type EnvName = "prod" | "staging" | "local";
459
+ /** The canonical env names, in order, for error messages + validation. */
460
+ declare const ENV_NAMES: readonly EnvName[];
461
+ /**
462
+ * Name of the environment variable consulted when `env` is not explicitly
463
+ * passed.
464
+ */
465
+ declare const ENV_VAR_NAME = "CHIPZEN_ENV";
466
+ /**
467
+ * Fully-resolved connection parameters ready for `runExternalBot`.
468
+ * Returned by {@link connectToChipzen}.
469
+ */
470
+ interface ConnectionConfig {
471
+ /** WebSocket URL the bot should connect to. */
472
+ readonly url: string;
473
+ /** Long-lived API token from a discovered `chipzen.toml`, or `null`. */
474
+ readonly token: string | null;
475
+ /** {@link RetryPolicy} controlling reconnect pacing. */
476
+ readonly retryPolicy: RetryPolicy;
477
+ /**
478
+ * The resolved environment name, or `null` if the URL was supplied
479
+ * verbatim via a config file (no env mapping applied). Mostly for logs.
480
+ */
481
+ readonly env: EnvName | null;
482
+ /**
483
+ * The {@link ChipzenConfig} discovered during resolution (if any), or
484
+ * `null`. Exposed so callers can pass it through to `runExternalBot`
485
+ * and avoid a second filesystem stat.
486
+ */
487
+ readonly config: ChipzenConfig | null;
488
+ }
489
+ /** Options for {@link connectToChipzen}. */
490
+ interface ConnectToChipzenOptions {
491
+ /** Override the reconnect-pacing policy. Defaults to {@link DEFAULT_RETRY_POLICY}. */
492
+ retryPolicy?: RetryPolicy;
493
+ /** Pre-loaded config to avoid a second filesystem stat. `undefined` triggers discovery. */
494
+ config?: ChipzenConfig | null;
495
+ }
496
+ /**
497
+ * Resolve a {@link ConnectionConfig} for the external-API lobby.
498
+ *
499
+ * Maps `env` to a canonical lobby URL and combines it with whatever
500
+ * config-file token / URL / retry policy the dev has set up.
501
+ *
502
+ * @param botId External-API bot UUID. Required, non-empty.
503
+ * @param env Target environment. `undefined`/`null` means "look at
504
+ * `$CHIPZEN_ENV` first, then fall back to `prod`".
505
+ * @param options Optional retry policy + pre-loaded config.
506
+ * @throws Error if `botId` is empty, `env` is unrecognized, or
507
+ * `$CHIPZEN_ENV` is set to an unrecognized value.
508
+ * @throws ChipzenConfigError if a discovered `chipzen.toml` is malformed.
509
+ */
510
+ declare function connectToChipzen(botId: string, env?: EnvName | null, options?: ConnectToChipzenOptions): ConnectionConfig;
511
+
512
+ /**
513
+ * External-API remote-play entry point: {@link runExternalBot}.
514
+ *
515
+ * Where `runBot` connects a bot to a *known* match URL (the
516
+ * containerized/upload path — the platform's executor hands the container
517
+ * its `/ws/match/{matchId}/{participantId}` URL), this module implements
518
+ * the **external-API remote-play** path: a developer runs their bot on
519
+ * their own machine, authenticates with a long-lived `cz_extbot_` token,
520
+ * and the platform matches and dispatches them like any other competitor.
521
+ *
522
+ * The flow:
523
+ *
524
+ * lobby WS /ws/external/bot/{botId} (token in authenticate frame)
525
+ * -> "matched" notify (carries matchId + gatewayWsUrl)
526
+ * -> per-match gateway WS /ws/external/match/{mid}/{pid}
527
+ * (token in Sec-WebSocket-Protocol header)
528
+ * -> two-layer bot handshake + game loop to match_end
529
+ *
530
+ * The **match data plane is identical** to the containerized path, so the
531
+ * game loop here reuses `_runSession` from `client.ts` verbatim — the only
532
+ * external-API-specific code is the lobby connection and the per-match
533
+ * gateway handshake. A developer writes ONE `Bot` subclass and it works on
534
+ * both paths.
535
+ *
536
+ * The lobby is held open for the bot's whole session and each `matched`
537
+ * plays in its own task (Promise), so the 15-second lobby heartbeat is
538
+ * answered even while a multi-minute match is in flight. This is what lets
539
+ * a single connection serve a whole tournament (the bot is "checked in"
540
+ * via lobby presence and matched once per round).
541
+ *
542
+ * Mirrors the Python SDK's `chipzen.external`, including the reconnect
543
+ * fix: a dropped gateway socket RECONNECTS and resumes; match-task
544
+ * ownership is hoisted to the top level so a lobby reconnect doesn't kill
545
+ * in-flight matches; teardown drains-then-cancels, never orphaning a task.
546
+ */
547
+
548
+ /**
549
+ * Sentinel subprotocol that marks the `cz_extbot_` token in the
550
+ * `Sec-WebSocket-Protocol` header (CZ issue 2932 moved the token off the
551
+ * query string, where it leaked into proxy access logs). Must match the
552
+ * value the platform's api gateway expects.
553
+ */
554
+ declare const BOT_TOKEN_SUBPROTOCOL = "chipzen-bot-token";
555
+ /**
556
+ * Build the `Sec-WebSocket-Protocol` offer that carries the bot token.
557
+ *
558
+ * Returns `[sentinel, token]` — the sentinel marks "the next value is my
559
+ * bot token". The api gateway extracts the token from this header (so it
560
+ * never appears in any access log / URL) and echoes the sentinel back on
561
+ * accept.
562
+ */
563
+ declare function botTokenSubprotocols(token: string): [string, string];
564
+ /**
565
+ * Resolve the `matched.gateway_ws_url` path against the lobby origin.
566
+ *
567
+ * The `matched` notification carries `gateway_ws_url` as a *path*
568
+ * (`/ws/external/match/{mid}/{pid}`). The `cz_extbot_` token is NOT on the
569
+ * query string — it travels in the `Sec-WebSocket-Protocol` header. A
570
+ * future server that returns a full URL is passed through unchanged.
571
+ */
572
+ declare function resolveGatewayUrl(lobbyUrl: string, gatewayWsPath: string): string;
573
+ /** A factory that produces a fresh {@link Bot} per match (or returns the same one). */
574
+ type BotFactory = () => Bot;
575
+ /** A connected WS-shaped object the lobby loop / `_runSession` drive. */
576
+ interface ExternalConnection {
577
+ /** Send a string frame. */
578
+ send(data: string): void | Promise<void>;
579
+ /** Pull-based message reader; resolves `null` when the socket closes. */
580
+ reader: AsyncMessageReader;
581
+ /** Close the underlying socket. */
582
+ close(): void;
583
+ }
584
+ /** Options handed to a {@link Transport} on connect. */
585
+ interface TransportConnectOptions {
586
+ /** WS `User-Agent` header value. */
587
+ userAgent: string;
588
+ /**
589
+ * Sec-WebSocket-Protocol offer (gateway leg carries `[sentinel, token]`).
590
+ * Omitted for the lobby leg.
591
+ */
592
+ subprotocols?: string[];
593
+ }
594
+ /** Opens WS connections for the external path. Injectable for tests. */
595
+ type Transport = (url: string, options: TransportConnectOptions) => Promise<ExternalConnection>;
596
+ /** Per-match result recorded by the session. */
597
+ interface MatchResult {
598
+ matchId: string | null;
599
+ end: Record<string, unknown> | null;
600
+ }
601
+ /** Options for {@link runExternalBot}. */
602
+ interface RunExternalBotOptions {
603
+ /** External-API bot UUID. Used to build the lobby URL when `url` is absent. */
604
+ botId?: string;
605
+ /** Target environment (`prod` / `staging` / `local`). `undefined` consults `$CHIPZEN_ENV`. */
606
+ env?: EnvName | null;
607
+ /** Explicit full lobby URL. Overrides `botId` / `env` derivation. */
608
+ url?: string;
609
+ /** Long-lived `cz_extbot_` API token. Falls back to `[external_api].token`. Required. */
610
+ token?: string | null;
611
+ /** Pre-loaded config, to avoid a second filesystem stat. `undefined` triggers discovery. */
612
+ config?: ChipzenConfig | null;
613
+ /** Reconnect-pacing policy. Defaults to {@link DEFAULT_RETRY_POLICY}. */
614
+ retryPolicy?: RetryPolicy;
615
+ /** Client software name sent in the per-match `hello`. */
616
+ clientName?: string;
617
+ /** Client software version. Defaults to the SDK version. */
618
+ clientVersion?: string;
619
+ /** When `true` (default), a `decide()` throw is folded; `false` propagates a {@link BotDecisionError}. */
620
+ safeMode?: boolean;
621
+ /** Stop after this many matches complete. `undefined` runs until the lobby closes / evict. */
622
+ maxMatches?: number | null;
623
+ /** Override the WS `User-Agent`. Defaults to `chipzen-sdk-js/<version>`. */
624
+ userAgent?: string;
625
+ /** Injectable transport (tests). Defaults to a real `ws.WebSocket`. */
626
+ transport?: Transport;
627
+ }
628
+ /**
629
+ * Run a bot on the Chipzen external-API remote-play path.
630
+ *
631
+ * Connects to the lobby, then plays every match the platform dispatches to
632
+ * this bot (a single challenge, or every round of a tournament) until the
633
+ * lobby closes, the bot is evicted, or `maxMatches` matches complete.
634
+ *
635
+ * @returns A list of per-match result objects (`{matchId, end}`), one per
636
+ * match played this session.
637
+ * @throws Error if no token can be resolved, or neither `url` nor a
638
+ * `botId` is available to build the lobby URL.
639
+ * @throws BotDecisionError if `safeMode` is `false` and `bot.decide()` throws.
640
+ */
641
+ declare function runExternalBot(bot: Bot | BotFactory, options?: RunExternalBotOptions): Promise<MatchResult[]>;
642
+
643
+ /**
644
+ * Single source of truth for the SDK version string.
645
+ *
646
+ * Reads `version` from the package's own `package.json` so the value the
647
+ * handshake reports always tracks the published package (chipzen-ai/chipzen-sdk#41)
648
+ * — no hardcoded literal to drift out of sync on a release bump.
649
+ *
650
+ * The import is resolved against `../package.json` (one level up from
651
+ * `src/`). `tsup` inlines the JSON at build time, so the bundled output
652
+ * carries the literal value with no runtime filesystem read.
653
+ */
654
+ /** The installed SDK version, e.g. `"0.3.0"`. */
655
+ declare const VERSION: string;
656
+
657
+ /**
658
+ * `chipzen-sdk init <name>` — scaffold a new Chipzen bot project.
659
+ *
660
+ * Mirrors the Python `chipzen.scaffold` shape so cross-language
661
+ * developers see the same outputs. The scaffolded project is plain
662
+ * ESM JavaScript (no TypeScript dep required to run); convert to TS
663
+ * if you want by renaming bot.js -> bot.ts and adding a tsconfig.
664
+ */
665
+ interface ScaffoldOptions {
666
+ /** Where to create the new project. Defaults to cwd. */
667
+ parentDir?: string;
668
+ }
669
+ declare function scaffoldBot(name: string, options?: ScaffoldOptions): Promise<string>;
670
+
671
+ /**
672
+ * `chipzen-sdk validate <path>` — pre-upload conformance checks.
673
+ *
674
+ * Mirrors the Python validator's check shape and severity model so a
675
+ * `(severity, name, message)` tuple from either language renders the
676
+ * same way in client tooling.
677
+ */
678
+ type Severity$1 = "pass" | "warn" | "fail";
679
+ interface ValidationResult {
680
+ severity: Severity$1;
681
+ name: string;
682
+ message: string;
683
+ }
684
+ interface ValidateOptions {
685
+ /** Override entry point filename (default: auto-detect bot.js / bot.mjs / bot.cjs). */
686
+ entryPoint?: string;
687
+ /** Hard-fail upload size threshold, in bytes. Defaults to 500 MB (platform cap). */
688
+ maxUploadBytes?: number;
689
+ /** Warn if `decide()` takes longer than this (ms). */
690
+ timeoutWarnMs?: number;
691
+ /**
692
+ * If true, run the protocol-conformance harness after the smoke test
693
+ * passes — drives the bot through one full canned match (handshake +
694
+ * 1 hand + match_end) against an in-process mock WebSocket.
695
+ */
696
+ checkConnectivity?: boolean;
697
+ /** Per-conformance-scenario timeout (ms). Default 10s. */
698
+ conformanceTimeoutMs?: number;
699
+ }
700
+ declare const DEFAULT_MAX_UPLOAD_BYTES: number;
701
+ declare const DEFAULT_TIMEOUT_WARN_MS = 100;
702
+ declare const PLATFORM_TIMEOUT_MS = 500;
703
+ declare function validateBot(botPath: string, options?: ValidateOptions): Promise<ValidationResult[]>;
704
+
705
+ /**
706
+ * Protocol-conformance harness used by `chipzen-sdk validate --check-connectivity`.
707
+ *
708
+ * Mirrors the Python harness in `packages/python/src/chipzen/conformance.py`
709
+ * — same scenario shape, same severity model, same canned protocol
710
+ * exchange (handshake + one full hand + match_end). A clean run means
711
+ * the upload pipeline will accept the bot on protocol grounds. It does
712
+ * NOT mean the bot is good.
713
+ *
714
+ * Implementation: drives `_runSession` from `client.ts` against an
715
+ * in-process mock WebSocket rather than spinning up a real server. The
716
+ * `ws` transport is well-tested upstream; what we verify here is the
717
+ * bot's own protocol handling, which is the only part the user's code
718
+ * influences.
719
+ */
720
+
721
+ type Severity = "pass" | "warn" | "fail";
722
+ /** Single conformance scenario result. Same shape as `ValidationResult`. */
723
+ interface ConformanceCheck {
724
+ severity: Severity;
725
+ name: string;
726
+ message: string;
727
+ }
728
+ interface RunConformanceOptions {
729
+ /** Per-scenario timeout in milliseconds. Default 10s. */
730
+ timeoutMs?: number;
731
+ }
732
+ /**
733
+ * Run every conformance scenario against `bot` and return per-check
734
+ * results. The same bot instance is reused across scenarios — matches
735
+ * the user's production usage shape.
736
+ *
737
+ * Note: the JavaScript harness does not currently include a hard
738
+ * wall-clock watchdog. A bot whose `decide()` blocks the event loop
739
+ * (busy-loop, sync `Atomics.wait`) will hang the harness because
740
+ * `setTimeout` cannot fire while the loop is starved. The Python SDK
741
+ * uses a daemon thread for this; the JS equivalent is a Worker and is
742
+ * deferred to a follow-up.
743
+ */
744
+ declare function runConformanceChecks(bot: Bot, options?: RunConformanceOptions): Promise<ConformanceCheck[]>;
745
+
746
+ export { Action, type ActionHistoryEntry, type ActionKind, BOT_TOKEN_SUBPROTOCOL, Bot, BotDecisionError, type BotFactory, CONFIG_FILENAME, type Card, type ChipzenConfig, ChipzenConfigError, type ConformanceCheck, type ConnectToChipzenOptions, type ConnectionConfig, DEFAULT_MAX_UPLOAD_BYTES, DEFAULT_RETRY_POLICY, DEFAULT_TIMEOUT_WARN_MS, ENV_NAMES, ENV_VAR_NAME, type EnvName, type GameState, type MatchResult, PLATFORM_TIMEOUT_MS, RetryPolicy, type RetryPolicyOptions, type RunBotOptions, type RunConformanceOptions, type RunExternalBotOptions, SECTION_NAME, SUPPORTED_PROTOCOL_VERSIONS, type ScaffoldOptions, type Severity$1 as Severity, VERSION, type ValidateOptions, type ValidationResult, botTokenSubprotocols, cardFromString, cardToString, connectToChipzen, loadChipzenConfig, parseGameState, resolveGatewayUrl, resolveToken, resolveUrl, runBot, runConformanceChecks, runExternalBot, scaffoldBot, validateBot };