@mrciphersmith/keryx 0.2.61 → 0.2.62

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.
Files changed (2) hide show
  1. package/dist/cli.js +648 -262
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -10173,7 +10173,90 @@ async function resolveModelsForPicker(fetchFn, provider, env = process.env, opts
10173
10173
  const apiKey = typeof raw === "string" && raw.length > 0 ? raw : undefined;
10174
10174
  return fetchOpenAiCompatModelsDetailed(fetchFn, { ...compat, ...provider.baseUrl !== undefined ? { baseUrl: provider.baseUrl } : {} }, apiKey, opts);
10175
10175
  }
10176
- var DEFAULT_MODELS_PATH = "/v1/models", OPENAI_COMPAT_PROVIDERS, MODELS_FETCH_TIMEOUT_MS = 1e4;
10176
+ function balanceCapableProvider(name) {
10177
+ const provider = providerByName(name);
10178
+ if (provider === undefined || provider.balancePath === undefined || provider.balanceKind === undefined) {
10179
+ return;
10180
+ }
10181
+ return provider;
10182
+ }
10183
+ function parseDeepSeekBalance(body) {
10184
+ if (typeof body !== "object" || body === null) {
10185
+ return;
10186
+ }
10187
+ const infos = body.balance_infos;
10188
+ if (!Array.isArray(infos)) {
10189
+ return;
10190
+ }
10191
+ for (const info of infos) {
10192
+ if (typeof info !== "object" || info === null) {
10193
+ continue;
10194
+ }
10195
+ const total = Number(info.total_balance);
10196
+ if (Number.isFinite(total)) {
10197
+ const currency = String(info.currency ?? "USD");
10198
+ return { currency, total, exact: true };
10199
+ }
10200
+ }
10201
+ return;
10202
+ }
10203
+ function parseOpenRouterBalance(body) {
10204
+ if (typeof body !== "object" || body === null) {
10205
+ return;
10206
+ }
10207
+ const credits = body.credits;
10208
+ if (typeof credits !== "object" || credits === null) {
10209
+ return;
10210
+ }
10211
+ const total = Number(credits.total);
10212
+ const used = Number(credits.used);
10213
+ if (!Number.isFinite(total)) {
10214
+ return;
10215
+ }
10216
+ const usedField = Number.isFinite(used) ? { used } : {};
10217
+ const remaining = Number.isFinite(used) ? total - used : undefined;
10218
+ return {
10219
+ currency: String(credits.currency ?? "USD"),
10220
+ total,
10221
+ ...usedField,
10222
+ ...remaining !== undefined ? { remaining } : {},
10223
+ exact: true
10224
+ };
10225
+ }
10226
+ async function fetchProviderBalance(fetchFn, provider, apiKey, opts) {
10227
+ if (provider.balancePath === undefined || provider.balanceKind === undefined) {
10228
+ return;
10229
+ }
10230
+ const url = `${provider.baseUrl.replace(/\/+$/, "")}${provider.balancePath}`;
10231
+ const timeoutMs = opts?.timeoutMs ?? BALANCE_FETCH_TIMEOUT_MS;
10232
+ const controller = new AbortController;
10233
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
10234
+ try {
10235
+ const init = { signal: controller.signal };
10236
+ if (apiKey !== undefined && apiKey.length > 0) {
10237
+ init.headers = { authorization: `Bearer ${apiKey}` };
10238
+ }
10239
+ const res = await fetchFn(url, init);
10240
+ if (!res.ok) {
10241
+ return;
10242
+ }
10243
+ const body = await res.json();
10244
+ return provider.balanceKind === "deepseek" ? parseDeepSeekBalance(body) : parseOpenRouterBalance(body);
10245
+ } catch {
10246
+ return;
10247
+ } finally {
10248
+ clearTimeout(timer);
10249
+ }
10250
+ }
10251
+ function providerApiKey(provider, env = process.env) {
10252
+ const envKey = provider.envKey;
10253
+ if (envKey === undefined) {
10254
+ return;
10255
+ }
10256
+ const raw = env[envKey];
10257
+ return typeof raw === "string" && raw.length > 0 ? raw : undefined;
10258
+ }
10259
+ var DEFAULT_MODELS_PATH = "/v1/models", OPENAI_COMPAT_PROVIDERS, MODELS_FETCH_TIMEOUT_MS = 1e4, BALANCE_FETCH_TIMEOUT_MS = 8000;
10177
10260
  var init_providers = __esm(() => {
10178
10261
  OPENAI_COMPAT_PROVIDERS = [
10179
10262
  {
@@ -10182,7 +10265,9 @@ var init_providers = __esm(() => {
10182
10265
  baseUrl: "https://openrouter.ai/api",
10183
10266
  envKey: "OPENROUTER_API_KEY",
10184
10267
  models: ["openai/gpt-4o-mini", "google/gemini-2.0-flash-001", "qwen/qwen-2.5-7b-instruct", "meta-llama/llama-3.1-8b-instruct"],
10185
- note: "hosted \xB7 400+ models"
10268
+ note: "hosted \xB7 400+ models",
10269
+ balancePath: "/api/v1/credits",
10270
+ balanceKind: "openrouter"
10186
10271
  },
10187
10272
  {
10188
10273
  name: "deepseek",
@@ -10190,7 +10275,9 @@ var init_providers = __esm(() => {
10190
10275
  baseUrl: "https://api.deepseek.com",
10191
10276
  envKey: "DEEPSEEK_API_KEY",
10192
10277
  models: ["deepseek-chat", "deepseek-reasoner"],
10193
- note: "cheap per-token"
10278
+ note: "cheap per-token",
10279
+ balancePath: "/user/balance",
10280
+ balanceKind: "deepseek"
10194
10281
  },
10195
10282
  {
10196
10283
  name: "zai",
@@ -13121,14 +13208,29 @@ async function runModelTurn(input2) {
13121
13208
  };
13122
13209
  let text = "";
13123
13210
  let error;
13211
+ let usage;
13212
+ let reasoning = false;
13213
+ const startedAt = performance.now();
13214
+ let firstByteAt;
13124
13215
  for await (const event of port.stream(request, { attemptId: request.requestId })) {
13216
+ if (firstByteAt === undefined) {
13217
+ firstByteAt = performance.now();
13218
+ }
13125
13219
  if (event.kind === "text_delta" && event.text) {
13126
13220
  text += event.text;
13127
13221
  } else if (event.kind === "provider_error" && event.error) {
13128
13222
  error = event.error;
13223
+ } else if (event.kind === "reasoning_delta") {
13224
+ reasoning = true;
13225
+ } else if (event.kind === "usage_update" && event.usage) {
13226
+ usage = event.usage;
13129
13227
  }
13130
13228
  }
13131
- return error ? { provider, model, credentialAvailable, text, error } : { provider, model, credentialAvailable, text };
13229
+ const latencyMs = firstByteAt !== undefined ? Math.max(0, Math.round(firstByteAt - startedAt)) : undefined;
13230
+ const usageField = usage !== undefined ? { usage } : {};
13231
+ const latencyField = latencyMs !== undefined ? { latencyMs } : {};
13232
+ const reasoningField = reasoning ? { reasoning } : {};
13233
+ return error ? { provider, model, credentialAvailable, text, error, ...usageField, ...latencyField, ...reasoningField } : { provider, model, credentialAvailable, text, ...usageField, ...latencyField, ...reasoningField };
13132
13234
  }
13133
13235
  var DEFAULT_PROVIDER = "anthropic", DEFAULT_MODELS;
13134
13236
  var init_single_turn = __esm(() => {
@@ -53454,7 +53556,7 @@ import { spawnSync as spawnSync2 } from "child_process";
53454
53556
  // package.json
53455
53557
  var package_default = {
53456
53558
  name: "@mrciphersmith/keryx",
53457
- version: "0.2.61",
53559
+ version: "0.2.62",
53458
53560
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
53459
53561
  private: false,
53460
53562
  publishConfig: {
@@ -57032,7 +57134,7 @@ var AGENT_SLASH_COMMANDS = [
57032
57134
  { name: "/theme", description: "Open the theme picker \u2014 /theme [name] applies immediately", modes: BOTH },
57033
57135
  {
57034
57136
  name: "/game",
57035
- description: "Play tic-tac-toe against the model \u2014 the game minimizes while the agent works",
57137
+ description: "Games against the model \u2014 /game [seconds] raises the model-turn deadline; add games via src/tui/games/",
57036
57138
  modes: AGENT_ONLY
57037
57139
  },
57038
57140
  {
@@ -57413,8 +57515,75 @@ function openThemePicker(otui, chrome, options) {
57413
57515
  return presentThemePicker((hostOtui, hostChrome, input2) => openModal(hostOtui, hostChrome, input2), otui, chrome, options);
57414
57516
  }
57415
57517
 
57416
- // src/tui/game-modal.ts
57417
- init_single_turn();
57518
+ // src/tui/games/registry.ts
57519
+ function createRegistry(games) {
57520
+ return {
57521
+ games,
57522
+ get(id) {
57523
+ return games.find((game) => game.id === id);
57524
+ }
57525
+ };
57526
+ }
57527
+
57528
+ // src/tui/games/stats.ts
57529
+ function emptyTurnTotals() {
57530
+ return {
57531
+ turns: 0,
57532
+ latencyMs: undefined,
57533
+ totalMs: undefined,
57534
+ inputTokens: undefined,
57535
+ outputTokens: undefined,
57536
+ localFallbacks: 0,
57537
+ errors: 0
57538
+ };
57539
+ }
57540
+ function addMaybe(a, b) {
57541
+ if (a === undefined && b === undefined) {
57542
+ return;
57543
+ }
57544
+ return (a ?? 0) + (b ?? 0);
57545
+ }
57546
+ function addTurn(totals, turn) {
57547
+ return {
57548
+ turns: totals.turns + 1,
57549
+ latencyMs: addMaybe(totals.latencyMs, turn.latencyMs),
57550
+ totalMs: addMaybe(totals.totalMs, turn.totalMs),
57551
+ inputTokens: addMaybe(totals.inputTokens, turn.inputTokens),
57552
+ outputTokens: addMaybe(totals.outputTokens, turn.outputTokens),
57553
+ localFallbacks: totals.localFallbacks + (turn.localFallback ? 1 : 0),
57554
+ errors: totals.errors + (turn.error ? 1 : 0)
57555
+ };
57556
+ }
57557
+ function formatMs(ms) {
57558
+ if (ms === undefined) {
57559
+ return "\u2013";
57560
+ }
57561
+ if (ms < 1000) {
57562
+ return `${ms}ms`;
57563
+ }
57564
+ return `${(ms / 1000).toFixed(1)}s`;
57565
+ }
57566
+ function formatTokens(n) {
57567
+ if (n === undefined) {
57568
+ return "\u2013";
57569
+ }
57570
+ if (n < 1000) {
57571
+ return `${n}`;
57572
+ }
57573
+ return `${(n / 1000).toFixed(1)}k`;
57574
+ }
57575
+
57576
+ // src/tui/games/constants.ts
57577
+ var GAME_MODEL_TIMEOUT_MS = 60000;
57578
+ var GAMES_FOOTER = [
57579
+ { key: "h/l", label: "move" },
57580
+ { key: "enter", label: "place" },
57581
+ { key: "r", label: "new game" },
57582
+ { key: "\u2190/\u2192", label: "games" },
57583
+ { key: "esc", label: "minimize" }
57584
+ ];
57585
+
57586
+ // src/tui/games/tic-tac-toe/core.ts
57418
57587
  var WIN_LINES = [
57419
57588
  [0, 1, 2],
57420
57589
  [3, 4, 5],
@@ -57444,6 +57613,29 @@ function checkWinner(board) {
57444
57613
  function freeCells(board) {
57445
57614
  return board.flatMap((cell, index) => cell === null ? [index] : []);
57446
57615
  }
57616
+ function placeMark(board, index, mark) {
57617
+ if (index < 0 || index > 8 || board[index] !== null) {
57618
+ return;
57619
+ }
57620
+ const next = board.slice();
57621
+ next[index] = mark;
57622
+ const win = checkWinner(next);
57623
+ const draw = win === null && next.every((cell) => cell !== null);
57624
+ return {
57625
+ board: next,
57626
+ turn: win === null && !draw ? mark === "X" ? "O" : "X" : mark,
57627
+ winner: win?.winner ?? null,
57628
+ winLine: win?.line ?? null,
57629
+ draw,
57630
+ cursor: index
57631
+ };
57632
+ }
57633
+ function freshGame() {
57634
+ return { board: emptyBoard(), turn: "X", winner: null, winLine: null, draw: false, cursor: 4 };
57635
+ }
57636
+ function isGameOver(state) {
57637
+ return state.winner !== null || state.draw;
57638
+ }
57447
57639
  function bestLocalMove(board, mark) {
57448
57640
  const free = freeCells(board);
57449
57641
  if (free.length === 0) {
@@ -57466,28 +57658,6 @@ function bestLocalMove(board, mark) {
57466
57658
  }
57467
57659
  return free[0];
57468
57660
  }
57469
- function placeMark(board, index, mark) {
57470
- if (index < 0 || index > 8 || board[index] !== null) {
57471
- return;
57472
- }
57473
- const next = board.slice();
57474
- next[index] = mark;
57475
- const win = checkWinner(next);
57476
- const draw = win === null && next.every((cell) => cell !== null);
57477
- return {
57478
- board: next,
57479
- turn: win === null && !draw ? mark === "X" ? "O" : "X" : mark,
57480
- winner: win?.winner ?? null,
57481
- winLine: win?.line ?? null,
57482
- draw
57483
- };
57484
- }
57485
- function freshGame() {
57486
- return { board: emptyBoard(), turn: "X", winner: null, winLine: null, draw: false };
57487
- }
57488
- function isGameOver(state) {
57489
- return state.winner !== null || state.draw;
57490
- }
57491
57661
  function parseModelMove(reply, board) {
57492
57662
  if (reply.length === 0) {
57493
57663
  return;
@@ -57499,6 +57669,11 @@ function parseModelMove(reply, board) {
57499
57669
  const index = Number(match[1]);
57500
57670
  return board[index] === null ? index : undefined;
57501
57671
  }
57672
+ function asTtt(state) {
57673
+ return state;
57674
+ }
57675
+
57676
+ // src/tui/games/tic-tac-toe/prompts.ts
57502
57677
  function gameSystemPrompt() {
57503
57678
  return [
57504
57679
  "You are playing tic-tac-toe as O against a human playing X.",
@@ -57520,31 +57695,43 @@ ${rows.join(`
57520
57695
 
57521
57696
  Make your move. Reply with one digit 0-8.`;
57522
57697
  }
57523
- async function modelMove(board, opts = {}) {
57524
- const turn = await runModelTurn({
57525
- system: gameSystemPrompt(),
57526
- user: gameUserPrompt(board),
57527
- ...opts.provider !== undefined ? { provider: opts.provider } : {},
57528
- ...opts.model !== undefined ? { model: opts.model } : {},
57529
- maxOutputTokens: 256,
57530
- requestId: "keryx-game",
57531
- ...opts.providerFactory !== undefined ? { providerFactory: opts.providerFactory } : {},
57532
- ...opts.env !== undefined ? { env: opts.env } : {}
57533
- });
57534
- if (turn.error !== undefined) {
57535
- return { move: undefined, error: `model error: ${turn.error.message}` };
57698
+
57699
+ // src/tui/games/tic-tac-toe/input.ts
57700
+ function wrapCursor(row, col) {
57701
+ return (row + 3) % 3 * 3 + (col + 3) % 3;
57702
+ }
57703
+ function onKey(state, key) {
57704
+ const name = key.name || key.sequence;
57705
+ if (isGameOver(state)) {
57706
+ return;
57536
57707
  }
57537
- if (!turn.credentialAvailable && opts.providerFactory === undefined) {
57538
- return { move: undefined, error: "no model credential \u2014 configure a provider first (/provider)" };
57708
+ const row = Math.floor(state.cursor / 3);
57709
+ const col = state.cursor % 3;
57710
+ if (name === "up" || name === "k") {
57711
+ return { ...state, cursor: wrapCursor(row - 1, col) };
57539
57712
  }
57540
- return { move: parseModelMove(turn.text, board), error: undefined };
57713
+ if (name === "down" || name === "j") {
57714
+ return { ...state, cursor: wrapCursor(row + 1, col) };
57715
+ }
57716
+ if (name === "left" || name === "h") {
57717
+ return { ...state, cursor: wrapCursor(row, col - 1) };
57718
+ }
57719
+ if (name === "right" || name === "l") {
57720
+ return { ...state, cursor: wrapCursor(row, col + 1) };
57721
+ }
57722
+ if (name === "return" || name === "enter" || name === "space" || name === " ") {
57723
+ if (state.turn !== "X") {
57724
+ return;
57725
+ }
57726
+ return placeMark(state.board, state.cursor, "X");
57727
+ }
57728
+ return;
57541
57729
  }
57542
- var GAME_FOOTER = [
57543
- { key: "\u2190\u2191\u2193\u2192", label: "move" },
57544
- { key: "enter", label: "place" },
57545
- { key: "r", label: "new game" },
57546
- { key: "esc", label: "minimize" }
57547
- ];
57730
+ function markFor(player) {
57731
+ return player === "human" ? "X" : "O";
57732
+ }
57733
+
57734
+ // src/tui/games/tic-tac-toe/layout.ts
57548
57735
  var GAME_CELL_GAP = 1;
57549
57736
  var GAME_BOARD_CHROME_X = 4;
57550
57737
  var GAME_CELL_SIZES = {
@@ -57558,7 +57745,198 @@ function resolveCellSize(availableWidth) {
57558
57745
  const large = GAME_CELL_SIZES.large;
57559
57746
  return gameBoardWidth(large.width) <= availableWidth ? large : GAME_CELL_SIZES.small;
57560
57747
  }
57561
- var GAME_MODEL_TIMEOUT_MS = 12000;
57748
+
57749
+ // src/tui/games/tic-tac-toe/render.ts
57750
+ function markColor(mark, theme) {
57751
+ return mark === "X" ? theme.ok ?? "" : theme.error ?? "";
57752
+ }
57753
+ function statusText(state) {
57754
+ if (state.winner !== null) {
57755
+ return `${state.winner} wins! (r \u2014 new game)`;
57756
+ }
57757
+ if (state.draw) {
57758
+ return "Draw! (r \u2014 new game)";
57759
+ }
57760
+ return state.turn === "X" ? "Your turn \u2014 X" : "Agent's turn \u2014 O";
57761
+ }
57762
+ function render2(state, ctx) {
57763
+ const core = ctx.core;
57764
+ const r = ctx.renderer;
57765
+ const theme = ctx.theme;
57766
+ const cellSize = resolveCellSize(ctx.width);
57767
+ const wrap2 = new core.BoxRenderable(r, {
57768
+ id: "game-wrap",
57769
+ width: "100%",
57770
+ flexDirection: "column",
57771
+ alignItems: "center"
57772
+ });
57773
+ const legend = new core.BoxRenderable(r, {
57774
+ id: "game-legend",
57775
+ flexDirection: "row",
57776
+ marginBottom: 1
57777
+ });
57778
+ legend.add(new core.TextRenderable(r, { id: "game-legend-x", content: "X you", fg: theme.ok }));
57779
+ legend.add(new core.TextRenderable(r, { id: "game-legend-sep", content: " \xB7 ", fg: theme.muted }));
57780
+ legend.add(new core.TextRenderable(r, { id: "game-legend-o", content: "O model", fg: theme.error }));
57781
+ wrap2.add(legend);
57782
+ const board = new core.BoxRenderable(r, {
57783
+ id: "game-board",
57784
+ flexDirection: "column",
57785
+ flexShrink: 0,
57786
+ border: true,
57787
+ borderStyle: "rounded",
57788
+ borderColor: theme.border,
57789
+ backgroundColor: theme.panel,
57790
+ paddingLeft: 1,
57791
+ paddingRight: 1
57792
+ });
57793
+ for (let row = 0;row < 3; row++) {
57794
+ const rowBox = new core.BoxRenderable(r, {
57795
+ id: `game-row-${row}`,
57796
+ flexDirection: "row",
57797
+ flexShrink: 0,
57798
+ gap: 1
57799
+ });
57800
+ for (let col = 0;col < 3; col++) {
57801
+ const index = row * 3 + col;
57802
+ const cell = state.board[index] ?? null;
57803
+ const isCursor = index === state.cursor && !isGameOver(state);
57804
+ const won = state.winLine?.includes(index) === true;
57805
+ const cellBox = new core.BoxRenderable(r, {
57806
+ id: `game-cell-${index}`,
57807
+ width: cellSize.width,
57808
+ height: cellSize.height,
57809
+ flexShrink: 0,
57810
+ flexGrow: 0,
57811
+ flexDirection: "row",
57812
+ alignItems: "center",
57813
+ justifyContent: "center",
57814
+ border: true,
57815
+ borderStyle: "rounded",
57816
+ borderColor: won ? markColor(state.winner ?? "X", theme) : isCursor ? theme.focus : theme.border,
57817
+ backgroundColor: isCursor || won ? theme.highlight : undefined
57818
+ });
57819
+ const cellText = new core.TextRenderable(r, {
57820
+ id: `game-cell-text-${index}`,
57821
+ content: cell ?? "\xB7",
57822
+ fg: cell === null ? theme.muted : markColor(cell, theme)
57823
+ });
57824
+ cellBox.add(cellText);
57825
+ rowBox.add(cellBox);
57826
+ }
57827
+ board.add(rowBox);
57828
+ }
57829
+ wrap2.add(board);
57830
+ wrap2.add(new core.TextRenderable(r, { id: "game-status", content: statusText(state), marginTop: 1 }));
57831
+ ctx.parent.add(wrap2);
57832
+ }
57833
+
57834
+ // src/tui/games/tic-tac-toe/game.ts
57835
+ function statusOf2(s) {
57836
+ if (s.winner !== null) {
57837
+ return `${s.winner} wins! (r \u2014 new game)`;
57838
+ }
57839
+ if (s.draw) {
57840
+ return "Draw! (r \u2014 new game)";
57841
+ }
57842
+ return s.turn === "X" ? "Your turn \u2014 X" : "Agent's turn \u2014 O";
57843
+ }
57844
+ var ticTacToeGame = {
57845
+ id: "tic-tac-toe",
57846
+ label: "Tic-tac-toe",
57847
+ fresh: () => freshGame(),
57848
+ turn: (state) => asTtt(state).turn === "X" ? "human" : "model",
57849
+ isOver: (state) => isGameOver(asTtt(state)),
57850
+ outcome: (state) => {
57851
+ const s = asTtt(state);
57852
+ if (s.winner === "X") {
57853
+ return "human";
57854
+ }
57855
+ if (s.winner === "O") {
57856
+ return "model";
57857
+ }
57858
+ return s.draw ? "draw" : null;
57859
+ },
57860
+ status: (state) => statusOf2(asTtt(state)),
57861
+ systemPrompt: () => gameSystemPrompt(),
57862
+ stateForModel: (state) => gameUserPrompt(asTtt(state).board),
57863
+ parseMove: (reply, state) => parseModelMove(reply, asTtt(state).board),
57864
+ applyMove: (state, move, by) => placeMark(asTtt(state).board, move, markFor(by)),
57865
+ localMove: (state, by) => bestLocalMove(asTtt(state).board, markFor(by)),
57866
+ pass: (state) => {
57867
+ const s = asTtt(state);
57868
+ return { ...s, turn: s.turn === "X" ? "O" : "X" };
57869
+ },
57870
+ onKey: (state, key) => onKey(asTtt(state), key),
57871
+ render: (state, ctx) => render2(asTtt(state), ctx)
57872
+ };
57873
+ // src/tui/games/model-turn.ts
57874
+ init_single_turn();
57875
+ async function runGameModelTurn(game, state, opts) {
57876
+ const startedAt = performance.now();
57877
+ const turn = await runModelTurn({
57878
+ system: game.systemPrompt(),
57879
+ user: game.stateForModel(state),
57880
+ ...opts.provider !== undefined ? { provider: opts.provider } : {},
57881
+ ...opts.model !== undefined ? { model: opts.model } : {},
57882
+ maxOutputTokens: 256,
57883
+ requestId: "keryx-game",
57884
+ ...opts.providerFactory !== undefined ? { providerFactory: opts.providerFactory } : {},
57885
+ ...opts.env !== undefined ? { env: opts.env } : {}
57886
+ });
57887
+ const totalMs = Math.max(0, Math.round(performance.now() - startedAt));
57888
+ const stats = {
57889
+ provider: turn.provider,
57890
+ model: turn.model,
57891
+ latencyMs: turn.latencyMs,
57892
+ totalMs,
57893
+ inputTokens: turn.usage?.inputTokens,
57894
+ outputTokens: turn.usage?.outputTokens,
57895
+ reasoning: turn.reasoning === true,
57896
+ localFallback: false,
57897
+ error: turn.error !== undefined
57898
+ };
57899
+ if (turn.error !== undefined) {
57900
+ return { move: undefined, error: `model error: ${turn.error.message}`, stats };
57901
+ }
57902
+ if (!turn.credentialAvailable && opts.providerFactory === undefined) {
57903
+ return { move: undefined, error: "no model credential \u2014 configure a provider first (/provider)", stats };
57904
+ }
57905
+ return { move: game.parseMove(turn.text, state), error: undefined, stats };
57906
+ }
57907
+
57908
+ // src/tui/games/agent-panel.ts
57909
+ function renderAgentPanel(game, parent, core, renderer, args2) {
57910
+ const theme = getTheme();
57911
+ const line = (content, fg, id) => {
57912
+ parent.add(new core.TextRenderable(renderer, { content, fg, ...id !== undefined ? { id } : {} }));
57913
+ };
57914
+ if (args2.modelBusy) {
57915
+ line("agent is thinking\u2026", theme.focus, "game-notice");
57916
+ } else {
57917
+ line(args2.notice ?? "", theme.error, "game-notice");
57918
+ }
57919
+ const sys = game.systemPrompt().split(`
57920
+ `);
57921
+ line(`system: ${sys.join(" \xB7 ")}`, theme.muted, "game-system");
57922
+ const lt = args2.lastTurn;
57923
+ const tot = args2.totals;
57924
+ const model = lt === undefined ? "\u2013" : `${lt.provider}/${lt.model}`;
57925
+ const parts = [
57926
+ `model: ${model}`,
57927
+ `first byte ${formatMs(lt?.latencyMs)}`,
57928
+ `total ${formatMs(lt?.totalMs)}`,
57929
+ `in ${formatTokens(lt?.inputTokens)}`,
57930
+ `out ${formatTokens(lt?.outputTokens)}`,
57931
+ lt?.reasoning === true ? "reasoning" : undefined,
57932
+ `turns ${tot.turns}`,
57933
+ `fallbacks ${tot.localFallbacks}`,
57934
+ tot.errors > 0 ? `errors ${tot.errors}` : undefined
57935
+ ];
57936
+ line(parts.filter((p) => p !== undefined).join(" \xB7 "), theme.muted, "game-stats");
57937
+ }
57938
+
57939
+ // src/tui/games/otui.ts
57562
57940
  function asOtui2(otui) {
57563
57941
  if (otui === undefined || otui === null) {
57564
57942
  return;
@@ -57569,60 +57947,90 @@ function asOtui2(otui) {
57569
57947
  }
57570
57948
  return cand;
57571
57949
  }
57572
- var currentGame = freshGame();
57573
- var modelBusy = false;
57574
- function resetGame() {
57575
- currentGame = freshGame();
57576
- modelBusy = false;
57577
- }
57578
- function markColor(mark) {
57579
- return mark === "X" ? getTheme().ok : getTheme().error;
57580
- }
57581
- function statusText(state) {
57582
- if (state.winner !== null) {
57583
- return `${state.winner} wins! (r \u2014 new game)`;
57584
- }
57585
- if (state.draw) {
57586
- return "Draw! (r \u2014 new game)";
57950
+
57951
+ // src/tui/games/modal.ts
57952
+ var DEFAULT_GAMES = [ticTacToeGame];
57953
+ function presentGamesModal(openModalFn, otui, chrome, options = {}, registry = createRegistry(DEFAULT_GAMES)) {
57954
+ const games = registry.games;
57955
+ const first = games[0];
57956
+ if (first === undefined) {
57957
+ return;
57587
57958
  }
57588
- return state.turn === "X" ? "Your turn \u2014 X" : "Agent's turn \u2014 O";
57589
- }
57590
- function presentGame(openModalFn, otui, chrome, options = {}) {
57591
- const renderer = options.renderer;
57592
57959
  const core = asOtui2(otui);
57960
+ const renderer = options.renderer;
57593
57961
  let handle;
57594
- let cells = [];
57595
- let statusBox;
57596
- let noticeBox;
57597
- let cursor = 4;
57962
+ let activeId = first.id;
57963
+ const states = new Map;
57964
+ const totals = new Map;
57965
+ const lastTurn = new Map;
57598
57966
  let notice;
57967
+ let modelBusy = false;
57599
57968
  let unsubscribeKey;
57969
+ let bodyRef;
57970
+ let bodyWidth = 0;
57971
+ const stateOf = (id) => {
57972
+ const game = registry.get(id);
57973
+ if (game === undefined) {
57974
+ throw new Error(`unknown game: ${id}`);
57975
+ }
57976
+ let state = states.get(id);
57977
+ if (state === undefined) {
57978
+ state = game.fresh();
57979
+ states.set(id, state);
57980
+ }
57981
+ return state;
57982
+ };
57983
+ const totalsOf = (id) => {
57984
+ let t = totals.get(id);
57985
+ if (t === undefined) {
57986
+ t = emptyTurnTotals();
57987
+ totals.set(id, t);
57988
+ }
57989
+ return t;
57990
+ };
57600
57991
  const paint = () => {
57601
- if (statusBox === undefined || noticeBox === undefined || cells.length !== 9) {
57992
+ if (core === undefined || bodyRef === undefined) {
57602
57993
  return;
57603
57994
  }
57604
- const theme = getTheme();
57605
- statusBox.content = statusText(currentGame);
57606
- statusBox.fg = theme.text;
57607
- if (modelBusy) {
57608
- noticeBox.content = "agent is thinking\u2026";
57609
- noticeBox.fg = theme.focus;
57610
- } else {
57611
- noticeBox.content = notice ?? "";
57612
- noticeBox.fg = theme.error;
57995
+ clearTranscriptChildren(bodyRef);
57996
+ const game = registry.get(activeId);
57997
+ if (game === undefined) {
57998
+ return;
57613
57999
  }
57614
- for (const [index, view] of cells.entries()) {
57615
- const cell = currentGame.board[index] ?? null;
57616
- const isCursor = index === cursor && !isGameOver(currentGame) && !modelBusy;
57617
- const won = currentGame.winLine?.includes(index) === true;
57618
- view.text.content = cell ?? "\xB7";
57619
- view.text.fg = cell === null ? theme.muted : markColor(cell);
57620
- view.box.borderColor = won ? markColor(currentGame.winner ?? "X") : isCursor ? theme.focus : theme.border;
57621
- view.box.backgroundColor = isCursor || won ? theme.highlight : undefined;
58000
+ const state = stateOf(activeId);
58001
+ const ctx = {
58002
+ core,
58003
+ renderer,
58004
+ theme: getTheme(),
58005
+ parent: bodyRef,
58006
+ width: bodyWidth
58007
+ };
58008
+ game.render(state, ctx);
58009
+ renderAgentPanel(game, bodyRef, core, renderer, {
58010
+ notice,
58011
+ modelBusy,
58012
+ lastTurn: lastTurn.get(activeId),
58013
+ totals: totalsOf(activeId)
58014
+ });
58015
+ };
58016
+ const restart = () => {
58017
+ const game = registry.get(activeId);
58018
+ if (game === undefined) {
58019
+ return;
57622
58020
  }
58021
+ states.set(activeId, game.fresh());
58022
+ totals.set(activeId, emptyTurnTotals());
58023
+ lastTurn.set(activeId, undefined);
58024
+ notice = undefined;
58025
+ paint();
57623
58026
  };
57624
58027
  const applyModelMove = async () => {
57625
- if (modelBusy || isGameOver(currentGame) || currentGame.turn !== "O") {
58028
+ const game = registry.get(activeId);
58029
+ if (game === undefined) {
58030
+ return;
58031
+ }
58032
+ const state = stateOf(activeId);
58033
+ if (modelBusy || game.isOver(state) || game.turn(state) !== "model") {
57626
58034
  return;
57627
58035
  }
57628
58036
  modelBusy = true;
@@ -57633,30 +58041,51 @@ function presentGame(openModalFn, otui, chrome, options = {}) {
57633
58041
  const deadline = new Promise((resolve3) => {
57634
58042
  timer = setTimeout(() => resolve3("timeout"), timeoutMs);
57635
58043
  });
57636
- const outcome = await Promise.race([
57637
- modelMove(currentGame.board, {
57638
- ...options.provider !== undefined ? { provider: options.provider } : {},
57639
- ...options.model !== undefined ? { model: options.model } : {},
57640
- ...options.providerFactory !== undefined ? { providerFactory: options.providerFactory } : {},
57641
- ...options.env !== undefined ? { env: options.env } : {}
57642
- }),
57643
- deadline
57644
- ]);
58044
+ const outcome = await Promise.race([runGameModelTurn(game, state, options), deadline]);
57645
58045
  if (timer !== undefined) {
57646
58046
  clearTimeout(timer);
57647
58047
  }
57648
58048
  modelBusy = false;
57649
58049
  const timedOut = outcome === "timeout";
57650
- const result = timedOut ? { move: undefined, error: undefined } : outcome;
57651
- let move = result.move;
58050
+ const result = timedOut ? undefined : outcome;
58051
+ let stats;
58052
+ if (timedOut) {
58053
+ stats = {
58054
+ provider: "\u2013",
58055
+ model: "\u2013",
58056
+ latencyMs: undefined,
58057
+ totalMs: timeoutMs,
58058
+ inputTokens: undefined,
58059
+ outputTokens: undefined,
58060
+ reasoning: false,
58061
+ localFallback: true,
58062
+ error: false
58063
+ };
58064
+ } else if (result !== undefined) {
58065
+ stats = result.stats;
58066
+ } else {
58067
+ stats = {
58068
+ provider: "\u2013",
58069
+ model: "\u2013",
58070
+ latencyMs: undefined,
58071
+ totalMs: undefined,
58072
+ inputTokens: undefined,
58073
+ outputTokens: undefined,
58074
+ reasoning: false,
58075
+ localFallback: true,
58076
+ error: true
58077
+ };
58078
+ }
58079
+ let move = result?.move;
57652
58080
  let playedLocally = false;
57653
- if (move === undefined && result.error === undefined) {
57654
- move = bestLocalMove(currentGame.board, "O");
58081
+ if (move === undefined && result?.error === undefined) {
58082
+ move = game.localMove(state, "model");
57655
58083
  playedLocally = move !== undefined;
58084
+ stats = { ...stats, localFallback: playedLocally };
57656
58085
  }
57657
- const placed = move === undefined ? undefined : placeMark(currentGame.board, move, "O");
57658
- currentGame = placed ?? { ...currentGame, turn: "X" };
57659
- if (result.error !== undefined) {
58086
+ const placed = move === undefined ? undefined : game.applyMove(state, move, "model");
58087
+ const next = placed ?? game.pass(state);
58088
+ if (result?.error !== undefined) {
57660
58089
  notice = `agent: ${result.error}`;
57661
58090
  } else if (timedOut && playedLocally) {
57662
58091
  notice = `agent timed out after ${Math.round(timeoutMs / 1000)}s \u2014 played a local move`;
@@ -57665,132 +58094,21 @@ function presentGame(openModalFn, otui, chrome, options = {}) {
57665
58094
  } else {
57666
58095
  notice = undefined;
57667
58096
  }
57668
- paint();
57669
- };
57670
- const userPlace = () => {
57671
- if (modelBusy || isGameOver(currentGame) || currentGame.turn !== "X") {
57672
- return;
57673
- }
57674
- const placed = placeMark(currentGame.board, cursor, "X");
57675
- if (placed === undefined) {
57676
- return;
57677
- }
57678
- currentGame = placed;
57679
- notice = undefined;
57680
- paint();
57681
- if (!isGameOver(currentGame) && currentGame.turn === "O") {
57682
- applyModelMove();
57683
- }
57684
- };
57685
- const moveCursor = (dr, dc) => {
57686
- if (modelBusy) {
57687
- return;
57688
- }
57689
- const row = Math.floor(cursor / 3);
57690
- const col = cursor % 3;
57691
- cursor = (row + dr + 3) % 3 * 3 + (col + dc + 3) % 3;
57692
- paint();
57693
- };
57694
- const restart = () => {
57695
- resetGame();
57696
- cursor = 4;
57697
- notice = undefined;
58097
+ states.set(activeId, next);
58098
+ totals.set(activeId, addTurn(totalsOf(activeId), stats));
58099
+ lastTurn.set(activeId, stats);
57698
58100
  paint();
57699
58101
  };
57700
58102
  handle = openModalFn(otui, chrome, {
57701
58103
  title: "/game",
57702
- tabs: [{ id: "game", label: "Tic-tac-toe" }],
57703
- footer: GAME_FOOTER,
57704
- onArrowKeys: (_key, direction) => {
57705
- moveCursor(0, direction === "left" ? -1 : 1);
57706
- return true;
57707
- },
58104
+ tabs: games.map((game) => ({ id: game.id, label: game.label })),
58105
+ footer: GAMES_FOOTER,
57708
58106
  renderTab: (_tabId, body, ctx) => {
57709
58107
  if (body === undefined || body === null) {
57710
58108
  return;
57711
58109
  }
57712
- const parent = body;
57713
- if (parent.add === undefined || core === undefined) {
57714
- return;
57715
- }
57716
- const theme = getTheme();
57717
- const r = renderer;
57718
- const cellSize = resolveCellSize(ctx.width);
57719
- const wrap2 = new core.BoxRenderable(r, {
57720
- id: "game-wrap",
57721
- width: "100%",
57722
- flexDirection: "column",
57723
- alignItems: "center"
57724
- });
57725
- const legend = new core.BoxRenderable(r, {
57726
- id: "game-legend",
57727
- flexDirection: "row",
57728
- marginBottom: 1
57729
- });
57730
- legend.add(new core.TextRenderable(r, { id: "game-legend-x", content: "X you", fg: theme.ok }));
57731
- legend.add(new core.TextRenderable(r, { id: "game-legend-sep", content: " \xB7 ", fg: theme.muted }));
57732
- legend.add(new core.TextRenderable(r, { id: "game-legend-o", content: "O model", fg: theme.error }));
57733
- wrap2.add(legend);
57734
- const board = new core.BoxRenderable(r, {
57735
- id: "game-board",
57736
- flexDirection: "column",
57737
- flexShrink: 0,
57738
- border: true,
57739
- borderStyle: "rounded",
57740
- borderColor: theme.border,
57741
- backgroundColor: theme.panel,
57742
- paddingLeft: 1,
57743
- paddingRight: 1
57744
- });
57745
- cells = [];
57746
- for (let row = 0;row < 3; row++) {
57747
- const rowBox = new core.BoxRenderable(r, {
57748
- id: `game-row-${row}`,
57749
- flexDirection: "row",
57750
- flexShrink: 0,
57751
- gap: GAME_CELL_GAP
57752
- });
57753
- for (let col = 0;col < 3; col++) {
57754
- const index = row * 3 + col;
57755
- const cellBox = new core.BoxRenderable(r, {
57756
- id: `game-cell-${index}`,
57757
- width: cellSize.width,
57758
- height: cellSize.height,
57759
- flexShrink: 0,
57760
- flexGrow: 0,
57761
- flexDirection: "row",
57762
- alignItems: "center",
57763
- justifyContent: "center",
57764
- border: true,
57765
- borderStyle: "rounded",
57766
- borderColor: theme.border
57767
- });
57768
- const cellText = new core.TextRenderable(r, {
57769
- id: `game-cell-text-${index}`,
57770
- content: "\xB7",
57771
- fg: theme.muted
57772
- });
57773
- cellBox.add(cellText);
57774
- rowBox.add(cellBox);
57775
- cells.push({ box: cellBox, text: cellText });
57776
- }
57777
- board.add(rowBox);
57778
- }
57779
- wrap2.add(board);
57780
- const status = new core.TextRenderable(r, {
57781
- id: "game-status",
57782
- content: "",
57783
- marginTop: 1
57784
- });
57785
- statusBox = status;
57786
- wrap2.add(status);
57787
- const noticeText = new core.TextRenderable(r, {
57788
- id: "game-notice",
57789
- content: ""
57790
- });
57791
- noticeBox = noticeText;
57792
- wrap2.add(noticeText);
57793
- parent.add(wrap2);
58110
+ bodyRef = body;
58111
+ bodyWidth = ctx.width;
57794
58112
  paint();
57795
58113
  },
57796
58114
  onClose: () => {
@@ -57803,39 +58121,92 @@ function presentGame(openModalFn, otui, chrome, options = {}) {
57803
58121
  if (options.onKeypress !== undefined) {
57804
58122
  unsubscribeKey = options.onKeypress((key) => {
57805
58123
  const token = key.name || key.sequence;
57806
- if (token === "up" || token === "k") {
57807
- moveCursor(-1, 0);
57808
- return;
57809
- }
57810
- if (token === "down" || token === "j") {
57811
- moveCursor(1, 0);
57812
- return;
57813
- }
57814
- if (token === "h") {
57815
- moveCursor(0, -1);
58124
+ if (token === "r" || token === "R") {
58125
+ restart();
57816
58126
  return;
57817
58127
  }
57818
- if (token === "l") {
57819
- moveCursor(0, 1);
58128
+ const game = registry.get(activeId);
58129
+ if (game === undefined || modelBusy) {
57820
58130
  return;
57821
58131
  }
57822
- if (token === "return" || token === "enter" || token === "space" || token === " ") {
57823
- userPlace();
58132
+ const state = stateOf(activeId);
58133
+ const next = game.onKey(state, key);
58134
+ if (next === undefined) {
57824
58135
  return;
57825
58136
  }
57826
- if (token === "r" || token === "R") {
57827
- restart();
58137
+ states.set(activeId, next);
58138
+ paint();
58139
+ if (!game.isOver(next) && game.turn(next) === "model") {
58140
+ applyModelMove();
57828
58141
  }
57829
58142
  });
57830
58143
  }
57831
58144
  return {
57832
58145
  close: () => handle?.close(),
57833
58146
  restart,
58147
+ activeGameId: () => activeId,
57834
58148
  modelThinking: () => modelBusy
57835
58149
  };
57836
58150
  }
57837
- function openGameModal(otui, chrome, options = {}) {
57838
- return presentGame((hostOtui, hostChrome, input2) => openModal(hostOtui, hostChrome, input2), otui, chrome, options);
58151
+ function openGamesModal(otui, chrome, options = {}) {
58152
+ return presentGamesModal((hostOtui, hostChrome, input2) => openModal(hostOtui, hostChrome, input2), otui, chrome, options);
58153
+ }
58154
+ // src/tui/balance-panel.ts
58155
+ init_providers();
58156
+ init_shell_config();
58157
+ function formatBalance(balance) {
58158
+ if (balance === undefined) {
58159
+ return "\u2014";
58160
+ }
58161
+ const amount = balance.remaining ?? balance.total;
58162
+ const symbol = balance.currency === "USD" ? "$" : balance.currency === "EUR" ? "\u20AC" : balance.currency === "GBP" ? "\xA3" : `${balance.currency} `;
58163
+ return `${symbol}${amount.toFixed(2)}`;
58164
+ }
58165
+ function mountBalancePanel(sidebarTop, otui, renderer, options) {
58166
+ const core = otui;
58167
+ const r = renderer;
58168
+ const env = envWithSavedApiKeys(options.env ?? process.env);
58169
+ let current;
58170
+ let textNode;
58171
+ const label = new core.TextRenderable(r, {
58172
+ id: "sb-balance-k",
58173
+ content: core.t`${core.dim("Balance")}`,
58174
+ marginTop: 1
58175
+ });
58176
+ sidebarTop.add(label);
58177
+ const value = new core.TextRenderable(r, {
58178
+ id: "sb-balance-v",
58179
+ content: core.t`${core.dim("\u2026")}`,
58180
+ onMouseDown: () => {
58181
+ refresh();
58182
+ }
58183
+ });
58184
+ textNode = value;
58185
+ sidebarTop.add(value);
58186
+ const paint = (balance) => {
58187
+ current = balance;
58188
+ if (textNode !== undefined) {
58189
+ textNode.content = core.t`${core.dim(formatBalance(balance))}`;
58190
+ }
58191
+ };
58192
+ const refresh = async () => {
58193
+ const provider = balanceCapableProvider(options.provider);
58194
+ if (provider === undefined) {
58195
+ paint(undefined);
58196
+ return;
58197
+ }
58198
+ const apiKey = providerApiKey(provider, env);
58199
+ if (apiKey === undefined) {
58200
+ paint(undefined);
58201
+ return;
58202
+ }
58203
+ const base = resolveProviderBaseUrl(provider, env);
58204
+ const withBase = { ...provider, baseUrl: base };
58205
+ const balance = await fetchProviderBalance(options.fetch ?? globalThis.fetch, withBase, apiKey, { ...options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {} });
58206
+ paint(balance);
58207
+ };
58208
+ refresh();
58209
+ return { refresh, current: () => current };
57839
58210
  }
57840
58211
 
57841
58212
  // src/tui/tui-shell.ts
@@ -58175,7 +58546,7 @@ function showComposerChoice(otui, r, dock, request) {
58175
58546
  } catch {}
58176
58547
  dock.visible = false;
58177
58548
  }
58178
- const onKey = (key) => {
58549
+ const onKey2 = (key) => {
58179
58550
  if (key.name === "escape") {
58180
58551
  finish(request.cancelId);
58181
58552
  key.preventDefault();
@@ -58222,7 +58593,7 @@ function showComposerChoice(otui, r, dock, request) {
58222
58593
  key.stopPropagation();
58223
58594
  }
58224
58595
  };
58225
- const unsub = onKeypress2(r, onKey);
58596
+ const unsub = onKeypress2(r, onKey2);
58226
58597
  optionsScroll.focus();
58227
58598
  });
58228
58599
  }
@@ -61339,6 +61710,7 @@ function attachUsageIo(io, chrome) {
61339
61710
  totalOut += usage.outputTokens ?? 0;
61340
61711
  chrome.setHeaderMeta(`\u2191${fmtTokens(totalIn)} \u2193${fmtTokens(totalOut)}`);
61341
61712
  chrome.setContextTotal(totalIn + totalOut);
61713
+ chrome.setUsage?.(totalIn, totalOut);
61342
61714
  };
61343
61715
  return Object.assign(io, {
61344
61716
  resetUsage() {
@@ -61835,7 +62207,7 @@ function promptApiKeyStep(otui, r, opts) {
61835
62207
  const keyInput = new otui.InputRenderable(r, { id: "kp-input", placeholder: opts.placeholder ?? "sk-...", marginTop: 1 });
61836
62208
  box.add(keyInput);
61837
62209
  keyInput.focus();
61838
- const onKey = (key) => {
62210
+ const onKey2 = (key) => {
61839
62211
  if (key.name === "escape") {
61840
62212
  cleanup();
61841
62213
  resolve3({ kind: "back" });
@@ -61843,7 +62215,7 @@ function promptApiKeyStep(otui, r, opts) {
61843
62215
  key.stopPropagation();
61844
62216
  }
61845
62217
  };
61846
- const unsub = onKeypress4(r, onKey);
62218
+ const unsub = onKeypress4(r, onKey2);
61847
62219
  const cleanup = () => {
61848
62220
  unsub();
61849
62221
  r.root.remove(box);
@@ -61875,7 +62247,7 @@ function pickProviderStep(otui, r, detected) {
61875
62247
  });
61876
62248
  box.add(provSelect);
61877
62249
  provSelect.focus();
61878
- const onKey = (key) => {
62250
+ const onKey2 = (key) => {
61879
62251
  if (key.name === "escape") {
61880
62252
  cleanup();
61881
62253
  resolve3(undefined);
@@ -61883,7 +62255,7 @@ function pickProviderStep(otui, r, detected) {
61883
62255
  key.stopPropagation();
61884
62256
  }
61885
62257
  };
61886
- const unsub = onKeypress4(r, onKey);
62258
+ const unsub = onKeypress4(r, onKey2);
61887
62259
  const cleanup = () => {
61888
62260
  unsub();
61889
62261
  r.root.remove(box);
@@ -61985,7 +62357,7 @@ function pickModelInTui(otui, r, models) {
61985
62357
  sel.options = matches2.length > 0 ? matches2.map((m) => ({ name: m, description: "" })) : [{ name: NO_MATCH, description: "" }];
61986
62358
  filterLine.content = otui.t`${otui.dim(q.length > 0 ? `filter: ${filter} (${matches2.length}/${all.length})` : "type to filter \xB7 \u2191/\u2193 Enter \xB7 Esc to go back")}`;
61987
62359
  };
61988
- const onKey = (key) => {
62360
+ const onKey2 = (key) => {
61989
62361
  if (key.name === "escape") {
61990
62362
  cleanup();
61991
62363
  resolve3(undefined);
@@ -62008,7 +62380,7 @@ function pickModelInTui(otui, r, models) {
62008
62380
  key.stopPropagation();
62009
62381
  }
62010
62382
  };
62011
- const unsub = onKeypress4(r, onKey);
62383
+ const unsub = onKeypress4(r, onKey2);
62012
62384
  const cleanup = () => {
62013
62385
  unsub();
62014
62386
  r.root.remove(box);
@@ -62077,7 +62449,7 @@ function pickSessionInTui(otui, r, sessions) {
62077
62449
  sel.selectedIndex = 0;
62078
62450
  };
62079
62451
  apply();
62080
- const onKey = (key) => {
62452
+ const onKey2 = (key) => {
62081
62453
  if (key.name === "escape") {
62082
62454
  cleanup();
62083
62455
  resolve3(undefined);
@@ -62100,7 +62472,7 @@ function pickSessionInTui(otui, r, sessions) {
62100
62472
  key.stopPropagation();
62101
62473
  }
62102
62474
  };
62103
- const unsub = onKeypress4(r, onKey);
62475
+ const unsub = onKeypress4(r, onKey2);
62104
62476
  const cleanup = () => {
62105
62477
  unsub();
62106
62478
  r.root.remove(box);
@@ -62220,6 +62592,12 @@ async function launchTuiAgentShell(opts) {
62220
62592
  sidebar.add(new otui.TextRenderable(r, { id: "sb-model-k", content: otui.t`${otui.dim("Model")}`, marginTop: 1 }));
62221
62593
  const sbModelV = new otui.TextRenderable(r, { id: "sb-model-v", content: otui.t`${otui.dim(`${sel.provider}/${sel.model}`)}` });
62222
62594
  sidebar.add(sbModelV);
62595
+ sidebar.add(new otui.TextRenderable(r, { id: "sb-usage-k", content: otui.t`${otui.dim("Usage")}`, marginTop: 1 }));
62596
+ const sbUsageV = new otui.TextRenderable(r, { id: "sb-usage-v", content: otui.t`${otui.dim("\u21910 \u21930")}` });
62597
+ sidebar.add(sbUsageV);
62598
+ const balancePanel = mountBalancePanel(sidebar, otui, r, {
62599
+ provider: sel.provider
62600
+ });
62223
62601
  mountCwdPanel(otui, r, sidebar, opts.session?.cwd ?? process.cwd());
62224
62602
  sidebar.add(new otui.TextRenderable(r, { id: "sb-workspace-k", content: otui.t`${otui.dim("Workspace")}`, marginTop: 1 }));
62225
62603
  const sbWorkspaceV = new otui.TextRenderable(r, {
@@ -62461,6 +62839,9 @@ async function launchTuiAgentShell(opts) {
62461
62839
  },
62462
62840
  onExactUsage: () => {
62463
62841
  hasExactUsage = true;
62842
+ },
62843
+ setUsage: (input3, output2) => {
62844
+ sbUsageV.content = otui.t`${otui.dim(`\u2191${fmtTokens(input3)} \u2193${fmtTokens(output2)}`)}`;
62464
62845
  }
62465
62846
  });
62466
62847
  let lastUsage;
@@ -63015,10 +63396,12 @@ Staying in the current session.
63015
63396
  });
63016
63397
  })();
63017
63398
  };
63018
- const showGame = () => {
63019
- openGameModal(otui, chrome, {
63399
+ const showGame = (line) => {
63400
+ const timeoutMatch = /\/game\s+(\d+)/.exec(line);
63401
+ openGamesModal(otui, chrome, {
63020
63402
  renderer: r,
63021
- ...inspectorKeys
63403
+ ...inspectorKeys,
63404
+ ...timeoutMatch !== null ? { timeoutMs: Math.max(1, Number(timeoutMatch[1])) * 1000 } : {}
63022
63405
  });
63023
63406
  };
63024
63407
  const showWorkspace = () => {
@@ -63683,7 +64066,7 @@ Staying in the current session.
63683
64066
  return;
63684
64067
  }
63685
64068
  case "game": {
63686
- showGame();
64069
+ showGame(line);
63687
64070
  return;
63688
64071
  }
63689
64072
  case "deferred": {
@@ -63946,7 +64329,7 @@ ${formatThemeList(getThemeId())}`);
63946
64329
  return;
63947
64330
  }
63948
64331
  if (command.name === "/game") {
63949
- showGame();
64332
+ showGame(line);
63950
64333
  return;
63951
64334
  }
63952
64335
  if (command.name === "/mode") {
@@ -64376,6 +64759,9 @@ async function mountChatShell(otui, renderer, opts) {
64376
64759
  sidebar.add(new otui.TextRenderable(r, { id: "sb-model-k", content: otui.t`${otui.dim("Model")}`, marginTop: 1 }));
64377
64760
  const sbModel = new otui.TextRenderable(r, { id: "sb-model-v", content: otui.t`${otui.dim(label())}` });
64378
64761
  sidebar.add(sbModel);
64762
+ mountBalancePanel(sidebar, otui, r, {
64763
+ provider: selection.provider
64764
+ });
64379
64765
  sidebar.add(new otui.TextRenderable(r, { id: "sb-ctx-k", content: otui.t`${otui.dim("Context")}`, marginTop: 1 }));
64380
64766
  const sbContext = new otui.TextRenderable(r, { id: "sb-ctx-v", content: otui.t`${otui.dim("~0 tokens (est)")}` });
64381
64767
  sidebar.add(sbContext);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.61",
3
+ "version": "0.2.62",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {