@cortexkit/opencode-anthropic-auth 1.14.0 → 1.15.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-anthropic-auth",
3
- "version": "1.14.0",
3
+ "version": "1.15.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "./tui": {
18
18
  "types": "./dist/tui.d.ts",
19
- "import": "./src/tui.tsx"
19
+ "import": "./src/tui/entry.mjs"
20
20
  },
21
21
  "./tui-prefs": {
22
22
  "types": "./dist/tui-preferences.d.ts",
@@ -36,7 +36,8 @@
36
36
  "files": [
37
37
  "dist",
38
38
  "src/tui.tsx",
39
- "src/tui/command-dialogs.tsx",
39
+ "src/tui",
40
+ "src/tui-compiled",
40
41
  "src/sidebar-state.ts",
41
42
  "src/tui-preferences.ts",
42
43
  "src/rpc",
@@ -44,24 +45,26 @@
44
45
  "LICENSE"
45
46
  ],
46
47
  "scripts": {
47
- "build": "rm -rf dist && bun build src/index.ts src/cli.ts src/sidebar-state.ts src/tui-preferences.ts src/rpc/rpc-client.ts src/rpc/port-file.ts src/rpc/protocol.ts src/rpc/rpc-dir.ts --outdir dist --target node --format esm --splitting --external @opencode-ai/plugin --minify && tsc -p tsconfig.build.json --emitDeclarationOnly",
48
+ "build": "rm -rf dist && bun build src/index.ts src/cli.ts src/sidebar-state.ts src/tui-preferences.ts src/rpc/rpc-client.ts src/rpc/port-file.ts src/rpc/protocol.ts src/rpc/rpc-dir.ts --outdir dist --target node --format esm --splitting --external @opencode-ai/plugin --minify && tsc -p tsconfig.build.json --emitDeclarationOnly && bun run build:tui",
49
+ "build:tui": "bun scripts/build-tui.ts",
50
+ "smoke:tui": "bun scripts/smoke-tui-pack-install.ts",
48
51
  "build:dev": "rm -rf dist && tsc -p tsconfig.build.json",
49
52
  "dev": "bun ../../scripts/dev.ts",
50
53
  "dev:clean": "bun ../../scripts/dev-clean.ts",
51
54
  "extract": "bun ../../scripts/extract-system-prompt.ts",
52
55
  "test": "bun test src/tests",
53
- "typecheck": "tsc",
54
- "types": "tsc",
56
+ "typecheck": "tsc && tsc -p tsconfig.scripts.json",
57
+ "types": "tsc && tsc -p tsconfig.scripts.json",
55
58
  "prepublishOnly": "bun run build"
56
59
  },
57
60
  "peerDependencies": {
58
61
  "@opencode-ai/plugin": "*"
59
62
  },
60
63
  "dependencies": {
61
- "@cortexkit/anthropic-auth-core": "1.14.0",
62
- "@opentui/core": ">=0.4.2",
63
- "@opentui/solid": ">=0.4.3",
64
+ "@cortexkit/anthropic-auth-core": "1.15.1",
65
+ "@opentui/core": "0.4.3",
66
+ "@opentui/solid": "0.4.3",
64
67
  "jsonc-parser": "^3.3.1",
65
- "solid-js": "^1.9.10"
68
+ "solid-js": "1.9.12"
66
69
  }
67
70
  }
@@ -27,6 +27,13 @@ export interface SidebarAccountState {
27
27
  needsReauth: boolean
28
28
  }
29
29
 
30
+ export interface FableRecoverySidebarState {
31
+ sessionId: string
32
+ mode: 'opus' | 'fable'
33
+ remaining: number
34
+ changedAt: number
35
+ }
36
+
30
37
  export interface SidebarState {
31
38
  main: {
32
39
  quota: AccountQuota | null
@@ -45,6 +52,7 @@ export interface SidebarState {
45
52
  window?: string
46
53
  trackedSessions?: number
47
54
  }
55
+ fableRecoveries?: FableRecoverySidebarState[]
48
56
  lastUpdated: number
49
57
  }
50
58
 
@@ -185,6 +193,32 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
185
193
  }
186
194
  }
187
195
 
196
+ const fableRecoveries: FableRecoverySidebarState[] = Array.isArray(
197
+ raw.fableRecoveries,
198
+ )
199
+ ? raw.fableRecoveries
200
+ .filter(isRecord)
201
+ .flatMap((recovery): FableRecoverySidebarState[] => {
202
+ if (
203
+ typeof recovery.sessionId !== 'string' ||
204
+ (recovery.mode !== 'opus' && recovery.mode !== 'fable') ||
205
+ typeof recovery.remaining !== 'number' ||
206
+ !Number.isFinite(recovery.remaining) ||
207
+ typeof recovery.changedAt !== 'number' ||
208
+ !Number.isFinite(recovery.changedAt)
209
+ )
210
+ return []
211
+ return [
212
+ {
213
+ sessionId: recovery.sessionId,
214
+ mode: recovery.mode,
215
+ remaining: Math.max(0, Math.floor(recovery.remaining)),
216
+ changedAt: recovery.changedAt,
217
+ },
218
+ ]
219
+ })
220
+ : []
221
+
188
222
  return {
189
223
  main,
190
224
  fallbacks,
@@ -197,6 +231,7 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
197
231
  ? raw.fastMode
198
232
  : DEFAULT_SIDEBAR_STATE.fastMode,
199
233
  cacheKeep,
234
+ fableRecoveries: fableRecoveries.length > 0 ? fableRecoveries : undefined,
200
235
  lastUpdated: typeof raw.lastUpdated === 'number' ? raw.lastUpdated : 0,
201
236
  }
202
237
  }
@@ -265,6 +300,18 @@ export function formatScopedQuotaLabel(title: string) {
265
300
  return /^fable$/i.test(label) ? 'Fa' : label
266
301
  }
267
302
 
303
+ export function getFableRecoverySummary(
304
+ state: SidebarState,
305
+ sessionId: string,
306
+ ): string | undefined {
307
+ const recovery = state.fableRecoveries?.find(
308
+ (candidate) => candidate.sessionId === sessionId,
309
+ )
310
+ if (!recovery) return undefined
311
+ if (recovery.mode === 'fable') return 'Fable 5 · restored'
312
+ return `Opus 4.8 · ${recovery.remaining} left`
313
+ }
314
+
268
315
  export function getCollapsedQuotaSummary(quota: AccountQuota | null): {
269
316
  fiveHourUsedPercent: number | null
270
317
  sevenDayUsedPercent: number | null
@@ -0,0 +1,37 @@
1
+ // Prefer the host OpenTUI runtime registry when it exists. OpenTUI 0.4.x
2
+ // registers these virtual modules process-wide, allowing the precompiled TUI to
3
+ // share the host's single Solid/OpenTUI runtime when loaded from node_modules.
4
+ const runtimeProbe = `opentui:runtime-module:${encodeURIComponent('@opentui/solid')}`
5
+
6
+ function isMissingRuntimeRegistry(error) {
7
+ const message = error instanceof Error ? error.message : String(error)
8
+ return (
9
+ /Cannot find|Could not resolve|Module not found|Unable to resolve/.test(
10
+ message,
11
+ ) && message.includes('opentui:runtime-module:')
12
+ )
13
+ }
14
+
15
+ let mod
16
+ try {
17
+ await import(runtimeProbe)
18
+ } catch (error) {
19
+ if (!isMissingRuntimeRegistry(error)) {
20
+ console.error('Anthropic Auth TUI runtime registry probe failed', error)
21
+ throw error
22
+ }
23
+ // Older hosts and bare Bun do not provide the virtual registry. Their source
24
+ // loader still applies the Solid transform, so retain the raw TSX fallback.
25
+ mod = await import('../tui.tsx')
26
+ }
27
+
28
+ if (!mod) {
29
+ try {
30
+ mod = await import('../tui-compiled/tui.tsx')
31
+ } catch (error) {
32
+ console.error('Anthropic Auth compiled TUI failed to load', error)
33
+ throw error
34
+ }
35
+ }
36
+
37
+ export default mod.default
@@ -0,0 +1,70 @@
1
+ import {
2
+ mkdir,
3
+ readdir,
4
+ readFile,
5
+ rename,
6
+ unlink,
7
+ writeFile,
8
+ } from 'node:fs/promises'
9
+ import { join } from 'node:path'
10
+
11
+ export interface PortFileEntry {
12
+ port: number
13
+ token: string
14
+ pid: number
15
+ startedAt: number
16
+ }
17
+
18
+ function pidAlive(pid: number): boolean {
19
+ try {
20
+ process.kill(pid, 0)
21
+ return true
22
+ } catch (error) {
23
+ return (error as NodeJS.ErrnoException).code === 'EPERM'
24
+ }
25
+ }
26
+
27
+ export async function writePortFile(
28
+ dir: string,
29
+ entry: { port: number; token: string; pid: number },
30
+ ): 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
38
+ }
39
+
40
+ export async function discoverPortFile(
41
+ dir: string,
42
+ expectedPid?: number,
43
+ ): Promise<PortFileEntry | null> {
44
+ let names: string[]
45
+ try {
46
+ names = await readdir(dir)
47
+ } catch {
48
+ return null
49
+ }
50
+ const live: PortFileEntry[] = []
51
+ for (const name of names) {
52
+ if (!name.startsWith('port-') || !name.endsWith('.json')) continue
53
+ try {
54
+ const parsed = JSON.parse(
55
+ await readFile(join(dir, name), 'utf8'),
56
+ ) as PortFileEntry
57
+ if (Number.isFinite(parsed.port)) {
58
+ if (pidAlive(parsed.pid)) live.push(parsed)
59
+ else unlink(join(dir, name)).catch(() => {})
60
+ }
61
+ } catch {}
62
+ }
63
+ if (live.length === 0) return null
64
+ const candidates =
65
+ expectedPid && expectedPid >= 1
66
+ ? live.filter((entry) => entry.pid === expectedPid)
67
+ : []
68
+ const entries = candidates.length > 0 ? candidates : live
69
+ return entries.sort((a, b) => b.startedAt - a.startedAt)[0] ?? null
70
+ }
@@ -0,0 +1,34 @@
1
+ export type CommandModalName =
2
+ | 'claude-account'
3
+ | 'claude-cache'
4
+ | 'claude-cachekeep'
5
+ | 'claude-quota'
6
+ | 'claude-dump'
7
+ | 'claude-fast'
8
+ | 'claude-routing'
9
+ | 'claude-killswitch'
10
+ | 'claude-logging'
11
+
12
+ export interface OpenDialogPayload {
13
+ command: CommandModalName
14
+ text: string
15
+ knobs: Record<string, unknown>
16
+ }
17
+
18
+ export interface RpcNotification {
19
+ id: number
20
+ type: 'open-dialog'
21
+ payload: OpenDialogPayload
22
+ sessionId?: string
23
+ }
24
+
25
+ export interface ApplyRequest {
26
+ command: CommandModalName
27
+ arguments: string
28
+ sessionId?: string
29
+ }
30
+
31
+ export interface ApplyResult {
32
+ text: string
33
+ knobs: Record<string, unknown>
34
+ }
@@ -0,0 +1,62 @@
1
+ import { discoverPortFile } from './port-file'
2
+ import type { ApplyRequest, ApplyResult, RpcNotification } from './protocol'
3
+
4
+ export interface RpcClient {
5
+ pending: (
6
+ lastReceivedId: number,
7
+ sessionId?: string,
8
+ ) => Promise<RpcNotification[]>
9
+ apply: (request: ApplyRequest) => Promise<ApplyResult>
10
+ }
11
+
12
+ async function call<T>(
13
+ dir: string,
14
+ method: string,
15
+ params: Record<string, unknown>,
16
+ expectedPid?: number,
17
+ ): Promise<T | null> {
18
+ const entry = await discoverPortFile(dir, expectedPid)
19
+ if (!entry) return null
20
+ const controller = new AbortController()
21
+ const timer = setTimeout(() => controller.abort(), 2_000)
22
+ try {
23
+ const res = await fetch(`http://127.0.0.1:${entry.port}/rpc/${method}`, {
24
+ method: 'POST',
25
+ headers: {
26
+ 'content-type': 'application/json',
27
+ authorization: `Bearer ${entry.token}`,
28
+ },
29
+ body: JSON.stringify(params),
30
+ signal: controller.signal,
31
+ })
32
+ if (!res.ok) return null
33
+ return (await res.json()) as T
34
+ } catch {
35
+ return null
36
+ } finally {
37
+ clearTimeout(timer)
38
+ }
39
+ }
40
+
41
+ export function createRpcClient(dir: string, expectedPid?: number): RpcClient {
42
+ return {
43
+ async pending(lastReceivedId, sessionId) {
44
+ const out = await call<{ messages: RpcNotification[] }>(
45
+ dir,
46
+ 'pending-notifications',
47
+ { lastReceivedId, sessionId },
48
+ expectedPid,
49
+ )
50
+ return out?.messages ?? []
51
+ },
52
+ async apply(request) {
53
+ const out = await call<ApplyResult>(
54
+ dir,
55
+ 'apply',
56
+ { ...request },
57
+ expectedPid,
58
+ )
59
+ return out ?? { text: 'apply failed', knobs: {} }
60
+ },
61
+ }
62
+ }
@@ -0,0 +1,19 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { homedir, tmpdir } from 'node:os'
3
+ import { join, resolve } from 'node:path'
4
+
5
+ const RPC_DIR_ENV = 'OPENCODE_ANTHROPIC_AUTH_RPC_DIR'
6
+
7
+ // Both processes must resolve the SAME dir from the SAME project directory.
8
+ export function getRpcDir(projectDirectory: string): string {
9
+ const override = process.env[RPC_DIR_ENV]?.trim()
10
+ if (override) return resolve(projectDirectory, override)
11
+ const hash = createHash('sha256')
12
+ .update(projectDirectory)
13
+ .digest('hex')
14
+ .slice(0, 16)
15
+ const base = process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state')
16
+ return join(base, 'cortexkit', 'anthropic-auth', 'rpc', hash)
17
+ }
18
+
19
+ export { tmpdir }