@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.
- package/dist/cli.js +1 -1
- package/dist/commands.d.ts +3 -1
- package/dist/core/accounts.d.ts +6 -0
- package/dist/core/cachekeep.d.ts +16 -0
- package/dist/core/oauth.d.ts +1 -1
- package/dist/core/quota-manager.d.ts +33 -1
- package/dist/{index-gwmhhrrc.js → index-1xmasv0x.js} +1 -1
- package/dist/{index-899gpxdd.js → index-5s67238f.js} +1 -1
- package/dist/{index-53j7bdev.js → index-e7m7qbne.js} +1 -1
- package/dist/{index-7w467pz8.js → index-k5zwf3j1.js} +1 -1
- package/dist/{index-qp2nja20.js → index-mrg2fms9.js} +2 -2
- package/dist/{index-jn8xd8fy.js → index-qdh7pt5a.js} +1 -1
- package/dist/index-y8d01tgn.js +92 -0
- package/dist/index.js +68 -62
- package/dist/{quota-normalize-dxftsgep.js → quota-normalize-dfdpwwm3.js} +1 -1
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/sidebar-state.js +1 -1
- package/dist/{token-28n5g707.js → token-01dcf92a.js} +1 -1
- package/dist/token-util-rkqd82q9.js +1 -0
- package/dist/tui-preferences.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/ws-pool.d.ts +11 -0
- package/dist/ws.d.ts +1 -1
- package/package.json +6 -4
- package/src/tui/command-dialogs.tsx +77 -0
- package/src/tui/entry.mjs +37 -0
- package/src/tui-compiled/logger.ts +161 -0
- package/src/tui-compiled/rpc/port-file.ts +70 -0
- package/src/tui-compiled/rpc/protocol.ts +31 -0
- package/src/tui-compiled/rpc/rpc-client.ts +78 -0
- package/src/tui-compiled/rpc/rpc-dir.ts +22 -0
- package/src/tui-compiled/sidebar-state.ts +316 -0
- package/src/tui-compiled/tui/command-dialogs.tsx +640 -0
- package/src/tui-compiled/tui-preferences.ts +328 -0
- package/src/tui-compiled/tui.tsx +1041 -0
- package/src/tui-compiled/util/open-url.ts +30 -0
- package/src/tui.tsx +16 -13
- package/dist/index-z21wp7ac.js +0 -92
- 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
|
+
}
|