@cortexkit/opencode-openai-auth 1.0.5 → 1.2.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.
Files changed (104) hide show
  1. package/dist/bin.js +9 -8
  2. package/dist/{pool-lifecycle-C_6YRmsh.d.ts → codex-request-CjXETpTA.d.ts} +190 -27
  3. package/dist/index-03em8hv1.js +69 -0
  4. package/dist/index-6n4pnfkg.js +2 -0
  5. package/dist/{index-pg305s90.js → index-bc89akak.js} +1 -1
  6. package/dist/{index-y4gxyxqd.js → index-ehjj89rx.js} +2 -2
  7. package/dist/index-f21addzm.js +90 -0
  8. package/dist/{index-fsky1rsh.js → index-ffg0sgg5.js} +2 -2
  9. package/dist/{index-nx2qh047.js → index-ftkhhr6d.js} +1 -1
  10. package/dist/{index-826s1yxa.js → index-htvcmtcx.js} +2 -2
  11. package/dist/{index-fsxbncpw.js → index-qp5hdpw8.js} +1 -1
  12. package/dist/index-qxbchh2d.js +8 -0
  13. package/dist/{index-116q849r.js → index-r2s51ac1.js} +2 -2
  14. package/dist/index-rwbrhkse.js +65 -0
  15. package/dist/{index-66asrje2.js → index-xwvze96g.js} +1 -1
  16. package/dist/index.d.ts +18 -8
  17. package/dist/index.js +1 -1
  18. package/dist/{protocol-DctqemRI.d.ts → model-CQcw10oW.d.ts} +67 -25
  19. package/dist/{port-file-ZPlxXB4G.d.ts → port-file-BDJnzgHH.d.ts} +1 -1
  20. package/dist/protocol-C-kagE6O.d.ts +29 -0
  21. package/dist/rpc/port-file.d.ts +1 -1
  22. package/dist/rpc/port-file.js +1 -1
  23. package/dist/rpc/protocol.d.ts +1 -1
  24. package/dist/rpc/protocol.js +1 -1
  25. package/dist/rpc/rpc-client.d.ts +3 -3
  26. package/dist/rpc/rpc-client.js +1 -1
  27. package/dist/rpc/rpc-dir.js +1 -1
  28. package/dist/{sidebar-state-z_M7Trnd.d.ts → sidebar-state-BfQ4mpZO.d.ts} +109 -10
  29. package/dist/sidebar-state.d.ts +2 -2
  30. package/dist/sidebar-state.js +1 -1
  31. package/dist/token-util-cf0nyh7n.js +1 -0
  32. package/dist/{token-bzhkgc29.js → token-z6qq7a1d.js} +1 -1
  33. package/dist/tui-preferences.js +1 -1
  34. package/dist/tui.d.ts +20 -2
  35. package/dist/v2/server.d.ts +180 -1
  36. package/dist/v2/server.js +3 -3
  37. package/package.json +3 -3
  38. package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +148 -26
  39. package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +371 -0
  40. package/src/tui-compiled/raw/shared/134b008900698fe0-backoff.js +92 -0
  41. package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +430 -0
  42. package/src/tui-compiled/raw/shared/2577ec69f9973ae9-quota-normalize.js +154 -0
  43. package/src/tui-compiled/raw/shared/2dce6e9613993329-quota-manager.js +473 -0
  44. package/src/tui-compiled/raw/shared/30f5bc480f31f51c-roster.js +566 -0
  45. package/src/tui-compiled/raw/shared/43fc9e6b994519c7-refresh-lock.js +0 -1
  46. package/src/tui-compiled/raw/shared/4593a4d71edf69fd-tombstone.js +25 -0
  47. package/src/tui-compiled/raw/shared/4d05c2d7a531aece-oauth.js +500 -0
  48. package/src/tui-compiled/raw/shared/6371b24a0573d79e-any-signal.js +42 -0
  49. package/src/tui-compiled/raw/shared/7c140680fc81be92-interlock.js +29 -0
  50. package/src/tui-compiled/raw/shared/85512cc5263ba259-provider.js +87 -0
  51. package/src/tui-compiled/raw/shared/abd062d633e29fd2-pool-routing.js +192 -0
  52. package/src/tui-compiled/raw/shared/b9588ad4725c1d10-enrollment.js +806 -0
  53. package/src/tui-compiled/raw/shared/bfbdd0f9aae56b47-pool-quota.js +85 -0
  54. package/src/tui-compiled/raw/shared/c852a15cfe489d12-pool.js +0 -1
  55. package/src/tui-compiled/raw/shared/ca0fccaba1d87af4-errors.js +8 -0
  56. package/src/tui-compiled/raw/shared/d0f0b6be200311af-insula.js +152 -0
  57. package/src/tui-compiled/raw/shared/d5a8f1061ee2f114-index.js +1 -1
  58. package/src/tui-compiled/raw/shared/d9d2892a4c371301-host-slot.js +72 -0
  59. package/src/tui-compiled/raw/shared/dbc61cd405941828-accounts.js +1470 -0
  60. package/src/tui-compiled/raw/shared/e8cd4929507508d8-refresh-file-lock.js +26 -12
  61. package/src/tui-compiled/raw/shared/e8d3c8da776ef9cd-index.js +24 -0
  62. package/src/tui-compiled/raw/sidebar-state.js +103 -14
  63. package/src/tui-compiled/raw/tui/command-dialogs.tsx +3 -34
  64. package/src/tui-compiled/raw/tui/section-text.js +21 -0
  65. package/src/tui-compiled/raw/tui/v2-dialog.js +137 -0
  66. package/src/tui-compiled/raw/tui/v2.tsx +133 -0
  67. package/src/tui-compiled/raw/tui.tsx +228 -70
  68. package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +148 -26
  69. package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +371 -0
  70. package/src/tui-compiled/runtime/shared/134b008900698fe0-backoff.js +92 -0
  71. package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +430 -0
  72. package/src/tui-compiled/runtime/shared/2577ec69f9973ae9-quota-normalize.js +154 -0
  73. package/src/tui-compiled/runtime/shared/2dce6e9613993329-quota-manager.js +473 -0
  74. package/src/tui-compiled/runtime/shared/30f5bc480f31f51c-roster.js +566 -0
  75. package/src/tui-compiled/runtime/shared/43fc9e6b994519c7-refresh-lock.js +0 -1
  76. package/src/tui-compiled/runtime/shared/4593a4d71edf69fd-tombstone.js +25 -0
  77. package/src/tui-compiled/runtime/shared/4d05c2d7a531aece-oauth.js +500 -0
  78. package/src/tui-compiled/runtime/shared/6371b24a0573d79e-any-signal.js +42 -0
  79. package/src/tui-compiled/runtime/shared/7c140680fc81be92-interlock.js +29 -0
  80. package/src/tui-compiled/runtime/shared/85512cc5263ba259-provider.js +87 -0
  81. package/src/tui-compiled/runtime/shared/abd062d633e29fd2-pool-routing.js +192 -0
  82. package/src/tui-compiled/runtime/shared/b9588ad4725c1d10-enrollment.js +806 -0
  83. package/src/tui-compiled/runtime/shared/bfbdd0f9aae56b47-pool-quota.js +85 -0
  84. package/src/tui-compiled/runtime/shared/c852a15cfe489d12-pool.js +0 -1
  85. package/src/tui-compiled/runtime/shared/ca0fccaba1d87af4-errors.js +8 -0
  86. package/src/tui-compiled/runtime/shared/d0f0b6be200311af-insula.js +152 -0
  87. package/src/tui-compiled/runtime/shared/d5a8f1061ee2f114-index.js +1 -1
  88. package/src/tui-compiled/runtime/shared/d9d2892a4c371301-host-slot.js +72 -0
  89. package/src/tui-compiled/runtime/shared/dbc61cd405941828-accounts.js +1470 -0
  90. package/src/tui-compiled/runtime/shared/e8cd4929507508d8-refresh-file-lock.js +26 -12
  91. package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +24 -0
  92. package/src/tui-compiled/runtime/sidebar-state.js +103 -14
  93. package/src/tui-compiled/runtime/tui/command-dialogs.js +3 -15
  94. package/src/tui-compiled/runtime/tui/section-text.js +21 -0
  95. package/src/tui-compiled/runtime/tui/v2-dialog.js +137 -0
  96. package/src/tui-compiled/runtime/tui/v2.js +95 -0
  97. package/src/tui-compiled/runtime/tui.js +351 -222
  98. package/dist/index-4fnghj9e.js +0 -65
  99. package/dist/index-a69gr76b.js +0 -2
  100. package/dist/index-h263aftt.js +0 -157
  101. package/dist/index-ssj7m2rd.js +0 -8
  102. package/dist/index-ydz3hbhc.js +0 -2
  103. package/dist/map-m6f5a_IA.d.ts +0 -56
  104. package/dist/token-util-4a2xxnpg.js +0 -1
@@ -1,8 +1,25 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { mkdir, open, readFile, rename, rm, stat, writeFile, } from "node:fs/promises";
2
+ import { mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import { createPrivateBytes, createPrivateDirectoryExclusive, ensurePrivateDirectory, PrivateFileError, privateOperation, readPrivateText, verifyPrivate, windowsPrivateFiles, writePrivateAtomic, } from "./375d9cca2def037d-private-files.js";
5
5
  import { LockOwnershipError, lockPathFor } from "./4825e03209d6ae3e-with-lock.js";
6
+ /**
7
+ * Create a new owner-only file and write `text` into it, failing with EEXIST if
8
+ * the file already exists. Writing through the handle rather than path-based
9
+ * `writeFile(path, …, { flag: 'wx' })` matters: Bun's path form also truncates
10
+ * the file it has just created, and on some Linux filesystems (XFS under load)
11
+ * that redundant ftruncate stalls for hundreds of milliseconds, inside every
12
+ * lock acquisition.
13
+ */
14
+ async function createExclusiveText(path, text) {
15
+ const handle = await open(path, 'wx', 0o600);
16
+ try {
17
+ await handle.writeFile(text, 'utf8');
18
+ }
19
+ finally {
20
+ await handle.close();
21
+ }
22
+ }
6
23
  const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis);
7
24
  const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis);
8
25
  // A concurrent contender renaming the freshly-created eviction-marker directory
@@ -14,7 +31,12 @@ export function isLostMarkerRaceError(error) {
14
31
  return (code === 'ENOENT' ||
15
32
  (!windowsPrivateFiles && (code === 'EINVAL' || code === 'ENOTDIR')));
16
33
  }
17
- export async function acquireRefreshFileLock(options) {
34
+ /** Filesystem leases share wall time across all callers and processes. */
35
+ export function acquireRefreshFileLock(options) {
36
+ return acquireRefreshFileLockWithClock({ ...options, now: Date.now });
37
+ }
38
+ /** @internal Clock seam for lease tests; not exported by the package's fs entry point. */
39
+ export async function acquireRefreshFileLockWithClock(options) {
18
40
  const lockPath = lockPathFor(options.path, options.name);
19
41
  const legacyOwnerPath = join(lockPath, 'owner.json');
20
42
  const ownerId = randomUUID();
@@ -152,11 +174,7 @@ export async function acquireRefreshFileLock(options) {
152
174
  if (windowsPrivateFiles)
153
175
  await createPrivateBytes(lockPath, bytes);
154
176
  else
155
- await writeFile(lockPath, bytes, {
156
- encoding: 'utf8',
157
- mode: 0o600,
158
- flag: 'wx',
159
- });
177
+ await createExclusiveText(lockPath, bytes);
160
178
  }
161
179
  async function leaseContention(error) {
162
180
  const code = error.code;
@@ -304,11 +322,7 @@ export async function acquireRefreshFileLock(options) {
304
322
  if (windowsPrivateFiles)
305
323
  await createPrivateBytes(evictOwnerPath, bytes);
306
324
  else
307
- await writeFile(evictOwnerPath, bytes, {
308
- encoding: 'utf8',
309
- mode: 0o600,
310
- flag: 'wx',
311
- });
325
+ await createExclusiveText(evictOwnerPath, bytes);
312
326
  }
313
327
  catch (error) {
314
328
  // A competing contender can rename our just-created marker directory
@@ -0,0 +1,24 @@
1
+ // Claustrum vault custody shared by the auth plugins: scoped enrollment (run
2
+ // from setup), discovery of this consumer's vault accounts as rows for this
3
+ // package's `/routing` subpath, per-send authorization, 401 reporting, the
4
+ // declined-account interlock and the host-slot guard. Needs the optional
5
+ // peer `@cortexkit/claustrum-client`.
6
+ import { ClaustrumClient, } from "@cortexkit/claustrum-client";
7
+ import { insulaUsageTransport } from "./d0f0b6be200311af-insula.js";
8
+ export { ClaustrumCredentialError, } from "@cortexkit/claustrum-client";
9
+ export { ClaustrumConsumer, } from "./169b2b4bf30909bc-consumer.js";
10
+ export { ClaustrumScopedCustody, decideScopedRetryAfter401, isScopedCredentialRotation, SERVING_MARGIN_MS, } from "./0f77ebc3c914c9c6-custody.js";
11
+ export { ClaustrumEnrollmentManager, classifyEnrollmentError, connectClaustrumEnrollmentClient, disconnectClaustrumEnrollment, enrollmentAuthority, enrollmentName, getClaustrumEnrollmentPaths, hostEnrollmentPaths, RETRYABLE_ENROLLMENT_CODES, readClaustrumEnrollmentStatus, readClaustrumEnrollmentToken, resetClaustrumEnrollmentState, TERMINAL_ENROLLMENT_CODES, } from "./b9588ad4725c1d10-enrollment.js";
12
+ export { ClaustrumConsumerError, } from "./ca0fccaba1d87af4-errors.js";
13
+ export { assertHostSlotMatchesMode, assertNotCustodyPlaceholder, CUSTODY_PLACEHOLDER_PREFIX, classifyHostSlot, custodyPlaceholder, custodyPlaceholderKey, isCustodyPlaceholder, isCustodyPlaceholderValue, } from "./d9d2892a4c371301-host-slot.js";
14
+ export { acceptAccount, declineAccount, isDeclined, } from "./7c140680fc81be92-interlock.js";
15
+ export { acceptVaultRoute, DEFAULT_ROUTE_PREFIX, declineVaultRoute, mutateVaultRoster, projectVaultRoster, readVaultRoster, recordVaultAccountQuota, recordVaultQuota, refreshVaultRoster, resolveVaultPrimary, vaultRoutingRows, } from "./30f5bc480f31f51c-roster.js";
16
+ /**
17
+ * Connect the client that lists and fetches this consumer's vault credentials
18
+ * on the request path. `connectionFile` is required: this library
19
+ * reads no environment, so the plugin resolves the vault's connection file.
20
+ */
21
+ export async function connectClaustrumScopedClient(options) {
22
+ const client = await ClaustrumClient.connect(options);
23
+ return Object.assign(client, { insula: insulaUsageTransport(options) });
24
+ }
@@ -1,8 +1,14 @@
1
1
  import {
2
2
  formatQuota,
3
+ isQuotaStale,
3
4
  projectQuota,
4
5
  quotaWindowName
5
6
  } from "./shared/d5a8f1061ee2f114-index.js";
7
+ import { orderForPlacement } from "./shared/3e7d45a77c6ad4d5-index.js";
8
+ import {
9
+ orderedPlacement,
10
+ vaultMainRouteId
11
+ } from "./shared/abd062d633e29fd2-pool-routing.js";
6
12
  const QUOTA_WINDOW_KEYS = ["primary", "secondary"], LEGACY_WINDOW_MINUTES = {
7
13
  primary: 300,
8
14
  secondary: 10080
@@ -167,11 +173,14 @@ function normalizeAccountList(raw) {
167
173
  return [];
168
174
  return raw.filter((entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry) && typeof entry.id === "string").map((e) => ({
169
175
  id: e.id,
176
+ ...typeof e.credentialId === "string" ? { credentialId: e.credentialId } : {},
170
177
  label: typeof e.label === "string" ? e.label : void 0,
171
178
  ...typeof e.accountId === "string" ? { accountId: e.accountId } : {},
172
179
  quota: "quota" in e ? e.quota : null,
173
180
  killed: typeof e.killed === "boolean" ? e.killed : !1,
174
181
  enabled: typeof e.enabled === "boolean" ? e.enabled : !0,
182
+ ...typeof e.routable === "boolean" ? { routable: e.routable } : {},
183
+ ...["needs sign-in", "turned off in vault", "unavailable"].includes(e.vaultState) ? { vaultState: e.vaultState } : {},
175
184
  ...resetCreditsField(e.resetCredits)
176
185
  }));
177
186
  }
@@ -192,7 +201,7 @@ export function isUsableRoutingEntry(entry, accounts, now = Date.now()) {
192
201
  return !1;
193
202
  if (accounts === void 0)
194
203
  return !0;
195
- return entry.activeId === "main" || accounts.some((account) => account.enabled !== !1 && account.killed !== !0 && account.id === entry.activeId);
204
+ return entry.activeId === "main" || accounts.some((account) => account.enabled !== !1 && account.routable !== !1 && account.killed !== !0 && account.id === entry.activeId);
196
205
  }
197
206
  export function spendControlExhaustedResetAt(quota, now = Date.now()) {
198
207
  const spendControl = quota?.spendControl;
@@ -229,28 +238,40 @@ export function resolveSessionStickyAccount(state, sessionId, now = Date.now())
229
238
  const assignment = state.stickyAssignments?.[hashSidebarSessionId(sessionId)];
230
239
  if (!assignment || assignment.lastSeenAt < now - STICKY_ASSIGNMENT_MAX_AGE_MS)
231
240
  return;
232
- if (assignment.accountId === "main")
241
+ if (assignment.accountId === "main" && state.vaultAccounts === void 0)
233
242
  return state.main?.killed || isQuotaExhausted(state.main?.quota, now) ? void 0 : "main";
234
243
  const fallback = routingAccountsOf(state).find((account) => account.id === assignment.accountId);
235
- if (!fallback || fallback.enabled === !1 || fallback.killed === !0 || isQuotaExhausted(fallback.quota, now))
244
+ if (!fallback || fallback.enabled === !1 || fallback.routable === !1 || fallback.killed === !0 || isQuotaExhausted(fallback.quota, now))
236
245
  return;
237
246
  return fallback.id;
238
247
  }
239
248
  export function resolveSessionSidebarRouting(state, sessionId, now = Date.now()) {
240
- if (!sessionId)
241
- return { activeId: state.activeId ?? "main", route: state.route };
249
+ if (!sessionId) {
250
+ if (state.vaultAccounts === void 0)
251
+ return { activeId: state.activeId ?? "main", route: state.route };
252
+ const active = state.vaultAccounts.find((account) => account.id === state.activeId && account.enabled && account.routable !== !1 && !account.killed);
253
+ if (active && !isQuotaExhausted(active.quota, now))
254
+ return { activeId: active.id, route: state.route };
255
+ }
242
256
  const own = sessionId ? state.activeRouting?.[sessionId] : void 0, accounts = routingAccountsOf(state), ownQuota = own?.activeId === "main" ? state.main.quota : accounts.find((account) => account.id === own?.activeId)?.quota;
243
- if (own && isUsableRoutingEntry(own, accounts, now) && !isQuotaExhausted(ownQuota, now))
257
+ if (own && (state.vaultAccounts === void 0 || own.activeId.startsWith("vault:")) && isUsableRoutingEntry(own, accounts, now) && !isQuotaExhausted(ownQuota, now))
244
258
  return { activeId: own.activeId, route: own.route };
245
259
  const stickyAccountId = resolveSessionStickyAccount(state, sessionId, now);
246
260
  if (stickyAccountId)
247
261
  return { activeId: stickyAccountId, route: state.route };
248
- const vaultMode = state.vaultAccounts !== void 0, enabledFallbacks = (vaultMode ? state.vaultAccounts ?? [] : state.fallbacks).filter((account) => account.enabled && !account.killed), fallback = enabledFallbacks.find((account) => !isQuotaExhausted(account.quota, now)) ?? enabledFallbacks[0];
262
+ const vaultMode = state.vaultAccounts !== void 0, enabledFallbacks = (vaultMode ? vaultAccountsInPlacementOrder(state) : state.fallbacks).filter((account) => account.enabled && account.routable !== !1 && !account.killed), fallback = enabledFallbacks.find((account) => !isQuotaExhausted(account.quota, now)) ?? enabledFallbacks[0];
249
263
  return {
250
- activeId: (vaultMode || state.route === "fallback-first") && fallback ? fallback.id : "main",
264
+ activeId: vaultMode ? fallback?.id ?? "vault" : state.route === "fallback-first" && fallback ? fallback.id : "main",
251
265
  route: state.route
252
266
  };
253
267
  }
268
+ function vaultAccountsInPlacementOrder(state) {
269
+ const accounts = state.vaultAccounts ?? [], byId = new Map(accounts.map((account) => [account.id, account]));
270
+ return orderForPlacement(accounts.map((account) => account.id), placementOf(state), vaultMainRouteId(accounts) ?? "").flatMap((id) => {
271
+ const account = byId.get(id);
272
+ return account ? [account] : [];
273
+ });
274
+ }
254
275
  export function pruneActiveRouting(activeRouting, accounts, now = Date.now(), removedSessionId) {
255
276
  if (!activeRouting)
256
277
  return;
@@ -407,7 +428,14 @@ function writeMergedSidebarState(file, merge, hooks) {
407
428
  if (hooks?.canWrite && !hooks.canWrite())
408
429
  return;
409
430
  const next = merge(latest);
410
- return next === void 0 ? void 0 : normalizeSidebarState(next);
431
+ if (next === void 0)
432
+ return;
433
+ if (next.vaultAccounts !== void 0) {
434
+ next.activeId = next.activeId?.startsWith("vault:") ? next.activeId : void 0;
435
+ next.activeRouting = Object.fromEntries(Object.entries(next.activeRouting ?? {}).filter(([, entry]) => entry.activeId.startsWith("vault:")));
436
+ next.stickyAssignments = Object.fromEntries(Object.entries(next.stickyAssignments ?? {}).filter(([, pin]) => pin.accountId.startsWith("vault:") && !next.vaultAccounts?.some((account) => account.id === pin.accountId && account.killed)));
437
+ }
438
+ return normalizeSidebarState(next);
411
439
  }, {
412
440
  beforeRecheck: async (...args) => {
413
441
  await hooks?.beforeRecheck?.(...args);
@@ -493,7 +521,7 @@ function mergeQuotaByWindow(incoming, existing) {
493
521
  export function setSidebarMachineState(machineState, file = getSidebarStateFile(), hooks) {
494
522
  return enqueueSidebarWrite(async () => {
495
523
  await writeMergedSidebarState(file, (latest) => {
496
- const latestFallbacks = new Map(latest.fallbacks.map((account) => [account.id, account])), vaultMode = machineState.vaultAccounts !== void 0, mainSameIdentity = sameAccountIdentity(machineState.main.mainAccountId, latest.main.mainAccountId), mergedMainQuota = mainSameIdentity ? mergeQuotaByWindow(machineState.main.quota, latest.main.quota) : freshestQuota(machineState.main.quota, latest.main.quota), now = Date.now(), stickyAssignments = pruneStickyAssignments(latest.stickyAssignments, usableRoutingAccountIds(routingAccountsOf(machineState)), now);
524
+ const latestFallbacks = new Map(latest.fallbacks.map((account) => [account.id, account])), vaultMode = machineState.vaultAccounts !== void 0, mainSameIdentity = sameAccountIdentity(machineState.main.mainAccountId, latest.main.mainAccountId), mergedMainQuota = mainSameIdentity ? mergeQuotaByWindow(machineState.main.quota, latest.main.quota) : freshestQuota(machineState.main.quota, latest.main.quota), now = Date.now(), stickyAssignments = pruneStickyAssignments(latest.stickyAssignments, vaultMode ? void 0 : usableRoutingAccountIds(routingAccountsOf(machineState)), now);
497
525
  return {
498
526
  ...latest,
499
527
  ...machineState,
@@ -530,7 +558,7 @@ export function upsertSidebarActiveRouting(input, accounts, file = getSidebarSta
530
558
  route: input.route,
531
559
  updatedAt: input.updatedAt
532
560
  }
533
- }, accounts, Date.now()), stickyAssignments = pruneStickyAssignments(latest.stickyAssignments, usableRoutingAccountIds(accounts), Date.now());
561
+ }, accounts, Date.now()), stickyAssignments = pruneStickyAssignments(latest.stickyAssignments, latest.vaultAccounts !== void 0 ? void 0 : usableRoutingAccountIds(accounts), Date.now());
534
562
  return {
535
563
  ...latest,
536
564
  activeId: input.activeId,
@@ -555,7 +583,7 @@ export function setSidebarLegacyRouting(input, file = getSidebarStateFile(), hoo
555
583
  export function removeSidebarActiveRouting(sessionId, accounts, file = getSidebarStateFile(), hooks) {
556
584
  return enqueueSidebarWrite(async () => {
557
585
  await writeMergedSidebarState(file, (latest) => {
558
- const now = Date.now(), activeRouting = pruneActiveRouting(latest.activeRouting, accounts, now, sessionId), stickyAssignments = pruneStickyAssignments(latest.stickyAssignments, usableRoutingAccountIds(accounts), now, hashSidebarSessionId(sessionId));
586
+ const now = Date.now(), activeRouting = pruneActiveRouting(latest.activeRouting, accounts, now, sessionId), stickyAssignments = pruneStickyAssignments(latest.stickyAssignments, latest.vaultAccounts !== void 0 ? void 0 : usableRoutingAccountIds(accounts), now, hashSidebarSessionId(sessionId));
559
587
  return {
560
588
  ...latest,
561
589
  activeRouting,
@@ -652,7 +680,7 @@ export function persistSidebarStickyAssignment(input, file = getSidebarStateFile
652
680
  const sessionHash = hashSidebarSessionId(input.sessionId), validPinnedAccountIds = new Set(input.validPinnedAccountIds);
653
681
  return enqueueSidebarWrite(async () => {
654
682
  await writeMergedSidebarState(file, (latest) => {
655
- const now = Date.now(), stickyAssignments = pruneStickyAssignments(latest.stickyAssignments, validPinnedAccountIds, now), current = stickyAssignments?.[sessionHash];
683
+ const now = Date.now(), stickyAssignments = pruneStickyAssignments(latest.stickyAssignments, latest.vaultAccounts !== void 0 ? void 0 : validPinnedAccountIds, now), current = stickyAssignments?.[sessionHash];
656
684
  if (current && current.assignedAt > input.assignment.assignedAt) {
657
685
  if (stickyAssignmentsEqual(latest.stickyAssignments, stickyAssignments))
658
686
  return;
@@ -875,7 +903,7 @@ export function createSidebarBookkeepingQueue(options) {
875
903
  export function resolveActiveAccount(state) {
876
904
  const activeId = state.activeId;
877
905
  if (state.vaultAccounts !== void 0) {
878
- const enabled = state.vaultAccounts.filter((account) => account.enabled), vaultAccount = enabled.find((account) => account.id === activeId) ?? enabled[0];
906
+ const enabled = vaultAccountsInPlacementOrder(state).filter((account) => account.enabled && account.routable !== !1), vaultAccount = enabled.find((account) => account.id === activeId) ?? enabled[0];
879
907
  return vaultAccount ? {
880
908
  id: vaultAccount.id,
881
909
  name: vaultAccount.label ?? vaultAccount.id,
@@ -935,6 +963,67 @@ export function getCollapsedQuotaSummary(quota, now = Date.now()) {
935
963
  text: !projection || projection.limits.length === 0 && !projection.budget ? null : formatQuota(projection, { now, form: "compact" })
936
964
  };
937
965
  }
966
+ function placementOf(state) {
967
+ return state.route === "sticky-balanced" ? "roster" : orderedPlacement(state.route);
968
+ }
969
+ export function getCollapsedAccountLines(state, options = {}) {
970
+ const now = options.now ?? Date.now(), activeId = resolveActiveAccount(state).id;
971
+ return (state.vaultAccounts !== void 0 ? vaultAccountsInPlacementOrder(state).map((account) => ({
972
+ id: account.id,
973
+ name: account.label ?? account.id,
974
+ quota: account.quota,
975
+ killed: account.killed
976
+ })) : localAccountsInPlacementOrder(state, activeId, options.showFallbacks ?? !0)).map((account) => {
977
+ const projection = sidebarQuotaProjection(account.quota, now), reading = projection && (projection.limits.length > 0 || projection.budget) ? projection : void 0;
978
+ return {
979
+ id: account.id,
980
+ name: account.name,
981
+ killed: account.killed,
982
+ active: account.id === activeId,
983
+ stale: reading ? isQuotaStale(reading, { now }) : !1,
984
+ text: reading ? formatQuota(reading, { now, form: "narrow" }) : null
985
+ };
986
+ });
987
+ }
988
+ function localAccountsInPlacementOrder(state, activeId, showFallbacks) {
989
+ const accounts = [
990
+ {
991
+ id: "main",
992
+ name: "main",
993
+ quota: state.main.quota,
994
+ killed: state.main.killed
995
+ },
996
+ ...state.fallbacks.filter((account) => account.enabled && (showFallbacks || account.id === activeId)).map((account) => ({
997
+ id: account.id,
998
+ name: account.label ?? account.id,
999
+ quota: account.quota,
1000
+ killed: account.killed
1001
+ }))
1002
+ ], byId = new Map(accounts.map((account) => [account.id, account]));
1003
+ return orderForPlacement(accounts.map((account) => account.id), placementOf(state), "main").flatMap((id) => {
1004
+ const account = byId.get(id);
1005
+ return account ? [account] : [];
1006
+ });
1007
+ }
1008
+ export function displayWidth(text) {
1009
+ const bun = globalThis.Bun;
1010
+ return bun?.stringWidth ? bun.stringWidth(text) : Array.from(text).length;
1011
+ }
1012
+ export function fitCollapsedLabel(label, columns) {
1013
+ if (displayWidth(label) <= columns)
1014
+ return label;
1015
+ if (columns < 1)
1016
+ return "";
1017
+ let kept = "", used = 0;
1018
+ for (const char of label) {
1019
+ const width = displayWidth(char);
1020
+ if (used + width > columns - 1)
1021
+ break;
1022
+ kept += char;
1023
+ used += width;
1024
+ }
1025
+ return `${kept}\u2026`;
1026
+ }
938
1027
  const PACING_MIN_ELAPSED_MS = 300000, PACING_MIN_ELAPSED_FRACTION = 0.01, ON_PACE_DELTA = 1;
939
1028
  export function computeQuotaPacing(window, windowMs, now) {
940
1029
  if (!window.resetsAt)
@@ -17,6 +17,7 @@ import type {
17
17
  ApplyResult,
18
18
  OpenDialogPayload,
19
19
  } from "../rpc/protocol.js"
20
+ import { factLines, sectionTitle } from "./section-text.js"
20
21
 
21
22
  const log = createLogger('rpc-tui')
22
23
 
@@ -48,31 +49,8 @@ export function sectionListOptions(menu: Menu): DrawerOption[] {
48
49
  }))
49
50
  }
50
51
 
51
- function factLines(facts: Record<string, unknown> | undefined): string[] {
52
- return Object.entries(facts ?? {}).map(
53
- ([name, value]) =>
54
- `${name}: ${typeof value === 'string' ? value : JSON.stringify(value)}`,
55
- )
56
- }
57
-
58
- /** Read-only text is a category header; every option navigates or runs an action. */
52
+ /** Every option navigates or runs an action; read-only context is in the title. */
59
53
  export function sectionOptions(section: Section): DrawerOption[] {
60
- const header = [
61
- ...section.lines,
62
- ...factLines(section.facts),
63
- ...section.items
64
- .filter((item) => item.actions.length === 0)
65
- .map((item) =>
66
- [
67
- item.group,
68
- [item.label, item.status].filter(Boolean).join(' · '),
69
- item.detail,
70
- ...factLines(item.facts),
71
- ]
72
- .filter(Boolean)
73
- .join('\n'),
74
- ),
75
- ]
76
54
  const options: DrawerOption[] = [
77
55
  ...section.items
78
56
  .filter((item) => item.actions.length > 0)
@@ -91,15 +69,6 @@ export function sectionOptions(section: Section): DrawerOption[] {
91
69
  })),
92
70
  { title: 'Back', value: BACK },
93
71
  ]
94
- const first = options[0]
95
- if (first && header.length > 0) {
96
- const category = first.category
97
- const text = [...header, category].filter(Boolean).join('\n')
98
- for (const option of options) {
99
- if (option.category !== category) break
100
- option.category = text
101
- }
102
- }
103
72
  return options
104
73
  }
105
74
 
@@ -200,7 +169,7 @@ function showSection(
200
169
  api.ui.dialog.setSize('xlarge')
201
170
  api.ui.dialog.replace(() => (
202
171
  <DialogSelect
203
- title={section.title}
172
+ title={sectionTitle(section)}
204
173
  options={sectionOptions(section)}
205
174
  onSelect={(option) => {
206
175
  const value = String(option.value)
@@ -0,0 +1,21 @@
1
+ export function factLines(facts) {
2
+ return Object.entries(facts ?? {}).map(([name, value]) => `${name}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
3
+ }
4
+ export function sectionText(section) {
5
+ return [
6
+ ...section.lines,
7
+ ...factLines(section.facts),
8
+ ...section.items.filter((item) => item.actions.length === 0).map((item) => [
9
+ item.group,
10
+ [item.label, item.status].filter(Boolean).join(" \xB7 "),
11
+ item.detail,
12
+ ...factLines(item.facts)
13
+ ].filter(Boolean).join(`
14
+ `))
15
+ ].filter(Boolean).join(`
16
+ `);
17
+ }
18
+ export function sectionTitle(section) {
19
+ return [section.title, sectionText(section)].filter(Boolean).join(`
20
+ `);
21
+ }
@@ -0,0 +1,137 @@
1
+ import { sectionText, sectionTitle } from "./section-text.js";
2
+ function inlineDescription(...parts) {
3
+ return parts.filter(Boolean).join(" \xB7 ").replace(/\r?\n/g, " \xB7 ");
4
+ }
5
+ export async function openControlDialog(dialog, initial, apply, active = () => !0) {
6
+ let payload = initial;
7
+ menu:
8
+ while (active()) {
9
+ const sectionId = await dialog.select({
10
+ title: payload.menu.title,
11
+ options: payload.menu.sections.map((section) => ({
12
+ title: section.title,
13
+ value: section.id,
14
+ description: inlineDescription(...section.lines)
15
+ }))
16
+ });
17
+ if (!active() || sectionId === void 0)
18
+ return;
19
+ const section = payload.menu.sections.find((entry) => entry.id === sectionId);
20
+ if (!section)
21
+ return;
22
+ const choices = [
23
+ ...section.items.filter((item) => item.actions.length > 0).map((item) => ({
24
+ title: item.label,
25
+ description: inlineDescription(item.status, item.detail),
26
+ value: { itemId: item.id, actions: item.actions }
27
+ })),
28
+ ...section.actions.map((action) => ({
29
+ title: action.label,
30
+ description: inlineDescription(action.description),
31
+ value: { itemId: void 0, actions: [action] }
32
+ }))
33
+ ];
34
+ if (choices.length === 0) {
35
+ await dialog.alert({
36
+ title: section.title,
37
+ message: sectionText(section)
38
+ });
39
+ continue;
40
+ }
41
+ const choice = await dialog.select({
42
+ title: sectionTitle(section),
43
+ options: [...choices, { title: "Back", value: void 0 }]
44
+ });
45
+ if (!active())
46
+ return;
47
+ if (!choice)
48
+ continue;
49
+ let actionId;
50
+ if (choice.itemId !== void 0) {
51
+ if (choice.actions.length === 0)
52
+ continue;
53
+ actionId = await dialog.select({
54
+ title: section.items.find((item) => item.id === choice.itemId)?.label ?? section.title,
55
+ options: [
56
+ ...choice.actions.map((action) => ({
57
+ title: action.label,
58
+ value: action.id
59
+ })),
60
+ { title: "Back", value: void 0 }
61
+ ]
62
+ });
63
+ } else
64
+ actionId = choice.actions[0]?.id;
65
+ if (!active())
66
+ return;
67
+ if (actionId === void 0)
68
+ continue;
69
+ const action = choice.actions.find((entry) => entry.id === actionId);
70
+ if (!action)
71
+ continue;
72
+ try {
73
+ const values = {};
74
+ for (const knob of action.knobs) {
75
+ let value;
76
+ if (knob.kind === "choice")
77
+ value = await dialog.select({
78
+ title: knob.label,
79
+ options: knob.choices.map((entry) => ({
80
+ title: entry.label,
81
+ value: entry.value
82
+ }))
83
+ });
84
+ else if (knob.kind === "toggle")
85
+ value = await dialog.select({
86
+ title: knob.label,
87
+ options: [
88
+ { title: `On${knob.value ? " (current)" : ""}`, value: !0 },
89
+ { title: `Off${!knob.value ? " (current)" : ""}`, value: !1 }
90
+ ]
91
+ });
92
+ else {
93
+ const text = await dialog.prompt({
94
+ title: knob.label,
95
+ ...knob.value !== void 0 ? { value: String(knob.value) } : {},
96
+ ...knob.kind === "text" && knob.placeholder ? { placeholder: knob.placeholder } : {}
97
+ });
98
+ value = text === void 0 ? void 0 : text === "" ? null : knob.kind === "number" && Number.isFinite(Number(text)) ? Number(text) : text;
99
+ }
100
+ if (!active())
101
+ return;
102
+ if (value === void 0)
103
+ continue menu;
104
+ values[knob.id] = value;
105
+ }
106
+ if (action.confirm) {
107
+ const confirmed = await dialog.confirm({
108
+ title: action.label,
109
+ message: action.confirm.message
110
+ });
111
+ if (!active())
112
+ return;
113
+ if (confirmed !== !0)
114
+ continue;
115
+ }
116
+ const result = await apply({
117
+ command: payload.command,
118
+ sectionId,
119
+ actionId,
120
+ ...choice.itemId !== void 0 ? { itemId: choice.itemId } : {},
121
+ ...action.knobs.length ? { values } : {},
122
+ ...action.confirm ? { confirmed: !0 } : {}
123
+ });
124
+ if (!active())
125
+ return;
126
+ payload = { command: result.command, menu: result.menu };
127
+ await dialog.alert({ title: payload.menu.title, message: result.text });
128
+ } catch (error) {
129
+ if (!active())
130
+ return;
131
+ await dialog.alert({
132
+ title: payload.menu.title,
133
+ message: error instanceof Error ? error.message : String(error)
134
+ });
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,133 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { Plugin } from "@opencode/plugin/tui"
3
+ import type { JSX } from "solid-js"
4
+ import {
5
+ type ApplyRequest,
6
+ type ApplyResult,
7
+ isNotifyPayload,
8
+ type OpenDialogPayload,
9
+ } from "../rpc/protocol.js"
10
+ import { createRpcClient } from "../rpc/rpc-client.js"
11
+ import { resolveRpcDir } from "../rpc/rpc-dir.js"
12
+ import { openControlDialog } from "./v2-dialog.js"
13
+
14
+ export interface TuiTransportOptions {
15
+ /** Test-only label for proving that the host can render a sidebar component. */
16
+ sidebarText?: string
17
+ sidebar?: {
18
+ render(input: { sessionID: string }): JSX.Element
19
+ dispose(): void
20
+ }
21
+ openDialog?: (
22
+ payload: OpenDialogPayload,
23
+ apply: (request: Omit<ApplyRequest, 'sessionId'>) => Promise<ApplyResult>,
24
+ ) => Promise<void>
25
+ }
26
+
27
+ const KEY = Symbol.for('cortexkit.openai-auth.v2.tui')
28
+ type Mount = { dispose(): void }
29
+ type State = { mount?: Mount; lastIds: Map<string, number> }
30
+
31
+ /** Uses OpenCode 2's transport without running the OpenCode 1 UI registration. */
32
+ export async function setupTui(
33
+ context: Plugin.Context,
34
+ options: TuiTransportOptions = {},
35
+ ) {
36
+ const processState = globalThis as typeof globalThis & { [KEY]?: State }
37
+ processState[KEY] ??= { lastIds: new Map() }
38
+ const state = processState[KEY]
39
+ state.mount?.dispose()
40
+ const openDialog: NonNullable<TuiTransportOptions['openDialog']> =
41
+ options.openDialog ??
42
+ ((payload, apply) =>
43
+ openControlDialog(context.ui.dialog, payload, apply, () => !disposed))
44
+ let disposed = false
45
+ let inFlight = false
46
+ let dialogOpen = false
47
+ let directory = ''
48
+ let client: ReturnType<typeof createRpcClient> | undefined
49
+ const unmount =
50
+ options.sidebarText === undefined
51
+ ? options.sidebar
52
+ ? context.ui.slot({
53
+ append: 'sidebar.content',
54
+ render: options.sidebar.render,
55
+ })
56
+ : undefined
57
+ : context.ui.slot({
58
+ append: 'sidebar.content',
59
+ render: () => <text>{options.sidebarText}</text>,
60
+ })
61
+
62
+ const poll = async () => {
63
+ if (disposed || inFlight || dialogOpen) return
64
+ const route = context.ui.router.current()
65
+ if (route.type !== 'session') return
66
+ const sessionId = route.sessionID
67
+ const session = context.data.session.get(sessionId)
68
+ if (!session) return
69
+ inFlight = true
70
+ try {
71
+ if (directory !== session.location.directory) {
72
+ directory = session.location.directory
73
+ const resolved = await resolveRpcDir(directory)
74
+ if (disposed) return
75
+ client = createRpcClient(resolved.dir, process.pid)
76
+ }
77
+ const rpc = client
78
+ if (!rpc) return
79
+ const messages = await rpc.pending(
80
+ state.lastIds.get(sessionId) ?? 0,
81
+ sessionId,
82
+ )
83
+ if (
84
+ disposed ||
85
+ context.ui.router.current().type !== 'session' ||
86
+ (context.ui.router.current() as { sessionID?: string }).sessionID !==
87
+ sessionId
88
+ )
89
+ return
90
+ for (const message of [...messages].sort((a, b) => a.id - b.id)) {
91
+ state.lastIds.set(
92
+ sessionId,
93
+ Math.max(state.lastIds.get(sessionId) ?? 0, message.id),
94
+ )
95
+ if (isNotifyPayload(message.payload))
96
+ context.ui.toast.show({
97
+ message: message.payload.notify.message,
98
+ sessionID: sessionId,
99
+ })
100
+ else {
101
+ dialogOpen = true
102
+ await openDialog(message.payload, (request) => {
103
+ if (disposed) throw new Error('The OpenAI dialog was closed')
104
+ return rpc.apply({ ...request, sessionId }, 90_000)
105
+ }).finally(() => {
106
+ dialogOpen = false
107
+ })
108
+ }
109
+ }
110
+ } catch {
111
+ // The server can be absent while a location starts or is reloaded.
112
+ } finally {
113
+ inFlight = false
114
+ }
115
+ }
116
+ const timer = setInterval(() => {
117
+ void poll()
118
+ }, 500)
119
+ const mount: Mount = {
120
+ dispose() {
121
+ if (disposed) return
122
+ disposed = true
123
+ clearInterval(timer)
124
+ unmount?.()
125
+ options.sidebar?.dispose()
126
+ if (dialogOpen) context.ui.dialog.clear()
127
+ if (state.mount === mount) state.mount = undefined
128
+ },
129
+ }
130
+ state.mount = mount
131
+ void poll()
132
+ return () => mount.dispose()
133
+ }