@mrciphersmith/keryx 0.2.81 → 0.2.83

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 (3) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.js +449 -92
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -233,7 +233,8 @@ What is in it today:
233
233
  `/resume`, `/sessions`, `/status`, `/flows`, `/compact`, `/new`, and
234
234
  `keryx sessions list|fork|export` — `fork` branches a conversation into a new
235
235
  session that keeps its ancestry, without editing a transcript by hand.
236
- `/status` is the session inspector; `/session-info` and `/info` are not aliases.
236
+ `/status` is the session inspector (identity, context window and limits when
237
+ the provider reported them); `/session-info` and `/info` are not aliases.
237
238
  - **Responsive busy-turn UX.** A running main turn can be interrupted with
238
239
  `/interrupt`; additional prompts are queued and answered as read-only side workers
239
240
  (`side-1`) in the TUI so the shell stays usable under long-running turns.
package/dist/cli.js CHANGED
@@ -36259,7 +36259,7 @@ async function loginDeviceCode(input) {
36259
36259
  saveOAuthGrant(input.provider, grantFromTokens("device-code", tokens, input.now), input.dir);
36260
36260
  return { ok: true, provider: input.provider };
36261
36261
  } catch (err) {
36262
- const message = err instanceof DeviceCodeError ? err.message : "device authorization failed";
36262
+ const message = err instanceof DeviceCodeError ? err.message : err instanceof Error ? err.message : "device authorization failed";
36263
36263
  return { ok: false, error: message };
36264
36264
  }
36265
36265
  }
@@ -70772,11 +70772,11 @@ async function runGoalCommand(params) {
70772
70772
 
70773
70773
  // src/tui/tui-shell.ts
70774
70774
  init_metaproject_adapter();
70775
- import { spawn as spawn5, spawnSync as spawnSync2 } from "child_process";
70775
+ import { spawnSync as spawnSync2 } from "child_process";
70776
70776
  // package.json
70777
70777
  var package_default = {
70778
70778
  name: "@mrciphersmith/keryx",
70779
- version: "0.2.81",
70779
+ version: "0.2.83",
70780
70780
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
70781
70781
  private: false,
70782
70782
  publishConfig: {
@@ -74085,6 +74085,345 @@ function openMcpTools(otui, chrome, options) {
74085
74085
  // src/tui/tui-shell.ts
74086
74086
  init_slate();
74087
74087
 
74088
+ // src/commands/model-limits.ts
74089
+ init_guard2();
74090
+ init_shell_config();
74091
+ init_providers();
74092
+ var MODEL_LIMITS_TIMEOUT_MS = 8000;
74093
+ var DEFAULT_OLLAMA_BASE_URL2 = "http://localhost:11434";
74094
+ var CONTEXT_WINDOW_KEYS = [
74095
+ "context_length",
74096
+ "context_window",
74097
+ "contextWindow",
74098
+ "max_model_len",
74099
+ "max_input_tokens",
74100
+ "maxInputTokens"
74101
+ ];
74102
+ var NESTED_LIMIT_KEYS = ["architecture", "top_provider", "topProvider", "meta", "limits", "capabilities"];
74103
+ function asPositiveInt(value) {
74104
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
74105
+ return Math.floor(value);
74106
+ }
74107
+ if (typeof value === "string" && value.trim().length > 0) {
74108
+ const parsed = Number(value);
74109
+ if (Number.isFinite(parsed) && parsed > 0) {
74110
+ return Math.floor(parsed);
74111
+ }
74112
+ }
74113
+ return;
74114
+ }
74115
+ function extractContextWindow(entry) {
74116
+ if (typeof entry !== "object" || entry === null) {
74117
+ return;
74118
+ }
74119
+ const rec = entry;
74120
+ for (const key of CONTEXT_WINDOW_KEYS) {
74121
+ const n = asPositiveInt(rec[key]);
74122
+ if (n !== undefined) {
74123
+ return n;
74124
+ }
74125
+ }
74126
+ for (const nested of NESTED_LIMIT_KEYS) {
74127
+ const inner = extractContextWindow(rec[nested]);
74128
+ if (inner !== undefined) {
74129
+ return inner;
74130
+ }
74131
+ }
74132
+ return;
74133
+ }
74134
+ function entryId(entry) {
74135
+ if (typeof entry !== "object" || entry === null) {
74136
+ return "";
74137
+ }
74138
+ const rec = entry;
74139
+ if (typeof rec.id === "string" && rec.id.length > 0) {
74140
+ return rec.id;
74141
+ }
74142
+ if (typeof rec.name === "string" && rec.name.length > 0) {
74143
+ return rec.name;
74144
+ }
74145
+ return "";
74146
+ }
74147
+ function findModelEntry(data, modelId) {
74148
+ const wanted = modelId.trim();
74149
+ if (wanted.length === 0) {
74150
+ return;
74151
+ }
74152
+ const lower = wanted.toLowerCase();
74153
+ const exact = data.find((entry) => entryId(entry).toLowerCase() === lower);
74154
+ if (exact !== undefined) {
74155
+ return exact;
74156
+ }
74157
+ const short = lower.includes("/") ? lower.split("/").pop() ?? lower : lower;
74158
+ return data.find((entry) => {
74159
+ const id = entryId(entry).toLowerCase();
74160
+ if (id.length === 0) {
74161
+ return false;
74162
+ }
74163
+ return id.endsWith(`/${short}`) || id.split("/").pop() === short;
74164
+ });
74165
+ }
74166
+ var REQUEST_LIMIT_HEADERS = [
74167
+ "x-ratelimit-limit-requests",
74168
+ "x-ratelimit-limit",
74169
+ "anthropic-ratelimit-requests-limit"
74170
+ ];
74171
+ var REQUEST_REMAINING_HEADERS = [
74172
+ "x-ratelimit-remaining-requests",
74173
+ "x-ratelimit-remaining",
74174
+ "anthropic-ratelimit-requests-remaining"
74175
+ ];
74176
+ var TOKEN_LIMIT_HEADERS = ["x-ratelimit-limit-tokens", "anthropic-ratelimit-tokens-limit"];
74177
+ var TOKEN_REMAINING_HEADERS = [
74178
+ "x-ratelimit-remaining-tokens",
74179
+ "anthropic-ratelimit-tokens-remaining"
74180
+ ];
74181
+ var RESET_HEADERS = [
74182
+ "x-ratelimit-reset-requests",
74183
+ "x-ratelimit-reset",
74184
+ "anthropic-ratelimit-requests-reset",
74185
+ "retry-after"
74186
+ ];
74187
+ function firstHeaderInt(headers, names) {
74188
+ for (const name of names) {
74189
+ const n = asPositiveInt(headers.get(name));
74190
+ if (n !== undefined) {
74191
+ return n;
74192
+ }
74193
+ }
74194
+ return;
74195
+ }
74196
+ function firstHeaderText(headers, names) {
74197
+ for (const name of names) {
74198
+ const raw = headers.get(name)?.trim();
74199
+ if (raw !== undefined && raw.length > 0) {
74200
+ return raw;
74201
+ }
74202
+ }
74203
+ return;
74204
+ }
74205
+ function parseRateLimitHeaders(headers) {
74206
+ const snapshot = {};
74207
+ const requestsLimit = firstHeaderInt(headers, REQUEST_LIMIT_HEADERS);
74208
+ const requestsRemaining = firstHeaderInt(headers, REQUEST_REMAINING_HEADERS);
74209
+ const tokensLimit = firstHeaderInt(headers, TOKEN_LIMIT_HEADERS);
74210
+ const tokensRemaining = firstHeaderInt(headers, TOKEN_REMAINING_HEADERS);
74211
+ const reset = firstHeaderText(headers, RESET_HEADERS);
74212
+ if (requestsLimit !== undefined)
74213
+ snapshot.requestsLimit = requestsLimit;
74214
+ if (requestsRemaining !== undefined)
74215
+ snapshot.requestsRemaining = requestsRemaining;
74216
+ if (tokensLimit !== undefined)
74217
+ snapshot.tokensLimit = tokensLimit;
74218
+ if (tokensRemaining !== undefined)
74219
+ snapshot.tokensRemaining = tokensRemaining;
74220
+ if (reset !== undefined)
74221
+ snapshot.reset = reset;
74222
+ return Object.keys(snapshot).length === 0 ? undefined : snapshot;
74223
+ }
74224
+ function formatRateLimit(limit) {
74225
+ if (limit === undefined) {
74226
+ return;
74227
+ }
74228
+ const parts = [];
74229
+ if (limit.requestsRemaining !== undefined && limit.requestsLimit !== undefined) {
74230
+ parts.push(`${limit.requestsRemaining}/${limit.requestsLimit} requests`);
74231
+ } else if (limit.requestsRemaining !== undefined) {
74232
+ parts.push(`${limit.requestsRemaining} requests left`);
74233
+ } else if (limit.requestsLimit !== undefined) {
74234
+ parts.push(`${limit.requestsLimit} request cap`);
74235
+ }
74236
+ if (limit.tokensRemaining !== undefined && limit.tokensLimit !== undefined) {
74237
+ parts.push(`${limit.tokensRemaining}/${limit.tokensLimit} tokens`);
74238
+ } else if (limit.tokensRemaining !== undefined) {
74239
+ parts.push(`${limit.tokensRemaining} tokens left`);
74240
+ }
74241
+ if (limit.reset !== undefined) {
74242
+ parts.push(`reset ${limit.reset}`);
74243
+ }
74244
+ return parts.length === 0 ? undefined : parts.join(" \xB7 ");
74245
+ }
74246
+ function extractOllamaContextWindow(body) {
74247
+ if (typeof body !== "object" || body === null) {
74248
+ return;
74249
+ }
74250
+ const rec = body;
74251
+ if (typeof rec.parameters === "string") {
74252
+ const match = /\bnum_ctx\s+(\d+)/.exec(rec.parameters);
74253
+ if (match?.[1] !== undefined) {
74254
+ const n = asPositiveInt(match[1]);
74255
+ if (n !== undefined) {
74256
+ return n;
74257
+ }
74258
+ }
74259
+ }
74260
+ const info = rec.model_info;
74261
+ if (typeof info === "object" && info !== null) {
74262
+ for (const [key, value] of Object.entries(info)) {
74263
+ if (key === "context_length" || key.endsWith(".context_length")) {
74264
+ const n = asPositiveInt(value);
74265
+ if (n !== undefined) {
74266
+ return n;
74267
+ }
74268
+ }
74269
+ }
74270
+ }
74271
+ return extractContextWindow(body);
74272
+ }
74273
+ async function timedFetch(fetchFn, url, init, timeoutMs) {
74274
+ const controller = new AbortController;
74275
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
74276
+ try {
74277
+ return await fetchFn(url, { ...init, signal: controller.signal });
74278
+ } catch {
74279
+ return;
74280
+ } finally {
74281
+ clearTimeout(timer);
74282
+ }
74283
+ }
74284
+ function ollamaShowAllowed(baseUrl) {
74285
+ let host;
74286
+ try {
74287
+ host = new URL(baseUrl).hostname;
74288
+ } catch {
74289
+ return false;
74290
+ }
74291
+ if (isPrivateEgressHost(host) && !isLoopbackHost(host)) {
74292
+ return false;
74293
+ }
74294
+ return true;
74295
+ }
74296
+ async function fetchCompatLimits(fetchFn, providerName, model, baseUrl, env, timeoutMs) {
74297
+ const compat = providerByName(providerName);
74298
+ if (compat === undefined) {
74299
+ return {};
74300
+ }
74301
+ const resolved = resolveProviderBaseUrl({ ...compat, ...baseUrl !== undefined ? { baseUrl } : {} }, env);
74302
+ const url = `${resolved.replace(/\/+$/, "")}${compat.modelsPath ?? DEFAULT_MODELS_PATH}`;
74303
+ const apiKey = providerApiKey(compat, env) ?? compat.apiKey;
74304
+ const init = {};
74305
+ if (apiKey !== undefined && apiKey.length > 0) {
74306
+ init.headers = { authorization: `Bearer ${apiKey}` };
74307
+ }
74308
+ const res = await timedFetch(fetchFn, url, init, timeoutMs);
74309
+ if (res === undefined || !res.ok) {
74310
+ return {};
74311
+ }
74312
+ const rateLimit = parseRateLimitHeaders(res.headers);
74313
+ let body;
74314
+ try {
74315
+ body = await res.json();
74316
+ } catch {
74317
+ return rateLimit === undefined ? {} : { rateLimit };
74318
+ }
74319
+ const data = body?.data;
74320
+ const entries = Array.isArray(data) ? data : Array.isArray(body) ? body : [];
74321
+ const entry = findModelEntry(entries, model);
74322
+ const contextWindow = extractContextWindow(entry);
74323
+ return {
74324
+ ...contextWindow !== undefined ? { contextWindow, contextSource: "live-models" } : {},
74325
+ ...rateLimit !== undefined ? { rateLimit } : {}
74326
+ };
74327
+ }
74328
+ async function fetchOllamaLimits(fetchFn, model, baseUrl, timeoutMs) {
74329
+ const resolved = (baseUrl ?? DEFAULT_OLLAMA_BASE_URL2).replace(/\/+$/, "");
74330
+ if (!ollamaShowAllowed(resolved)) {
74331
+ return {};
74332
+ }
74333
+ const res = await timedFetch(fetchFn, `${resolved}/api/show`, {
74334
+ method: "POST",
74335
+ headers: { "content-type": "application/json" },
74336
+ body: JSON.stringify({ name: model })
74337
+ }, timeoutMs);
74338
+ if (res === undefined || !res.ok) {
74339
+ return {};
74340
+ }
74341
+ let body;
74342
+ try {
74343
+ body = await res.json();
74344
+ } catch {
74345
+ return {};
74346
+ }
74347
+ const contextWindow = extractOllamaContextWindow(body);
74348
+ return contextWindow === undefined ? {} : { contextWindow, contextSource: "ollama-show" };
74349
+ }
74350
+ async function loadSessionLimits(input) {
74351
+ const fetchFn = input.fetch ?? globalThis.fetch;
74352
+ const env = envWithSavedApiKeys(input.env ?? process.env);
74353
+ const timeoutMs = input.timeoutMs ?? MODEL_LIMITS_TIMEOUT_MS;
74354
+ const provider = input.provider.trim();
74355
+ const model = input.model.trim();
74356
+ const limitsPromise = provider === "ollama" ? fetchOllamaLimits(fetchFn, model, input.baseUrl, timeoutMs) : providerByName(provider) !== undefined ? fetchCompatLimits(fetchFn, provider, model, input.baseUrl, env, timeoutMs) : Promise.resolve({});
74357
+ const capable = balanceCapableProvider(provider);
74358
+ const balancePromise = capable === undefined ? Promise.resolve(undefined) : fetchProviderBalance(fetchFn, { ...capable, ...input.baseUrl !== undefined ? { baseUrl: input.baseUrl } : {} }, providerApiKey(capable, env) ?? capable.apiKey, { timeoutMs });
74359
+ const [limits, balance] = await Promise.all([limitsPromise, balancePromise]);
74360
+ return {
74361
+ ...limits,
74362
+ ...balance !== undefined ? { balance } : {}
74363
+ };
74364
+ }
74365
+
74366
+ // src/tui/balance-panel.ts
74367
+ init_providers();
74368
+ init_shell_config();
74369
+ function formatBalance(balance) {
74370
+ if (balance === undefined) {
74371
+ return "\u2014";
74372
+ }
74373
+ const amount = balance.remaining ?? balance.total;
74374
+ const symbol = balance.currency === "USD" ? "$" : balance.currency === "EUR" ? "\u20AC" : balance.currency === "GBP" ? "\xA3" : `${balance.currency} `;
74375
+ return `${symbol}${amount.toFixed(2)}`;
74376
+ }
74377
+ function mountBalancePanel(sidebarTop, otui, renderer, options) {
74378
+ const core = otui;
74379
+ const r = renderer;
74380
+ const env = envWithSavedApiKeys(options.env ?? process.env);
74381
+ let current;
74382
+ let textNode;
74383
+ const capable = balanceCapableProvider(options.provider) !== undefined;
74384
+ if (capable) {
74385
+ const label = new core.TextRenderable(r, {
74386
+ id: "sb-balance-k",
74387
+ content: core.t`${core.dim("Balance")}`,
74388
+ marginTop: 1
74389
+ });
74390
+ sidebarTop.add(label);
74391
+ const value = new core.TextRenderable(r, {
74392
+ id: "sb-balance-v",
74393
+ content: core.t`${core.dim("\u2026")}`,
74394
+ onMouseDown: () => {
74395
+ refresh();
74396
+ }
74397
+ });
74398
+ textNode = value;
74399
+ sidebarTop.add(value);
74400
+ }
74401
+ const paint = (balance) => {
74402
+ current = balance;
74403
+ if (textNode !== undefined) {
74404
+ textNode.content = core.t`${core.dim(formatBalance(balance))}`;
74405
+ }
74406
+ };
74407
+ const refresh = async () => {
74408
+ const provider = balanceCapableProvider(options.provider);
74409
+ if (provider === undefined) {
74410
+ paint(undefined);
74411
+ return;
74412
+ }
74413
+ const apiKey = providerApiKey(provider, env);
74414
+ if (apiKey === undefined) {
74415
+ paint(undefined);
74416
+ return;
74417
+ }
74418
+ const base = resolveProviderBaseUrl(provider, env);
74419
+ const withBase = { ...provider, baseUrl: base };
74420
+ const balance = await fetchProviderBalance(options.fetch ?? globalThis.fetch, withBase, apiKey, { ...options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {} });
74421
+ paint(balance);
74422
+ };
74423
+ refresh();
74424
+ return { refresh, current: () => current };
74425
+ }
74426
+
74088
74427
  // src/tui/context-usage.ts
74089
74428
  var CONTEXT_BAR_WIDTH = 28;
74090
74429
  function lastTurnUsed(usage) {
@@ -74103,6 +74442,14 @@ function renderUsageBar(filled, width = CONTEXT_BAR_WIDTH) {
74103
74442
  const n = Math.max(0, Math.min(width, filled > 0 ? width : 0));
74104
74443
  return `[${"\u2588".repeat(n)}${"\u2591".repeat(width - n)}]`;
74105
74444
  }
74445
+ function renderWindowBar(used, window, width = CONTEXT_BAR_WIDTH) {
74446
+ if (window <= 0) {
74447
+ return renderUsageBar(used, width);
74448
+ }
74449
+ const ratio = Math.max(0, Math.min(1, used / window));
74450
+ const filled = Math.round(ratio * width);
74451
+ return `[${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}]`;
74452
+ }
74106
74453
  function buildContextUsage(source) {
74107
74454
  const lastIn = source.usage?.inputTokens;
74108
74455
  const lastOut = source.usage?.outputTokens;
@@ -74110,6 +74457,7 @@ function buildContextUsage(source) {
74110
74457
  const hasEstimate = source.estimateTokens !== undefined;
74111
74458
  const total = hasEstimate ? source.estimateTokens ?? 0 : last ?? 0;
74112
74459
  const estimated = hasEstimate || last === undefined;
74460
+ const window = source.contextWindow !== undefined && source.contextWindow > 0 ? source.contextWindow : undefined;
74113
74461
  const segments = [];
74114
74462
  if (hasEstimate) {
74115
74463
  segments.push({ id: "history", label: "history (est.)", tokens: source.estimateTokens ?? 0 });
@@ -74120,12 +74468,16 @@ function buildContextUsage(source) {
74120
74468
  if (lastOut !== undefined) {
74121
74469
  segments.push({ id: "last-out", label: "last out", tokens: lastOut });
74122
74470
  }
74471
+ if (window !== undefined) {
74472
+ segments.push({ id: "free", label: "free", tokens: Math.max(0, window - total) });
74473
+ }
74123
74474
  return {
74124
74475
  total,
74125
74476
  estimated,
74477
+ ...window !== undefined ? { window, usedPct: Math.min(100, total / window * 100), free: Math.max(0, window - total) } : {},
74126
74478
  segments,
74127
- bar: renderUsageBar(total),
74128
- note: total === 0 ? "No context usage yet." : "No model context window is known. The bar is relative to used tokens, not a billed limit."
74479
+ bar: window === undefined ? renderUsageBar(total) : renderWindowBar(total, window),
74480
+ note: total === 0 ? "No context usage yet." : window === undefined ? "No model context window is known. The bar is relative to used tokens, not a billed limit." : `Context window ${window.toLocaleString()} tokens (provider-reported).`
74129
74481
  };
74130
74482
  }
74131
74483
  function formatContextUsageText(view) {
@@ -74137,7 +74489,8 @@ function formatContextUsageText(view) {
74137
74489
  const kind = view.estimated ? "tokens (estimate)" : "tokens";
74138
74490
  const width = view.segments.reduce((max, segment) => Math.max(max, segment.label.length), 0);
74139
74491
  const rows = view.segments.map((segment) => ` ${segment.label.padEnd(width)} ${segment.tokens}`);
74140
- return [`Context`, ` Used ${view.total} ${kind}`, ` ${view.bar}`, ...rows, "", ` ${view.note}`, ""].join(`
74492
+ const usedLine = view.window !== undefined ? ` Used ${view.total.toLocaleString()} / ${view.window.toLocaleString()} ${kind} (${(view.usedPct ?? 0).toFixed(1)}%)` : ` Used ${view.total} ${kind}`;
74493
+ return [`Context`, usedLine, ` ${view.bar}`, ...rows, "", ` ${view.note}`, ""].join(`
74141
74494
  `);
74142
74495
  }
74143
74496
 
@@ -74182,12 +74535,17 @@ function usageUsed(usage) {
74182
74535
  return (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
74183
74536
  }
74184
74537
  function contextRow(source) {
74538
+ const window = source.limits?.contextWindow;
74185
74539
  const used = usageUsed(source.usage);
74186
- if (used !== undefined) {
74187
- return `${used} tokens`;
74540
+ const usedLabel = used !== undefined ? `${used} tokens` : source.estimateTokens !== undefined ? `${source.estimateTokens} tokens (estimate)` : undefined;
74541
+ if (usedLabel !== undefined && window !== undefined) {
74542
+ return `${usedLabel} / ${window.toLocaleString()}`;
74543
+ }
74544
+ if (usedLabel !== undefined) {
74545
+ return usedLabel;
74188
74546
  }
74189
- if (source.estimateTokens !== undefined) {
74190
- return `${source.estimateTokens} tokens (estimate)`;
74547
+ if (window !== undefined) {
74548
+ return `window ${window.toLocaleString()}`;
74191
74549
  }
74192
74550
  return MISSING;
74193
74551
  }
@@ -74218,11 +74576,23 @@ function buildSessionInfoSnapshot(source) {
74218
74576
  label: "Last turn output",
74219
74577
  value: source.usage?.outputTokens === undefined ? MISSING : String(source.usage.outputTokens)
74220
74578
  },
74221
- { label: "Context estimate", value: estimate }
74579
+ { label: "Context estimate", value: estimate },
74580
+ {
74581
+ label: "Context window",
74582
+ value: source.limits?.contextWindow === undefined ? MISSING : source.limits.contextWindow.toLocaleString()
74583
+ }
74222
74584
  ];
74585
+ const rate = formatRateLimit(source.limits?.rateLimit);
74586
+ if (rate !== undefined) {
74587
+ usageRows.push({ label: "Rate limit", value: rate });
74588
+ }
74589
+ if (source.limits?.balance !== undefined) {
74590
+ usageRows.push({ label: "Balance", value: formatBalance(source.limits.balance) });
74591
+ }
74223
74592
  const context = buildContextUsage({
74224
74593
  estimateTokens: source.estimateTokens,
74225
- usage: source.usage
74594
+ usage: source.usage,
74595
+ ...source.limits?.contextWindow !== undefined ? { contextWindow: source.limits.contextWindow } : {}
74226
74596
  });
74227
74597
  const sessionWorkspaces = workspacesInSession(source.workspaces ?? [], {
74228
74598
  sessionId: id,
@@ -74350,7 +74720,7 @@ function presentSessionInfo(openModal, otui, chrome, options) {
74350
74720
  `), width);
74351
74721
  return;
74352
74722
  }
74353
- paintRows(otui, renderer, body, snapshot.sessionRows, width);
74723
+ paintRows(otui, renderer, body, [...snapshot.sessionRows, ...snapshot.usageRows], width);
74354
74724
  },
74355
74725
  onClose: () => {
74356
74726
  unsubscribeKey?.();
@@ -74437,7 +74807,7 @@ var AGENT_SLASH_COMMANDS = [
74437
74807
  { name: "/sessions", description: "Open the session list and switch to one", modes: AGENT_ONLY },
74438
74808
  {
74439
74809
  name: "/status",
74440
- description: "Show session identity, context, workspaces, and flows",
74810
+ description: "Show session identity, context window, limits, workspaces, and flows",
74441
74811
  modes: BOTH
74442
74812
  },
74443
74813
  { name: "/flows", description: "Browse project flows and inspect one", modes: BOTH },
@@ -75568,73 +75938,47 @@ function presentGamesModal(openModalFn, otui, chrome, options = {}, registry = c
75568
75938
  function openGamesModal(otui, chrome, options = {}) {
75569
75939
  return presentGamesModal((hostOtui, hostChrome, input) => openModal(hostOtui, hostChrome, input), otui, chrome, options);
75570
75940
  }
75571
- // src/tui/balance-panel.ts
75941
+ // src/tui/tui-shell.ts
75572
75942
  init_providers();
75573
- init_shell_config();
75574
- function formatBalance(balance) {
75575
- if (balance === undefined) {
75576
- return "\u2014";
75943
+ init_patch_risk();
75944
+ init_catalog();
75945
+ init_grants();
75946
+ init_login();
75947
+
75948
+ // src/lib/oauth/open-url.ts
75949
+ import { spawn as spawn5 } from "child_process";
75950
+ function browserOpenPlan(url, platform = process.platform, env = process.env) {
75951
+ if (url.length === 0) {
75952
+ return;
75577
75953
  }
75578
- const amount = balance.remaining ?? balance.total;
75579
- const symbol = balance.currency === "USD" ? "$" : balance.currency === "EUR" ? "\u20AC" : balance.currency === "GBP" ? "\xA3" : `${balance.currency} `;
75580
- return `${symbol}${amount.toFixed(2)}`;
75954
+ if (platform === "darwin") {
75955
+ return { cmd: "open", args: [url] };
75956
+ }
75957
+ if (platform === "win32") {
75958
+ return { cmd: "cmd", args: ["/c", "start", "", url] };
75959
+ }
75960
+ if (typeof env.DISPLAY === "string" && env.DISPLAY.length > 0) {
75961
+ return { cmd: "xdg-open", args: [url] };
75962
+ }
75963
+ if (typeof env.WAYLAND_DISPLAY === "string" && env.WAYLAND_DISPLAY.length > 0) {
75964
+ return { cmd: "xdg-open", args: [url] };
75965
+ }
75966
+ return;
75581
75967
  }
75582
- function mountBalancePanel(sidebarTop, otui, renderer, options) {
75583
- const core = otui;
75584
- const r = renderer;
75585
- const env = envWithSavedApiKeys(options.env ?? process.env);
75586
- let current;
75587
- let textNode;
75588
- const capable = balanceCapableProvider(options.provider) !== undefined;
75589
- if (capable) {
75590
- const label = new core.TextRenderable(r, {
75591
- id: "sb-balance-k",
75592
- content: core.t`${core.dim("Balance")}`,
75593
- marginTop: 1
75594
- });
75595
- sidebarTop.add(label);
75596
- const value = new core.TextRenderable(r, {
75597
- id: "sb-balance-v",
75598
- content: core.t`${core.dim("\u2026")}`,
75599
- onMouseDown: () => {
75600
- refresh();
75601
- }
75602
- });
75603
- textNode = value;
75604
- sidebarTop.add(value);
75968
+ function openVerificationUrl(url, deps = {}) {
75969
+ const plan = browserOpenPlan(url, deps.platform ?? process.platform, deps.env ?? process.env);
75970
+ if (plan === undefined) {
75971
+ return;
75605
75972
  }
75606
- const paint = (balance) => {
75607
- current = balance;
75608
- if (textNode !== undefined) {
75609
- textNode.content = core.t`${core.dim(formatBalance(balance))}`;
75610
- }
75611
- };
75612
- const refresh = async () => {
75613
- const provider = balanceCapableProvider(options.provider);
75614
- if (provider === undefined) {
75615
- paint(undefined);
75616
- return;
75617
- }
75618
- const apiKey = providerApiKey(provider, env);
75619
- if (apiKey === undefined) {
75620
- paint(undefined);
75621
- return;
75622
- }
75623
- const base = resolveProviderBaseUrl(provider, env);
75624
- const withBase = { ...provider, baseUrl: base };
75625
- const balance = await fetchProviderBalance(options.fetch ?? globalThis.fetch, withBase, apiKey, { ...options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {} });
75626
- paint(balance);
75627
- };
75628
- refresh();
75629
- return { refresh, current: () => current };
75973
+ const spawnFn = deps.spawn ?? spawn5;
75974
+ try {
75975
+ const child = spawnFn(plan.cmd, plan.args, { stdio: "ignore", detached: true });
75976
+ child.on("error", () => {});
75977
+ child.unref();
75978
+ } catch {}
75630
75979
  }
75631
75980
 
75632
75981
  // src/tui/tui-shell.ts
75633
- init_providers();
75634
- init_patch_risk();
75635
- init_catalog();
75636
- init_grants();
75637
- init_login();
75638
75982
  init_shell_config();
75639
75983
  init_provider_config();
75640
75984
 
@@ -80094,26 +80438,26 @@ function runDeviceLoginInTui(otui, r, provider) {
80094
80438
  fetch: (input, init) => globalThis.fetch(input, init),
80095
80439
  signal: controller.signal,
80096
80440
  onChallenge: (challenge) => {
80097
- status.content = otui.t`${otui.bold(challenge.userCode)}\n${otui.dim(challenge.verificationUri)}\n${otui.dim("Waiting for authorization\u2026")}`;
80098
- const url = challenge.verificationUriComplete ?? challenge.verificationUri;
80099
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? undefined : "xdg-open";
80100
- if (cmd !== undefined) {
80101
- try {
80102
- spawn5(cmd, [url], { stdio: "ignore", detached: true }).unref();
80103
- } catch {}
80104
- }
80441
+ status.content = otui.t`${otui.bold(challenge.userCode)}\n${otui.dim(challenge.verificationUri)}\n${otui.dim("Open that URL on any device and enter the code. Waiting for authorization\u2026")}`;
80442
+ openVerificationUrl(challenge.verificationUriComplete ?? challenge.verificationUri);
80105
80443
  }
80106
80444
  }).then((result) => {
80107
80445
  if (controller.signal.aborted) {
80108
80446
  return;
80109
80447
  }
80110
- cleanup();
80111
80448
  if (result.ok) {
80449
+ cleanup();
80112
80450
  applyOAuthAccessToEnv();
80113
80451
  resolve(true);
80114
80452
  return;
80115
80453
  }
80116
- resolve(false);
80454
+ status.content = otui.t`${otui.red("\u2717")} ${otui.bold(result.error)} ${otui.dim("(Esc to go back)")}`;
80455
+ }).catch((err) => {
80456
+ if (controller.signal.aborted) {
80457
+ return;
80458
+ }
80459
+ const message = err instanceof Error ? err.message : "device authorization failed";
80460
+ status.content = otui.t`${otui.red("\u2717")} ${otui.bold(message)} ${otui.dim("(Esc to go back)")}`;
80117
80461
  });
80118
80462
  });
80119
80463
  }
@@ -81334,12 +81678,18 @@ Staying in the current session.
81334
81678
  (async () => {
81335
81679
  const cwd = inspectorCwd();
81336
81680
  const [workspaces, flows] = await Promise.all([loadInspectorWorkspaces(cwd), loadInspectorFlows(cwd)]);
81681
+ const limits = await loadSessionLimits({
81682
+ provider: currentSel.provider,
81683
+ model: currentSel.model,
81684
+ ...currentSel.baseUrl !== undefined ? { baseUrl: currentSel.baseUrl } : {}
81685
+ });
81337
81686
  const snapshot = buildSessionInfoSnapshot({
81338
81687
  summary: liveSession.summary,
81339
81688
  selection: currentSel,
81340
81689
  version: package_default.version,
81341
81690
  usage: lastUsage,
81342
81691
  estimateTokens: estimateContextTokens(history),
81692
+ limits,
81343
81693
  sessionText: history.map((message) => message.content).join(`
81344
81694
  `),
81345
81695
  workspaces,
@@ -82887,11 +83237,17 @@ ${formatThemeList(getThemeId())}`)}`);
82887
83237
  return;
82888
83238
  }
82889
83239
  const summary = cwd !== undefined ? latestSession(cwd) : undefined;
83240
+ const limits = await loadSessionLimits({
83241
+ provider: selection.provider,
83242
+ model: selection.model,
83243
+ ...selection.baseUrl !== undefined ? { baseUrl: selection.baseUrl } : {}
83244
+ });
82890
83245
  const snapshot = buildSessionInfoSnapshot({
82891
83246
  summary,
82892
83247
  selection,
82893
83248
  version: package_default.version,
82894
83249
  estimateTokens: estimateContextTokens(seen),
83250
+ limits,
82895
83251
  sessionText: seen.map((message) => message.content).join(`
82896
83252
  `),
82897
83253
  workspaces,
@@ -83126,6 +83482,11 @@ Starting a new session.
83126
83482
  selection: { provider: providerName, model: modelName },
83127
83483
  version: package_default.version,
83128
83484
  estimateTokens: estimateContextTokens(history),
83485
+ limits: await loadSessionLimits({
83486
+ provider: providerName,
83487
+ model: modelName,
83488
+ ...baseUrl !== undefined ? { baseUrl } : {}
83489
+ }),
83129
83490
  sessionText: history.map((message) => message.content).join(`
83130
83491
  `),
83131
83492
  workspaces,
@@ -83955,6 +84316,10 @@ New session ${shortSessionId(live.summary.id)}.
83955
84316
  version: package_default.version,
83956
84317
  usage: lastUsage,
83957
84318
  estimateTokens: estimateContextTokens(history),
84319
+ limits: await loadSessionLimits({
84320
+ provider: deps.providerId,
84321
+ model: deps.modelId
84322
+ }),
83958
84323
  sessionText: history.map((message) => message.content).join(`
83959
84324
  `),
83960
84325
  workspaces,
@@ -90567,7 +90932,6 @@ init_providers();
90567
90932
  init_catalog();
90568
90933
  init_grants();
90569
90934
  init_login();
90570
- import { spawn as spawn8 } from "child_process";
90571
90935
  function printAuthHelp() {
90572
90936
  console.log(`Usage:
90573
90937
  keryx auth list [--json]
@@ -90579,13 +90943,6 @@ Providers with a sanctioned subscription login: grok (SuperGrok), openai (ChatGP
90579
90943
  Claude Pro/Max, Gemini Google-account login, and DeepSeek OAuth are not offered.
90580
90944
  `);
90581
90945
  }
90582
- function openVerificationUrl(url) {
90583
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
90584
- const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
90585
- try {
90586
- spawn8(cmd, args, { stdio: "ignore", detached: true }).unref();
90587
- } catch {}
90588
- }
90589
90946
  async function authCommand(args) {
90590
90947
  const command = args[0];
90591
90948
  if (!command || command === "--help" || command === "-h") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.81",
3
+ "version": "0.2.83",
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": {