@cortexkit/opencode-openai-auth 0.4.3 → 0.5.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.
- package/README.md +8 -4
- package/dist/cli.js +1 -1
- package/dist/codex-http.d.ts +1 -1
- package/dist/commands.d.ts +6 -0
- package/dist/core/accounts.d.ts +6 -1
- package/dist/core/background-quota-refresh.d.ts +46 -0
- package/dist/core/cachekeep.d.ts +4 -0
- package/dist/core/oauth.d.ts +1 -1
- package/dist/core/quota-manager.d.ts +11 -4
- package/dist/core/refresh-all-quota.d.ts +12 -1
- package/dist/core/sticky-routing.d.ts +43 -0
- package/dist/dump.d.ts +1 -0
- package/dist/{index-xcf8tm40.js → index-6rnw2cgs.js} +1 -1
- package/dist/index-89mh4ahn.js +89 -0
- package/dist/index-rfgyxxxq.js +1 -0
- package/dist/index-s5fbqqwx.js +3 -0
- package/dist/index-vrdc90yd.js +6 -0
- package/dist/index-wnxfzjfr.js +2 -0
- package/dist/{index-xbq3aq6f.js → index-wpd7mg3p.js} +1 -1
- package/dist/index-zkzb0twr.js +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +99 -86
- package/dist/{quota-normalize-754rewnb.js → quota-normalize-n02dfdx0.js} +1 -1
- package/dist/rpc/port-file.d.ts +4 -0
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/protocol.d.ts +1 -0
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.d.ts +6 -0
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/rpc/rpc-server.d.ts +2 -0
- package/dist/sidebar-state.d.ts +44 -5
- package/dist/sidebar-state.js +1 -1
- package/dist/{token-xsc39hv8.js → token-5hdnazxp.js} +1 -1
- package/dist/token-util-3d2nj45d.js +1 -0
- package/dist/tui-preferences.js +1 -1
- package/dist/tui.d.ts +9 -0
- package/dist/version.d.ts +1 -1
- package/package.json +2 -2
- package/src/rpc/port-file.ts +115 -8
- package/src/rpc/protocol.ts +1 -0
- package/src/rpc/rpc-dir.ts +31 -7
- package/src/rpc/rpc-server.ts +18 -2
- package/src/sidebar-state.ts +744 -27
- package/src/tui/command-dialogs.tsx +25 -3
- package/src/tui-compiled/rpc/port-file.ts +115 -8
- package/src/tui-compiled/rpc/protocol.ts +1 -0
- package/src/tui-compiled/rpc/rpc-dir.ts +31 -7
- package/src/tui-compiled/sidebar-state.ts +744 -27
- package/src/tui-compiled/tui/command-dialogs.tsx +17 -4
- package/src/tui-compiled/tui.tsx +50 -15
- package/src/tui.tsx +57 -19
- package/dist/index-9k2gxqc1.js +0 -89
- package/dist/index-ptsh66z7.js +0 -2
- package/dist/index-rckqt97a.js +0 -1
- package/dist/index-reagbdg3.js +0 -2
- package/dist/index-ya6hw803.js +0 -7
- package/dist/token-util-xzavn760.js +0 -1
|
@@ -18,6 +18,7 @@ type ApplyFn = (
|
|
|
18
18
|
export function buildCachekeepDialogOptions(payload: OpenDialogPayload) {
|
|
19
19
|
const enabled = payload.knobs.enabled === true
|
|
20
20
|
const subagents = payload.knobs.subagents === true
|
|
21
|
+
const sustain = payload.knobs.sustain === true
|
|
21
22
|
const running = payload.knobs.running === true
|
|
22
23
|
const tracked = Number(payload.knobs.tracked ?? 0)
|
|
23
24
|
const generatedAt = Number(payload.knobs.generatedAt ?? Date.now())
|
|
@@ -31,7 +32,7 @@ export function buildCachekeepDialogOptions(payload: OpenDialogPayload) {
|
|
|
31
32
|
| undefined
|
|
32
33
|
const windowLabel = windowKnob
|
|
33
34
|
? `${String(windowKnob.startHour).padStart(2, '0')}-${String(windowKnob.endHour).padStart(2, '0')}`
|
|
34
|
-
: 'always'
|
|
35
|
+
: 'always (no window)'
|
|
35
36
|
const lastWarm = lastWarmAt
|
|
36
37
|
? `${Math.ceil((generatedAt - lastWarmAt) / 1000)}s ago`
|
|
37
38
|
: 'none yet'
|
|
@@ -44,6 +45,7 @@ export function buildCachekeepDialogOptions(payload: OpenDialogPayload) {
|
|
|
44
45
|
`last warm ${lastWarm}`,
|
|
45
46
|
`window ${windowLabel}`,
|
|
46
47
|
`${idleWindow}m idle cap`,
|
|
48
|
+
`sustain ${sustain ? 'on' : 'off'}`,
|
|
47
49
|
`subagent idle ${subIdleWindow}m`,
|
|
48
50
|
].filter((part) => part.length > 0)
|
|
49
51
|
|
|
@@ -67,6 +69,14 @@ export function buildCachekeepDialogOptions(payload: OpenDialogPayload) {
|
|
|
67
69
|
? `Warm subagent sessions (${subIdleWindow}m idle cap). Disable to skip subagents.`
|
|
68
70
|
: `Skip subagent sessions. Enable to warm them too (${subIdleWindow}m idle cap).`,
|
|
69
71
|
},
|
|
72
|
+
{
|
|
73
|
+
title: sustain
|
|
74
|
+
? 'Sustain main sessions: on'
|
|
75
|
+
: 'Sustain main sessions: off',
|
|
76
|
+
value: sustain ? 'sustain off' : 'sustain on',
|
|
77
|
+
description:
|
|
78
|
+
'main-only, idle-cap only; the configured clock window still applies.',
|
|
79
|
+
},
|
|
70
80
|
{
|
|
71
81
|
title: windowKnob ? `Warm window: ${windowLabel}` : 'Set warm window…',
|
|
72
82
|
value: 'set_window',
|
|
@@ -166,6 +176,18 @@ export function openCommandDialog(
|
|
|
166
176
|
value: 'fallback-first',
|
|
167
177
|
description: 'Prefer fallback accounts, preserve main',
|
|
168
178
|
},
|
|
179
|
+
{
|
|
180
|
+
title: 'Sticky balanced',
|
|
181
|
+
value: 'sticky-balanced',
|
|
182
|
+
description:
|
|
183
|
+
'Keep each session on one account while balancing new sessions',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
title: "Reset this session's pin",
|
|
187
|
+
value: 'reset',
|
|
188
|
+
description:
|
|
189
|
+
'Unpin this session; the next request may still choose the same account',
|
|
190
|
+
},
|
|
169
191
|
]}
|
|
170
192
|
onSelect={(option) => {
|
|
171
193
|
void apply('openai-routing', String(option.value)).then((r) => {
|
|
@@ -436,8 +458,8 @@ function openAccountDialog(
|
|
|
436
458
|
showAddFlow()
|
|
437
459
|
return
|
|
438
460
|
}
|
|
439
|
-
// Main has no per-account actions
|
|
440
|
-
//
|
|
461
|
+
// Main has no per-account actions: it is not removable or reorderable,
|
|
462
|
+
// regardless of whether the active session uses mode or sticky routing.
|
|
441
463
|
if (option.value === 'main') return
|
|
442
464
|
showL2Fallback(option.value)
|
|
443
465
|
}}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
import type { Dirent } from 'node:fs'
|
|
1
2
|
import {
|
|
3
|
+
chmod,
|
|
2
4
|
mkdir,
|
|
3
5
|
readdir,
|
|
4
6
|
readFile,
|
|
5
7
|
rename,
|
|
8
|
+
rmdir,
|
|
6
9
|
unlink,
|
|
7
10
|
writeFile,
|
|
8
11
|
} from 'node:fs/promises'
|
|
9
|
-
import { join } from 'node:path'
|
|
12
|
+
import { join, resolve } from 'node:path'
|
|
13
|
+
import { createLogger } from '../logger'
|
|
14
|
+
|
|
15
|
+
const log = createLogger('rpc')
|
|
10
16
|
|
|
11
17
|
export interface PortFileEntry {
|
|
12
18
|
port: number
|
|
@@ -24,17 +30,118 @@ function pidAlive(pid: number): boolean {
|
|
|
24
30
|
}
|
|
25
31
|
}
|
|
26
32
|
|
|
33
|
+
function isManagedRpcStateDir(name: string): boolean {
|
|
34
|
+
return /^(?:openai-auth-)?[0-9a-f]{16}$/.test(name)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isUsablePortFileEntry(value: unknown): value is PortFileEntry {
|
|
38
|
+
return (
|
|
39
|
+
value !== null &&
|
|
40
|
+
typeof value === 'object' &&
|
|
41
|
+
!Array.isArray(value) &&
|
|
42
|
+
typeof (value as { pid?: unknown }).pid === 'number' &&
|
|
43
|
+
Number.isFinite((value as { pid: number }).pid) &&
|
|
44
|
+
typeof (value as { port?: unknown }).port === 'number' &&
|
|
45
|
+
Number.isFinite((value as { port: number }).port)
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function removeCorruptPortFile(portFile: string): Promise<void> {
|
|
50
|
+
log.debug('rpc corrupt port file', { pid: process.pid, portFile })
|
|
51
|
+
await unlink(portFile).catch(() => {})
|
|
52
|
+
}
|
|
53
|
+
|
|
27
54
|
export async function writePortFile(
|
|
28
55
|
dir: string,
|
|
29
56
|
entry: { port: number; token: string; pid: number },
|
|
57
|
+
options: {
|
|
58
|
+
secureDir?: boolean
|
|
59
|
+
beforeWrite?: () => void | Promise<void>
|
|
60
|
+
} = {},
|
|
30
61
|
): Promise<string> {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
62
|
+
// The directory can be removed by another project's sweep between our
|
|
63
|
+
// mkdir and the first writeFile/rename, so the whole create-then-rename
|
|
64
|
+
// unit is retried once on ENOENT. The retry recreates the directory; a
|
|
65
|
+
// persistent ENOENT (e.g. permission, read-only parent) will surface on
|
|
66
|
+
// the second attempt — failing fast beats an unbounded loop.
|
|
67
|
+
const writeOnce = async (): Promise<string> => {
|
|
68
|
+
await mkdir(dir, {
|
|
69
|
+
recursive: true,
|
|
70
|
+
mode: options.secureDir ? 0o700 : undefined,
|
|
71
|
+
})
|
|
72
|
+
if (options.secureDir) await chmod(dir, 0o700)
|
|
73
|
+
await options.beforeWrite?.()
|
|
74
|
+
const full: PortFileEntry = { ...entry, startedAt: Date.now() }
|
|
75
|
+
const target = join(dir, `port-${entry.pid}.json`)
|
|
76
|
+
const tmp = `${target}.${process.pid}.tmp`
|
|
77
|
+
await writeFile(tmp, JSON.stringify(full), {
|
|
78
|
+
encoding: 'utf8',
|
|
79
|
+
mode: 0o600,
|
|
80
|
+
})
|
|
81
|
+
await rename(tmp, target)
|
|
82
|
+
return target
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return await writeOnce()
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
88
|
+
return await writeOnce()
|
|
89
|
+
}
|
|
90
|
+
throw error
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function sweepRpcState(
|
|
95
|
+
root: string,
|
|
96
|
+
activeDir: string,
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
let projectDirs: Dirent<string>[]
|
|
99
|
+
try {
|
|
100
|
+
projectDirs = await readdir(root, { withFileTypes: true })
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
|
|
103
|
+
throw error
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const active = resolve(activeDir)
|
|
107
|
+
for (const projectDir of projectDirs) {
|
|
108
|
+
if (!projectDir.isDirectory() || !isManagedRpcStateDir(projectDir.name)) {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
const dir = join(root, projectDir.name)
|
|
112
|
+
let names: string[]
|
|
113
|
+
try {
|
|
114
|
+
names = await readdir(dir)
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue
|
|
117
|
+
throw error
|
|
118
|
+
}
|
|
119
|
+
for (const name of names) {
|
|
120
|
+
if (!name.startsWith('port-') || !name.endsWith('.json')) continue
|
|
121
|
+
const portFile = join(dir, name)
|
|
122
|
+
let raw: string | undefined
|
|
123
|
+
try {
|
|
124
|
+
raw = await readFile(portFile, 'utf8')
|
|
125
|
+
} catch {
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
if (raw === undefined) continue
|
|
129
|
+
let parsed: unknown
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(raw)
|
|
132
|
+
} catch {
|
|
133
|
+
await removeCorruptPortFile(portFile)
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
if (!isUsablePortFileEntry(parsed)) {
|
|
137
|
+
await removeCorruptPortFile(portFile)
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
const entry = parsed
|
|
141
|
+
if (!pidAlive(entry.pid)) await unlink(portFile).catch(() => {})
|
|
142
|
+
}
|
|
143
|
+
if (resolve(dir) !== active) await rmdir(dir).catch(() => {})
|
|
144
|
+
}
|
|
38
145
|
}
|
|
39
146
|
|
|
40
147
|
export async function discoverPortFile(
|
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
import { homedir, tmpdir } from 'node:os'
|
|
3
|
-
import { join, resolve } from 'node:path'
|
|
3
|
+
import { dirname, join, resolve } from 'node:path'
|
|
4
4
|
|
|
5
5
|
const RPC_DIR_ENV = 'OPENCODE_OPENAI_AUTH_RPC_DIR'
|
|
6
6
|
|
|
7
|
+
export interface RpcDirResolution {
|
|
8
|
+
dir: string
|
|
9
|
+
secureDir: boolean
|
|
10
|
+
sweepRoot?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function rpcHash(projectDirectory: string): string {
|
|
14
|
+
return createHash('sha256')
|
|
15
|
+
.update(projectDirectory)
|
|
16
|
+
.digest('hex')
|
|
17
|
+
.slice(0, 16)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function defaultRpcRoot(): string {
|
|
21
|
+
const base = process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state')
|
|
22
|
+
return join(base, 'cortexkit', 'openai-auth', 'rpc')
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
// Both processes must resolve the SAME dir from the SAME project directory.
|
|
8
26
|
export function getRpcDir(projectDirectory: string): string {
|
|
9
27
|
const override = process.env[RPC_DIR_ENV]?.trim()
|
|
@@ -11,12 +29,18 @@ export function getRpcDir(projectDirectory: string): string {
|
|
|
11
29
|
// so server and TUI halves always resolve the same dir. An absolute override is
|
|
12
30
|
// used as-is (resolve(base, absolute) returns the absolute path unchanged).
|
|
13
31
|
if (override) return resolve(projectDirectory, override)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
return join(defaultRpcRoot(), `openai-auth-${rpcHash(projectDirectory)}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function resolveRpcDir(
|
|
36
|
+
projectDirectory: string,
|
|
37
|
+
): Promise<RpcDirResolution> {
|
|
38
|
+
const override = process.env[RPC_DIR_ENV]?.trim()
|
|
39
|
+
if (override) {
|
|
40
|
+
return { dir: resolve(projectDirectory, override), secureDir: false }
|
|
41
|
+
}
|
|
42
|
+
const dir = getRpcDir(projectDirectory)
|
|
43
|
+
return { dir, secureDir: true, sweepRoot: dirname(dir) }
|
|
20
44
|
}
|
|
21
45
|
|
|
22
46
|
export { tmpdir }
|