@cortexkit/opencode-openai-auth 0.4.3 → 0.5.1

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 (57) hide show
  1. package/README.md +8 -4
  2. package/dist/cli.js +1 -1
  3. package/dist/codex-http.d.ts +1 -1
  4. package/dist/commands.d.ts +6 -0
  5. package/dist/core/accounts.d.ts +6 -1
  6. package/dist/core/background-quota-refresh.d.ts +46 -0
  7. package/dist/core/cachekeep.d.ts +4 -0
  8. package/dist/core/oauth.d.ts +1 -1
  9. package/dist/core/quota-manager.d.ts +11 -4
  10. package/dist/core/refresh-all-quota.d.ts +12 -1
  11. package/dist/core/sticky-routing.d.ts +43 -0
  12. package/dist/dump.d.ts +1 -0
  13. package/dist/{index-xcf8tm40.js → index-6rnw2cgs.js} +1 -1
  14. package/dist/index-j1364eag.js +89 -0
  15. package/dist/index-rfgyxxxq.js +1 -0
  16. package/dist/index-s5fbqqwx.js +3 -0
  17. package/dist/index-vrdc90yd.js +6 -0
  18. package/dist/index-wnxfzjfr.js +2 -0
  19. package/dist/{index-xbq3aq6f.js → index-wpd7mg3p.js} +1 -1
  20. package/dist/index-zkzb0twr.js +2 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +99 -86
  23. package/dist/{quota-normalize-754rewnb.js → quota-normalize-n02dfdx0.js} +1 -1
  24. package/dist/rpc/port-file.d.ts +4 -0
  25. package/dist/rpc/port-file.js +1 -1
  26. package/dist/rpc/protocol.d.ts +1 -0
  27. package/dist/rpc/rpc-client.js +1 -1
  28. package/dist/rpc/rpc-dir.d.ts +6 -0
  29. package/dist/rpc/rpc-dir.js +1 -1
  30. package/dist/rpc/rpc-server.d.ts +2 -0
  31. package/dist/sidebar-state.d.ts +44 -5
  32. package/dist/sidebar-state.js +1 -1
  33. package/dist/{token-xsc39hv8.js → token-5hdnazxp.js} +1 -1
  34. package/dist/token-util-3d2nj45d.js +1 -0
  35. package/dist/tui-preferences.js +1 -1
  36. package/dist/tui.d.ts +9 -0
  37. package/dist/version.d.ts +1 -1
  38. package/package.json +2 -2
  39. package/src/rpc/port-file.ts +115 -8
  40. package/src/rpc/protocol.ts +1 -0
  41. package/src/rpc/rpc-dir.ts +31 -7
  42. package/src/rpc/rpc-server.ts +18 -2
  43. package/src/sidebar-state.ts +744 -27
  44. package/src/tui/command-dialogs.tsx +25 -3
  45. package/src/tui-compiled/rpc/port-file.ts +115 -8
  46. package/src/tui-compiled/rpc/protocol.ts +1 -0
  47. package/src/tui-compiled/rpc/rpc-dir.ts +31 -7
  48. package/src/tui-compiled/sidebar-state.ts +744 -27
  49. package/src/tui-compiled/tui/command-dialogs.tsx +17 -4
  50. package/src/tui-compiled/tui.tsx +50 -15
  51. package/src/tui.tsx +57 -19
  52. package/dist/index-9k2gxqc1.js +0 -89
  53. package/dist/index-ptsh66z7.js +0 -2
  54. package/dist/index-rckqt97a.js +0 -1
  55. package/dist/index-reagbdg3.js +0 -2
  56. package/dist/index-ya6hw803.js +0 -7
  57. 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 (routing is mode-driven; main is
440
- // not removable or reorderable) — the row is informational only.
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
- await mkdir(dir, { recursive: true })
32
- const full: PortFileEntry = { ...entry, startedAt: Date.now() }
33
- const target = join(dir, `port-${entry.pid}.json`)
34
- const tmp = `${target}.${process.pid}.tmp`
35
- await writeFile(tmp, JSON.stringify(full), { encoding: 'utf8', mode: 0o600 })
36
- await rename(tmp, target)
37
- return target
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(
@@ -23,6 +23,7 @@ export interface RpcNotification {
23
23
  export interface ApplyRequest {
24
24
  command: CommandModalName
25
25
  arguments: string
26
+ sessionId?: string
26
27
  }
27
28
 
28
29
  export interface ApplyResult {
@@ -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
- const hash = createHash('sha256')
15
- .update(projectDirectory)
16
- .digest('hex')
17
- .slice(0, 16)
18
- const base = process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state')
19
- return join(base, 'cortexkit', 'openai-auth', 'rpc', hash)
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 }