@cortexkit/opencode-openai-auth 0.3.2 → 0.3.4

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 (41) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/commands.d.ts +3 -1
  3. package/dist/core/accounts.d.ts +6 -0
  4. package/dist/core/cachekeep.d.ts +16 -0
  5. package/dist/core/oauth.d.ts +1 -1
  6. package/dist/core/quota-manager.d.ts +33 -1
  7. package/dist/{index-gwmhhrrc.js → index-1xmasv0x.js} +1 -1
  8. package/dist/{index-899gpxdd.js → index-5s67238f.js} +1 -1
  9. package/dist/{index-53j7bdev.js → index-e7m7qbne.js} +1 -1
  10. package/dist/{index-7w467pz8.js → index-k5zwf3j1.js} +1 -1
  11. package/dist/{index-qp2nja20.js → index-mrg2fms9.js} +2 -2
  12. package/dist/{index-jn8xd8fy.js → index-qdh7pt5a.js} +1 -1
  13. package/dist/index-y8d01tgn.js +92 -0
  14. package/dist/index.js +68 -62
  15. package/dist/{quota-normalize-dxftsgep.js → quota-normalize-dfdpwwm3.js} +1 -1
  16. package/dist/rpc/port-file.js +1 -1
  17. package/dist/rpc/rpc-client.js +1 -1
  18. package/dist/rpc/rpc-dir.js +1 -1
  19. package/dist/sidebar-state.js +1 -1
  20. package/dist/{token-28n5g707.js → token-01dcf92a.js} +1 -1
  21. package/dist/token-util-rkqd82q9.js +1 -0
  22. package/dist/tui-preferences.js +1 -1
  23. package/dist/version.d.ts +1 -1
  24. package/dist/ws-pool.d.ts +11 -0
  25. package/dist/ws.d.ts +1 -1
  26. package/package.json +6 -4
  27. package/src/tui/command-dialogs.tsx +77 -0
  28. package/src/tui/entry.mjs +37 -0
  29. package/src/tui-compiled/logger.ts +161 -0
  30. package/src/tui-compiled/rpc/port-file.ts +70 -0
  31. package/src/tui-compiled/rpc/protocol.ts +31 -0
  32. package/src/tui-compiled/rpc/rpc-client.ts +78 -0
  33. package/src/tui-compiled/rpc/rpc-dir.ts +22 -0
  34. package/src/tui-compiled/sidebar-state.ts +316 -0
  35. package/src/tui-compiled/tui/command-dialogs.tsx +640 -0
  36. package/src/tui-compiled/tui-preferences.ts +328 -0
  37. package/src/tui-compiled/tui.tsx +1041 -0
  38. package/src/tui-compiled/util/open-url.ts +30 -0
  39. package/src/tui.tsx +16 -13
  40. package/dist/index-z21wp7ac.js +0 -92
  41. package/dist/token-util-gmr2b13z.js +0 -1
@@ -0,0 +1,316 @@
1
+ export interface QuotaWindow {
2
+ usedPercent: number
3
+ remainingPercent: number
4
+ resetsAt?: string
5
+ }
6
+
7
+ export interface AccountQuota {
8
+ primary?: QuotaWindow
9
+ secondary?: QuotaWindow
10
+ }
11
+
12
+ export interface SidebarAccountState {
13
+ id: string
14
+ label: string | undefined
15
+ quota: AccountQuota | null
16
+ killed: boolean
17
+ enabled: boolean
18
+ }
19
+
20
+ export interface SidebarState {
21
+ main: {
22
+ quota: AccountQuota | null
23
+ killed: boolean
24
+ quotaBackedOff?: boolean
25
+ quotaBackoffUntil?: number
26
+ refreshBackedOff?: boolean
27
+ refreshBackoffUntil?: number
28
+ }
29
+ fallbacks: SidebarAccountState[]
30
+ activeId: string | undefined
31
+ route: string
32
+ planType?: string
33
+ credits?: number
34
+ lastUpdated: number
35
+ }
36
+
37
+ import { randomUUID } from 'node:crypto'
38
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
39
+ import { tmpdir } from 'node:os'
40
+ import { dirname, join } from 'node:path'
41
+ import { createLogger } from './logger'
42
+
43
+ const logSb = createLogger('sidebar')
44
+
45
+ const STATE_FILE_ENV = 'OPENCODE_OPENAI_AUTH_SIDEBAR_STATE_FILE'
46
+ const DEFAULT_STATE_DIR = join(tmpdir(), 'opencode-openai-auth')
47
+ const DEFAULT_STATE_FILE = join(DEFAULT_STATE_DIR, 'sidebar-state.json')
48
+
49
+ export function getSidebarStateFile(): string {
50
+ return process.env[STATE_FILE_ENV] || DEFAULT_STATE_FILE
51
+ }
52
+
53
+ export const DEFAULT_SIDEBAR_STATE: SidebarState = {
54
+ main: { quota: null, killed: false },
55
+ fallbacks: [],
56
+ activeId: undefined,
57
+ route: 'main',
58
+ lastUpdated: 0,
59
+ }
60
+
61
+ /**
62
+ * Normalize an arbitrary parsed value into a well-formed SidebarState.
63
+ *
64
+ * JSON.parse + `as SidebarState` is an unchecked cast — a partial, old, or
65
+ * malformed state file passes through and the TUI's `state().main.quota` /
66
+ * `state().fallbacks.filter(...)` throw at runtime. This helper guarantees
67
+ * every required field is present and correctly typed before the value leaves
68
+ * the I/O boundary, so a bad file can never crash the host TUI.
69
+ */
70
+ export function normalizeSidebarState(raw: unknown): SidebarState {
71
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
72
+ return DEFAULT_SIDEBAR_STATE
73
+ }
74
+
75
+ const r = raw as Record<string, unknown>
76
+
77
+ // main — must be an object with at least quota and killed
78
+ const rawMain = r.main
79
+ let main: SidebarState['main']
80
+ if (
81
+ rawMain !== null &&
82
+ typeof rawMain === 'object' &&
83
+ !Array.isArray(rawMain)
84
+ ) {
85
+ const m = rawMain as Record<string, unknown>
86
+ main = {
87
+ quota: ('quota' in m ? m.quota : null) as AccountQuota | null,
88
+ killed: typeof m.killed === 'boolean' ? m.killed : false,
89
+ // Preserve optional backoff fields if present
90
+ ...(typeof m.quotaBackedOff === 'boolean'
91
+ ? { quotaBackedOff: m.quotaBackedOff }
92
+ : {}),
93
+ ...(typeof m.quotaBackoffUntil === 'number'
94
+ ? { quotaBackoffUntil: m.quotaBackoffUntil }
95
+ : {}),
96
+ ...(typeof m.refreshBackedOff === 'boolean'
97
+ ? { refreshBackedOff: m.refreshBackedOff }
98
+ : {}),
99
+ ...(typeof m.refreshBackoffUntil === 'number'
100
+ ? { refreshBackoffUntil: m.refreshBackoffUntil }
101
+ : {}),
102
+ }
103
+ } else {
104
+ main = { quota: null, killed: false }
105
+ }
106
+
107
+ // fallbacks — must be an array; keep entries that are objects with a string
108
+ // id, and normalize each entry's inner fields so the TUI never reads a
109
+ // wrong-typed value (e.g. a string `enabled`) off a malformed file.
110
+ const rawFallbacks = r.fallbacks
111
+ const fallbacks: SidebarAccountState[] = Array.isArray(rawFallbacks)
112
+ ? rawFallbacks
113
+ .filter(
114
+ (entry): entry is Record<string, unknown> =>
115
+ entry !== null &&
116
+ typeof entry === 'object' &&
117
+ !Array.isArray(entry) &&
118
+ typeof (entry as Record<string, unknown>).id === 'string',
119
+ )
120
+ .map((e) => ({
121
+ id: e.id as string,
122
+ label: typeof e.label === 'string' ? e.label : undefined,
123
+ quota: ('quota' in e ? e.quota : null) as AccountQuota | null,
124
+ killed: typeof e.killed === 'boolean' ? e.killed : false,
125
+ enabled: typeof e.enabled === 'boolean' ? e.enabled : true,
126
+ }))
127
+ : []
128
+
129
+ // activeId — string or undefined
130
+ const activeId = typeof r.activeId === 'string' ? r.activeId : undefined
131
+
132
+ // route — string, default 'main'
133
+ const route =
134
+ typeof r.route === 'string' ? r.route : DEFAULT_SIDEBAR_STATE.route
135
+
136
+ // lastUpdated — number, default 0
137
+ const lastUpdated = typeof r.lastUpdated === 'number' ? r.lastUpdated : 0
138
+
139
+ // Optional top-level fields
140
+ const planType = typeof r.planType === 'string' ? r.planType : undefined
141
+ const credits = typeof r.credits === 'number' ? r.credits : undefined
142
+
143
+ return {
144
+ main,
145
+ fallbacks,
146
+ activeId,
147
+ route,
148
+ lastUpdated,
149
+ ...(planType !== undefined ? { planType } : {}),
150
+ ...(credits !== undefined ? { credits } : {}),
151
+ }
152
+ }
153
+
154
+ export async function getSidebarState(): Promise<SidebarState> {
155
+ try {
156
+ const raw = await readFile(getSidebarStateFile(), 'utf8')
157
+ return normalizeSidebarState(JSON.parse(raw))
158
+ } catch {
159
+ return DEFAULT_SIDEBAR_STATE
160
+ }
161
+ }
162
+
163
+ // Serialization chain: concurrent calls are queued so a stale background
164
+ // write cannot land after a newer one and corrupt the file.
165
+ let sidebarWriteChain: Promise<void> = Promise.resolve()
166
+
167
+ /**
168
+ * Write sidebar state to disk, serialized through a promise chain so
169
+ * concurrent callers never interleave or let a stale write land last.
170
+ *
171
+ * @param state The state to persist.
172
+ * @param file Explicit path override — callers that bind the path at init
173
+ * time (e.g. the index.ts loader) pass this so late callbacks
174
+ * always write to the path that was current when the loader ran,
175
+ * even if the env changes underneath them during tests.
176
+ * Defaults to getSidebarStateFile() (per-call resolution).
177
+ */
178
+ export function setSidebarState(
179
+ state: SidebarState,
180
+ file = getSidebarStateFile(),
181
+ ): Promise<void> {
182
+ sidebarWriteChain = sidebarWriteChain
183
+ .then(() => doWriteSidebarState(state, file))
184
+ .catch(() => {})
185
+ return sidebarWriteChain
186
+ }
187
+
188
+ async function doWriteSidebarState(
189
+ state: SidebarState,
190
+ file: string,
191
+ ): Promise<void> {
192
+ const tempPath = `${file}.${randomUUID()}.tmp`
193
+ try {
194
+ await mkdir(dirname(file), { recursive: true })
195
+ await writeFile(tempPath, JSON.stringify(state), 'utf8')
196
+ await rename(tempPath, file)
197
+ } catch (e) {
198
+ await rm(tempPath, { force: true }).catch(() => {})
199
+ logSb.warn('sidebar write failed', {
200
+ pid: process.pid,
201
+ error: e instanceof Error ? e.message : String(e),
202
+ })
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Await all pending sidebar writes. Tests call this before restoring env
208
+ * vars in teardown so no in-flight write can re-resolve getSidebarStateFile()
209
+ * after the env is changed.
210
+ */
211
+ export function drainSidebarWrites(): Promise<void> {
212
+ return sidebarWriteChain
213
+ }
214
+
215
+ // Resolve the currently-active account from activeId for the collapsed sidebar
216
+ // view. activeId === 'main' (or undefined/unmatched/disabled) → the main
217
+ // account; otherwise the enabled fallback whose id matches.
218
+ export function resolveActiveAccount(state: SidebarState): {
219
+ id: string
220
+ name: string
221
+ quota: AccountQuota | null
222
+ killed: boolean
223
+ } {
224
+ const activeId = state.activeId
225
+ if (activeId && activeId !== 'main') {
226
+ const fallback = state.fallbacks.find(
227
+ (account) => account.enabled && account.id === activeId,
228
+ )
229
+ if (fallback) {
230
+ return {
231
+ id: fallback.id,
232
+ name: fallback.label ?? fallback.id,
233
+ quota: fallback.quota,
234
+ killed: fallback.killed,
235
+ }
236
+ }
237
+ }
238
+ return {
239
+ id: 'main',
240
+ name: 'main',
241
+ quota: state.main.quota,
242
+ killed: state.main.killed,
243
+ }
244
+ }
245
+
246
+ export function getCollapsedQuotaSummary(quota: AccountQuota | null): {
247
+ primaryUsedPercent: number | null
248
+ secondaryUsedPercent: number | null
249
+ text: string | null
250
+ } {
251
+ const primaryUsedPercent = quota?.primary?.usedPercent ?? null
252
+ const secondaryUsedPercent = quota?.secondary?.usedPercent ?? null
253
+ if (primaryUsedPercent == null && secondaryUsedPercent == null) {
254
+ return { primaryUsedPercent, secondaryUsedPercent, text: null }
255
+ }
256
+
257
+ return {
258
+ primaryUsedPercent,
259
+ secondaryUsedPercent,
260
+ text: `5h: ${primaryUsedPercent == null ? '\u2014' : `${Math.round(primaryUsedPercent)}%`} 7d: ${secondaryUsedPercent == null ? '\u2014' : `${Math.round(secondaryUsedPercent)}%`}`,
261
+ }
262
+ }
263
+
264
+ export const FIVE_HOUR_MS = 5 * 60 * 60 * 1000
265
+ export const SEVEN_DAY_MS = 7 * 24 * 60 * 60 * 1000
266
+
267
+ const PACING_MIN_ELAPSED_MS = 5 * 60 * 1000
268
+ const PACING_MIN_ELAPSED_FRACTION = 0.01
269
+ const ON_PACE_DELTA = 1
270
+
271
+ export interface QuotaPacing {
272
+ pacePercent: number
273
+ deltaPercent: number
274
+ state: 'deficit' | 'reserve' | 'on-pace'
275
+ runsOutAt: string | null
276
+ }
277
+
278
+ // Even-burn pacing for a quota window. The window start is inferred from the
279
+ // reset timestamp minus the window length. Two metrics: deltaPercent compares
280
+ // usage against a uniform burn-down (positive = deficit), and runsOutAt
281
+ // projects the current average burn rate forward — null means the window
282
+ // lasts until reset at that rate. Returns null when there is no reset
283
+ // timestamp or the elapsed time is too small to give a meaningful rate.
284
+ export function computeQuotaPacing(
285
+ window: QuotaWindow,
286
+ windowMs: number,
287
+ now: number,
288
+ ): QuotaPacing | null {
289
+ if (!window.resetsAt) return null
290
+ const resetsAt = new Date(window.resetsAt).getTime()
291
+ if (!Number.isFinite(resetsAt)) return null
292
+ const start = resetsAt - windowMs
293
+ const elapsed = now - start
294
+ if (elapsed < PACING_MIN_ELAPSED_MS) return null
295
+ if (elapsed < windowMs * PACING_MIN_ELAPSED_FRACTION) return null
296
+ if (elapsed >= windowMs) return null
297
+
298
+ const used = window.usedPercent
299
+ const pacePercent = Math.min(Math.max((elapsed / windowMs) * 100, 0), 100)
300
+ const deltaPercent = used - pacePercent
301
+ const state =
302
+ Math.abs(deltaPercent) < ON_PACE_DELTA
303
+ ? 'on-pace'
304
+ : deltaPercent > 0
305
+ ? 'deficit'
306
+ : 'reserve'
307
+
308
+ let runsOutAt: string | null = null
309
+ if (used > 0) {
310
+ const msToFull = (elapsed * 100) / used
311
+ const runOut = start + msToFull
312
+ if (runOut < resetsAt) runsOutAt = new Date(runOut).toISOString()
313
+ }
314
+
315
+ return { pacePercent, deltaPercent, state, runsOutAt }
316
+ }