@cortexkit/opencode-openai-auth 1.0.5 → 1.1.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 (102) hide show
  1. package/dist/bin.js +9 -8
  2. package/dist/{pool-lifecycle-C_6YRmsh.d.ts → codex-request-BXDXToyt.d.ts} +158 -27
  3. package/dist/index-0yh85bwb.js +69 -0
  4. package/dist/{index-826s1yxa.js → index-4c8yw54k.js} +2 -2
  5. package/dist/{index-y4gxyxqd.js → index-7c6sxtd6.js} +2 -2
  6. package/dist/index-7eb3dw37.js +90 -0
  7. package/dist/index-8re8rprk.js +2 -0
  8. package/dist/{index-66asrje2.js → index-csky2xpg.js} +1 -1
  9. package/dist/index-edbdscy7.js +65 -0
  10. package/dist/{index-pg305s90.js → index-f2fmb596.js} +1 -1
  11. package/dist/{index-fsxbncpw.js → index-j8v2xwq6.js} +1 -1
  12. package/dist/{index-fsky1rsh.js → index-k6cydkgt.js} +2 -2
  13. package/dist/{index-116q849r.js → index-m5qmvr6c.js} +2 -2
  14. package/dist/index-msx2m78k.js +8 -0
  15. package/dist/{index-nx2qh047.js → index-zpm95mt5.js} +1 -1
  16. package/dist/index.d.ts +15 -7
  17. package/dist/index.js +1 -1
  18. package/dist/{map-m6f5a_IA.d.ts → map-Buq-GW7l.d.ts} +1 -1
  19. package/dist/{protocol-DctqemRI.d.ts → model-YRg7Uowo.d.ts} +67 -25
  20. package/dist/{port-file-ZPlxXB4G.d.ts → port-file-CxEpgSHY.d.ts} +1 -1
  21. package/dist/protocol-CDV1Pc6h.d.ts +29 -0
  22. package/dist/rpc/port-file.d.ts +1 -1
  23. package/dist/rpc/port-file.js +1 -1
  24. package/dist/rpc/protocol.d.ts +1 -1
  25. package/dist/rpc/protocol.js +1 -1
  26. package/dist/rpc/rpc-client.d.ts +3 -3
  27. package/dist/rpc/rpc-client.js +1 -1
  28. package/dist/rpc/rpc-dir.js +1 -1
  29. package/dist/{sidebar-state-z_M7Trnd.d.ts → sidebar-state-DjTWUbmf.d.ts} +15 -9
  30. package/dist/sidebar-state.d.ts +1 -1
  31. package/dist/sidebar-state.js +1 -1
  32. package/dist/{token-bzhkgc29.js → token-rz4t3ay6.js} +1 -1
  33. package/dist/token-util-tpy2c7h6.js +1 -0
  34. package/dist/tui-preferences.js +1 -1
  35. package/dist/tui.d.ts +20 -2
  36. package/dist/v2/server.d.ts +73 -1
  37. package/dist/v2/server.js +3 -3
  38. package/package.json +2 -2
  39. package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +13 -8
  40. package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +354 -0
  41. package/src/tui-compiled/raw/shared/134b008900698fe0-backoff.js +92 -0
  42. package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +393 -0
  43. package/src/tui-compiled/raw/shared/2577ec69f9973ae9-quota-normalize.js +154 -0
  44. package/src/tui-compiled/raw/shared/2dce6e9613993329-quota-manager.js +473 -0
  45. package/src/tui-compiled/raw/shared/30f5bc480f31f51c-roster.js +566 -0
  46. package/src/tui-compiled/raw/shared/43fc9e6b994519c7-refresh-lock.js +0 -1
  47. package/src/tui-compiled/raw/shared/4593a4d71edf69fd-tombstone.js +25 -0
  48. package/src/tui-compiled/raw/shared/4d05c2d7a531aece-oauth.js +500 -0
  49. package/src/tui-compiled/raw/shared/6371b24a0573d79e-any-signal.js +42 -0
  50. package/src/tui-compiled/raw/shared/7c140680fc81be92-interlock.js +29 -0
  51. package/src/tui-compiled/raw/shared/85512cc5263ba259-provider.js +87 -0
  52. package/src/tui-compiled/raw/shared/abd062d633e29fd2-pool-routing.js +192 -0
  53. package/src/tui-compiled/raw/shared/b9588ad4725c1d10-enrollment.js +806 -0
  54. package/src/tui-compiled/raw/shared/bfbdd0f9aae56b47-pool-quota.js +85 -0
  55. package/src/tui-compiled/raw/shared/c852a15cfe489d12-pool.js +0 -1
  56. package/src/tui-compiled/raw/shared/ca0fccaba1d87af4-errors.js +8 -0
  57. package/src/tui-compiled/raw/shared/d0f0b6be200311af-insula.js +152 -0
  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 +41 -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 +146 -40
  68. package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +13 -8
  69. package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +354 -0
  70. package/src/tui-compiled/runtime/shared/134b008900698fe0-backoff.js +92 -0
  71. package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +393 -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/d9d2892a4c371301-host-slot.js +72 -0
  88. package/src/tui-compiled/runtime/shared/dbc61cd405941828-accounts.js +1470 -0
  89. package/src/tui-compiled/runtime/shared/e8cd4929507508d8-refresh-file-lock.js +26 -12
  90. package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +24 -0
  91. package/src/tui-compiled/runtime/sidebar-state.js +41 -14
  92. package/src/tui-compiled/runtime/tui/command-dialogs.js +3 -15
  93. package/src/tui-compiled/runtime/tui/section-text.js +21 -0
  94. package/src/tui-compiled/runtime/tui/v2-dialog.js +137 -0
  95. package/src/tui-compiled/runtime/tui/v2.js +95 -0
  96. package/src/tui-compiled/runtime/tui.js +255 -163
  97. package/dist/index-4fnghj9e.js +0 -65
  98. package/dist/index-a69gr76b.js +0 -2
  99. package/dist/index-h263aftt.js +0 -157
  100. package/dist/index-ssj7m2rd.js +0 -8
  101. package/dist/index-ydz3hbhc.js +0 -2
  102. 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
+ }
@@ -3,6 +3,11 @@ import {
3
3
  projectQuota,
4
4
  quotaWindowName
5
5
  } from "./shared/d5a8f1061ee2f114-index.js";
6
+ import { orderForPlacement } from "./shared/3e7d45a77c6ad4d5-index.js";
7
+ import {
8
+ orderedPlacement,
9
+ vaultMainRouteId
10
+ } from "./shared/abd062d633e29fd2-pool-routing.js";
6
11
  const QUOTA_WINDOW_KEYS = ["primary", "secondary"], LEGACY_WINDOW_MINUTES = {
7
12
  primary: 300,
8
13
  secondary: 10080
@@ -167,11 +172,14 @@ function normalizeAccountList(raw) {
167
172
  return [];
168
173
  return raw.filter((entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry) && typeof entry.id === "string").map((e) => ({
169
174
  id: e.id,
175
+ ...typeof e.credentialId === "string" ? { credentialId: e.credentialId } : {},
170
176
  label: typeof e.label === "string" ? e.label : void 0,
171
177
  ...typeof e.accountId === "string" ? { accountId: e.accountId } : {},
172
178
  quota: "quota" in e ? e.quota : null,
173
179
  killed: typeof e.killed === "boolean" ? e.killed : !1,
174
180
  enabled: typeof e.enabled === "boolean" ? e.enabled : !0,
181
+ ...typeof e.routable === "boolean" ? { routable: e.routable } : {},
182
+ ...["needs sign-in", "turned off in vault", "unavailable"].includes(e.vaultState) ? { vaultState: e.vaultState } : {},
175
183
  ...resetCreditsField(e.resetCredits)
176
184
  }));
177
185
  }
@@ -192,7 +200,7 @@ export function isUsableRoutingEntry(entry, accounts, now = Date.now()) {
192
200
  return !1;
193
201
  if (accounts === void 0)
194
202
  return !0;
195
- return entry.activeId === "main" || accounts.some((account) => account.enabled !== !1 && account.killed !== !0 && account.id === entry.activeId);
203
+ return entry.activeId === "main" || accounts.some((account) => account.enabled !== !1 && account.routable !== !1 && account.killed !== !0 && account.id === entry.activeId);
196
204
  }
197
205
  export function spendControlExhaustedResetAt(quota, now = Date.now()) {
198
206
  const spendControl = quota?.spendControl;
@@ -229,28 +237,40 @@ export function resolveSessionStickyAccount(state, sessionId, now = Date.now())
229
237
  const assignment = state.stickyAssignments?.[hashSidebarSessionId(sessionId)];
230
238
  if (!assignment || assignment.lastSeenAt < now - STICKY_ASSIGNMENT_MAX_AGE_MS)
231
239
  return;
232
- if (assignment.accountId === "main")
240
+ if (assignment.accountId === "main" && state.vaultAccounts === void 0)
233
241
  return state.main?.killed || isQuotaExhausted(state.main?.quota, now) ? void 0 : "main";
234
242
  const fallback = routingAccountsOf(state).find((account) => account.id === assignment.accountId);
235
- if (!fallback || fallback.enabled === !1 || fallback.killed === !0 || isQuotaExhausted(fallback.quota, now))
243
+ if (!fallback || fallback.enabled === !1 || fallback.routable === !1 || fallback.killed === !0 || isQuotaExhausted(fallback.quota, now))
236
244
  return;
237
245
  return fallback.id;
238
246
  }
239
247
  export function resolveSessionSidebarRouting(state, sessionId, now = Date.now()) {
240
- if (!sessionId)
241
- return { activeId: state.activeId ?? "main", route: state.route };
248
+ if (!sessionId) {
249
+ if (state.vaultAccounts === void 0)
250
+ return { activeId: state.activeId ?? "main", route: state.route };
251
+ const active = state.vaultAccounts.find((account) => account.id === state.activeId && account.enabled && account.routable !== !1 && !account.killed);
252
+ if (active && !isQuotaExhausted(active.quota, now))
253
+ return { activeId: active.id, route: state.route };
254
+ }
242
255
  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))
256
+ if (own && (state.vaultAccounts === void 0 || own.activeId.startsWith("vault:")) && isUsableRoutingEntry(own, accounts, now) && !isQuotaExhausted(ownQuota, now))
244
257
  return { activeId: own.activeId, route: own.route };
245
258
  const stickyAccountId = resolveSessionStickyAccount(state, sessionId, now);
246
259
  if (stickyAccountId)
247
260
  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];
261
+ 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
262
  return {
250
- activeId: (vaultMode || state.route === "fallback-first") && fallback ? fallback.id : "main",
263
+ activeId: vaultMode ? fallback?.id ?? "vault" : state.route === "fallback-first" && fallback ? fallback.id : "main",
251
264
  route: state.route
252
265
  };
253
266
  }
267
+ function vaultAccountsInPlacementOrder(state) {
268
+ const accounts = state.vaultAccounts ?? [], byId = new Map(accounts.map((account) => [account.id, account]));
269
+ return orderForPlacement(accounts.map((account) => account.id), state.route === "sticky-balanced" ? "roster" : orderedPlacement(state.route), vaultMainRouteId(accounts) ?? "").flatMap((id) => {
270
+ const account = byId.get(id);
271
+ return account ? [account] : [];
272
+ });
273
+ }
254
274
  export function pruneActiveRouting(activeRouting, accounts, now = Date.now(), removedSessionId) {
255
275
  if (!activeRouting)
256
276
  return;
@@ -407,7 +427,14 @@ function writeMergedSidebarState(file, merge, hooks) {
407
427
  if (hooks?.canWrite && !hooks.canWrite())
408
428
  return;
409
429
  const next = merge(latest);
410
- return next === void 0 ? void 0 : normalizeSidebarState(next);
430
+ if (next === void 0)
431
+ return;
432
+ if (next.vaultAccounts !== void 0) {
433
+ next.activeId = next.activeId?.startsWith("vault:") ? next.activeId : void 0;
434
+ next.activeRouting = Object.fromEntries(Object.entries(next.activeRouting ?? {}).filter(([, entry]) => entry.activeId.startsWith("vault:")));
435
+ next.stickyAssignments = Object.fromEntries(Object.entries(next.stickyAssignments ?? {}).filter(([, pin]) => pin.accountId.startsWith("vault:") && !next.vaultAccounts?.some((account) => account.id === pin.accountId && account.killed)));
436
+ }
437
+ return normalizeSidebarState(next);
411
438
  }, {
412
439
  beforeRecheck: async (...args) => {
413
440
  await hooks?.beforeRecheck?.(...args);
@@ -493,7 +520,7 @@ function mergeQuotaByWindow(incoming, existing) {
493
520
  export function setSidebarMachineState(machineState, file = getSidebarStateFile(), hooks) {
494
521
  return enqueueSidebarWrite(async () => {
495
522
  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);
523
+ 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
524
  return {
498
525
  ...latest,
499
526
  ...machineState,
@@ -530,7 +557,7 @@ export function upsertSidebarActiveRouting(input, accounts, file = getSidebarSta
530
557
  route: input.route,
531
558
  updatedAt: input.updatedAt
532
559
  }
533
- }, accounts, Date.now()), stickyAssignments = pruneStickyAssignments(latest.stickyAssignments, usableRoutingAccountIds(accounts), Date.now());
560
+ }, accounts, Date.now()), stickyAssignments = pruneStickyAssignments(latest.stickyAssignments, latest.vaultAccounts !== void 0 ? void 0 : usableRoutingAccountIds(accounts), Date.now());
534
561
  return {
535
562
  ...latest,
536
563
  activeId: input.activeId,
@@ -555,7 +582,7 @@ export function setSidebarLegacyRouting(input, file = getSidebarStateFile(), hoo
555
582
  export function removeSidebarActiveRouting(sessionId, accounts, file = getSidebarStateFile(), hooks) {
556
583
  return enqueueSidebarWrite(async () => {
557
584
  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));
585
+ 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
586
  return {
560
587
  ...latest,
561
588
  activeRouting,
@@ -652,7 +679,7 @@ export function persistSidebarStickyAssignment(input, file = getSidebarStateFile
652
679
  const sessionHash = hashSidebarSessionId(input.sessionId), validPinnedAccountIds = new Set(input.validPinnedAccountIds);
653
680
  return enqueueSidebarWrite(async () => {
654
681
  await writeMergedSidebarState(file, (latest) => {
655
- const now = Date.now(), stickyAssignments = pruneStickyAssignments(latest.stickyAssignments, validPinnedAccountIds, now), current = stickyAssignments?.[sessionHash];
682
+ const now = Date.now(), stickyAssignments = pruneStickyAssignments(latest.stickyAssignments, latest.vaultAccounts !== void 0 ? void 0 : validPinnedAccountIds, now), current = stickyAssignments?.[sessionHash];
656
683
  if (current && current.assignedAt > input.assignment.assignedAt) {
657
684
  if (stickyAssignmentsEqual(latest.stickyAssignments, stickyAssignments))
658
685
  return;
@@ -875,7 +902,7 @@ export function createSidebarBookkeepingQueue(options) {
875
902
  export function resolveActiveAccount(state) {
876
903
  const activeId = state.activeId;
877
904
  if (state.vaultAccounts !== void 0) {
878
- const enabled = state.vaultAccounts.filter((account) => account.enabled), vaultAccount = enabled.find((account) => account.id === activeId) ?? enabled[0];
905
+ const enabled = vaultAccountsInPlacementOrder(state).filter((account) => account.enabled && account.routable !== !1), vaultAccount = enabled.find((account) => account.id === activeId) ?? enabled[0];
879
906
  return vaultAccount ? {
880
907
  id: vaultAccount.id,
881
908
  name: vaultAccount.label ?? vaultAccount.id,
@@ -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
+ }